diff --git a/Cargo.lock b/Cargo.lock index 0df15ae..b4369e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,19 +487,20 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rtklib-ffi" -version = "0.2.0" +version = "0.3.0" dependencies = [ "bitflags", "hifitime", "num_enum", "rtklib-sys", "serial_test", + "strum", "thiserror", ] [[package]] name = "rtklib-sys" -version = "0.2.0" +version = "0.3.0" dependencies = [ "bindgen", "cc", @@ -639,6 +640,27 @@ dependencies = [ "syn", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.87" diff --git a/Cargo.toml b/Cargo.toml index 441f13b..d05206b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rtklib-ffi" description = "Rust wrapper for RTKLIB" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["Kevin Webb", "Joseph Fox-Rabinovitz"] repository = "https://github.com/kpwebb/rtklib-ffi" @@ -23,15 +23,17 @@ gis = ["rtklib-sys/gis"] hifitime = ["dep:hifitime"] net = ["rtklib-sys/net"] ppk = ["rtklib-sys/ppk"] -raw = ["rtklib-sys/raw"] +receivers = ["rtklib-sys/receivers"] rtcm = ["rtklib-sys/rtcm"] +strum = ["dep:strum"] tle = ["rtklib-sys/tle"] [dependencies] bitflags = "2" hifitime = { version = "4", optional = true } num_enum = "0.7.3" -rtklib-sys = { path = "rtklib-sys/", version = "0.2.0" } +rtklib-sys = { path = "rtklib-sys/", version = "0.3.0" } +strum = { version = "0.27", features = ["derive"], optional = true } thiserror = "2" [dev-dependencies] diff --git a/README.md b/README.md index 423f8da..aaf1979 100644 --- a/README.md +++ b/README.md @@ -1 +1,493 @@ -Safe Rust bindings for [RTKLIB](https://github.com/rtklibexplorer/RTKLIB). The unsafe bindings are fairly comprehensive. Safe bindings are available for RTCM3 message decoding and PPK post-processing. YMMV. +Safe Rust bindings for [RTKLIB](https://github.com/rtklibexplorer/RTKLIB). + +The unsafe bindings in `rtklib-sys` expose the full RTKLIB API. The safe wrappers +in `rtklib-ffi` cover a subset - see the coverage tables below for current status. + +## Features + +| Feature | Description | +|---------|-------------| +| `conv` | RINEX and other file format conversion; implies `receivers` because `convrnx` references `init_raw` unconditionally at link time | +| `gis` | GIS data support | +| `hifitime` | Conversions between `GpsTime` and `hifitime::Epoch` | +| `net` | Network streaming | +| `ppk` | Post-processed kinematic positioning via `postpos()` and solution I/O | +| `receivers` | All supported hardware receiver decoders: BINEX, Hemisphere Crescent, Javad/Topcon, NovAtel OEM, NVS, Septentrio SBF, SkyTraq, Swift Navigation SBP, Trimble RT17, u-blox UBX, and Unicore | +| `rtcm` | RTCM3 message decoding | +| `strum` | `Display` for enums via the `strum` crate | +| `tle` | TLE satellite tracking | + +## Quick Start + +```toml +[dependencies] +rtklib-ffi = { version = "0.3", features = ["ppk"] } +``` + +```rust +use rtklib_ffi::ppk::{FilOpt, PrcOpt, SolOpt, postpos}; + +let popt = PrcOpt::kinematic(); +let sopt = SolOpt::default(); +let fopt = FilOpt::default(); + +postpos( + "rover.obs", "base.obs", + &["nav.nav"], "output.pos", + &popt, &sopt, &fopt, +).unwrap(); +``` + +## RTKLIB Coverage + +`[x]` = safe wrapper, `[ ]` = unsafe bindings only. + +--- + +### Core + +Always compiled. + +**`rtkcmn.c`** + +Coordinate transforms, time conversions, satellite utilities, and linear algebra. + +- [x] `ecef2pos`: ECEF to geodetic +- [x] `pos2ecef`: geodetic to ECEF +- [x] `satno`: constellation + PRN to satellite number +- [x] `gpst2time`: GPS week/TOW to `gtime_t` +- [x] `getbitu`: unsigned bit extraction +- [ ] `ecef2enu`: ECEF to local ENU +- [ ] `enu2ecef`: local ENU to ECEF +- [ ] `xyz2enu`: rotation matrix ECEF to ENU +- [ ] `covecef`: covariance ECEF to ENU +- [ ] `covenu`: covariance ENU to ECEF +- [ ] `geodist`: geometric distance between satellite and receiver +- [ ] `satazel`: satellite azimuth/elevation +- [ ] `satexclude`: check satellite exclusion +- [ ] `satsys`: satellite system from satellite number +- [ ] `satid2no`: satellite ID string to number +- [ ] `satno2id`: satellite number to ID string +- [ ] `ionmodel`: Klobuchar ionosphere model +- [ ] `tropmodel`: Saastamoinen troposphere model +- [ ] `ionmapf`: ionosphere mapping function +- [ ] `tropmapf`: troposphere mapping function +- [ ] `antmodel`: antenna phase center offset model +- [ ] `antmodel_s`: satellite antenna phase center offset model +- [ ] `obs2code`: observation code string to number +- [ ] `code2obs`: code number to observation code string +- [ ] `code2freq`: code number to carrier frequency +- [ ] `code2idx`: code number to frequency index +- [ ] `sat2freq`: satellite + code to carrier frequency +- [ ] `seliflc`: select iono-free linear combination +- [ ] `dops`: dilution of precision +- [ ] `epoch2time`: calendar epoch to `gtime_t` +- [ ] `time2epoch`: `gtime_t` to calendar epoch +- [ ] `time2epoch_n`: `gtime_t` to calendar epoch with nanoseconds +- [ ] `time2gpst`: `gtime_t` to GPS week/TOW +- [ ] `gpst2utc`: GPS time to UTC +- [ ] `utc2gpst`: UTC to GPS time +- [ ] `gpst2bdt`: GPS time to BeiDou time +- [ ] `bdt2gpst`: BeiDou time to GPS time +- [ ] `bdt2time`: BeiDou week/TOW to `gtime_t` +- [ ] `time2bdt`: `gtime_t` to BeiDou week/TOW +- [ ] `gst2time`: Galileo week/TOW to `gtime_t` +- [ ] `time2gst`: `gtime_t` to Galileo week/TOW +- [ ] `timeadd`: add seconds to `gtime_t` +- [ ] `timediff`: difference of two `gtime_t` values +- [ ] `time2str`: `gtime_t` to string +- [ ] `str2time`: string to `gtime_t` +- [ ] `time2doy`: `gtime_t` to day of year +- [ ] `adjgpsweek`: adjust GPS week rollover +- [ ] `screent`: time interval screen +- [ ] `readpcv`: read antenna parameter file +- [ ] `searchpcv`: search antenna parameters +- [ ] `readerp`: read earth rotation parameters +- [ ] `geterp`: get earth rotation parameter at time +- [ ] `readblq`: read ocean tide loading parameters +- [ ] `readpos`: read reference station positions +- [ ] `getstapos`: get station position +- [ ] `readnav`: read RINEX navigation data +- [ ] `savenav`: save navigation data to file +- [ ] `freeobs`: free observation data +- [ ] `freenav`: free navigation data +- [ ] `sortobs`: sort and remove duplicate observations +- [ ] `uniqnav`: remove duplicate navigation data +- [ ] `lsq`: least-squares estimation +- [ ] `filter`: Kalman filter update +- [ ] `smoother`: Rauch-Tung-Striebel smoother +- [ ] `matmul` / `matinv` / `solve`: matrix operations +- [ ] `mat` / `imat` / `zeros` / `eye`: matrix allocation +- [ ] `dot` / `dot2` / `dot3` / `norm` / `cross3` / `normv3`: vector operations +- [ ] `str2num` / `deg2dms` / `dms2deg`: string and angle utilities +- [ ] `rtk_crc24q` / `rtk_crc16` / `rtk_crc32`: CRC functions +- [ ] `setbitu` / `setbits` / `getbits`: bit manipulation +- [ ] `decode_word`: GPS navigation word decode +- [ ] `rtk_uncompress`: RTCM3 data decompression +- [ ] `sunmoonpos`: sun/moon position in ECEF +- [ ] `eci2ecef`: ECI to ECEF rotation +- [ ] `utc2gmst`: UTC to Greenwich Mean Sidereal Time +- [ ] `ionppp`: ionosphere pierce point +- [ ] `testsnr`: test signal-to-noise ratio +- [ ] `setcodepri` / `getcodepri`: code priority +- [ ] `showmsg`: progress message callback +- [ ] `timeget` / `timeset` / `timereset`: system time +- [ ] `tickget` / `sleepms`: tick counter and sleep +- [ ] `execcmd` / `expath` / `createdir`: OS utilities +- [ ] `reppath` / `reppaths`: path substitution +- [ ] `read_leaps`: read leap second file + +**`trace.c`** + +- [ ] `traceopen` / `traceclose`: open/close trace file +- [ ] `tracelevel` / `gettracelevel`: set/get trace level +- [ ] `trace_impl` / `tracet_impl` / `traceb_impl`: trace output +- [ ] `tracemat_impl` / `traceobs_impl` / `tracenav_impl`: trace data structures +- [ ] `tracegnav_impl` / `tracehnav_impl` / `tracepeph_impl` / `tracepclk_impl`: trace nav data + +**`geoid.c`** + +- [ ] `opengeoid`: open geoid data file +- [ ] `closegeoid`: close geoid data file +- [ ] `geoidh`: geoid height at position + +**`datum.c`** + +- [ ] `loaddatump`: load datum parameters +- [ ] `tokyo2jgd`: Tokyo datum to JGD2000 +- [ ] `jgd2tokyo`: JGD2000 to Tokyo datum + +--- + +### PPK - `ppk` feature + +**`postpos.c`** + +- [x] `postpos`: full PPK post-processing pipeline + +**`solution.c`** + +- [x] `readsol`: read solution files into buffer +- [x] `readsolt`: read solution files with time window +- [x] `freesolbuf`: free solution buffer +- [x] `outsolheads`: write solution file header +- [x] `outsols`: write one solution record +- [ ] `initsolbuf`: initialize solution buffer +- [ ] `addsol`: append a solution record +- [ ] `getsol`: get solution record by time +- [ ] `inputsol`: decode one solution record from byte stream +- [ ] `outsol`: write solution to stream +- [ ] `outsolex`: write extended solution to stream +- [ ] `outsolexs`: write extended solution as string +- [ ] `outsolhead`: write solution header to stream +- [ ] `outprcopt`: write processing options to stream +- [ ] `outprcopts`: write processing options as string +- [ ] `outnmea_gga`: output NMEA GGA sentence +- [ ] `outnmea_rmc`: output NMEA RMC sentence +- [ ] `outnmea_gsa`: output NMEA GSA sentence +- [ ] `outnmea_gsv`: output NMEA GSV sentence +- [ ] `outnmea_gst`: output NMEA GST sentence +- [ ] `readsolstat`: read solution status file +- [ ] `readsolstatt`: read solution status file with time window +- [ ] `freesolstatbuf`: free solution status buffer + +**`rtkpos.c`** + +- [ ] `rtkinit`: initialize RTK control struct +- [ ] `rtkfree`: free RTK control struct +- [ ] `rtkpos`: single-epoch RTK positioning +- [ ] `rtkopenstat`: open RTK status file +- [ ] `rtkclosestat`: close RTK status file +- [ ] `rtkoutstat`: output RTK status record + +**`pntpos.c`** + +- [ ] `pntpos`: single-point positioning +- [ ] `ionocorr`: ionosphere correction +- [ ] `tropcorr`: troposphere correction + +**`rinex.c`** + +- [ ] `init_rnxctr`: initialize RINEX controller +- [ ] `free_rnxctr`: free RINEX controller +- [ ] `open_rnxctr`: open RINEX file via controller +- [ ] `input_rnxctr`: read one epoch via controller +- [ ] `readrnx`: read RINEX obs/nav file +- [ ] `readrnxt`: read RINEX file with time window +- [ ] `readrnxc`: read RINEX file via controller +- [ ] `outrnxobsh` / `outrnxobsb`: write RINEX observation header/body +- [ ] `outrnxnavh` / `outrnxnavb`: write RINEX GPS nav header/body +- [ ] `outrnxgnavh` / `outrnxgnavb`: write RINEX GLONASS nav header/body +- [ ] `outrnxhnavh` / `outrnxhnavb`: write RINEX SBAS nav header/body +- [ ] `outrnxqnavh`: write RINEX QZSS nav header +- [ ] `outrnxlnavh`: write RINEX Galileo nav header +- [ ] `outrnxcnavh`: write RINEX BeiDou nav header +- [ ] `outrnxinavh`: write RINEX NavIC nav header +- [ ] `rnxcomment`: add comment to RINEX header + +**`ephemeris.c`** + +- [ ] `eph2pos`: broadcast GPS/Galileo/BeiDou satellite position +- [ ] `eph2clk`: broadcast satellite clock +- [ ] `geph2pos`: GLONASS satellite position +- [ ] `geph2clk`: GLONASS satellite clock +- [ ] `seph2pos`: SBAS satellite position +- [ ] `seph2clk`: SBAS satellite clock +- [ ] `satpos`: satellite position and clock +- [ ] `satposs`: satellite positions for all observations +- [ ] `alm2pos`: almanac satellite position +- [ ] `getseleph` / `setseleph`: ephemeris selection + +**`preceph.c`** + +- [ ] `readsp3`: read SP3 precise ephemeris file +- [ ] `peph2pos`: precise satellite position and clock +- [ ] `pephclk`: precise satellite clock +- [ ] `readdcb`: read differential code bias file +- [ ] `readsap`: read satellite antenna parameters +- [ ] `satantoff`: satellite antenna phase center offset +- [ ] `code2bias`: look up signal code bias + +**`lambda.c`** + +- [ ] `lambda`: LAMBDA ambiguity resolution +- [ ] `lambda_reduction`: LAMBDA decorrelation +- [ ] `lambda_search`: LAMBDA integer search + +**`ionex.c`** + +- [ ] `readtec`: read IONEX TEC grid file +- [ ] `iontec`: ionosphere delay from TEC grid + +**`sbas.c`** + +- [ ] `sbsdecodemsg`: decode SBAS message +- [ ] `sbsupdatecorr`: update SBAS corrections +- [ ] `sbssatcorr`: apply SBAS satellite corrections +- [ ] `sbsioncorr`: SBAS ionosphere correction +- [ ] `sbstropcorr`: SBAS troposphere correction +- [ ] `sbsreadmsg`: read SBAS message log +- [ ] `sbsreadmsgt`: read SBAS message log with time window +- [ ] `sbsoutmsg`: write SBAS message to stream + +**`options.c`** + +- [ ] `loadopts`: load processing options from file +- [ ] `saveopts`: save processing options to file +- [ ] `getsysopts` / `setsysopts`: get/set global system options +- [ ] `resetsysopts`: reset to defaults +- [ ] `searchopt`: search option by name +- [ ] `opt2str` / `opt2buf` / `str2opt`: option string conversion + +**`ppp.c`** + +- [ ] `pppos`: PPP positioning +- [ ] `pppnx`: PPP state vector size +- [ ] `pppoutstat`: output PPP status +- [ ] `yaw_angle`: satellite yaw angle + +**`ppp_ar.c`** + +- [ ] `ppp_ar`: PPP ambiguity resolution + +**`tides.c`** + +- [ ] `tidedisp`: tidal displacement + +--- + +### RTCM - `ppk` or `rtcm` feature + +**`rtcm.c`** + +- [x] `init_rtcm`: initialize RTCM control struct +- [x] `free_rtcm`: free RTCM control struct +- [x] `input_rtcm3`: decode RTCM3 message byte by byte +- [ ] `input_rtcm2`: decode RTCM2 message byte by byte +- [ ] `input_rtcm3f`: decode RTCM3 from file +- [ ] `input_rtcm2f`: decode RTCM2 from file +- [ ] `gen_rtcm2`: generate RTCM2 message +- [ ] `gen_rtcm3`: generate RTCM3 message + +**`rtcm2.c`** + +Called internally by `rtcm.c`. Not intended for direct use. + +**`rtcm3.c`** + +Called internally by `rtcm.c`. Not intended for direct use. + +**`rtcm3e.c`** + +Called internally by `rtcm.c`. Not intended for direct use. + +--- + +### Raw Receiver Decoding - `receivers` feature + +All receiver format files are compiled together. `rcvraw.c` calls init, free, +and input functions for every supported format unconditionally, so all format +files must be present in the same link unit. + +**`rcvraw.c`** + +Generic frame decoders used by all receiver-specific decoders. + +- [x] `init_raw`: initialize raw receiver control struct +- [x] `free_raw`: free raw receiver control struct +- [ ] `input_raw`: decode one byte of raw receiver data +- [ ] `input_rawf`: decode raw receiver data from file +- [ ] `decode_frame`: decode GPS navigation frame +- [ ] `decode_glostr`: decode GLONASS navigation string +- [ ] `test_glostr`: test GLONASS string parity +- [ ] `decode_bds_d1` / `decode_bds_d2`: decode BeiDou D1/D2 navigation messages +- [ ] `decode_gal_fnav` / `decode_gal_inav`: decode Galileo F/NAV and I/NAV messages +- [ ] `decode_irn_nav`: decode NavIC navigation message + +**`rcv/binex.c`** + +- [ ] `input_bnx` / `input_bnxf`: BINEX decoder + +~~**`rcv/comnav.c`**~~ - uses APIs removed upstream; excluded until updated + +- ~~[ ] `input_cnav` / `input_cnavf`: ComNav decoder~~ + +**`rcv/crescent.c`** + +- [ ] `input_cres` / `input_cresf`: Hemisphere Crescent decoder + +**`rcv/javad.c`** + +- [ ] `input_javad` / `input_javadf`: Javad/Topcon decoder + +**`rcv/novatel.c`** + +- [ ] `input_oem4` / `input_oem4f`: NovAtel OEM4/6/7 decoder +- [ ] `input_oem3` / `input_oem3f`: NovAtel OEM3 decoder + +**`rcv/nvs.c`** + +- [ ] `input_nvs` / `input_nvsf`: NVS decoder +- [ ] `gen_nvs`: generate NVS command + +**`rcv/rt17.c`** + +- [ ] `input_rt17` / `input_rt17f`: Trimble RT17 decoder + +**`rcv/septentrio.c`** + +- [x] `init_sbf` / `free_sbf`: initialize/free Septentrio SBF struct +- [x] `input_sbf` / `input_sbff`: Septentrio SBF decoder + +**`rcv/skytraq.c`** + +- [ ] `input_stq` / `input_stqf`: SkyTraq decoder +- [ ] `gen_stq`: generate SkyTraq command + +**`rcv/swiftnav.c`** + +- [ ] `input_sbp` / `input_sbpf` / `input_sbpjsonf`: Swift Navigation SBP decoder + +~~**`rcv/tersus.c`**~~ - uses APIs removed upstream; excluded until updated + +- ~~[ ] `input_tersus` / `input_tersusf`: Tersus decoder~~ + +**`rcv/ublox.c`** + +- [ ] `input_ubx` / `input_ubxf`: u-blox UBX decoder +- [ ] `gen_ubx`: generate u-blox command + +**`rcv/unicore.c`** + +- [ ] `input_unicore` / `input_unicoref`: Unicore decoder + +--- + +### File Format Conversion - `conv` feature + +**`convrnx.c`** + +- [ ] `convrnx`: convert receiver raw data to RINEX + +**`convkml.c`** + +- [ ] `convkml`: convert solution to KML +- [ ] `convcsv`: convert solution to CSV + +**`convgpx.c`** + +- [ ] `convgpx`: convert solution to GPX + +--- + +### GIS - `gis` feature + +**`gis.c`** + +- [ ] `gis_read`: read GIS data file +- [ ] `gis_free`: free GIS data + +--- + +### Streaming - `net` feature + +**`stream.c`** + +- [ ] `strinit`: initialize stream +- [ ] `stropen` / `strclose`: open/close stream +- [ ] `strread` / `strwrite`: read/write stream +- [ ] `strsync`: synchronize two streams +- [ ] `strstat` / `strstatx`: stream status +- [ ] `strsum`: stream statistics +- [ ] `strgettime`: stream time tag +- [ ] `strsendnmea` / `strsendcmd`: send NMEA or command to stream +- [ ] `strsetopt`: set stream options +- [ ] `strsetdir` / `strsetproxy` / `strsettimeout`: stream configuration +- [ ] `strinitcom`: initialize COM port + +**`streamsvr.c`** + +- [ ] `strsvrinit`: initialize stream server +- [ ] `strsvrstart` / `strsvrstop`: start/stop stream server +- [ ] `strsvrstat`: stream server status +- [ ] `strsvrpeek`: peek stream server buffer +- [ ] `strconvnew` / `strconvfree`: create/free stream converter + +**`rtksvr.c`** + +- [ ] `rtksvrinit`: initialize RTK server +- [ ] `rtksvrstart` / `rtksvrstop`: start/stop RTK server +- [ ] `rtksvrlock` / `rtksvrunlock`: lock/unlock RTK server +- [ ] `rtksvropenstr` / `rtksvrclosestr`: manage server streams +- [ ] `rtksvrstat` / `rtksvrsstat` / `rtksvrostat`: server status +- [ ] `rtksvrmark`: mark event in RTK server +- [ ] `rtksvrfree`: free RTK server + +**`download.c`** + +- [ ] `dl_readurls`: read download URL list +- [ ] `dl_readstas`: read station list +- [ ] `dl_exec`: execute downloads +- [ ] `dl_test`: test download URL +- [ ] `execcmd_to`: execute command with timeout + +--- + +### TLE - `tle` feature + +**`tle.c`** + +- [ ] `tle_read`: read TLE file +- [ ] `tle_name_read`: read TLE file indexed by satellite name +- [ ] `tle_pos`: satellite position from TLE + +--- + +### SOFA + +**`sofa.c`** + +IAU SOFA routines for high-accuracy sun/moon position. Only compiled when the +`SUNPOS_SOFA` or `MOONPOS_SOFA` macros are defined; this build does not set them, +so `sofa.c` is not a compilation unit here. The lower-accuracy built-in paths in +`rtkcmn.c` are used instead. diff --git a/rtklib-sys/Cargo.toml b/rtklib-sys/Cargo.toml index 87edaf0..f013ac0 100644 --- a/rtklib-sys/Cargo.toml +++ b/rtklib-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtklib-sys" -version = "0.2.0" +version = "0.3.0" rust-version = "1.82.0" edition = "2018" authors = ["Kevin Webb", "Joseph Fox-Rabinovitz"] @@ -13,15 +13,14 @@ categories = [ "api-bindings", "rtklib", "external-ffi-bindings" ] links = "rtklib-ffi" [features] -conv = [] +conv = ["receivers"] gis = [] net = [] ppk = [] -raw = [] +receivers = [] rtcm = [] tle = [] - [build-dependencies] bindgen = "0.70.1" cc = "1.1.21" diff --git a/rtklib-sys/build.rs b/rtklib-sys/build.rs index 7998cc9..0378e59 100644 --- a/rtklib-sys/build.rs +++ b/rtklib-sys/build.rs @@ -52,32 +52,59 @@ fn main() { println!("cargo:rustc-link-lib=pthread"); } - #[cfg(feature = "ppk")] + // These files are needed by both ppk and conv. convrnx.c calls pntpos for + // auto-position estimation; pntpos.c calls into preceph.c and ionex.c; + // rinex.c and sbas.c are referenced unconditionally by convrnx.c. + #[cfg(any(feature = "ppk", feature = "conv"))] { - build.file("rtklib/src/postpos.c"); - build.file("rtklib/src/rtkpos.c"); - build.file("rtklib/src/pntpos.c"); build.file("rtklib/src/rinex.c"); build.file("rtklib/src/ephemeris.c"); + build.file("rtklib/src/sbas.c"); + build.file("rtklib/src/pntpos.c"); build.file("rtklib/src/preceph.c"); + build.file("rtklib/src/ionex.c"); + } + + #[cfg(feature = "ppk")] + { + build.file("rtklib/src/postpos.c"); + build.file("rtklib/src/rtkpos.c"); build.file("rtklib/src/lambda.c"); build.file("rtklib/src/solution.c"); - build.file("rtklib/src/ionex.c"); - build.file("rtklib/src/sbas.c"); build.file("rtklib/src/options.c"); build.file("rtklib/src/ppp.c"); build.file("rtklib/src/ppp_ar.c"); build.file("rtklib/src/tides.c"); } - #[cfg(feature = "raw")] + // All receiver format files must be compiled together with rcvraw.c. + // rcvraw.c calls init, free, and input functions for every supported + // format unconditionally - there are no #ifdef guards - so all format + // .c files must be present in the same link unit whenever rcvraw.c is. + #[cfg(feature = "receivers")] { build.file("rtklib/src/rcvraw.c"); + build.include("rtklib/src"); + build.file("rtklib/src/rcv/binex.c"); + build.file("rtklib/src/rcv/crescent.c"); + build.file("rtklib/src/rcv/javad.c"); + build.file("rtklib/src/rcv/novatel.c"); + build.file("rtklib/src/rcv/nvs.c"); + build.file("rtklib/src/rcv/rt17.c"); + build.file("rtklib/src/rcv/septentrio.c"); + build.file("rtklib/src/rcv/skytraq.c"); + build.file("rtklib/src/rcv/swiftnav.c"); + build.file("rtklib/src/rcv/ublox.c"); + build.file("rtklib/src/rcv/unicore.c"); + // comnav.c and tersus.c use satwavelen() and lam_carr[] which were + // removed upstream; excluded until updated to use sat2freq(). + // build.file("rtklib/src/rcv/comnav.c"); + // build.file("rtklib/src/rcv/tersus.c"); } - // RTCM files are needed by both ppk and rtcm features. - // PPK uses them for SSR corrections. - #[cfg(any(feature = "ppk", feature = "rtcm"))] + // RTCM files are needed by ppk, rtcm, and conv. + // PPK uses them for SSR corrections; conv uses them for RTCM input. + #[cfg(any(feature = "ppk", feature = "rtcm", feature = "conv"))] { build.file("rtklib/src/rtcm.c"); build.file("rtklib/src/rtcm2.c"); @@ -93,6 +120,7 @@ fn main() { #[cfg(unix)] println!("cargo:rustc-link-lib=m"); + build.opt_level_str(&env::var("OPT_LEVEL").unwrap()); build.warnings(false); build.compile("rtklib"); diff --git a/rtklib-sys/rtklib b/rtklib-sys/rtklib index d574080..28ad77c 160000 --- a/rtklib-sys/rtklib +++ b/rtklib-sys/rtklib @@ -1 +1 @@ -Subproject commit d57408048eadf7d087f30e7ee11147a79439b33a +Subproject commit 28ad77c06ffc16b215858d6d5184d85333d44db9 diff --git a/src/conv.rs b/src/conv.rs new file mode 100644 index 0000000..ea328f7 --- /dev/null +++ b/src/conv.rs @@ -0,0 +1,460 @@ +//! RINEX file format conversion. +//! +//! ```no_run +//! use rtklib_ffi::conv::{RnxOpt, RnxOutputFiles, StreamFmt, convrnx}; +//! +//! let mut opt = RnxOpt::default(); +//! let ofiles = RnxOutputFiles::new("out.obs").with_nav("out.nav"); +//! convrnx(StreamFmt::Rinex, &mut opt, "input.rnx", &ofiles).unwrap(); +//! ``` + +use crate::{ + util::{copy_osstr, CStringArray}, + GpsTime, NavSys, +}; +use num_enum::TryFromPrimitive; +use rtklib_sys::rtklib as ffi; +use std::ffi::{OsStr, OsString}; +use thiserror::Error; + +/// Input stream format for [`convrnx`]. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum StreamFmt { + /// RTCM 2. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RTCM2"))] + Rtcm2 = ffi::STRFMT_RTCM2, + /// RTCM 3. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RTCM3"))] + Rtcm3 = ffi::STRFMT_RTCM3, + /// NovAtel OEM4/6/7. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_OEM4"))] + Oem4 = ffi::STRFMT_OEM4, + /// u-blox UBX. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_UBX"))] + Ubx = ffi::STRFMT_UBX, + /// Swift Navigation SBP. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_SBP"))] + Sbp = ffi::STRFMT_SBP, + /// Hemisphere Crescent. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_CRES"))] + Crescent = ffi::STRFMT_CRES, + /// SkyTraq. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_STQ"))] + SkyTraq = ffi::STRFMT_STQ, + /// Javad/Topcon GRIL/GREIS. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_JAVAD"))] + Javad = ffi::STRFMT_JAVAD, + /// NVS NVC08C. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_NVS"))] + Nvs = ffi::STRFMT_NVS, + /// BINEX. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_BINEX"))] + Binex = ffi::STRFMT_BINEX, + /// Trimble RT17. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RT17"))] + Rt17 = ffi::STRFMT_RT17, + /// Septentrio SBF. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_SEPT"))] + Sbf = ffi::STRFMT_SEPT, + /// Unicore. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_UNICORE"))] + Unicore = ffi::STRFMT_UNICORE, + /// RINEX observation or navigation file. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RINEX"))] + Rinex = ffi::STRFMT_RINEX, + /// SP3 precise ephemeris. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_SP3"))] + Sp3 = ffi::STRFMT_SP3, + /// RINEX clock file. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RNXCLK"))] + RinexClk = ffi::STRFMT_RNXCLK, + /// SBAS log. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_SBAS"))] + Sbas = ffi::STRFMT_SBAS, + /// NMEA 0183. + #[cfg_attr(feature = "strum", strum(to_string = "STRFMT_NMEA"))] + Nmea = ffi::STRFMT_NMEA, +} + +bitflags::bitflags! { + /// Observation types to include in the output RINEX. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ObsType: u32 { + /// Pseudorange. + const Pr = ffi::OBSTYPE_PR; + /// Carrier phase. + const Cp = ffi::OBSTYPE_CP; + /// Doppler frequency. + const Dop = ffi::OBSTYPE_DOP; + /// Signal-to-noise ratio. + const Snr = ffi::OBSTYPE_SNR; + /// All observation types. + const All = ffi::OBSTYPE_ALL; + } +} + +bitflags::bitflags! { + /// Frequency bands to include in the output RINEX. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct FreqType: u32 { + /// L1/G1/E1/B1. + const L1 = ffi::FREQTYPE_L1; + /// L2/G2/E5b/B2. + const L2 = ffi::FREQTYPE_L2; + /// L5/G3/E5a/B2a. + const L3 = ffi::FREQTYPE_L3; + /// L6/E6/B3. + const L4 = ffi::FREQTYPE_L4; + /// E5ab/B1C/B1A. + const L5 = ffi::FREQTYPE_L5; + /// B2ab. + const L6 = ffi::FREQTYPE_L6; + /// All frequency bands. + const All = ffi::FREQTYPE_ALL; + } +} + +/// RINEX format version. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(i32)] +pub enum RinexVersion { + /// RINEX 2.10. + V210 = 210, + /// RINEX 2.11. + V211 = 211, + /// RINEX 2.12. + V212 = 212, + /// RINEX 3.03. + V303 = 303, + /// RINEX 3.04. + V304 = 304, + /// RINEX 3.05. + V305 = 305, +} + +/// RINEX conversion options. +#[derive(Clone, Copy)] +pub struct RnxOpt(ffi::rnxopt_t); + +impl Default for RnxOpt { + /// Creates options defaulting to RINEX 3.03, all navigation systems, + /// all observation types, all frequency bands, and no time window. + fn default() -> Self { + let mut inner = unsafe { std::mem::zeroed::() }; + inner.rnxver = RinexVersion::V303 as i32; + inner.navsys = ffi::SYS_ALL as i32; + inner.obstype = ffi::OBSTYPE_ALL as i32; + inner.freqtype = ffi::FREQTYPE_ALL as i32; + Self(inner) + } +} + +impl RnxOpt { + /// Set the RINEX output version. + pub fn with_version(mut self, ver: RinexVersion) -> Self { + self.0.rnxver = ver as i32; + self + } + + /// The RINEX output version. Returns `None` if the stored value is not a + /// recognized version. + pub fn version(&self) -> Option { + RinexVersion::try_from(self.0.rnxver).ok() + } + + /// Set the navigation systems to include in the output. + pub fn with_navsys(mut self, sys: NavSys) -> Self { + self.0.navsys = sys.bits() as i32; + self + } + + /// The navigation systems included in the output. + pub fn navsys(&self) -> NavSys { + NavSys::from_bits_truncate(self.0.navsys as u32) + } + + /// Set the observation types to include in the output. + pub fn with_obstype(mut self, obs: ObsType) -> Self { + self.0.obstype = obs.bits() as i32; + self + } + + /// The observation types included in the output. + pub fn obstype(&self) -> ObsType { + ObsType::from_bits_truncate(self.0.obstype as u32) + } + + /// Set the frequency bands to include in the output. + pub fn with_freqtype(mut self, freq: FreqType) -> Self { + self.0.freqtype = freq.bits() as i32; + self + } + + /// The frequency bands included in the output. + pub fn freqtype(&self) -> FreqType { + FreqType::from_bits_truncate(self.0.freqtype as u32) + } + + /// Set the output sampling interval in seconds. Zero outputs all epochs. + pub fn with_interval(mut self, tint: f64) -> Self { + self.0.tint = tint; + self + } + + /// The output sampling interval in seconds. + pub fn interval(&self) -> f64 { + self.0.tint + } + + /// Set the time tolerance for matching epochs in seconds. + pub fn with_time_tolerance(mut self, ttol: f64) -> Self { + self.0.ttol = ttol; + self + } + + /// The time tolerance for matching epochs in seconds. + pub fn time_tolerance(&self) -> f64 { + self.0.ttol + } + + /// Set the session length for multi-file output in seconds. Zero produces + /// a single output file. + pub fn with_time_unit(mut self, tunit: f64) -> Self { + self.0.tunit = tunit; + self + } + + /// The session length for multi-file output in seconds. + pub fn time_unit(&self) -> f64 { + self.0.tunit + } + + /// Restrict conversion to a time window. Both times zero means no + /// restriction. + pub fn with_time_window(mut self, start: GpsTime, end: GpsTime) -> Self { + self.0.ts = start.0; + self.0.te = end.0; + self + } + + /// Include ionosphere correction parameters in the navigation output. + pub fn with_outiono(mut self, enable: bool) -> Self { + self.0.outiono = enable as i32; + self + } + + /// Whether ionosphere correction parameters are included in the output. + pub fn outiono(&self) -> bool { + self.0.outiono != 0 + } + + /// Include time system correction parameters in the navigation output. + pub fn with_outtime(mut self, enable: bool) -> Self { + self.0.outtime = enable as i32; + self + } + + /// Whether time system correction parameters are included in the output. + pub fn outtime(&self) -> bool { + self.0.outtime != 0 + } + + /// Include leap second parameters in the navigation output. + pub fn with_outleaps(mut self, enable: bool) -> Self { + self.0.outleaps = enable as i32; + self + } + + /// Whether leap second parameters are included in the output. + pub fn outleaps(&self) -> bool { + self.0.outleaps != 0 + } + + /// Detect the approximate receiver position automatically from the data. + pub fn with_autopos(mut self, enable: bool) -> Self { + self.0.autopos = enable as i32; + self + } + + /// Whether approximate receiver position is detected automatically. + pub fn autopos(&self) -> bool { + self.0.autopos != 0 + } + + /// Write separate navigation files for each constellation. + pub fn with_sep_nav(mut self, enable: bool) -> Self { + self.0.sep_nav = enable as i32; + self + } + + /// Whether navigation files are written separately per constellation. + pub fn sep_nav(&self) -> bool { + self.0.sep_nav != 0 + } + + /// Set the station ID written to the RINEX header. + pub fn with_station_id(mut self, id: impl AsRef) -> Self { + copy_osstr(&mut self.0.staid, id.as_ref()); + self + } + + /// Set the marker name written to the RINEX header. + pub fn with_marker_name(mut self, name: impl AsRef) -> Self { + copy_osstr(&mut self.0.marker, name.as_ref()); + self + } + + /// Set the marker number written to the RINEX header. + pub fn with_marker_number(mut self, number: impl AsRef) -> Self { + copy_osstr(&mut self.0.markerno, number.as_ref()); + self + } + + /// Set the marker type written to the RINEX header. + pub fn with_marker_type(mut self, t: impl AsRef) -> Self { + copy_osstr(&mut self.0.markertype, t.as_ref()); + self + } + + /// Set the observer name written to the RINEX header. + pub fn with_observer(mut self, observer: impl AsRef) -> Self { + copy_osstr(&mut self.0.name[0], observer.as_ref()); + self + } + + /// Set the agency name written to the RINEX header. + pub fn with_agency(mut self, agency: impl AsRef) -> Self { + copy_osstr(&mut self.0.name[1], agency.as_ref()); + self + } +} + +/// Output file paths for [`convrnx`]. An empty path disables the corresponding output. +#[repr(C)] +#[derive(Default)] +pub struct RnxOutputFiles { + obs: OsString, + nav: OsString, + gnav: OsString, + hnav: OsString, + qnav: OsString, + lnav: OsString, + cnav: OsString, + inav: OsString, + sbas: OsString, +} + +impl RnxOutputFiles { + /// Create output file paths with only the observation file set. All other outputs are + /// disabled. + pub fn new(obs: impl AsRef) -> Self { + Self { + obs: obs.as_ref().to_owned(), + ..Self::default() + } + } + + pub(crate) fn as_slice(&self) -> &[OsString] { + // SAFETY: #[repr(C)] with 9 contiguous same-type fields is layout-equivalent to + // [OsString; 9]. + unsafe { std::slice::from_raw_parts(self as *const Self as *const OsString, 9) } + } + + /// Set the GPS navigation output file. + pub fn with_nav(mut self, nav: impl AsRef) -> Self { + self.nav = nav.as_ref().to_owned(); + self + } + + /// Set the GLONASS navigation output file. + pub fn with_gnav(mut self, gnav: impl AsRef) -> Self { + self.gnav = gnav.as_ref().to_owned(); + self + } + + /// Set the GEO/SBAS navigation output file. + pub fn with_hnav(mut self, hnav: impl AsRef) -> Self { + self.hnav = hnav.as_ref().to_owned(); + self + } + + /// Set the QZSS navigation output file. + pub fn with_qnav(mut self, qnav: impl AsRef) -> Self { + self.qnav = qnav.as_ref().to_owned(); + self + } + + /// Set the LEX navigation output file. + pub fn with_lnav(mut self, lnav: impl AsRef) -> Self { + self.lnav = lnav.as_ref().to_owned(); + self + } + + /// Set the BeiDou navigation output file. + pub fn with_cnav(mut self, cnav: impl AsRef) -> Self { + self.cnav = cnav.as_ref().to_owned(); + self + } + + /// Set the IRNSS navigation output file. + pub fn with_inav(mut self, inav: impl AsRef) -> Self { + self.inav = inav.as_ref().to_owned(); + self + } + + /// Set the SBAS log output file. + pub fn with_sbas(mut self, sbas: impl AsRef) -> Self { + self.sbas = sbas.as_ref().to_owned(); + self + } +} + +/// Error from [`convrnx`]. +#[derive(Debug, Error)] +pub enum ConvrnxError { + /// A path contains a nul byte. + #[error("path contains a nul byte: {0:?}")] + NulByte(OsString), + /// The conversion failed. + #[error("conversion failed")] + Failed, + /// The conversion was aborted by the progress callback. + #[error("conversion aborted")] + Aborted, + /// RTKLIB returned an unrecognized status code. + #[error("unknown return code: {0}")] + Unknown(i32), +} + +/// Convert a raw receiver data file to RINEX. +/// +/// Returns `Ok(())` on success. Returns [`ConvrnxError::Failed`] if RTKLIB reports failure, or +/// [`ConvrnxError::Aborted`] if the conversion was interrupted. +pub fn convrnx( + format: StreamFmt, + opt: &mut RnxOpt, + file: impl AsRef, + ofile: &RnxOutputFiles, +) -> Result<(), ConvrnxError> { + let file_arr = CStringArray::try_single(file.as_ref()).map_err(|e| ConvrnxError::NulByte(e.to_owned()))?; + let mut ofile_arr = CStringArray::try_new(ofile.as_slice()).map_err(|e| ConvrnxError::NulByte(e.to_owned()))?; + + let ret = unsafe { + ffi::convrnx( + format as i32, + &mut opt.0, + file_arr.first(), + ofile_arr.as_mut_ptr() as *mut *mut i8, + ) + }; + + match ret { + 1 => Ok(()), + 0 => Err(ConvrnxError::Failed), + -1 => Err(ConvrnxError::Aborted), + _ => Err(ConvrnxError::Unknown(ret)), + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d177aa..140ac75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,14 +4,20 @@ //! //! Enable functionality via Cargo features: //! -//! - **`ppk`** — Post-processed kinematic positioning via [`postpos()`]. -//! - **`rtcm`** — RTCM3 message decoding via [`RtcmDecoder`]. -//! - **`conv`** — File format conversion. -//! - **`raw`** — Raw receiver data decoding. -//! - **`net`** — Network streaming. -//! - **`gis`** — GIS data support. -//! - **`tle`** — TLE satellite tracking. -//! - **`hifitime`** — Conversions between [`GpsTime`] and [`hifitime::Epoch`]. +//! - **`conv`**: File format conversion. Implies `receivers` - `convrnx` calls +//! `init_raw` at link time regardless of input format, which requires all +//! receiver format files to be present. +//! - **`gis`**: GIS data support. +//! - **`hifitime`**: Conversions between [`GpsTime`] and [`hifitime::Epoch`]. +//! - **`net`**: Network streaming. +//! - **`ppk`**: Post-processed kinematic positioning via [`postpos()`]. +//! - **`receivers`**: All supported hardware receiver decoders: BINEX, Hemisphere +//! Crescent, Javad/Topcon, NovAtel OEM, NVS, Septentrio SBF, SkyTraq, Swift +//! Navigation SBP, Trimble RT17, u-blox UBX, and Unicore. ComNav and Tersus are +//! not included; their source files use APIs removed in the current upstream. +//! - **`rtcm`**: RTCM3 message decoding via [`RtcmDecoder`]. +//! - **`strum`**: Adds [`std::fmt::Display`] support for enums via the optional [`strum`](https://docs.rs/strum) dependency. +//! - **`tle`**: TLE satellite tracking. #[cfg(feature = "hifitime")] use hifitime::Epoch; @@ -28,6 +34,26 @@ pub mod solution; #[cfg(feature = "ppk")] pub use solution::*; +pub mod meas; +pub use meas::*; + +mod util; + +/// Error returned when a decoder fails to initialize. +#[derive(Debug, thiserror::Error)] +#[error("failed to initialize decoder")] +pub struct DecoderInitError; + +#[cfg(feature = "receivers")] +pub mod receiver; +#[cfg(feature = "receivers")] +pub use receiver::*; + +#[cfg(feature = "conv")] +pub mod conv; +#[cfg(feature = "conv")] +pub use conv::*; + #[cfg(feature = "rtcm")] pub mod rtcm; #[cfg(feature = "rtcm")] @@ -177,6 +203,8 @@ impl From for Epoch { } /// Solution quality status. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[cfg_attr(feature = "strum", strum(serialize_all = "SCREAMING_SNAKE_CASE"))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum SolStatus { diff --git a/src/meas.rs b/src/meas.rs new file mode 100644 index 0000000..27519f9 --- /dev/null +++ b/src/meas.rs @@ -0,0 +1,284 @@ +//! GNSS measurement and navigation data types. +//! +//! These types wrap the FFI structs produced by format decoders such as +//! [`RtcmDecoder`](crate::rtcm::RtcmDecoder) and [`SbfDecoder`](crate::raw::SbfDecoder). + +use crate::GpsTime; +use num_enum::TryFromPrimitive; +use rtklib_sys::rtklib as ffi; +use std::{convert::TryFrom, slice::from_raw_parts}; + +/// A single GNSS observation record. +/// +/// Transparent wrapper around the FFI `obsd_t` struct. Produced by format decoders. +#[repr(transparent)] +pub struct ObsData(ffi::obsd_t); + +impl ObsData { + /// Satellite number in RTKLIB internal numbering. + pub fn sat(&self) -> u8 { + self.0.sat + } + + /// Carrier phase measurements for up to 3 frequencies, in cycles. + pub fn carrier_phase(&self) -> &[f64; 3] { + &self.0.L + } + + /// Pseudorange measurements for up to 3 frequencies, in meters. + pub fn pseudorange(&self) -> &[f64; 3] { + &self.0.P + } + + /// Doppler measurements for up to 3 frequencies, in Hz. + pub fn doppler(&self) -> &[f32; 3] { + &self.0.D + } + + /// Signal-to-noise ratio for up to 3 frequencies, in dB-Hz. + pub fn snr(&self) -> &[f32; 3] { + &self.0.SNR + } + + /// Signal code identifiers for up to 3 frequencies. + pub fn code(&self) -> &[u8; 3] { + &self.0.code + } + + /// Loss-of-lock indicators for up to 3 frequencies. + pub fn lli(&self) -> &[u8; 3] { + &self.0.LLI + } +} + +/// GPS/Galileo/BeiDou/QZSS broadcast ephemeris record. +/// +/// Transparent wrapper around the FFI `eph_t` struct. +#[repr(transparent)] +pub struct Eph(ffi::eph_t); + +impl Eph { + /// Satellite number in RTKLIB internal numbering. + pub fn sat(&self) -> i32 { self.0.sat } + /// Issue of data, ephemeris. + pub fn iode(&self) -> i32 { self.0.iode } + /// Issue of data, clock. + pub fn iodc(&self) -> i32 { self.0.iodc } + /// SV accuracy index (URA). + pub fn sva(&self) -> i32 { self.0.sva } + /// Raw SV health field. Use [`is_healthy`](Self::is_healthy) for a simple check. + pub fn svh(&self) -> i32 { self.0.svh } + /// Returns true if the SV health field indicates no issues. + pub fn is_healthy(&self) -> bool { self.0.svh == 0 } + /// GPS/QZS week number; Galileo week number for GAL. + pub fn week(&self) -> i32 { self.0.week } + /// Signal codes: GPS/QZS L2 code type; GAL/BDS data source bitmask. + pub fn code(&self) -> i32 { self.0.code } + /// Flags: GPS/QZS L2 P data flag; BDS nav message type. + pub fn flag(&self) -> i32 { self.0.flag } + /// Time of ephemeris. + pub fn toe(&self) -> GpsTime { GpsTime(self.0.toe) } + /// Time of clock. + pub fn toc(&self) -> GpsTime { GpsTime(self.0.toc) } + /// Signal transmission time. + pub fn ttr(&self) -> GpsTime { GpsTime(self.0.ttr) } + /// Semi-major axis, in meters. + pub fn semi_major_axis(&self) -> f64 { self.0.A } + /// Eccentricity. + pub fn eccentricity(&self) -> f64 { self.0.e } + /// Inclination at reference time, in radians. + pub fn inclination(&self) -> f64 { self.0.i0 } + /// Longitude of ascending node at weekly epoch, in radians. + pub fn right_ascension(&self) -> f64 { self.0.OMG0 } + /// Argument of perigee, in radians. + pub fn argument_of_perigee(&self) -> f64 { self.0.omg } + /// Mean anomaly at reference time, in radians. + pub fn mean_anomaly(&self) -> f64 { self.0.M0 } + /// Mean motion difference from computed value, in rad/s. + pub fn mean_motion_diff(&self) -> f64 { self.0.deln } + /// Rate of right ascension, in rad/s. + pub fn right_ascension_rate(&self) -> f64 { self.0.OMGd } + /// Rate of inclination, in rad/s. + pub fn inclination_rate(&self) -> f64 { self.0.idot } + /// Orbit radius harmonic sine correction, in meters. + pub fn crs(&self) -> f64 { self.0.crs } + /// Orbit radius harmonic cosine correction, in meters. + pub fn crc(&self) -> f64 { self.0.crc } + /// Argument of latitude harmonic sine correction, in radians. + pub fn cus(&self) -> f64 { self.0.cus } + /// Argument of latitude harmonic cosine correction, in radians. + pub fn cuc(&self) -> f64 { self.0.cuc } + /// Inclination harmonic sine correction, in radians. + pub fn cis(&self) -> f64 { self.0.cis } + /// Inclination harmonic cosine correction, in radians. + pub fn cic(&self) -> f64 { self.0.cic } + /// Time of ephemeris within the week, in seconds. + pub fn toes(&self) -> f64 { self.0.toes } + /// Fit interval, in hours. + pub fn fit_interval(&self) -> f64 { self.0.fit } + /// Clock bias, in seconds. + pub fn clock_bias(&self) -> f64 { self.0.f0 } + /// Clock drift, in s/s. + pub fn clock_drift(&self) -> f64 { self.0.f1 } + /// Clock drift rate, in s/s². + pub fn clock_drift_rate(&self) -> f64 { self.0.f2 } + /// Group delay parameters, in seconds. + /// GPS/QZS index 0 is TGD. GAL indices 0-1 are BGD_E1E5a and BGD_E1E5b. + /// BDS indices 0-5 are TGD_B1I, TGD_B2I, TGD_B1Cp, TGD_B2ap, ISC_B1Cd, ISC_B2ad. + pub fn group_delay(&self) -> &[f64; 6] { &self.0.tgd } + /// Semi-major axis rate of change for CNAV, in m/s. + pub fn semi_major_axis_rate(&self) -> f64 { self.0.Adot } + /// Mean motion rate for CNAV, in rad/s². + pub fn mean_motion_rate(&self) -> f64 { self.0.ndot } +} + +/// GLONASS satellite generation, from the M field of the status flags. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u8)] +pub enum GloSatelliteType { + Glonass = 0, + GlonassM = 1, + GlonassK1 = 3, +} + +/// GLONASS broadcast ephemeris record. +/// +/// Transparent wrapper around the FFI `geph_t` struct. +#[repr(transparent)] +pub struct GloEph(ffi::geph_t); + +impl GloEph { + /// Satellite number in RTKLIB internal numbering. + pub fn sat(&self) -> i32 { self.0.sat } + /// Issue of data. + pub fn iode(&self) -> i32 { self.0.iode } + /// Frequency channel number. + pub fn frequency_number(&self) -> i32 { self.0.frq } + /// Raw SV health field. Use [`is_healthy`](Self::is_healthy) for a simple check. + pub fn svh(&self) -> i32 { self.0.svh } + /// Returns true if the SV health field indicates no issues. + pub fn is_healthy(&self) -> bool { self.0.svh == 0 } + /// Raw status flags field. + pub fn raw_flags(&self) -> i32 { self.0.flags } + /// String type identifier. Flags P field in bits 0-1. + pub fn p_string_type(&self) -> u8 { (self.0.flags & 0x3) as u8 } + /// Time interval between adjacent tb values, in minutes. Flags P1 field in bits 2-3. + pub fn p1_interval_minutes(&self) -> u8 { + match (self.0.flags >> 2) & 0x3 { + 0 => 0, + 1 => 30, + 2 => 45, + _ => 60, + } + } + /// Odd/even flag. Flags P2 field in bit 4. + pub fn p2_odd(&self) -> bool { (self.0.flags >> 4) & 1 != 0 } + /// True if the current frame contains 5 almanac satellites. Flags P3 field in bit 5. + pub fn p3_flag(&self) -> bool { (self.0.flags >> 5) & 1 != 0 } + /// True if ephemeris has been updated. Flags P4 field in bit 6. + pub fn p4_updated(&self) -> bool { (self.0.flags >> 6) & 1 != 0 } + /// Satellite generation. Flags M field in bits 7-8. + pub fn satellite_type(&self) -> Option { + GloSatelliteType::try_from((self.0.flags >> 7) as u8 & 0x3).ok() + } + /// SV accuracy index. + pub fn sva(&self) -> i32 { self.0.sva } + /// Age of operation, in days. + pub fn age(&self) -> i32 { self.0.age } + /// Epoch of ephemeris. + pub fn toe(&self) -> GpsTime { GpsTime(self.0.toe) } + /// Message frame time. + pub fn tof(&self) -> GpsTime { GpsTime(self.0.tof) } + /// Satellite position in ECEF, in meters. + pub fn position(&self) -> &[f64; 3] { &self.0.pos } + /// Satellite velocity in ECEF, in m/s. + pub fn velocity(&self) -> &[f64; 3] { &self.0.vel } + /// Satellite acceleration in ECEF, in m/s². + pub fn acceleration(&self) -> &[f64; 3] { &self.0.acc } + /// SV clock bias, in seconds. + pub fn clock_bias(&self) -> f64 { self.0.taun } + /// Relative frequency bias. + pub fn freq_bias(&self) -> f64 { self.0.gamn } + /// L1/L2 inter-frequency delay, in seconds. + pub fn l1_l2_delay(&self) -> f64 { self.0.dtaun } +} + +/// SBAS satellite ephemeris record. +/// +/// Transparent wrapper around the FFI `seph_t` struct. +#[repr(transparent)] +pub struct SbasEph(ffi::seph_t); + +impl SbasEph { + /// Satellite number in RTKLIB internal numbering. + pub fn sat(&self) -> i32 { self.0.sat } + /// Reference epoch time. + pub fn t0(&self) -> GpsTime { GpsTime(self.0.t0) } + /// Message frame time. + pub fn tof(&self) -> GpsTime { GpsTime(self.0.tof) } + /// SV accuracy index (URA). + pub fn sva(&self) -> i32 { self.0.sva } + /// Raw SV health field. Use [`is_healthy`](Self::is_healthy) for a simple check. + pub fn svh(&self) -> i32 { self.0.svh } + /// Returns true if the SV health field indicates no issues. + pub fn is_healthy(&self) -> bool { self.0.svh == 0 } + /// Satellite position in ECEF, in meters. + pub fn position(&self) -> &[f64; 3] { &self.0.pos } + /// Satellite velocity in ECEF, in m/s. + pub fn velocity(&self) -> &[f64; 3] { &self.0.vel } + /// Satellite acceleration in ECEF, in m/s². + pub fn acceleration(&self) -> &[f64; 3] { &self.0.acc } + /// Clock offset, in seconds. + pub fn clock_offset(&self) -> f64 { self.0.af0 } + /// Clock drift, in s/s. + pub fn clock_drift(&self) -> f64 { self.0.af1 } +} + +/// Navigation data store. +/// +/// Wraps the FFI `nav_t` struct. Holds ephemeris and correction tables +/// populated by format decoders. +#[repr(transparent)] +pub struct Nav(ffi::nav_t); + +impl Nav { + /// GPS/Galileo/BeiDou/QZSS ephemeris records. + pub fn eph(&self) -> &[Eph] { + let n = self.0.n as usize; + if n == 0 || self.0.eph.is_null() { return &[]; } + unsafe { from_raw_parts(self.0.eph as *const Eph, n) } + } + + /// GLONASS ephemeris records. + pub fn glo_eph(&self) -> &[GloEph] { + let n = self.0.ng as usize; + if n == 0 || self.0.geph.is_null() { return &[]; } + unsafe { from_raw_parts(self.0.geph as *const GloEph, n) } + } + + /// SBAS ephemeris records. + pub fn sbas_eph(&self) -> &[SbasEph] { + let n = self.0.ns as usize; + if n == 0 || self.0.seph.is_null() { return &[]; } + unsafe { from_raw_parts(self.0.seph as *const SbasEph, n) } + } + + /// GPS ionospheric correction parameters, 8 values. + pub fn ion_gps(&self) -> &[f64; 8] { &self.0.ion_gps } + + /// Galileo ionospheric correction parameters, 4 values. + pub fn ion_gal(&self) -> &[f64; 4] { &self.0.ion_gal } + + /// QZSS ionospheric correction parameters, 8 values. + pub fn ion_qzs(&self) -> &[f64; 8] { &self.0.ion_qzs } + + /// BeiDou ionospheric correction parameters, 8 values. + pub fn ion_cmp(&self) -> &[f64; 8] { &self.0.ion_cmp } + + /// NavIC ionospheric correction parameters, 8 values. + pub fn ion_irn(&self) -> &[f64; 8] { &self.0.ion_irn } + + /// GLONASS frequency channel assignments, indexed by slot number. + pub fn glo_fcn(&self) -> &[i32; 32] { &self.0.glo_fcn } +} diff --git a/src/ppk.rs b/src/ppk.rs index 3bf1ffc..8ea74d8 100644 --- a/src/ppk.rs +++ b/src/ppk.rs @@ -14,99 +14,164 @@ //! ).unwrap(); //! ``` -use crate::NavSys; +use crate::{util::CStringArray, NavSys}; use num_enum::TryFromPrimitive; use rtklib_sys::rtklib as ffi; -use std::ffi::CString; +use std::ffi::{CString, OsStr, OsString}; /// Positioning mode. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum PosMode { /// Single point positioning. From PMODE_SINGLE. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_SINGLE"))] Single = ffi::PMODE_SINGLE, /// Differential GPS / DGNSS. From PMODE_DGPS. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_DGPS"))] Dgps = ffi::PMODE_DGPS, /// Kinematic positioning. From PMODE_KINEMA. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_KINEMA"))] Kinematic = ffi::PMODE_KINEMA, /// Static positioning. From PMODE_STATIC. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_STATIC"))] Static = ffi::PMODE_STATIC, /// Static positioning starting from a known position. From PMODE_STATIC_START. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_STATIC_START"))] StaticStart = ffi::PMODE_STATIC_START, /// Moving base station. From PMODE_MOVEB. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_MOVEB"))] MovingBase = ffi::PMODE_MOVEB, /// Fixed position. From PMODE_FIXED. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_FIXED"))] Fixed = ffi::PMODE_FIXED, /// Precise Point Positioning, kinematic. From PMODE_PPP_KINEMA. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_PPP_KINEMA"))] PppKinematic = ffi::PMODE_PPP_KINEMA, /// Precise Point Positioning, static. From PMODE_PPP_STATIC. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_PPP_STATIC"))] PppStatic = ffi::PMODE_PPP_STATIC, /// Precise Point Positioning, fixed. From PMODE_PPP_FIXED. + #[cfg_attr(feature = "strum", strum(to_string = "PMODE_PPP_FIXED"))] PppFixed = ffi::PMODE_PPP_FIXED, } +/// Time format for solution output. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum TimeFormat { + /// GPS seconds of week: `sssss.s`. + GpsSeconds = 0, + /// Calendar date and time: `yyyy/mm/dd hh:mm:ss.s`. + Calendar = 1, +} + /// Solution output format. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum SolFormat { /// Latitude, longitude, and height. From SOLF_LLH. + #[cfg_attr(feature = "strum", strum(to_string = "SOLF_LLH"))] Llh = ffi::SOLF_LLH, /// X, Y, Z in ECEF coordinates. From SOLF_XYZ. + #[cfg_attr(feature = "strum", strum(to_string = "SOLF_XYZ"))] Xyz = ffi::SOLF_XYZ, /// East, north, up baseline components. From SOLF_ENU. + #[cfg_attr(feature = "strum", strum(to_string = "SOLF_ENU"))] Enu = ffi::SOLF_ENU, /// NMEA-0183 sentences. From SOLF_NMEA. + #[cfg_attr(feature = "strum", strum(to_string = "SOLF_NMEA"))] Nmea = ffi::SOLF_NMEA, } /// Ionosphere correction option. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum IonoOpt { /// Ionosphere correction disabled. From IONOOPT_OFF. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_OFF"))] Off = ffi::IONOOPT_OFF, /// Klobuchar broadcast model. From IONOOPT_BRDC. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_BRDC"))] Broadcast = ffi::IONOOPT_BRDC, /// SBAS ionosphere model. From IONOOPT_SBAS. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_SBAS"))] Sbas = ffi::IONOOPT_SBAS, /// Iono-free linear combination of L1/L2 or L1/L5. From IONOOPT_IFLC. - IonFreeLC = ffi::IONOOPT_IFLC, + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_IFLC"))] + IonFreeLc = ffi::IONOOPT_IFLC, /// Ionosphere delay estimation. From IONOOPT_EST. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_EST"))] Estimation = ffi::IONOOPT_EST, /// IONEX TEC grid model. From IONOOPT_TEC. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_TEC"))] Tec = ffi::IONOOPT_TEC, /// QZSS broadcast ionosphere model. From IONOOPT_QZS. + #[cfg_attr(feature = "strum", strum(to_string = "IONOOPT_QZS"))] Qzs = ffi::IONOOPT_QZS, } /// Troposphere correction option. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum TropOpt { /// Troposphere correction disabled. From TROPOPT_OFF. + #[cfg_attr(feature = "strum", strum(to_string = "TROPOPT_OFF"))] Off = ffi::TROPOPT_OFF, /// Saastamoinen model. From TROPOPT_SAAS. + #[cfg_attr(feature = "strum", strum(to_string = "TROPOPT_SAAS"))] Saastamoinen = ffi::TROPOPT_SAAS, /// SBAS troposphere model. From TROPOPT_SBAS. + #[cfg_attr(feature = "strum", strum(to_string = "TROPOPT_SBAS"))] Sbas = ffi::TROPOPT_SBAS, /// Zenith total delay estimation. From TROPOPT_EST. + #[cfg_attr(feature = "strum", strum(to_string = "TROPOPT_EST"))] Estimation = ffi::TROPOPT_EST, /// Zenith total delay plus horizontal gradient estimation. From TROPOPT_ESTG. + #[cfg_attr(feature = "strum", strum(to_string = "TROPOPT_ESTG"))] EstimationGrad = ffi::TROPOPT_ESTG, } /// Ambiguity resolution mode. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u32)] pub enum ArMode { /// Ambiguity resolution disabled. - Off = 0, + #[cfg_attr(feature = "strum", strum(to_string = "ARMODE_OFF"))] + Off = ffi::ARMODE_OFF, /// Continuous ambiguity resolution. - Continuous = 1, + #[cfg_attr(feature = "strum", strum(to_string = "ARMODE_CONT"))] + Continuous = ffi::ARMODE_CONT, /// Instantaneous ambiguity resolution. - Instantaneous = 2, + #[cfg_attr(feature = "strum", strum(to_string = "ARMODE_INST"))] + Instantaneous = ffi::ARMODE_INST, /// Fix-and-hold ambiguity resolution. - FixAndHold = 3, + #[cfg_attr(feature = "strum", strum(to_string = "ARMODE_FIXHOLD"))] + FixAndHold = ffi::ARMODE_FIXHOLD, +} + +/// Filter solution type. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum SolutionType { + /// Forward filter only. From SOLTYPE_FORWARD. + #[cfg_attr(feature = "strum", strum(to_string = "SOLTYPE_FORWARD"))] + Forward = ffi::SOLTYPE_FORWARD, + /// Backward filter only. From SOLTYPE_BACKWARD. + #[cfg_attr(feature = "strum", strum(to_string = "SOLTYPE_BACKWARD"))] + Backward = ffi::SOLTYPE_BACKWARD, + /// Combined forward+backward. From SOLTYPE_COMBINED. + #[cfg_attr(feature = "strum", strum(to_string = "SOLTYPE_COMBINED"))] + Combined = ffi::SOLTYPE_COMBINED, + /// Combined forward+backward without phase reset. From SOLTYPE_COMBINED_NORESET. + #[cfg_attr(feature = "strum", strum(to_string = "SOLTYPE_COMBINED_NORESET"))] + CombinedNoReset = ffi::SOLTYPE_COMBINED_NORESET, } /// Processing options wrapper around `prcopt_t`. @@ -123,7 +188,9 @@ impl PrcOpt { pub fn kinematic() -> Self { let mut opt = Self::default(); opt.0.mode = PosMode::Kinematic as i32; - opt.0.soltype = 2; + opt.0.soltype = SolutionType::Combined as i32; + opt.0.modear = ArMode::Continuous as i32; + opt.0.nf = 2; opt } @@ -131,12 +198,14 @@ impl PrcOpt { pub fn static_mode() -> Self { let mut opt = Self::default(); opt.0.mode = PosMode::Static as i32; - opt.0.soltype = 2; + opt.0.soltype = SolutionType::Combined as i32; + opt.0.modear = ArMode::Continuous as i32; + opt.0.nf = 2; opt } - /// Set positioning mode. - pub fn set_mode(&mut self, mode: PosMode) -> &mut Self { + /// Positioning mode. + pub fn with_mode(mut self, mode: PosMode) -> Self { self.0.mode = mode as i32; self } @@ -147,8 +216,20 @@ impl PrcOpt { PosMode::try_from(self.0.mode as u32).unwrap() } - /// Set enabled navigation systems. - pub fn set_navsys(&mut self, sys: NavSys) -> &mut Self { + /// Solution type. + pub fn with_solution_type(mut self, sol: SolutionType) -> Self { + self.0.soltype = sol as i32; + self + } + + /// Get solution type. + pub fn solution_type(&self) -> SolutionType { + // Only set via typed setters or RTKLIB defaults; an invalid value is an unreachable bug. + SolutionType::try_from(self.0.soltype as u32).unwrap() + } + + /// Enabled navigation systems. + pub fn with_navsys(mut self, sys: NavSys) -> Self { self.0.navsys = sys.bits() as i32; self } @@ -158,8 +239,8 @@ impl PrcOpt { NavSys::from_bits_truncate(self.0.navsys as u32) } - /// Set number of frequencies. 1=L1, 2=L1+L2, 3=L1+L2+L5. - pub fn set_frequencies(&mut self, nf: i32) -> &mut Self { + /// Number of frequencies. 1=L1, 2=L1+L2, 3=L1+L2+L5. + pub fn with_frequencies(mut self, nf: i32) -> Self { self.0.nf = nf; self } @@ -169,8 +250,8 @@ impl PrcOpt { self.0.nf } - /// Set elevation mask angle in degrees. - pub fn set_elevation_mask(&mut self, deg: f64) -> &mut Self { + /// Elevation mask angle in degrees. + pub fn with_elevation_mask(mut self, deg: f64) -> Self { self.0.elmin = deg.to_radians(); self } @@ -180,8 +261,8 @@ impl PrcOpt { self.0.elmin } - /// Set ambiguity resolution mode. - pub fn set_ar_mode(&mut self, mode: ArMode) -> &mut Self { + /// Ambiguity resolution mode. + pub fn with_ar_mode(mut self, mode: ArMode) -> Self { self.0.modear = mode as i32; self } @@ -192,8 +273,8 @@ impl PrcOpt { ArMode::try_from(self.0.modear as u32).unwrap() } - /// Set ionosphere correction option. - pub fn set_ionosphere(&mut self, opt: IonoOpt) -> &mut Self { + /// Ionosphere correction option. + pub fn with_ionosphere(mut self, opt: IonoOpt) -> Self { self.0.ionoopt = opt as i32; self } @@ -204,20 +285,20 @@ impl PrcOpt { IonoOpt::try_from(self.0.ionoopt as u32).unwrap() } - /// Set base station position in ECEF coordinates (meters). + /// Base station position in ECEF coordinates (meters). /// /// Equivalent to the `-r` flag in `rnx2rtkp`. - pub fn set_base_position_ecef(&mut self, x: f64, y: f64, z: f64) -> &mut Self { + pub fn with_base_position_ecef(mut self, x: f64, y: f64, z: f64) -> Self { self.0.refpos = ffi::POSOPT_POS_XYZ as i32; self.0.rb = [x, y, z]; self } - /// Set base station position in geodetic coordinates + /// Base station position in geodetic coordinates /// (latitude and longitude in degrees, height in meters). /// /// Equivalent to the `-l` flag in `rnx2rtkp`. - pub fn set_base_position_llh(&mut self, lat_deg: f64, lon_deg: f64, height: f64) -> &mut Self { + pub fn with_base_position_llh(mut self, lat_deg: f64, lon_deg: f64, height: f64) -> Self { self.0.refpos = ffi::POSOPT_POS_LLH as i32; let pos = [lat_deg.to_radians(), lon_deg.to_radians(), height]; unsafe { ffi::pos2ecef(pos.as_ptr(), self.0.rb.as_mut_ptr()) }; @@ -229,8 +310,8 @@ impl PrcOpt { self.0.rb } - /// Set troposphere correction option. - pub fn set_troposphere(&mut self, opt: TropOpt) -> &mut Self { + /// Troposphere correction option. + pub fn with_troposphere(mut self, opt: TropOpt) -> Self { self.0.tropopt = opt as i32; self } @@ -256,8 +337,8 @@ impl Default for SolOpt { } impl SolOpt { - /// Set solution output format. - pub fn set_format(&mut self, format: SolFormat) -> &mut Self { + /// Solution output format. + pub fn with_format(mut self, format: SolFormat) -> Self { self.0.posf = format as i32; self } @@ -268,19 +349,19 @@ impl SolOpt { SolFormat::try_from(self.0.posf as u32).unwrap() } - /// Set time format. 0=sssss.s, 1=yyyy/mm/dd hh:mm:ss.s. - pub fn set_time_format(&mut self, timef: i32) -> &mut Self { - self.0.timef = timef; + /// Time format. + pub fn with_time_format(mut self, timef: TimeFormat) -> Self { + self.0.timef = timef as i32; self } /// Get time format. - pub fn time_format(&self) -> i32 { - self.0.timef + pub fn time_format(&self) -> TimeFormat { + TimeFormat::try_from(self.0.timef as u32).unwrap() } - /// Set number of decimal places for time output. - pub fn set_time_decimals(&mut self, timeu: i32) -> &mut Self { + /// Number of decimal places for time output. + pub fn with_time_decimals(mut self, timeu: i32) -> Self { self.0.timeu = timeu; self } @@ -290,8 +371,8 @@ impl SolOpt { self.0.timeu } - /// Enable or disable output header. - pub fn set_output_header(&mut self, enable: bool) -> &mut Self { + /// Output header. + pub fn with_output_header(mut self, enable: bool) -> Self { self.0.outhead = enable as i32; self } @@ -325,8 +406,8 @@ impl FilOpt { #[derive(Debug, thiserror::Error)] pub enum PostposError { /// A file path contained an interior null byte. - #[error("path contains null byte: {0}")] - NulByte(String), + #[error("path contains null byte: {0:?}")] + NulByte(OsString), /// Too many input files for RTKLIB's fixed-size array. #[error("{count} > {max} input files")] TooManyInputFiles { count: usize, max: usize }, @@ -335,14 +416,20 @@ pub enum PostposError { ProcessingFailed(i32), } +impl From<&OsStr> for PostposError { + fn from(s: &OsStr) -> Self { + Self::NulByte(s.to_owned()) + } +} + /// Run PPK post-processing on RINEX observation and navigation files. /// /// Returns `Ok(())` on success. Results are written to the output file. -pub fn postpos( - rover_obs: &str, - base_obs: &str, - nav_files: &[&str], - output: &str, +pub fn postpos>( + rover_obs: impl AsRef, + base_obs: impl AsRef, + nav_files: &[T], + output: impl AsRef, popt: &PrcOpt, sopt: &SolOpt, fopt: &FilOpt, @@ -357,26 +444,20 @@ pub fn postpos( }); } - let mut cstrings = Vec::with_capacity(total); - let mut paths = vec![rover_obs, base_obs]; - paths.extend_from_slice(nav_files); - - for p in &paths { - cstrings.push(CString::new(*p).map_err(|_| PostposError::NulByte(p.to_string()))?); - } + let mut all_inputs: Vec<&OsStr> = vec![rover_obs.as_ref(), base_obs.as_ref()]; + all_inputs.extend(nav_files.iter().map(|f| f.as_ref())); // C signature is `const char **infile` — the strings are const but the // pointer to the array is not, so bindgen generates `*mut *const c_char`. // The function does not actually mutate the array. - let mut ptrs: Vec<*const i8> = cstrings.iter().map(|s| s.as_ptr()).collect(); - let outfile = CString::new(output).map_err(|_| PostposError::NulByte(output.to_string()))?; + let mut infile_arr = CStringArray::try_new(&all_inputs)?; + let out_arr = CStringArray::try_new(&[output.as_ref()])?; + let rov = CString::new("").unwrap(); + let base = CString::new("").unwrap(); let ts = ffi::gtime_t { time: 0, sec: 0.0 }; let te = ffi::gtime_t { time: 0, sec: 0.0 }; - let rov = CString::new("").unwrap(); - let base = CString::new("").unwrap(); - let ret = unsafe { ffi::postpos( ts, @@ -386,9 +467,9 @@ pub fn postpos( popt.as_ffi(), sopt.as_ffi(), fopt.as_ffi(), - ptrs.as_mut_ptr(), - ptrs.len() as i32, - outfile.as_ptr(), + infile_arr.as_mut_ptr(), + infile_arr.len() as i32, + out_arr.first(), rov.as_ptr(), base.as_ptr(), ) diff --git a/src/receiver/mod.rs b/src/receiver/mod.rs new file mode 100644 index 0000000..aedbeb6 --- /dev/null +++ b/src/receiver/mod.rs @@ -0,0 +1,88 @@ +//! Hardware receiver data decoding. + +use crate::{ + meas::{Nav, ObsData}, + DecoderInitError, +}; +use num_enum::TryFromPrimitive; +use rtklib_sys::rtklib as ffi; +use std::{ + alloc::{alloc_zeroed, handle_alloc_error, Layout}, + slice::from_raw_parts, +}; + +pub mod septentrio; +pub use septentrio::*; + +/// Shared raw receiver state wrapping the RTKLIB `raw_t` struct. +pub struct RawReceiver(pub(in crate::receiver) Box); + +impl RawReceiver { + /// Allocate and initialize a `raw_t` with the given format code. + pub(crate) fn init(format: i32) -> Result { + unsafe { + // raw_t is ~673KB, too large for the stack. + let layout = Layout::new::(); + let ptr = alloc_zeroed(layout) as *mut ffi::raw_t; + if ptr.is_null() { + handle_alloc_error(layout); + } + let mut raw = Box::from_raw(ptr); + // init_raw sets raw->format and allocates the obs/nav buffers. + if ffi::init_raw(raw.as_mut(), format) == 0 { + return Err(DecoderInitError); + } + Ok(Self(raw)) + } + } + + /// Number of observation records in the current message. + pub fn observation_count(&self) -> usize { + self.0.obs.n as usize + } + + /// Observation data from the last decoded observation message. + pub fn observations(&self) -> &[ObsData] { + let n = self.0.obs.n as usize; + if n == 0 || self.0.obs.data.is_null() { + return &[]; + } + unsafe { from_raw_parts(self.0.obs.data as *const ObsData, n) } + } + + /// Navigation data updated as ephemeris messages arrive. + pub fn nav(&self) -> &Nav { + unsafe { &*(&self.0.nav as *const ffi::nav_t as *const Nav) } + } + + /// Satellite number of the most recently decoded ephemeris. + pub fn ephemeris_sat(&self) -> i32 { + self.0.ephsat + } +} + +impl Drop for RawReceiver { + fn drop(&mut self) { + // free_raw frees the obs/nav buffers and calls the format-specific free. + unsafe { ffi::free_raw(self.0.as_mut()); } + } +} + +/// Outcome of feeding a byte into a receiver decoder. +#[cfg_attr(feature = "strum", derive(strum::Display))] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(i32)] +pub enum DecodeStatus { + /// Incomplete frame; feed more bytes. + Incomplete = 0, + /// Observation data decoded. + Observation = 1, + /// Ephemeris decoded. + Ephemeris = 2, + /// SBAS corrections decoded. + SbasCorrections = 3, + /// Station position/antenna parameters decoded. + StationInfo = 5, + /// Ionosphere/UTC parameters decoded. + IonOrUtc = 9, +} diff --git a/src/receiver/septentrio.rs b/src/receiver/septentrio.rs new file mode 100644 index 0000000..5fd6cf1 --- /dev/null +++ b/src/receiver/septentrio.rs @@ -0,0 +1,57 @@ +//! Septentrio SBF receiver decoder. +//! +//! ```no_run +//! use rtklib_ffi::receiver::{DecodeStatus, SbfDecoder}; +//! +//! let mut decoder = SbfDecoder::try_new().unwrap(); +//! # let sbf_bytes: Vec = vec![]; +//! +//! for &byte in &sbf_bytes { +//! let Some(status) = decoder.decode(byte) else { continue; }; +//! match status { +//! DecodeStatus::Observation => { +//! let obs = decoder.observations(); +//! // process observations... +//! } +//! DecodeStatus::Ephemeris => { +//! let sat = decoder.ephemeris_sat(); +//! // handle ephemeris update for satellite sat... +//! } +//! _ => {} +//! } +//! } +//! ``` + +use super::{DecodeStatus, RawReceiver}; +use crate::DecoderInitError; +use rtklib_sys::rtklib as ffi; +use std::{convert::TryFrom, ops::Deref}; + +/// Septentrio SBF receiver data decoder. +pub struct SbfDecoder(RawReceiver); + +impl SbfDecoder { + /// Create a new SBF decoder. + /// + /// Returns `Err` if RTKLIB cannot allocate internal buffers. + pub fn try_new() -> Result { + // init_raw sets raw->format = STRFMT_SEPT and allocates the obs/nav + // buffers. init_sbf checks raw->format and returns 0 if it is not + // set, so init_sbf alone cannot be used here. + RawReceiver::init(ffi::STRFMT_SEPT as i32).map(Self) + } + + /// Feed one byte into the SBF decoder. + /// + /// Returns `None` if the byte did not complete a recognized message. + pub fn decode(&mut self, byte: u8) -> Option { + let ret = unsafe { ffi::input_sbf(self.0.0.as_mut(), byte) }; + DecodeStatus::try_from(ret).ok() + } +} + +impl Deref for SbfDecoder { + type Target = RawReceiver; + + fn deref(&self) -> &RawReceiver { &self.0 } +} diff --git a/src/rtcm.rs b/src/rtcm.rs index db4738d..3cf4317 100644 --- a/src/rtcm.rs +++ b/src/rtcm.rs @@ -3,16 +3,17 @@ //! ```no_run //! use rtklib_ffi::rtcm::{DecodeResult, MsgType, RtcmDecoder}; //! -//! let mut decoder = RtcmDecoder::new().unwrap(); +//! let mut decoder = RtcmDecoder::try_new().unwrap(); //! # let rtcm_bytes: Vec = vec![]; //! //! for &byte in &rtcm_bytes { -//! match decoder.decode(byte) { -//! Ok(DecodeResult::Observation) => { +//! let Some(status) = decoder.decode(byte) else { continue; }; +//! match status { +//! DecodeResult::Observation => { //! let obs = decoder.observations(); //! // process observations... //! } -//! Ok(DecodeResult::Ephemeris) => { +//! DecodeResult::Ephemeris => { //! let msg_type = decoder.message_type().unwrap(); //! // handle ephemeris... //! } @@ -21,30 +22,18 @@ //! } //! ``` +use crate::{meas::ObsData, DecoderInitError}; use num_enum::TryFromPrimitive; use rtklib_sys::rtklib as ffi; use std::convert::TryFrom; -/// Errors from RTCM decoding. -#[derive(Debug, thiserror::Error)] -pub enum RtcmError { - /// `init_rtcm` failed to allocate internal buffers. - #[error("failed to initialize RTCM decoder (allocation failure)")] - InitFailed, - /// `input_rtcm3` returned a decode error. - #[error("RTCM3 decode error")] - DecodeError, - /// Unrecognized RTCM3 message type number. - #[error("unknown RTCM3 message type: {0}")] - UnknownMessageType(u16), -} - /// Outcome of feeding a byte into the RTCM3 decoder. /// /// Maps the return codes documented in `rtcm.c`: /// `-1`=error, `0`=no message, `1`=observation, `2`=ephemeris, /// `5`=station info, `6`=time params, `7`=DGPS corrections, /// `9`=special message, `20`=SSR corrections. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(i32)] pub enum DecodeResult { @@ -66,64 +55,19 @@ pub enum DecodeResult { SsrCorrections = 20, } -/// A single GNSS observation record. -/// -/// Transparent wrapper around the FFI `obsd_t` struct. References are -/// obtained via [`RtcmDecoder::observations`]. -#[repr(transparent)] -pub struct ObsData(ffi::obsd_t); - -impl ObsData { - /// Satellite number (RTKLIB internal numbering). - pub fn sat(&self) -> u8 { - self.0.sat - } - - /// Carrier phase measurements (cycles) for up to 3 frequencies. - pub fn carrier_phase(&self) -> &[f64; 3] { - &self.0.L - } - - /// Pseudorange measurements (meters) for up to 3 frequencies. - pub fn pseudorange(&self) -> &[f64; 3] { - &self.0.P - } - - /// Doppler measurements (Hz) for up to 3 frequencies. - pub fn doppler(&self) -> &[f32; 3] { - &self.0.D - } - - /// Signal-to-noise ratio (dB-Hz) for up to 3 frequencies. - pub fn snr(&self) -> &[f32; 3] { - &self.0.SNR - } - - /// Signal code identifiers for up to 3 frequencies. - pub fn code(&self) -> &[u8; 3] { - &self.0.code - } - - /// Loss-of-lock indicators for up to 3 frequencies. - pub fn lli(&self) -> &[u8; 3] { - &self.0.LLI - } -} - /// RTCM3 message decoder. /// -/// Wraps the RTKLIB `rtcm_t` struct. Call [`new`](Self::new) to create, +/// Wraps the RTKLIB `rtcm_t` struct. Call [`try_new`](Self::try_new) to create, /// then feed bytes via [`decode`](Self::decode). When `decode` returns -/// [`DecodeResult::Observation`], read the observations with +/// [`Some(DecodeResult::Observation)`], read the observations with /// [`observations`](Self::observations). pub struct RtcmDecoder(Box); impl RtcmDecoder { /// Create a new RTCM decoder. /// - /// Returns `Err(RtcmError::InitFailed)` if RTKLIB cannot allocate - /// internal buffers. - pub fn new() -> Result { + /// Returns `Err` if RTKLIB cannot allocate internal buffers. + pub fn try_new() -> Result { unsafe { // rtcm_t is ~886KB, too large for the stack. Allocate zeroed // memory directly on the heap to avoid stack overflow. @@ -134,25 +78,27 @@ impl RtcmDecoder { } let mut rtcm = Box::from_raw(ptr); if ffi::init_rtcm(rtcm.as_mut()) == 0 { - return Err(RtcmError::InitFailed); + return Err(DecoderInitError); } Ok(Self(rtcm)) } } /// Feed one byte into the RTCM3 decoder. - pub fn decode(&mut self, byte: u8) -> Result { + /// + /// Returns `None` if the byte did not complete a recognized message. + pub fn decode(&mut self, byte: u8) -> Option { let ret = unsafe { ffi::input_rtcm3(self.0.as_mut(), byte) }; - DecodeResult::try_from(ret).map_err(|_| RtcmError::DecodeError) + DecodeResult::try_from(ret).ok() } /// The RTCM3 message type of the last decoded message. /// - /// Returns `Err(UnknownMessageType)` if the type number is not recognized. - /// Only meaningful after `decode` returns a non-`Incomplete` result. - pub fn message_type(&self) -> Result { + /// Returns `None` if the type number is not recognized. + /// Only meaningful after `decode` returns `Some`. + pub fn message_type(&self) -> Option { let raw = unsafe { ffi::getbitu(self.0.buff.as_ptr(), 24, 12) as u16 }; - MsgType::try_from(raw).map_err(|_| RtcmError::UnknownMessageType(raw)) + MsgType::try_from(raw).ok() } /// Number of observation records in the current message. @@ -190,6 +136,7 @@ impl Drop for RtcmDecoder { /// /// Reference: [RTCM 3 Message List](https://www.use-snip.com/kb/knowledge-base/rtcm-3-message-list/) /// and RTCM Standard 10403.x. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u16)] pub enum MsgType { diff --git a/src/solution.rs b/src/solution.rs index c29e776..31fd1bd 100644 --- a/src/solution.rs +++ b/src/solution.rs @@ -4,17 +4,18 @@ //! [`read_solt`] wraps `readsolt`, and [`write_sol`] wraps `outsolheads` and //! `outsols` from `solution.c`. -use crate::{ppk::SolOpt, GpsTime, Llh, SolStatus}; +use crate::{ppk::SolOpt, util::CStringArray, GpsTime, Llh, SolStatus}; use num_enum::TryFromPrimitive; use rtklib_sys::rtklib as ffi; use std::{ - ffi::CString, + ffi::{OsStr, OsString}, fs::File, io::{Error as IoError, Write}, }; use thiserror::Error; /// Coordinate type stored in a [`Solution`]. +#[cfg_attr(feature = "strum", derive(strum::Display))] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[repr(u8)] pub enum CoordType { @@ -28,8 +29,8 @@ pub enum CoordType { #[derive(Debug, Error)] pub enum SolError { /// A file path contained an interior null byte. - #[error("path contains null byte: {0}")] - NulByte(String), + #[error("path contains null byte: {0:?}")] + NulByte(OsString), /// Reading the solution file failed or returned no data. #[error("read failed")] ReadFailed, @@ -194,20 +195,17 @@ fn validate_coord_types(buf: SolBuf) -> Result { Ok(buf) } -fn build_cstring_ptrs(paths: &[&str]) -> Result<(Vec, Vec<*const i8>), SolError> { - let cstrings = paths - .iter() - .map(|p| CString::new(*p).map_err(|_| SolError::NulByte(p.to_string()))) - .collect::, _>>()?; - let ptrs = cstrings.iter().map(|s| s.as_ptr()).collect(); - Ok((cstrings, ptrs)) +impl From<&OsStr> for SolError { + fn from(s: &OsStr) -> Self { + Self::NulByte(s.to_owned()) + } } /// Read solution records from one or more `.pos` files. -pub fn read_sol(paths: &[&str]) -> Result { - let (_cstrings, mut ptrs) = build_cstring_ptrs(paths)?; +pub fn read_sol>(paths: &[T]) -> Result { + let mut arr = CStringArray::try_new(paths)?; let mut buf = unsafe { std::mem::zeroed::() }; - let ret = unsafe { ffi::readsol(ptrs.as_mut_ptr(), ptrs.len() as i32, &mut buf) }; + let ret = unsafe { ffi::readsol(arr.as_mut_ptr(), arr.len() as i32, &mut buf) }; if ret == 0 { return Err(SolError::ReadFailed); } @@ -215,20 +213,20 @@ pub fn read_sol(paths: &[&str]) -> Result { } /// Read solution records within a time window from one or more `.pos` files. -pub fn read_solt( - paths: &[&str], +pub fn read_solt>( + paths: &[T], ts: GpsTime, te: GpsTime, tint: f64, qflag: i32, mean: bool, ) -> Result { - let (_cstrings, mut ptrs) = build_cstring_ptrs(paths)?; + let mut arr = CStringArray::try_new(paths)?; let mut buf = unsafe { std::mem::zeroed::() }; let ret = unsafe { ffi::readsolt( - ptrs.as_mut_ptr(), - ptrs.len() as i32, + arr.as_mut_ptr(), + arr.len() as i32, ts.0, te.0, tint, @@ -244,8 +242,8 @@ pub fn read_solt( } /// Write solution records to a file using the given output options. -pub fn write_sol(path: &str, buf: &SolBuf, opt: &SolOpt) -> Result<(), SolError> { - let mut file = File::create(path)?; +pub fn write_sol(path: impl AsRef, buf: &SolBuf, opt: &SolOpt) -> Result<(), SolError> { + let mut file = File::create(path.as_ref())?; let mut scratch = [0u8; 513]; let n = unsafe { ffi::outsolheads(scratch.as_mut_ptr(), opt.as_ffi()) }; file.write_all(&scratch[..n as usize])?; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..f3966e1 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,63 @@ +use std::{ + ffi::{CString, OsStr}, + os::unix::ffi::OsStrExt, +}; + +/// Copy an `OsStr` into a fixed-size null-terminated `[i8; N]` C buffer. +/// Truncates silently if `src` is longer than `N - 1` bytes. +pub(crate) fn copy_osstr(dst: &mut [i8; N], src: &OsStr) { + let src = src.as_bytes(); + let n = src.len().min(N - 1); + unsafe { + std::ptr::copy_nonoverlapping(src.as_ptr() as *const i8, dst.as_mut_ptr(), n); + dst[n] = 0; + } +} + +/// Owned array of C strings with a stable pointer list for FFI calls. +/// +/// `as_ptr` holds pointers into each `CString`'s owned string buffer. Moving this +/// struct retains the validity of the pointers to the heap-allocated strings. +pub(crate) struct CStringArray { + _strings: Vec, + ptrs: Vec<*const i8>, +} + +impl CStringArray { + /// Build from a slice of paths. Returns the offending path if one contains a NUL byte. + pub(crate) fn try_new>(paths: &[T]) -> Result { + let strings = paths + .iter() + .map(|p| { + let os = p.as_ref(); + CString::new(os.as_bytes()).map_err(|_| os) + }) + .collect::, _>>()?; + let ptrs = strings.iter().map(|s| s.as_ptr()).collect(); + Ok(Self { _strings: strings, ptrs }) + } + + /// Build from a single path. Returns the offending path if it contains a NUL byte. + pub(crate) fn try_single(path: &OsStr) -> Result { + let string = CString::new(path.as_bytes()).map_err(|_| path)?; + Ok(Self { + ptrs: vec![string.as_ptr()], + _strings: vec![string], + }) + } + + /// Pointer to the first string. Panics if empty. + pub(crate) fn first(&self) -> *const i8 { + self.ptrs[0] + } + + /// Mutable pointer to the start of the pointer array, for passing to C as `char **`. + pub(crate) fn as_mut_ptr(&mut self) -> *mut *const i8 { + self.ptrs.as_mut_ptr() + } + + /// Number of strings in the array. + pub(crate) fn len(&self) -> usize { + self.ptrs.len() + } +} diff --git a/tests/all_blocks_0000.sbf b/tests/all_blocks_0000.sbf new file mode 100644 index 0000000..ef01bc4 Binary files /dev/null and b/tests/all_blocks_0000.sbf differ diff --git a/tests/log_0000.sbf b/tests/log_0000.sbf new file mode 100644 index 0000000..ba8144d Binary files /dev/null and b/tests/log_0000.sbf differ diff --git a/tests/ppk.rs b/tests/ppk.rs index af665eb..12d5def 100644 --- a/tests/ppk.rs +++ b/tests/ppk.rs @@ -7,7 +7,8 @@ use rtklib_ffi::{ ppk::{ - postpos, ArMode, FilOpt, IonoOpt, PosMode, PostposError, PrcOpt, SolFormat, SolOpt, TropOpt, + postpos, ArMode, FilOpt, IonoOpt, PosMode, PostposError, PrcOpt, SolFormat, SolOpt, + TimeFormat, TropOpt, }, NavSys, }; @@ -16,30 +17,30 @@ use std::{fs, path::Path}; #[test] fn prcopt_kinematic_builder() { - let mut opt = PrcOpt::kinematic(); - opt.set_navsys(NavSys::Gps | NavSys::Glo | NavSys::Gal) - .set_frequencies(2) - .set_elevation_mask(15.0) - .set_ar_mode(ArMode::FixAndHold) - .set_ionosphere(IonoOpt::IonFreeLC) - .set_troposphere(TropOpt::Saastamoinen); + let opt = PrcOpt::kinematic() + .with_navsys(NavSys::Gps | NavSys::Glo | NavSys::Gal) + .with_frequencies(2) + .with_elevation_mask(15.0) + .with_ar_mode(ArMode::FixAndHold) + .with_ionosphere(IonoOpt::IonFreeLc) + .with_troposphere(TropOpt::Saastamoinen); assert_eq!(opt.mode(), PosMode::Kinematic); assert_eq!(opt.navsys(), NavSys::Gps | NavSys::Glo | NavSys::Gal); assert_eq!(opt.frequencies(), 2); assert!((opt.elevation_mask() - 15.0_f64.to_radians()).abs() < 1e-12); assert_eq!(opt.ar_mode(), ArMode::FixAndHold); - assert_eq!(opt.ionosphere(), IonoOpt::IonFreeLC); + assert_eq!(opt.ionosphere(), IonoOpt::IonFreeLc); assert_eq!(opt.troposphere(), TropOpt::Saastamoinen); } #[test] fn prcopt_static_builder() { - let mut opt = PrcOpt::static_mode(); - opt.set_navsys(NavSys::Gps) - .set_frequencies(1) - .set_elevation_mask(10.0) - .set_ar_mode(ArMode::Continuous); + let opt = PrcOpt::static_mode() + .with_navsys(NavSys::Gps) + .with_frequencies(1) + .with_elevation_mask(10.0) + .with_ar_mode(ArMode::Continuous); assert_eq!(opt.mode(), PosMode::Static); assert_eq!(opt.navsys(), NavSys::Gps); @@ -50,14 +51,14 @@ fn prcopt_static_builder() { #[test] fn solopt_setters() { - let mut sopt = SolOpt::default(); - sopt.set_format(SolFormat::Xyz) - .set_time_format(1) - .set_time_decimals(3) - .set_output_header(true); + let sopt = SolOpt::default() + .with_format(SolFormat::Xyz) + .with_time_format(TimeFormat::Calendar) + .with_time_decimals(3) + .with_output_header(true); assert_eq!(sopt.format(), SolFormat::Xyz); - assert_eq!(sopt.time_format(), 1); + assert_eq!(sopt.time_format(), TimeFormat::Calendar); assert_eq!(sopt.time_decimals(), 3); assert!(sopt.output_header()); } @@ -71,18 +72,18 @@ fn ppk_with_rinex2_test_data() { let nav = format!("{}/07590920.05n", data); let output = "/tmp/rtklib-ffi-test-output.pos"; - let mut popt = PrcOpt::kinematic(); - popt.set_navsys(NavSys::Gps) - .set_frequencies(1) - .set_elevation_mask(15.0) - .set_ar_mode(ArMode::Continuous) - .set_ionosphere(IonoOpt::Broadcast) - .set_troposphere(TropOpt::Saastamoinen); - - let mut sopt = SolOpt::default(); - sopt.set_format(SolFormat::Llh) - .set_time_format(1) - .set_time_decimals(3); + let popt = PrcOpt::kinematic() + .with_navsys(NavSys::Gps) + .with_frequencies(1) + .with_elevation_mask(15.0) + .with_ar_mode(ArMode::Continuous) + .with_ionosphere(IonoOpt::Broadcast) + .with_troposphere(TropOpt::Saastamoinen); + + let sopt = SolOpt::default() + .with_format(SolFormat::Llh) + .with_time_format(TimeFormat::Calendar) + .with_time_decimals(9); let fopt = FilOpt::default(); diff --git a/tests/rtcm.rs b/tests/rtcm.rs index f63aa0f..4b9f5fc 100644 --- a/tests/rtcm.rs +++ b/tests/rtcm.rs @@ -11,7 +11,7 @@ use rtklib_ffi::{satno, NavSys}; #[test] fn decode_rtcm3_ephemeris() { let data = std::fs::read("tests/debug.rtcm").expect("failed to read test file"); - let mut decoder = RtcmDecoder::new().expect("failed to init RTCM decoder"); + let mut decoder = RtcmDecoder::try_new().expect("failed to init RTCM decoder"); let mut ephemeris_count = 0u32; let mut msg_types: Vec = Vec::new(); @@ -20,7 +20,8 @@ fn decode_rtcm3_ephemeris() { let mut prev_obs_n = 0usize; for &byte in &data { - match decoder.decode(byte).expect("RTCM3 decode error") { + let Some(status) = decoder.decode(byte) else { continue; }; + match status { DecodeResult::Incomplete => { // MSM7 messages with sync=1 return Incomplete but still // store observations in the internal buffer. Detect this @@ -30,8 +31,8 @@ fn decode_rtcm3_ephemeris() { prev_obs_n = n; let sats: Vec = decoder.observations().iter().map(|o| o.sat()).collect(); match decoder.message_type() { - Ok(MsgType::GpsMsm7) => gps_sats = sats, - Ok(MsgType::GalMsm7) => gal_sats = sats, + Some(MsgType::GpsMsm7) => gps_sats = sats, + Some(MsgType::GalMsm7) => gal_sats = sats, _ => {} } } @@ -42,8 +43,9 @@ fn decode_rtcm3_ephemeris() { msg_types.push(mt); } _ => { - let mt = decoder.message_type().expect("unknown message type"); - msg_types.push(mt); + if let Some(mt) = decoder.message_type() { + msg_types.push(mt); + } } } } diff --git a/tests/sbf.rs b/tests/sbf.rs new file mode 100644 index 0000000..bf49e3f --- /dev/null +++ b/tests/sbf.rs @@ -0,0 +1,41 @@ +//! Smoke tests for the Septentrio SBF decoder. +#![cfg(feature = "receivers")] + +#[cfg(feature = "conv")] +use rtklib_ffi::conv::{convrnx, RnxOpt, RnxOutputFiles, StreamFmt}; +use rtklib_ffi::receiver::{DecodeStatus, SbfDecoder}; + +#[test] +fn decode_sbf_all_blocks() { + let data = std::fs::read("tests/all_blocks_0000.sbf").expect("failed to read test file"); + let mut decoder = SbfDecoder::try_new().expect("failed to init SBF decoder"); + + // all_blocks_0000.sbf has empty MeasEpoch blocks (n1=0), so no observations + // are decoded. It does contain GPSNav and GALNav blocks. + let mut eph_count = 0u32; + + for &byte in &data { + let Some(status) = decoder.decode(byte) else { + continue; + }; + match status { + DecodeStatus::Ephemeris => eph_count += 1, + _ => {} + } + } + + assert!(eph_count > 0, "expected at least one ephemeris message"); +} + +#[cfg(feature = "conv")] +#[test] +fn convrnx_sbf_to_rinex() { + let obs_path = std::env::temp_dir().join("rtklib_ffi_test_sbf.obs"); + let ofiles = RnxOutputFiles::new(&obs_path); + let result = convrnx(StreamFmt::Sbf, &mut RnxOpt::default(), "tests/log_0000.sbf", &ofiles); + + assert!(result.is_ok(), "convrnx failed: {result:?}"); + let meta = std::fs::metadata(&obs_path).expect("output file not created"); + assert!(meta.len() > 0, "output file is empty"); + let _ = std::fs::remove_file(&obs_path); +}