├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── daily.yml │ ├── release.yml │ └── rust.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── doc ├── cggtts.md ├── delay.md └── track.md ├── rustfmt.toml └── src ├── buffer.rs ├── crc.rs ├── errors.rs ├── header ├── code.rs ├── delay.rs ├── formatting.rs ├── hardware.rs ├── mod.rs ├── parsing.rs ├── reference_time.rs └── version.rs ├── lib.rs ├── processing.rs ├── scheduler ├── calendar.rs ├── mod.rs └── period.rs ├── tests ├── mod.rs ├── parser.rs └── toolkit.rs ├── track ├── class.rs ├── formatting.rs ├── glonass.rs ├── mod.rs └── parsing.rs ├── tracker ├── fit.rs ├── fitted.rs └── mod.rs └── version.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/daily.yml: -------------------------------------------------------------------------------- 1 | name: Daily 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: 7 | - "*" 8 | pull_request: 9 | branches: [ main ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | fetch-depth: 0 23 | 24 | - uses: actions-rs/toolchain@v1 25 | name: Install Rust 26 | with: 27 | toolchain: 1.82.0 28 | override: true 29 | 30 | - uses: actions-rs/cargo@v1 31 | name: Build 32 | with: 33 | command: build 34 | args: --all-features 35 | 36 | - uses: actions-rs/cargo@v1 37 | name: Test 38 | with: 39 | command: test 40 | args: --all-features 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | continue-on-error: true 16 | if: github.ref_type == 'tag' 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Install stable 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | - name: Publish 25 | env: 26 | TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 27 | run: | 28 | cargo login $TOKEN 29 | cargo publish --allow-dirty 30 | 31 | release: 32 | runs-on: ubuntu-latest 33 | needs: ['publish'] 34 | steps: 35 | - name: Create Release 36 | id: create_release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | uses: actions/create-release@v1 40 | with: 41 | draft: true 42 | tag_name: ${{ github.ref_name }} 43 | release_name: ${{ github.ref_name }} 44 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: 7 | - "*" 8 | pull_request: 9 | branches: [ main ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | fetch-depth: 0 23 | 24 | - uses: actions-rs/cargo@v1 25 | name: Linter 26 | with: 27 | command: fmt 28 | args: --all -- --check 29 | 30 | - uses: actions-rs/toolchain@v1 31 | name: Install Rust 32 | with: 33 | toolchain: 1.82.0 34 | override: true 35 | components: rustfmt, clippy 36 | 37 | - uses: actions-rs/cargo@v1 38 | name: Build 39 | with: 40 | command: build 41 | 42 | - uses: actions-rs/cargo@v1 43 | name: Test 44 | with: 45 | command: test 46 | args: --verbose 47 | 48 | - uses: actions-rs/cargo@v1 49 | name: Build (all features) 50 | with: 51 | command: build 52 | args: --all-features 53 | 54 | - uses: actions-rs/cargo@v1 55 | name: Test (all features) 56 | with: 57 | command: test 58 | args: --verbose --all-features 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | *.swo 4 | *.swp 5 | *.html 6 | test.txt 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "data"] 2 | path = data 3 | url = https://github.com/rtk-rs/data 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cggtts" 3 | version = "4.3.0" 4 | license = "MPL-2.0" 5 | authors = ["Guillaume W. Bres "] 6 | description = "CGGTTS data parsing and synthesis" 7 | homepage = "https://github.com/rtk-rs" 8 | repository = "https://github.com/rtk-rs/cggtts" 9 | keywords = ["geo", "gnss", "timing", "gps"] 10 | categories = ["science", "science", "parsing"] 11 | edition = "2018" 12 | readme = "README.md" 13 | exclude = [ 14 | "data/*", 15 | ] 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--cfg", "docrs", "--generate-link-to-definition"] 20 | 21 | [features] 22 | default = [] # no features by default 23 | 24 | # Unlock common view period definitions and scheduling 25 | scheduler = [] 26 | 27 | # Satellite tracker and fit method 28 | tracker = [ 29 | "dep:polyfit-rs", 30 | "dep:log", 31 | ] 32 | 33 | [dependencies] 34 | thiserror = "2" 35 | scan_fmt = "0.1.3" 36 | strum = "0.27" 37 | itertools = "0.14" 38 | strum_macros = "0.27" 39 | flate2 = { version = "1", optional = true } 40 | log = { version = "0.4", optional = true } 41 | polyfit-rs = { version = "0.2", optional = true } 42 | gnss-rs = { version = "2.4.0", features = ["serde"] } 43 | hifitime = { version = "4.1.0", features = ["serde", "std"] } 44 | serde = { version = "1.0", optional = true, features = ["derive"] } 45 | 46 | [dev-dependencies] 47 | rand = "0.8" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CGGTTS 2 | ====== 3 | 4 | Rust package to parse and generate CGGTTS data. 5 | 6 | [![Rust](https://github.com/rtk-rs/cggtts/actions/workflows/rust.yml/badge.svg)](https://github.com/rtk-rs/cggtts/actions/workflows/rust.yml) 7 | [![Rust](https://github.com/rtk-rs/cggtts/actions/workflows/daily.yml/badge.svg)](https://github.com/rtk-rs/cggtts/actions/workflows/daily.yml) 8 | [![crates.io](https://docs.rs/cggtts/badge.svg)](https://docs.rs/cggtts/) 9 | [![crates.io](https://img.shields.io/crates/d/cggtts.svg)](https://crates.io/crates/cggtts) 10 | 11 | [![MRSV](https://img.shields.io/badge/MSRV-1.82.0-orange?style=for-the-badge)](https://github.com/rust-lang/rust/releases/tag/1.82.0) 12 | [![License](https://img.shields.io/badge/license-MPL_2.0-orange?style=for-the-badge&logo=mozilla)](https://github.com/rtk-rs/sp3/blob/main/LICENSE) 13 | 14 | ## License 15 | 16 | This library is part of the [RTK-rs framework](https://github.com/rtk-rs) which 17 | is delivered under the [Mozilla V2 Public](https://www.mozilla.org/en-US/MPL/2.0) license. 18 | 19 | ## CGGTTS 20 | 21 | CGGTTS is a file format designed to describe the state of a local clock with respect to spacecraft that belong 22 | to GNSS constellation, ie., a GNSS timescale. 23 | Exchanging CGGTTS files allows direct clock comparison between two remote sites, by comparing how the clock behaves 24 | with respect to a specific spacecraft (ie., on board clock). 25 | This is called the _common view_ time transfer technique. Although it is more accurate to say CGGTTS is just the comparison method, 26 | what you do from the final results is up to end application. Usually, the final end goal is to have the B site track the A site 27 | and replicate the remote clock. It is for example, one option to generate a UTC replica. 28 | 29 | CGGTTS is specified by the Bureau International des Poids & des Mesures (BIPM): 30 | [CGGTTS 2E specifications](https://www.bipm.org/documents/20126/52718503/G1-2015.pdf/f49995a3-970b-a6a5-9124-cc0568f85450) 31 | 32 | This library only supports revision **2E**, and will _reject_ other revisions. 33 | 34 | ## Features 35 | 36 | - `serdes` 37 | - `scheduler`: unlock CGGTS track scheduling 38 | 39 | ## CGGTTS track scheduling 40 | 41 | If you compiled the crate with the _scheduler_ feature, you can access the 42 | `Scheduler` structure that helps you generate synchronous CGGTTS tracks. 43 | 44 | Synchronous CGGTTS is convenient because it allows direct exchange of CGGTTS files 45 | and therefore, direct remote clocks comparison. 46 | 47 | The `Scheduler` structure works according to the BIPM definitions but we allow for a different 48 | tracking duration. The default being 980s, you can use shorter tracking duration and faster 49 | CGGTTS generation. You can only modify the tracking duration if you can do so on both remote clocks, 50 | so they share the same production parameters at all times. 51 | 52 | ## System Time delays 53 | 54 | A built in API allows accurate system delay description as defined in CGGTTS. 55 | 56 | ## Getting started 57 | 58 | This library only supports revision **2E**, and will _reject_ other revisions. 59 | 60 | Add "cggtts" to your Cargo.toml 61 | 62 | ```toml 63 | cggtts = "4" 64 | ``` 65 | 66 | Use CGGTTS to parse local files 67 | 68 | ```rust 69 | use cggtts::prelude::CGGTTS; 70 | 71 | let cggtts = CGGTTS::from_file("data/CGGTTS/GZGTR560.258"); 72 | assert!(cggtts.is_ok()); 73 | 74 | let cggtts = cggtts.unwrap(); 75 | assert_eq!(cggtts.header.station, "LAB"); 76 | assert_eq!(cggtts.tracks.len(), 2097); 77 | ``` 78 | -------------------------------------------------------------------------------- /doc/cggtts.md: -------------------------------------------------------------------------------- 1 | CGGTTS 2 | ====== 3 | 4 | CGGTTS is the main structure, is can be parsed from a standard CGGTTS file, and can be dumped into a file following standards. 5 | 6 | Cggtts parser 7 | ============= 8 | 9 | ## Known behavior 10 | 11 | * The parser supports current revision **2E** only, it will reject files that have different revision number. 12 | * This parser does not care for file naming conventions 13 | 14 | * While standard specifications says header lines order do matter, 15 | this parser is tolerant and only expects the first CGGTT REVISION header to come first. 16 | 17 | * BLANKs between header & measurements data must be respected 18 | * This parser does not care for whitespaces, padding, it is not disturbed by their abscence 19 | * This parser is case sensitive at the moment, all data fields and labels should be provided in upper case, 20 | as specified in standards 21 | 22 | * We accept several \"COMMENTS =\" lines, although it is not specified in CGGTTS. Several comments are then parsed. 23 | 24 | Notes on System delays and this parser 25 | 26 | * This parser follows standard specifications, 27 | if \"TOT\" = Total delay is specified, we actually discard 28 | any other possibly provided delay value, and this one superceeds 29 | and becomes the only known system delay. 30 | Refer to the System Delay documentation to understand what they 31 | mean and how to specify/use them. 32 | -------------------------------------------------------------------------------- /doc/delay.md: -------------------------------------------------------------------------------- 1 | Delay: measurement systems delay 2 | ================================= 3 | 4 | `delay.rs` exposes several structures 5 | 6 | * `Delay` : a generic delay value, always specified in nanoseconds 7 | * `SystemDelay` : used by Cggtts to describe the measurement systems delay. 8 | 9 | ## `SystemDelay` object 10 | 11 | ``` 12 | +--------+ +---------- system ---------+ +++++++++++ 13 | + + cable +------+ + counter + 14 | + ANT + ------------> + RCVR + -------------------> + DUT + 15 | +--------+ +------+ + + 16 | ^ + + 17 | +++++++ -------------------| ref_dly + + 18 | + REF + --------------------------------------------> + REF + 19 | +++++++ +++++++++++ 20 | ``` 21 | 22 | SystemDelay represents the summation of all delays: 23 | 24 | * RF/cable delay 25 | * Reference delay 26 | * System or internal delay, that are carrier dependent 27 | 28 | System or internal delays are calibrated against 29 | a specific GNSS constellation. 30 | -------------------------------------------------------------------------------- /doc/track.md: -------------------------------------------------------------------------------- 1 | Track: CGGTTS measurements 2 | ========================== 3 | 4 | CGGTTS measurements (referred to as _tracks_) are Common View realizations. 5 | 6 | Two classes of measurement exist: 7 | * `CommonViewClass::Single` 8 | * `CommonViewClass::Multiple` - actually Dual frequency 9 | 10 | A track comprises several data fields, refer to the crate official documentation 11 | for their definition. 12 | 13 | ```rust 14 | let first = cggtts.tracks.first() 15 | .unwrap(); 16 | assert_eq!(first.elevation, 1E-9); 17 | assert_eq!(first.azimuth, 1E-10); 18 | ``` 19 | 20 | Follows BIPM tracking recommendations: 21 | 22 | ```rust 23 | assert_eq!(first.follows_bipm_specs(), true); 24 | ``` 25 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | match_block_trailing_comma = true 2 | newline_style = "Unix" 3 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::str::{from_utf8, Utf8Error}; 2 | 3 | pub struct Utf8Buffer { 4 | inner: Vec, 5 | } 6 | 7 | #[cfg(test)] 8 | use std::io::Write; 9 | 10 | /// [Write] implementation is only used when testing. 11 | #[cfg(test)] 12 | impl Write for Utf8Buffer { 13 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 14 | for b in buf { 15 | self.inner.push(*b); 16 | } 17 | Ok(buf.len()) 18 | } 19 | 20 | fn flush(&mut self) -> std::io::Result<()> { 21 | self.inner.clear(); 22 | Ok(()) 23 | } 24 | } 25 | 26 | impl Utf8Buffer { 27 | /// Allocated new [Utf8Buffer]. 28 | pub fn new(size: usize) -> Self { 29 | Self { 30 | inner: Vec::with_capacity(size), 31 | } 32 | } 33 | 34 | /// Clear (discard) internal content 35 | pub fn clear(&mut self) { 36 | self.inner.clear(); 37 | } 38 | 39 | /// Pushes "content" (valid Utf-8) into internal buffer. 40 | pub fn push_str(&mut self, content: &str) { 41 | let bytes = content.as_bytes(); 42 | self.inner.extend_from_slice(&bytes); 43 | } 44 | 45 | pub fn calculate_crc(&self) -> u8 { 46 | let mut crc = 0u8; 47 | for byte in self.inner.iter() { 48 | if *byte != b'\n' && *byte != b'\r' { 49 | crc = crc.wrapping_add(*byte); 50 | } 51 | } 52 | crc 53 | } 54 | 55 | pub fn to_utf8_ascii<'a>(&'a self) -> Result<&'a str, Utf8Error> { 56 | from_utf8(&self.inner[..]) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod test { 62 | use super::Utf8Buffer; 63 | 64 | #[test] 65 | fn test_crc_tracks_buffering() { 66 | let mut buf = Utf8Buffer::new(1024); 67 | 68 | buf.push_str("R24 FF 57000 000600 780 347 394 +1186342 +0 163 +0 40 2 141 +22 23 -1 23 -1 29 +2 0 L3P"); 69 | assert_eq!(buf.calculate_crc(), 0x0f); 70 | 71 | buf.clear(); 72 | 73 | buf.push_str("G99 99 59509 002200 0780 099 0099 +9999999999 +99999 +9999989831 -724 35 999 9999 +999 9999 +999 00 00 L1C"); 74 | assert_eq!(buf.calculate_crc(), 0x71); 75 | } 76 | 77 | #[test] 78 | fn test_crc_header_buffering() { 79 | let mut buf = Utf8Buffer::new(1024); 80 | 81 | let content = "CGGTTS GENERIC DATA FORMAT VERSION = 2E 82 | REV DATE = 2023-06-27 83 | RCVR = GTR51 2204005 1.12.0 84 | CH = 20 85 | IMS = GTR51 2204005 1.12.0 86 | LAB = LAB 87 | X = +3970727.80 m 88 | Y = +1018888.02 m 89 | Z = +4870276.84 m 90 | FRAME = FRAME 91 | COMMENTS = NO COMMENTS 92 | INT DLY = 34.6 ns (GAL E1), 0.0 ns (GAL E5), 0.0 ns (GAL E6), 0.0 ns (GAL E5b), 25.6 ns (GAL E5a) CAL_ID = 1015-2021 93 | CAB DLY = 155.2 ns 94 | REF DLY = 0.0 ns 95 | REF = REF_IN 96 | CKSUM = "; 97 | 98 | buf.push_str(&content); 99 | assert_eq!(buf.calculate_crc(), 0xD7); 100 | 101 | let mut buf = Utf8Buffer::new(1024); 102 | 103 | let content = "CGGTTS GENERIC DATA FORMAT VERSION = 2E 104 | REV DATE = 2023-06-27 105 | RCVR = GTR51 2204005 1.12.0 106 | CH = 20 107 | IMS = GTR51 2204005 1.12.0 108 | LAB = LAB 109 | X = +3970727.80 m 110 | Y = +1018888.02 m 111 | Z = +4870276.84 m 112 | FRAME = FRAME 113 | COMMENTS = NO COMMENTS 114 | INT DLY = 32.9 ns (GPS C1), 32.9 ns (GPS P1), 0.0 ns (GPS C2), 25.8 ns (GPS P2), 0.0 ns (GPS L5), 0.0 ns (GPS L1C) CAL_ID = 1015-2021 115 | CAB DLY = 155.2 ns 116 | REF DLY = 0.0 ns 117 | REF = REF_IN 118 | CKSUM = "; 119 | 120 | buf.push_str(&content); 121 | assert_eq!(buf.calculate_crc(), 0x07); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/crc.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(PartialEq, Debug, Error)] 4 | pub enum Error { 5 | #[error("can only calculate over valid utf8 data")] 6 | NonUtf8Data, 7 | #[error("checksum error, got \"{0}\" but \"{1}\" locally computed")] 8 | ChecksumError(u8, u8), 9 | } 10 | 11 | /// computes crc for given str content 12 | pub(crate) fn calc_crc(content: &str) -> Result { 13 | match content.is_ascii() { 14 | true => { 15 | let mut ck: u8 = 0; 16 | let mut ptr = content.encode_utf16(); 17 | for _ in 0..ptr.clone().count() { 18 | ck = ck.wrapping_add(ptr.next().unwrap() as u8) 19 | } 20 | Ok(ck) 21 | }, 22 | false => Err(Error::NonUtf8Data), 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod test { 28 | use crate::crc::calc_crc; 29 | #[test] 30 | fn test_crc() { 31 | let content = ["R24 FF 57000 000600 780 347 394 +1186342 +0 163 +0 40 2 141 +22 23 -1 23 -1 29 +2 0 L3P", 32 | "G99 99 59509 002200 0780 099 0099 +9999999999 +99999 +9999989831 -724 35 999 9999 +999 9999 +999 00 00 L1C"]; 33 | let expected = [0x0F, 0x71]; 34 | for i in 0..content.len() { 35 | let ck = calc_crc(content[i]).unwrap(); 36 | let expect = expected[i]; 37 | assert_eq!( 38 | ck, expect, 39 | "Failed for \"{}\", expect \"{}\" but \"{}\" locally computed", 40 | content[i], expect, ck 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! CGGTTS errors 2 | use thiserror::Error; 3 | 4 | use crate::track::Error as TrackError; 5 | 6 | /// Errors related to CRC parsing 7 | /// and calculations specifically. 8 | #[derive(PartialEq, Debug, Error)] 9 | pub enum CrcError { 10 | #[error("can only calculate over valid utf8 data")] 11 | NonUtf8Data, 12 | #[error("checksum error, got \"{0}\" but \"{1}\" locally computed")] 13 | ChecksumError(u8, u8), 14 | } 15 | 16 | /// Errors strictly related to file parsing. 17 | #[derive(Debug, Error)] 18 | pub enum ParsingError { 19 | #[error("only revision 2E is supported")] 20 | VersionMismatch, 21 | #[error("invalid version")] 22 | VersionFormat, 23 | #[error("invalid CGGTTS format")] 24 | InvalidFormat, 25 | #[error("invalid Coordinates format")] 26 | Coordinates, 27 | #[error("invalid revision date")] 28 | RevisionDateFormat, 29 | #[error("invalid channel number")] 30 | ChannelNumber, 31 | #[error("non supported file revision")] 32 | NonSupportedRevision, 33 | #[error("delay calibration format")] 34 | CalibrationFormat, 35 | #[error("mixing constellations is not allowed in CGGTTS")] 36 | MixedConstellation, 37 | #[error("failed to identify delay value in line \"{0}\"")] 38 | DelayIdentificationError(String), 39 | #[error("failed to parse frequency dependent delay from \"{0}\"")] 40 | FrequencyDependentDelayParsingError(String), 41 | #[error("invalid common view class")] 42 | CommonViewClass, 43 | #[error("checksum format error")] 44 | ChecksumFormat, 45 | #[error("failed to parse checksum value")] 46 | ChecksumParsing, 47 | #[error("invalid crc value")] 48 | ChecksumValue, 49 | #[error("missing crc field")] 50 | CrcMissing, 51 | #[error("track parsing error")] 52 | TrackParsing(#[from] TrackError), 53 | #[error("antenna cable delay")] 54 | AntennaCableDelay, 55 | #[error("local ref delay")] 56 | LocalRefDelay, 57 | } 58 | 59 | /// Errors strictly related to CGGTTS formatting 60 | #[derive(Debug, Error)] 61 | pub enum FormattingError { 62 | #[error("bad utf-8 data")] 63 | Utf8(#[from] std::str::Utf8Error), 64 | #[error("i/o error: {0}")] 65 | Stdio(#[from] std::io::Error), 66 | } 67 | -------------------------------------------------------------------------------- /src/header/code.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use strum_macros::EnumString; 5 | 6 | #[derive(Clone, Copy, PartialEq, Debug, EnumString)] 7 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 8 | #[derive(Default)] 9 | pub enum Code { 10 | #[default] 11 | C1, 12 | C2, 13 | P1, 14 | P2, 15 | E1, 16 | E5, 17 | B1, 18 | B2, 19 | } 20 | 21 | impl std::fmt::Display for Code { 22 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 23 | match self { 24 | Code::C1 => fmt.write_str("C1"), 25 | Code::C2 => fmt.write_str("C2"), 26 | Code::P1 => fmt.write_str("P1"), 27 | Code::P2 => fmt.write_str("P2"), 28 | Code::E1 => fmt.write_str("E1"), 29 | Code::E5 => fmt.write_str("E5"), 30 | Code::B1 => fmt.write_str("B1"), 31 | Code::B2 => fmt.write_str("B2"), 32 | } 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use super::*; 39 | use std::str::FromStr; 40 | 41 | #[test] 42 | fn test_code() { 43 | assert_eq!(Code::default(), Code::C1); 44 | assert_eq!(Code::from_str("C2").unwrap(), Code::C2); 45 | assert_eq!(Code::from_str("P1").unwrap(), Code::P1); 46 | assert_eq!(Code::from_str("P2").unwrap(), Code::P2); 47 | assert_eq!(Code::from_str("E5").unwrap(), Code::E5); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/header/delay.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::ParsingError, header::Code}; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cfg(docsrs)] 7 | use crate::prelude::CGGTTS; 8 | 9 | /// Indication about precise system delay calibration process, 10 | /// as found in [CGGTTS]. 11 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 12 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 13 | pub struct CalibrationID { 14 | /// ID # of this calibration process 15 | pub process_id: u16, 16 | /// Year of calibration 17 | pub year: u16, 18 | } 19 | 20 | impl std::str::FromStr for CalibrationID { 21 | type Err = ParsingError; 22 | fn from_str(s: &str) -> Result { 23 | let mut parsed_items = 0; 24 | let (mut process_id, mut year) = (0, 0); 25 | 26 | for (nth, item) in s.trim().split('-').enumerate() { 27 | if nth == 0 { 28 | if let Ok(value) = item.parse::() { 29 | process_id = value; 30 | parsed_items += 1; 31 | } 32 | } else if nth == 1 { 33 | if let Ok(value) = item.parse::() { 34 | year = value; 35 | parsed_items += 1; 36 | } 37 | } 38 | } 39 | 40 | if parsed_items == 2 { 41 | Ok(Self { process_id, year }) 42 | } else { 43 | Err(ParsingError::CalibrationFormat) 44 | } 45 | } 46 | } 47 | 48 | /// [Delay] describes all supported types of propagation delay. 49 | /// NB: the specified value is always in nanoseconds. 50 | #[derive(Debug, PartialEq, Clone, Copy)] 51 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 52 | pub enum Delay { 53 | /// Delay defined as internal in nanoseconds 54 | Internal(f64), 55 | /// Systemic delay, in nanoseconds 56 | System(f64), 57 | } 58 | 59 | impl Default for Delay { 60 | fn default() -> Delay { 61 | Delay::System(0.0_f64) 62 | } 63 | } 64 | 65 | impl Delay { 66 | /// Define new internal [Delay] 67 | pub fn new_internal_nanos(nanos: f64) -> Self { 68 | Self::Internal(nanos) 69 | } 70 | 71 | /// Define new systemic [Delay] 72 | pub fn new_systemic(nanos: f64) -> Self { 73 | Self::System(nanos) 74 | } 75 | 76 | /// Returns total delay in nanoseconds, whatever its kind. 77 | pub fn total_nanoseconds(&self) -> f64 { 78 | match self { 79 | Delay::Internal(d) => *d, 80 | Delay::System(d) => *d, 81 | } 82 | } 83 | 84 | /// Returns total delay in seconds, whatever its kind. 85 | pub fn total_seconds(&self) -> f64 { 86 | self.total_nanoseconds() * 1.0E-9 87 | } 88 | 89 | /// Adds specific amount of nanoseconds to internal delay, 90 | /// whatever its definition. 91 | pub fn add_nanos(&self, rhs: f64) -> Self { 92 | match self { 93 | Delay::System(d) => Delay::System(*d + rhs), 94 | Delay::Internal(d) => Delay::Internal(*d + rhs), 95 | } 96 | } 97 | } 98 | 99 | /// [SystemDelay] describes total measurement systems delay. 100 | /// This is used in [CGGTTS] to describe the measurement system 101 | /// accurately. 102 | /// 103 | /// Example of simplistic definition, compatible with 104 | /// very precise single frequency Common View: 105 | /// ``` 106 | /// use cggtts::prelude::SystemDelay; 107 | /// 108 | /// let system_specs = SystemDelay::default() 109 | /// .with_antenna_cable_delay(10.0) 110 | /// .with_ref_delay(20.0); 111 | /// 112 | /// assert_eq!(system_specs.total_cable_delay_nanos(), 30.0); 113 | /// ``` 114 | /// 115 | /// Example of advanced definition, compatible with 116 | /// ultra precise dual frequency Common View: 117 | /// ``` 118 | /// use cggtts::prelude::SystemDelay; 119 | /// 120 | /// ``` 121 | #[derive(Clone, Default, PartialEq, Debug)] 122 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 123 | pub struct SystemDelay { 124 | /// Delay induced by GNSS antenna cable length. 125 | pub antenna_cable_delay: f64, 126 | /// Delay induced by cable between measurement system 127 | /// and local clock. 128 | pub local_ref_delay: f64, 129 | /// Carrier frequency dependend delays 130 | pub freq_dependent_delays: Vec<(Code, Delay)>, 131 | /// Possible calibration ID 132 | pub calibration_id: Option, 133 | } 134 | 135 | impl SystemDelay { 136 | /// Define new [SystemDelay] with desired readable calibration ID. 137 | /// This is usually the official ID of the calibration process. 138 | pub fn with_calibration_id(&self, calibration: CalibrationID) -> Self { 139 | Self { 140 | antenna_cable_delay: self.antenna_cable_delay, 141 | local_ref_delay: self.local_ref_delay, 142 | freq_dependent_delays: self.freq_dependent_delays.clone(), 143 | calibration_id: Some(calibration), 144 | } 145 | } 146 | 147 | /// Define new [SystemDelay] with desired 148 | /// RF cable delay in nanoseconds ie., 149 | /// delay induced by the antenna cable length itself. 150 | pub fn with_antenna_cable_delay(&self, nanos: f64) -> Self { 151 | let mut s = self.clone(); 152 | s.antenna_cable_delay = nanos; 153 | s 154 | } 155 | 156 | /// Define new [SystemDelay] with REF delay in nanoseconds, 157 | /// ie., the delay induced by cable between the measurement 158 | /// system and the local clock. 159 | pub fn with_ref_delay(&self, nanos: f64) -> Self { 160 | let mut s = self.clone(); 161 | s.local_ref_delay = nanos; 162 | s 163 | } 164 | 165 | /// Returns total cable delay in nanoseconds, that will affect all measurements. 166 | pub fn total_cable_delay_nanos(&self) -> f64 { 167 | self.antenna_cable_delay + self.local_ref_delay 168 | } 169 | 170 | /// Returns total system delay, in nanoseconds, 171 | /// for desired frequency represented by [Code], if we 172 | /// do have specifications for it. 173 | /// 174 | /// ``` 175 | /// ``` 176 | pub fn total_frequency_dependent_delay_nanos(&self, code: &Code) -> Option { 177 | for (k, v) in self.freq_dependent_delays.iter() { 178 | if k == code { 179 | return Some(v.total_nanoseconds() + self.total_cable_delay_nanos()); 180 | } 181 | } 182 | None 183 | } 184 | 185 | /// Iterates over all frequency dependent delays, per carrier frequency, 186 | /// in nanoseconds of propagation delay for said frequency. 187 | pub fn frequency_dependent_nanos_delay_iter( 188 | &self, 189 | ) -> Box + '_> { 190 | Box::new( 191 | self.freq_dependent_delays 192 | .iter() 193 | .map(move |(k, v)| (k, v.total_nanoseconds() + self.total_cable_delay_nanos())), 194 | ) 195 | } 196 | } 197 | 198 | #[cfg(test)] 199 | mod test { 200 | 201 | use super::*; 202 | use std::str::FromStr; 203 | 204 | #[test] 205 | fn calibration_id() { 206 | let calibration = CalibrationID::from_str("1015-2024").unwrap(); 207 | assert_eq!(calibration.process_id, 1015); 208 | assert_eq!(calibration.year, 2024); 209 | 210 | assert!(CalibrationID::from_str("NA").is_err()); 211 | assert!(CalibrationID::from_str("1nnn-2024").is_err()); 212 | } 213 | 214 | #[test] 215 | fn test_delay() { 216 | let delay = Delay::Internal(10.0); 217 | assert_eq!(delay.total_nanoseconds(), 10.0); 218 | 219 | assert_eq!(delay.total_seconds(), 10.0E-9); 220 | assert!(delay == Delay::Internal(10.0)); 221 | assert!(delay != Delay::System(10.0)); 222 | 223 | let d = delay.add_nanos(20.0); 224 | assert_eq!(d, Delay::Internal(30.0)); 225 | assert_eq!(delay.total_nanoseconds() + 20.0, d.total_nanoseconds()); 226 | assert_eq!(Delay::default(), Delay::System(0.0)); 227 | 228 | let delay = Delay::System(30.5); 229 | assert_eq!(delay.total_nanoseconds(), 30.5); 230 | 231 | let d = delay.add_nanos(20.0); 232 | assert_eq!(d.total_nanoseconds(), 50.5); 233 | } 234 | 235 | #[test] 236 | fn test_system_delay() { 237 | let delay = SystemDelay::default(); 238 | assert_eq!(delay.antenna_cable_delay, 0.0); 239 | assert_eq!(delay.local_ref_delay, 0.0); 240 | 241 | let delay = SystemDelay::default().with_antenna_cable_delay(10.0); 242 | 243 | assert_eq!(delay.antenna_cable_delay, 10.0); 244 | assert_eq!(delay.local_ref_delay, 0.0); 245 | 246 | let delay = SystemDelay::default() 247 | .with_antenna_cable_delay(10.0) 248 | .with_ref_delay(20.0); 249 | 250 | assert_eq!(delay.antenna_cable_delay, 10.0); 251 | assert_eq!(delay.local_ref_delay, 20.0); 252 | assert_eq!(delay.total_cable_delay_nanos(), 30.0); 253 | 254 | assert_eq!(delay.antenna_cable_delay, 10.0); 255 | assert_eq!(delay.local_ref_delay, 20.0); 256 | 257 | assert!(delay 258 | .total_frequency_dependent_delay_nanos(&Code::C1) 259 | .is_none()); 260 | 261 | for (k, v) in delay.frequency_dependent_nanos_delay_iter() { 262 | assert_eq!(*k, Code::C1); 263 | assert_eq!(v, 80.0); 264 | } 265 | 266 | assert!(delay 267 | .total_frequency_dependent_delay_nanos(&Code::P1) 268 | .is_none()); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/header/formatting.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | buffer::Utf8Buffer, 3 | errors::FormattingError, 4 | prelude::{Header, Version}, 5 | }; 6 | 7 | use std::io::{BufWriter, Write}; 8 | 9 | impl Header { 10 | /// Formats this [CGGTTS] following standard specifications. 11 | pub fn format( 12 | &self, 13 | writer: &mut BufWriter, 14 | buf: &mut Utf8Buffer, 15 | ) -> Result<(), FormattingError> { 16 | // clear potential past residues 17 | buf.clear(); 18 | 19 | buf.push_str(&format!( 20 | "CGGTTS GENERIC DATA FORMAT VERSION = {}\n", 21 | Version::Version2E, 22 | )); 23 | 24 | let (y, m, d, _, _, _, _) = self.revision_date.to_gregorian_utc(); 25 | buf.push_str(&format!("REV DATE = {:04}-{:02}-{:02}\n", y, m, d)); 26 | buf.push_str(&format!("RCVR = {:x}\n", &self.receiver)); 27 | buf.push_str(&format!("CH = {}\n", self.nb_channels)); 28 | 29 | if let Some(ims) = &self.ims_hardware { 30 | buf.push_str(&format!("IMS = {:x}\n", ims)); 31 | } 32 | 33 | buf.push_str(&format!("LAB = {}\n", self.station)); 34 | 35 | buf.push_str(&format!("X = {:12.3} m\n", self.apc_coordinates.x)); 36 | buf.push_str(&format!("Y = {:12.3} m\n", self.apc_coordinates.y)); 37 | buf.push_str(&format!("Z = {:12.3} m\n", self.apc_coordinates.z)); 38 | buf.push_str(&format!("FRAME = {}\n", self.reference_frame)); 39 | 40 | if let Some(comments) = &self.comments { 41 | buf.push_str(&format!("COMMENTS = {}\n", comments.trim())); 42 | } else { 43 | buf.push_str(&format!("COMMENTS = NO COMMENTS\n")); 44 | } 45 | 46 | // TODO system delay formatting 47 | // let delays = self.delay.delays.clone(); 48 | // let constellation = if !self.tracks.is_empty() { 49 | // self.tracks[0].sv.constellation 50 | // } else { 51 | // Constellation::default() 52 | // }; 53 | 54 | // if delays.len() == 1 { 55 | // // Single frequency 56 | // let (code, value) = delays[0]; 57 | // match value { 58 | // Delay::Internal(v) => { 59 | // content.push_str(&format!( 60 | // "INT DLY = {:.1} ns ({:X} {})\n", 61 | // v, constellation, code 62 | // )); 63 | // }, 64 | // Delay::System(v) => { 65 | // content.push_str(&format!( 66 | // "SYS DLY = {:.1} ns ({:X} {})\n", 67 | // v, constellation, code 68 | // )); 69 | // }, 70 | // } 71 | // if let Some(cal_id) = &self.delay.cal_id { 72 | // content.push_str(&format!(" CAL_ID = {}\n", cal_id)); 73 | // } else { 74 | // content.push_str(" CAL_ID = NA\n"); 75 | // } 76 | // } else if delays.len() == 2 { 77 | // // Dual frequency 78 | // let (c1, v1) = delays[0]; 79 | // let (c2, v2) = delays[1]; 80 | // match v1 { 81 | // Delay::Internal(_) => { 82 | // content.push_str(&format!( 83 | // "INT DLY = {:.1} ns ({:X} {}), {:.1} ns ({:X} {})\n", 84 | // v1.value(), 85 | // constellation, 86 | // c1, 87 | // v2.value(), 88 | // constellation, 89 | // c2 90 | // )); 91 | // }, 92 | // Delay::System(_) => { 93 | // content.push_str(&format!( 94 | // "SYS DLY = {:.1} ns ({:X} {}), {:.1} ns ({:X} {})\n", 95 | // v1.value(), 96 | // constellation, 97 | // c1, 98 | // v2.value(), 99 | // constellation, 100 | // c2 101 | // )); 102 | // }, 103 | // } 104 | // if let Some(cal_id) = &self.delay.cal_id { 105 | // content.push_str(&format!(" CAL_ID = {}\n", cal_id)); 106 | // } else { 107 | // content.push_str(" CAL_ID = NA\n"); 108 | // } 109 | // } 110 | 111 | buf.push_str(&format!( 112 | "CAB DLY = {:05.1} ns\n", 113 | self.delay.antenna_cable_delay, 114 | )); 115 | 116 | buf.push_str(&format!( 117 | "REF DLY = {:05.1} ns\n", 118 | self.delay.local_ref_delay 119 | )); 120 | 121 | buf.push_str(&format!("REF = {}\n", self.reference_time)); 122 | 123 | // push last bytes contributing to CRC 124 | buf.push_str("CKSUM = "); 125 | 126 | // Run CK calculation 127 | let ck = buf.calculate_crc(); 128 | 129 | // Append CKSUM 130 | buf.push_str(&format!("{:02X}\n", ck)); 131 | 132 | // interprate 133 | let ascii_utf8 = buf.to_utf8_ascii()?; 134 | 135 | // forward to user 136 | write!(writer, "{}", ascii_utf8)?; 137 | 138 | Ok(()) 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod test { 144 | 145 | use std::io::BufWriter; 146 | use std::path::Path; 147 | 148 | use crate::{buffer::Utf8Buffer, CGGTTS}; 149 | 150 | #[test] 151 | fn header_crc_buffering() { 152 | let mut utf8 = Utf8Buffer::new(1024); 153 | let mut buf = BufWriter::new(Utf8Buffer::new(1024)); 154 | 155 | // This file does not have comments 156 | // run once 157 | let path = Path::new(env!("CARGO_MANIFEST_DIR")) 158 | .join("data/CGGTTS") 159 | .join("EZGTR60.258"); 160 | 161 | let cggtts = CGGTTS::from_file(path).unwrap(); 162 | 163 | let header = &cggtts.header; 164 | 165 | assert_eq!(header.receiver.manufacturer, "GTR51"); 166 | assert_eq!(header.receiver.model, "2204005"); 167 | 168 | let ims = header.ims_hardware.as_ref().expect("missing IMS"); 169 | assert_eq!(ims.manufacturer, "GTR51"); 170 | assert_eq!(ims.model, "2204005"); 171 | 172 | header.format(&mut buf, &mut utf8).unwrap(); 173 | 174 | let inner = buf.into_inner().unwrap_or_else(|_| panic!("oops")); 175 | let ascii_utf8 = inner.to_utf8_ascii().expect("generated invalid utf-8!"); 176 | 177 | // TODO: missing 178 | // INT DLY = 34.6 ns (GAL E1), 0.0 ns (GAL E5), 0.0 ns (GAL E6), 0.0 ns (GAL E5b), 25.6 ns (GAL E5a) CAL_ID = 1015-2021 179 | 180 | let expected = "CGGTTS GENERIC DATA FORMAT VERSION = 2E 181 | REV DATE = 2023-06-27 182 | RCVR = GTR51 2204005 1.12.0 0 183 | CH = 20 184 | IMS = GTR51 2204005 1.12.0 0 185 | LAB = LAB 186 | X = 3970727.800 m 187 | Y = 1018888.020 m 188 | Z = 4870276.840 m 189 | FRAME = FRAME 190 | COMMENTS = NO COMMENTS 191 | CAB DLY = 155.2 ns 192 | REF DLY = 000.0 ns 193 | REF = REF_IN 194 | CKSUM = 53"; 195 | 196 | for (content, expected) in ascii_utf8.lines().zip(expected.lines()) { 197 | assert_eq!(content, expected); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/header/hardware.rs: -------------------------------------------------------------------------------- 1 | /// [Hardware] is used to describe a piece of equipment. 2 | /// Usually the GNSS receiver. 3 | #[derive(Clone, PartialEq, Debug, Default)] 4 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 5 | pub struct Hardware { 6 | /// Model type. 7 | pub model: String, 8 | /// Manufacturer 9 | pub manufacturer: String, 10 | /// Readable serial number. 11 | pub serial_number: String, 12 | /// Year of production or release 13 | pub year: u16, 14 | /// Software or firmware version 15 | pub release: String, 16 | } 17 | 18 | impl Hardware { 19 | /// Define a new [Hardware] with desired model name 20 | pub fn with_model(&self, model: &str) -> Self { 21 | let mut s = self.clone(); 22 | s.model = model.to_string(); 23 | s 24 | } 25 | 26 | /// Define a new [Hardware] with desired manufacturer 27 | pub fn with_manufacturer(&self, manufacturer: &str) -> Self { 28 | let mut s = self.clone(); 29 | s.manufacturer = manufacturer.to_string(); 30 | s 31 | } 32 | 33 | /// Define a new [Hardware] with desired serial number 34 | pub fn with_serial_number(&self, serial_number: &str) -> Self { 35 | let mut s = self.clone(); 36 | s.serial_number = serial_number.to_string(); 37 | s 38 | } 39 | 40 | /// Define a new [Hardware] with desired year of production 41 | /// or release. 42 | pub fn with_release_year(&self, y: u16) -> Self { 43 | let mut s = self.clone(); 44 | s.year = y; 45 | s 46 | } 47 | 48 | /// Define a new [Hardware] with desired firmware or 49 | /// software release version. 50 | pub fn with_release_version(&self, version: &str) -> Self { 51 | let mut s = self.clone(); 52 | s.release = version.to_string(); 53 | s 54 | } 55 | } 56 | 57 | impl std::fmt::LowerHex for Hardware { 58 | /// Formats [Hardware] as used in a CGGTTS header. 59 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 60 | fmt.write_str(&format!( 61 | "{} {} {} {} {}", 62 | self.manufacturer, self.model, self.serial_number, self.year, self.release 63 | )) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod test { 69 | use crate::header::hardware::Hardware; 70 | 71 | #[test] 72 | fn hardware_parsing() { 73 | let hw = Hardware::default() 74 | .with_manufacturer("TEST") 75 | .with_model("MODEL") 76 | .with_release_year(2024) 77 | .with_serial_number("1234") 78 | .with_release_version("v00"); 79 | 80 | assert_eq!(format!("{:x}", hw), "TEST MODEL 1234 2024 v00"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/header/mod.rs: -------------------------------------------------------------------------------- 1 | mod code; 2 | mod delay; 3 | mod formatting; 4 | mod hardware; 5 | mod parsing; 6 | mod reference_time; 7 | mod version; 8 | 9 | #[cfg(docsrs)] 10 | use crate::prelude::CGGTTS; 11 | 12 | pub use crate::header::{ 13 | code::Code, 14 | delay::{CalibrationID, Delay, SystemDelay}, 15 | hardware::Hardware, 16 | reference_time::ReferenceTime, 17 | version::Version, 18 | }; 19 | 20 | use crate::prelude::{Epoch, TimeScale}; 21 | 22 | #[derive(PartialEq, Debug, Clone, Copy, Default)] 23 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 24 | pub struct Coordinates { 25 | pub x: f64, 26 | pub y: f64, 27 | pub z: f64, 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 32 | pub struct Header { 33 | /// CGGTTS [Version] used at production time of this [CGGTTS]. 34 | pub version: Version, 35 | /// Date this [Header] was revised. Updated each 36 | /// time a new field appears. 37 | pub revision_date: Epoch, 38 | /// Station name, usually the data producer (agency, laboratory..). 39 | pub station: String, 40 | /// Information about GNSS receiver 41 | pub receiver: Hardware, 42 | /// # of channels this GNSS receiver possesses 43 | pub nb_channels: u16, 44 | /// Possible Ionospheric Measurement System (IMS) information. 45 | /// Should always be attached to multi channel / modern [CGGTTS]. 46 | pub ims_hardware: Option, 47 | /// [ReferenceTime] used in the solving process of each [Track] 48 | pub reference_time: ReferenceTime, 49 | /// Name of the ECEF Coordinates system in which the APC 50 | /// [Coordinates] are expressed in. 51 | pub reference_frame: String, 52 | /// Antenna Phase Center (APC) coordinates in meters 53 | pub apc_coordinates: Coordinates, 54 | /// Short readable comments (if any) 55 | pub comments: Option, 56 | /// Measurement [SystemDelay] 57 | pub delay: SystemDelay, 58 | } 59 | 60 | impl Default for Header { 61 | fn default() -> Self { 62 | let version = Version::default(); 63 | Self { 64 | version, 65 | station: String::from("LAB"), 66 | nb_channels: Default::default(), 67 | apc_coordinates: Default::default(), 68 | receiver: Default::default(), 69 | ims_hardware: Default::default(), 70 | comments: Default::default(), 71 | delay: Default::default(), 72 | reference_time: Default::default(), 73 | reference_frame: Default::default(), 74 | revision_date: Epoch::from_gregorian_utc_at_midnight(2014, 2, 20), 75 | } 76 | } 77 | } 78 | 79 | impl Header { 80 | /// Returns [Header] with desired station name 81 | pub fn with_station(&self, station: &str) -> Self { 82 | let mut c = self.clone(); 83 | c.station = station.to_string(); 84 | c 85 | } 86 | 87 | /// Adds readable comments to this [Header]. 88 | /// Try to keep it short, because it will eventually be 89 | /// wrapped in a single line. 90 | pub fn with_comment(&self, comment: &str) -> Self { 91 | let mut s = self.clone(); 92 | s.comments = Some(comment.to_string()); 93 | s 94 | } 95 | 96 | /// Returns a new [Header] with desired number of channels. 97 | pub fn with_channels(&self, ch: u16) -> Self { 98 | let mut c = self.clone(); 99 | c.nb_channels = ch; 100 | c 101 | } 102 | 103 | /// Returns a new [Header] with [Hardware] information about 104 | /// the GNSS receiver. 105 | pub fn with_receiver_hardware(&self, receiver: Hardware) -> Self { 106 | let mut c = self.clone(); 107 | c.receiver = receiver; 108 | c 109 | } 110 | 111 | /// Returns a new [Header] with [Hardware] information about 112 | /// the device that help estimate the Ionosphere parameters. 113 | pub fn with_ims_hardware(&self, ims: Hardware) -> Self { 114 | let mut c = self.clone(); 115 | c.ims_hardware = Some(ims); 116 | c 117 | } 118 | 119 | /// Returns new [Header] with desired APC coordinates in ECEF. 120 | pub fn with_apc_coordinates(&self, apc: Coordinates) -> Self { 121 | let mut c = self.clone(); 122 | c.apc_coordinates = apc; 123 | c 124 | } 125 | 126 | /// Returns new [Header] with [TimeScale::UTC] reference system time. 127 | pub fn with_utc_reference_time(&self) -> Self { 128 | let mut c = self.clone(); 129 | c.reference_time = TimeScale::UTC.into(); 130 | c 131 | } 132 | 133 | /// Returns new [Header] with desired [ReferenceTime] system 134 | pub fn with_reference_time(&self, reference: ReferenceTime) -> Self { 135 | let mut c = self.clone(); 136 | c.reference_time = reference; 137 | c 138 | } 139 | 140 | /// Returns new [Header] with desired Reference Frame 141 | pub fn with_reference_frame(&self, reference: &str) -> Self { 142 | let mut c = self.clone(); 143 | c.reference_frame = reference.to_string(); 144 | c 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/header/parsing.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParsingError, 3 | header::{CalibrationID, Code, Coordinates, Delay, SystemDelay}, 4 | prelude::{Epoch, Hardware, Header, ReferenceTime, Version}, 5 | }; 6 | 7 | use scan_fmt::scan_fmt; 8 | 9 | use std::{ 10 | io::{BufRead, BufReader, Read}, 11 | str::FromStr, 12 | }; 13 | 14 | fn parse_header_version(s: &str) -> Result { 15 | const MARKER: &str = "CGGTTS GENERIC DATA FORMAT VERSION = "; 16 | const SIZE: usize = MARKER.len(); 17 | 18 | if !s.starts_with(MARKER) { 19 | return Err(ParsingError::VersionFormat); 20 | }; 21 | 22 | let content = s[SIZE..].trim(); 23 | let version = Version::from_str(&content)?; 24 | Ok(version) 25 | } 26 | 27 | fn parse_header_date(s: &str) -> Result { 28 | const SIZE: usize = "REV DATE = ".len(); 29 | 30 | let t = Epoch::from_format_str(s[SIZE..].trim(), "%Y-%m-%d") 31 | .or(Err(ParsingError::RevisionDateFormat))?; 32 | 33 | Ok(t) 34 | } 35 | 36 | fn parse_hardware(s: &str) -> Result { 37 | let mut hw = Hardware::default(); 38 | 39 | for (i, item) in s.split_ascii_whitespace().enumerate() { 40 | if i == 0 { 41 | hw.manufacturer = item.trim().to_string(); 42 | } else if i == 1 { 43 | hw.model = item.trim().to_string(); 44 | } else if i == 2 { 45 | hw.serial_number = item.trim().to_string(); 46 | } else if i == 3 { 47 | hw.year = item 48 | .trim() 49 | .parse::() 50 | .or(Err(ParsingError::InvalidFormat))?; 51 | } else if i == 4 { 52 | hw.release = item.trim().to_string(); 53 | } 54 | } 55 | 56 | Ok(hw) 57 | } 58 | 59 | impl Header { 60 | /// Parse [Header] from any [Read]able input. 61 | pub fn parse(reader: &mut BufReader) -> Result { 62 | const CKSUM_PATTERN: &str = "CKSUM = "; 63 | const CKSUM_LEN: usize = CKSUM_PATTERN.len(); 64 | 65 | let mut lines_iter = reader.lines(); 66 | 67 | // init variables 68 | let mut crc = 0u8; 69 | let mut system_delay = SystemDelay::default(); 70 | 71 | let (mut blank, mut field_labels, mut unit_labels) = (false, false, false); 72 | 73 | let mut revision_date = Epoch::default(); 74 | let mut nb_channels: u16 = 0; 75 | 76 | let mut receiver = Hardware::default(); 77 | let mut ims_hardware: Option = None; 78 | 79 | let mut station = String::from("LAB"); 80 | 81 | let mut comments: Option = None; 82 | let mut reference_frame = String::with_capacity(16); 83 | let mut apc_coordinates = Coordinates::default(); 84 | 85 | let mut reference_time = ReferenceTime::default(); 86 | 87 | // VERSION must come first 88 | let first_line = lines_iter.next().ok_or(ParsingError::VersionFormat)?; 89 | let first_line = first_line.map_err(|_| ParsingError::VersionFormat)?; 90 | let version = parse_header_version(&first_line)?; 91 | 92 | // calculate first CRC contributions 93 | for byte in first_line.as_bytes().iter() { 94 | if *byte != b'\r' && *byte != b'\n' { 95 | crc = crc.wrapping_add(*byte); 96 | } 97 | } 98 | 99 | for line in lines_iter { 100 | if line.is_err() { 101 | continue; 102 | } 103 | 104 | let line = line.unwrap(); 105 | let line_len = line.len(); 106 | 107 | // CRC contribution 108 | let crc_max = if line.starts_with(CKSUM_PATTERN) { 109 | CKSUM_LEN 110 | } else { 111 | line_len 112 | }; 113 | 114 | for byte in line.as_bytes()[..crc_max].iter() { 115 | if *byte != b'\r' && *byte != b'\n' { 116 | crc = crc.wrapping_add(*byte); 117 | } 118 | } 119 | 120 | if line.starts_with("REV DATE = ") { 121 | revision_date = parse_header_date(&line)?; 122 | } else if line.starts_with("RCVR = ") { 123 | receiver = parse_hardware(&line[7..])?; 124 | } else if line.starts_with("IMS = ") { 125 | ims_hardware = Some(parse_hardware(&line[6..])?); 126 | } else if line.starts_with("CH = ") { 127 | nb_channels = line[5..] 128 | .trim() 129 | .parse::() 130 | .or(Err(ParsingError::ChannelNumber))?; 131 | } else if line.starts_with("LAB = ") { 132 | station = line[5..].trim().to_string(); 133 | } else if line.starts_with("X = ") { 134 | apc_coordinates.x = line[3..line_len - 1] 135 | .trim() 136 | .parse::() 137 | .or(Err(ParsingError::Coordinates))?; 138 | } else if line.starts_with("Y = ") { 139 | apc_coordinates.y = line[3..line_len - 1] 140 | .trim() 141 | .parse::() 142 | .or(Err(ParsingError::Coordinates))?; 143 | } else if line.starts_with("Z = ") { 144 | apc_coordinates.z = line[3..line_len - 1] 145 | .trim() 146 | .parse::() 147 | .or(Err(ParsingError::Coordinates))?; 148 | } else if line.starts_with("FRAME = ") { 149 | reference_frame = line[8..].trim().to_string(); 150 | } else if line.starts_with("COMMENTS = ") { 151 | let c = line.strip_prefix("COMMENTS =").unwrap().trim(); 152 | if !c.eq("NO COMMENTS") { 153 | comments = Some(c.to_string()); 154 | } 155 | } else if line.starts_with("REF = ") { 156 | reference_time = line[5..].trim().parse::()?; 157 | } else if line.contains("DLY = ") { 158 | let items: Vec<&str> = line.split_ascii_whitespace().collect(); 159 | 160 | let dual_carrier = line.contains(','); 161 | 162 | if items.len() < 4 { 163 | continue; // format mismatch 164 | } 165 | 166 | match items[0] { 167 | "CAB" => { 168 | system_delay.antenna_cable_delay = items[3] 169 | .trim() 170 | .parse::() 171 | .or(Err(ParsingError::AntennaCableDelay))?; 172 | }, 173 | "REF" => { 174 | system_delay.local_ref_delay = items[3] 175 | .trim() 176 | .parse::() 177 | .or(Err(ParsingError::LocalRefDelay))?; 178 | }, 179 | "SYS" => { 180 | if line.contains("CAL_ID") { 181 | let offset = line.rfind('=').ok_or(ParsingError::CalibrationFormat)?; 182 | 183 | if let Ok(cal_id) = CalibrationID::from_str(&line[offset + 1..]) { 184 | system_delay = system_delay.with_calibration_id(cal_id); 185 | } 186 | } 187 | 188 | if dual_carrier { 189 | if let Ok(value) = f64::from_str(items[3]) { 190 | let code = items[6].replace("),", ""); 191 | if let Ok(code) = Code::from_str(&code) { 192 | system_delay 193 | .freq_dependent_delays 194 | .push((code, Delay::System(value))); 195 | } 196 | } 197 | if let Ok(value) = f64::from_str(items[7]) { 198 | let code = items[9].replace(')', ""); 199 | if let Ok(code) = Code::from_str(&code) { 200 | system_delay 201 | .freq_dependent_delays 202 | .push((code, Delay::System(value))); 203 | } 204 | } 205 | } else { 206 | let value = f64::from_str(items[3]).unwrap(); 207 | let code = items[6].replace(')', ""); 208 | if let Ok(code) = Code::from_str(&code) { 209 | system_delay 210 | .freq_dependent_delays 211 | .push((code, Delay::System(value))); 212 | } 213 | } 214 | }, 215 | "INT" => { 216 | if line.contains("CAL_ID") { 217 | let offset = line.rfind('=').ok_or(ParsingError::CalibrationFormat)?; 218 | 219 | if let Ok(cal_id) = CalibrationID::from_str(&line[offset + 1..]) { 220 | system_delay = system_delay.with_calibration_id(cal_id); 221 | } 222 | } 223 | 224 | if dual_carrier { 225 | if let Ok(value) = f64::from_str(items[3]) { 226 | let code = items[6].replace("),", ""); 227 | if let Ok(code) = Code::from_str(&code) { 228 | system_delay 229 | .freq_dependent_delays 230 | .push((code, Delay::Internal(value))); 231 | } 232 | } 233 | if let Ok(value) = f64::from_str(items[7]) { 234 | let code = items[10].replace(')', ""); 235 | if let Ok(code) = Code::from_str(&code) { 236 | system_delay 237 | .freq_dependent_delays 238 | .push((code, Delay::Internal(value))); 239 | } 240 | } 241 | } else if let Ok(value) = f64::from_str(items[3]) { 242 | let code = items[6].replace(')', ""); 243 | if let Ok(code) = Code::from_str(&code) { 244 | system_delay 245 | .freq_dependent_delays 246 | .push((code, Delay::Internal(value))); 247 | } 248 | } 249 | }, 250 | "TOT" => { 251 | if line.contains("CAL_ID") { 252 | let offset = line.rfind('=').ok_or(ParsingError::CalibrationFormat)?; 253 | 254 | if let Ok(cal_id) = CalibrationID::from_str(&line[offset + 1..]) { 255 | system_delay = system_delay.with_calibration_id(cal_id); 256 | } 257 | } 258 | 259 | if dual_carrier { 260 | if let Ok(value) = f64::from_str(items[3]) { 261 | let code = items[6].replace("),", ""); 262 | if let Ok(code) = Code::from_str(&code) { 263 | system_delay 264 | .freq_dependent_delays 265 | .push((code, Delay::System(value))); 266 | } 267 | } 268 | if let Ok(value) = f64::from_str(items[7]) { 269 | let code = items[9].replace(')', ""); 270 | if let Ok(code) = Code::from_str(&code) { 271 | system_delay 272 | .freq_dependent_delays 273 | .push((code, Delay::System(value))); 274 | } 275 | } 276 | } else if let Ok(value) = f64::from_str(items[3]) { 277 | let code = items[6].replace(')', ""); 278 | if let Ok(code) = Code::from_str(&code) { 279 | system_delay 280 | .freq_dependent_delays 281 | .push((code, Delay::System(value))); 282 | } 283 | } 284 | }, 285 | _ => {}, // non recognized delay type 286 | }; 287 | } else if line.starts_with("CKSUM = ") { 288 | // CRC verification 289 | let value = match scan_fmt!(&line, "CKSUM = {x}", String) { 290 | Some(s) => match u8::from_str_radix(&s, 16) { 291 | Ok(hex) => hex, 292 | _ => return Err(ParsingError::ChecksumParsing), 293 | }, 294 | _ => return Err(ParsingError::ChecksumFormat), 295 | }; 296 | 297 | if value != crc { 298 | return Err(ParsingError::ChecksumValue); 299 | } 300 | 301 | // CKSUM initiates the end of header section 302 | blank = true; 303 | } else if blank { 304 | // Field labels expected next 305 | blank = false; 306 | field_labels = true; 307 | } else if field_labels { 308 | // Unit labels expected next 309 | field_labels = false; 310 | unit_labels = true; 311 | } else if unit_labels { 312 | // last line that concludes this section 313 | break; 314 | } 315 | } 316 | 317 | Ok(Self { 318 | version, 319 | revision_date, 320 | nb_channels, 321 | receiver, 322 | ims_hardware, 323 | station, 324 | reference_frame, 325 | apc_coordinates, 326 | comments, 327 | delay: system_delay, 328 | reference_time, 329 | }) 330 | } 331 | } 332 | 333 | #[cfg(test)] 334 | mod test { 335 | use super::{parse_hardware, parse_header_date, parse_header_version}; 336 | use crate::prelude::Version; 337 | use hifitime::Epoch; 338 | 339 | #[test] 340 | fn version_parsing() { 341 | for (content, version) in [( 342 | "CGGTTS GENERIC DATA FORMAT VERSION = 2E", 343 | Version::Version2E, 344 | )] { 345 | let parsed = parse_header_version(content).unwrap(); 346 | assert_eq!(parsed, version); 347 | } 348 | } 349 | 350 | #[test] 351 | fn date_parsing() { 352 | for (content, date) in [( 353 | "REV DATE = 2023-06-27", 354 | Epoch::from_gregorian_utc_at_midnight(2023, 06, 27), 355 | )] { 356 | let parsed = parse_header_date(content).unwrap(); 357 | assert_eq!(parsed, date); 358 | } 359 | } 360 | 361 | #[test] 362 | fn hardware_parsing() { 363 | for (content, manufacturer, model, serial) in 364 | [("GTR51 2204005 1.12.0", "GTR51", "2204005", "1.12.0")] 365 | { 366 | let parsed = parse_hardware(content).unwrap(); 367 | assert_eq!(parsed.manufacturer, manufacturer); 368 | assert_eq!(parsed.model, model); 369 | assert_eq!(parsed.serial_number, serial); 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/header/reference_time.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ParsingError; 2 | use hifitime::TimeScale; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Reference Time System 8 | #[derive(Clone, PartialEq, Debug)] 9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 10 | pub enum ReferenceTime { 11 | /// TAI: Temps Atomic International 12 | TAI, 13 | /// UTC: Universal Coordinate Time 14 | UTC, 15 | /// UTC(k) laboratory local copy 16 | UTCk(String), 17 | /// Custom Reference time system 18 | Custom(String), 19 | } 20 | 21 | impl Default for ReferenceTime { 22 | fn default() -> Self { 23 | Self::UTC 24 | } 25 | } 26 | 27 | impl std::str::FromStr for ReferenceTime { 28 | type Err = ParsingError; 29 | fn from_str(s: &str) -> Result { 30 | if s.eq("TAI") { 31 | Ok(Self::TAI) 32 | } else if s.eq("UTC") { 33 | Ok(Self::UTC) 34 | } else if s.starts_with("UTC(") && s.ends_with(')') { 35 | let len = s.len(); 36 | let utc_k = &s[4..len - 1]; 37 | Ok(Self::UTCk(utc_k.to_string())) 38 | } else { 39 | Ok(Self::Custom(s.to_string())) 40 | } 41 | } 42 | } 43 | 44 | impl From for ReferenceTime { 45 | fn from(ts: TimeScale) -> Self { 46 | match ts { 47 | TimeScale::UTC => Self::UTC, 48 | TimeScale::TAI => Self::TAI, 49 | _ => Self::TAI, /* incorrect usage */ 50 | } 51 | } 52 | } 53 | 54 | impl std::fmt::Display for ReferenceTime { 55 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 56 | match self { 57 | Self::TAI => fmt.write_str("TAI"), 58 | Self::UTC => fmt.write_str("UTC"), 59 | Self::UTCk(lab) => write!(fmt, "UTC({})", lab), 60 | Self::Custom(s) => fmt.write_str(s), 61 | } 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod test { 67 | use super::ReferenceTime; 68 | use std::str::FromStr; 69 | #[test] 70 | fn from_str() { 71 | assert_eq!(ReferenceTime::default(), ReferenceTime::UTC); 72 | assert_eq!(ReferenceTime::from_str("TAI").unwrap(), ReferenceTime::TAI); 73 | assert_eq!(ReferenceTime::from_str("UTC").unwrap(), ReferenceTime::UTC); 74 | assert_eq!( 75 | ReferenceTime::from_str("UTC(LAB-X)").unwrap(), 76 | ReferenceTime::UTCk(String::from("LAB-X")) 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/header/version.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ParsingError; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord, Hash)] 7 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 8 | pub enum Version { 9 | #[default] 10 | Version2E, 11 | } 12 | 13 | impl std::str::FromStr for Version { 14 | type Err = ParsingError; 15 | fn from_str(s: &str) -> Result { 16 | if s.eq("2E") { 17 | Ok(Self::Version2E) 18 | } else { 19 | Err(ParsingError::NonSupportedRevision) 20 | } 21 | } 22 | } 23 | 24 | impl std::fmt::Display for Version { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | Self::Version2E => write!(f, "2E"), 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod test { 34 | use crate::prelude::Version; 35 | use std::str::FromStr; 36 | 37 | #[test] 38 | fn version_parsing() { 39 | let version_2e = Version::from_str("2E").unwrap(); 40 | assert_eq!(version_2e.to_string(), "2E"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_logo_url = "https://raw.githubusercontent.com/rtk-rs/.github/master/logos/logo2.jpg")] 2 | #![doc = include_str!("../README.md")] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | 5 | /* 6 | * CGGTTS is part of the rtk-rs framework. 7 | * Authors: Guillaume W. Bres et al. 8 | * (cf. https://github.com/rtk-rs/cggtts/graphs/contributors) 9 | * This framework is shipped under Mozilla Public V2 license. 10 | * 11 | * Documentation: https://github.com/rtk-rs/cggtts 12 | */ 13 | 14 | extern crate gnss_rs as gnss; 15 | 16 | use gnss::prelude::{Constellation, SV}; 17 | use hifitime::{Duration, Epoch, TimeScale}; 18 | 19 | use std::{ 20 | fs::File, 21 | io::{BufRead, BufReader, BufWriter, Read, Write}, 22 | path::Path, 23 | str::FromStr, 24 | }; 25 | 26 | #[cfg(feature = "flate2")] 27 | use flate2::{read::GzDecoder, write::GzEncoder, Compression as GzCompression}; 28 | 29 | mod header; 30 | 31 | #[cfg(feature = "scheduler")] 32 | #[cfg_attr(docsrs, doc(cfg(feature = "scheduler")))] 33 | mod scheduler; 34 | 35 | #[cfg(feature = "tracker")] 36 | #[cfg_attr(docsrs, doc(cfg(feature = "tracker")))] 37 | mod tracker; 38 | 39 | #[cfg(test)] 40 | mod tests; 41 | 42 | pub mod buffer; 43 | pub mod errors; 44 | pub mod track; 45 | 46 | #[cfg(feature = "serde")] 47 | #[macro_use] 48 | extern crate serde; 49 | 50 | pub mod prelude { 51 | 52 | pub use crate::{ 53 | header::*, 54 | track::{CommonViewClass, IonosphericData, Track, TrackData}, 55 | CGGTTS, 56 | }; 57 | 58 | #[cfg(feature = "scheduler")] 59 | pub use crate::scheduler::{calendar::CommonViewCalendar, period::CommonViewPeriod}; 60 | 61 | #[cfg(feature = "tracker")] 62 | pub use crate::tracker::{FitError, FittedData, Observation, SVTracker, SkyTracker}; 63 | 64 | // pub re-export 65 | pub use gnss::prelude::{Constellation, SV}; 66 | pub use hifitime::prelude::{Duration, Epoch, TimeScale}; 67 | } 68 | 69 | #[cfg(feature = "serde")] 70 | use serde::{Deserialize, Serialize}; 71 | 72 | use crate::{ 73 | buffer::Utf8Buffer, 74 | errors::{FormattingError, ParsingError}, 75 | header::{Header, ReferenceTime}, 76 | track::{CommonViewClass, Track}, 77 | }; 78 | 79 | /// [CGGTTS] is a structure split in two: 80 | /// - the [Header] section gives general information 81 | /// about the measurement system and context 82 | /// - [Track]s, ofen times referred to as measurements, 83 | /// describe the behavior of the measurement system's local clock 84 | /// with resepect to satellite onboard clocks. Each [Track] 85 | /// was solved by tracking satellites individually. 86 | /// NB: Correct [CGGTTS] only contain [Track]s of the same [Constellation]. 87 | /// 88 | /// Remote (measurement systems) clock comparison is then allowed by 89 | /// exchanging remote [CGGTTS] (from both sites), and comparing synchronous 90 | /// (on both sites) [Track]s referring to identical satellite vehicles. 91 | /// This is called the common view time transfer technique. 92 | #[derive(Debug, Default, Clone)] 93 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 94 | pub struct CGGTTS { 95 | /// [Header] gives general information 96 | pub header: Header, 97 | /// [Track]s describe the result of track fitting, 98 | /// in chronological order. 99 | pub tracks: Vec, 100 | } 101 | 102 | impl CGGTTS { 103 | /// Returns true if all [Track]s (measurements) seems compatible 104 | /// with the [CommonViewPeriod] recommended by BIPM. 105 | /// This cannot be a complete confirmation, because only the receiver 106 | /// that generated this data knows if [Track] collection and fitting 107 | /// was implemented correctly. 108 | pub fn follows_bipm_tracking(&self) -> bool { 109 | for track in self.tracks.iter() { 110 | if !track.follows_bipm_tracking() { 111 | return false; 112 | } 113 | } 114 | true 115 | } 116 | 117 | /// Returns true if all tracks (measurements) contained in this 118 | /// [CGGTTS] have ionospheric parameters estimate. 119 | pub fn has_ionospheric_data(&self) -> bool { 120 | for track in self.tracks.iter() { 121 | if !track.has_ionospheric_data() { 122 | return false; 123 | } 124 | } 125 | true 126 | } 127 | 128 | /// Returns [CommonViewClass] used in this file. 129 | /// ## Returns 130 | /// - [CommonViewClass::MultiChannel] if at least one track (measurement) 131 | /// is [CommonViewClass::MultiChannel] measurement 132 | /// - [CommonViewClass::SingleChannel] if all tracks (measurements) 133 | /// are [CommonViewClass::SingleChannel] measurements 134 | pub fn common_view_class(&self) -> CommonViewClass { 135 | for trk in self.tracks.iter() { 136 | if trk.class != CommonViewClass::SingleChannel { 137 | return CommonViewClass::MultiChannel; 138 | } 139 | } 140 | CommonViewClass::SingleChannel 141 | } 142 | 143 | /// Returns true if this [CGGTTS] is a single channel [CGGTTS], 144 | /// meaning, all tracks (measurements) are [CommonViewClass::SingleChannel] measurements 145 | pub fn single_channel(&self) -> bool { 146 | self.common_view_class() == CommonViewClass::SingleChannel 147 | } 148 | 149 | /// Returns true if this [CGGTTS] is a single channel [CGGTTS], 150 | /// meaning, all tracks (measurements) are [CommonViewClass::MultiChannel] measurements 151 | pub fn multi_channel(&self) -> bool { 152 | self.common_view_class() == CommonViewClass::MultiChannel 153 | } 154 | 155 | /// Returns true if this is a [Constellation::GPS] [CGGTTS]. 156 | /// Meaning, all measurements [Track]ed this constellation. 157 | pub fn is_gps_cggtts(&self) -> bool { 158 | if let Some(first) = self.tracks.first() { 159 | first.sv.constellation == Constellation::GPS 160 | } else { 161 | false 162 | } 163 | } 164 | 165 | /// Returns true if this is a [Constellation::Galileo] [CGGTTS]. 166 | /// Meaning, all measurements [Track]ed this constellation. 167 | pub fn is_galileo_cggtts(&self) -> bool { 168 | if let Some(first) = self.tracks.first() { 169 | first.sv.constellation == Constellation::Galileo 170 | } else { 171 | false 172 | } 173 | } 174 | 175 | /// Returns true if this is a [Constellation::BeiDou] [CGGTTS]. 176 | /// Meaning, all measurements [Track]ed this constellation. 177 | pub fn is_beidou_cggtts(&self) -> bool { 178 | if let Some(first) = self.tracks.first() { 179 | first.sv.constellation == Constellation::BeiDou 180 | } else { 181 | false 182 | } 183 | } 184 | 185 | /// Returns true if this is a [Constellation::Glonass] [CGGTTS]. 186 | /// Meaning, all measurements[Track]ed this constellation. 187 | pub fn is_glonass_cggtts(&self) -> bool { 188 | if let Some(first) = self.tracks.first() { 189 | first.sv.constellation == Constellation::Glonass 190 | } else { 191 | false 192 | } 193 | } 194 | 195 | /// Returns true if this is a [Constellation::QZSS] [CGGTTS]. 196 | /// Meaning, all measurements[Track]ed this constellation. 197 | pub fn is_qzss_cggtts(&self) -> bool { 198 | if let Some(first) = self.tracks.first() { 199 | first.sv.constellation == Constellation::QZSS 200 | } else { 201 | false 202 | } 203 | } 204 | 205 | /// Returns true if this is a [Constellation::IRNSS] [CGGTTS]. 206 | /// Meaning, all measurements [Track]ed this constellation. 207 | pub fn is_irnss_cggtts(&self) -> bool { 208 | if let Some(first) = self.tracks.first() { 209 | first.sv.constellation == Constellation::IRNSS 210 | } else { 211 | false 212 | } 213 | } 214 | 215 | /// Returns true if this is a [Constellation::SBAS] [CGGTTS]. 216 | /// Meaning, all measurements[Track]ed geostationary vehicles. 217 | pub fn is_sbas_cggtts(&self) -> bool { 218 | if let Some(first) = self.tracks.first() { 219 | first.sv.constellation.is_sbas() 220 | } else { 221 | false 222 | } 223 | } 224 | 225 | /// Returns [Track]s (measurements) Iterator 226 | pub fn tracks_iter(&self) -> impl Iterator { 227 | self.tracks.iter() 228 | } 229 | 230 | /// Iterate over [Track]s (measurements) that result from tracking 231 | /// this particular [SV] only. 232 | pub fn sv_tracks(&self, sv: SV) -> impl Iterator { 233 | self.tracks 234 | .iter() 235 | .filter_map(move |trk| if trk.sv == sv { Some(trk) } else { None }) 236 | } 237 | 238 | /// Returns first Epoch contained in this file. 239 | pub fn first_epoch(&self) -> Option { 240 | self.tracks.first().map(|trk| trk.epoch) 241 | } 242 | 243 | /// Returns last Epoch contained in this file. 244 | pub fn last_epoch(&self) -> Option { 245 | self.tracks.last().map(|trk| trk.epoch) 246 | } 247 | 248 | /// Returns total [Duration] of this [CGGTTS]. 249 | pub fn total_duration(&self) -> Duration { 250 | if let Some(t1) = self.last_epoch() { 251 | if let Some(t0) = self.first_epoch() { 252 | return t1 - t0; 253 | } 254 | } 255 | Duration::ZERO 256 | } 257 | 258 | /// Generates a standardized file name that would describes 259 | /// this [CGGTTS] correctly according to naming conventions. 260 | /// This method is infaillible, but might generate incomplete 261 | /// results. In particular, this [CGGTTS] should not be empty 262 | /// and must contain [Track]s measurements for this to work correctly. 263 | /// ## Inputs 264 | /// - custom_lab: Possible LAB ID overwrite and customization. 265 | /// Two characters are expected here, the result will not 266 | /// respect the standard convention if you provide less. 267 | /// When not defined, we use the LAB ID that was previously parsed. 268 | /// - custom_id: Possible GNSS RX identification number 269 | /// or whatever custom ID number you desire. 270 | /// Two characters are expected here, the result will not 271 | /// respect the standard convention if you provide less. 272 | /// When not defined, we use the first two digits of the serial number 273 | /// that was previously parsed. 274 | pub fn standardized_file_name( 275 | &self, 276 | custom_lab: Option<&str>, 277 | custom_id: Option<&str>, 278 | ) -> String { 279 | let mut ret = String::new(); 280 | 281 | // Grab first letter of constellation 282 | if let Some(first) = self.tracks.first() { 283 | ret.push_str(&format!("{:x}", first.sv.constellation)); 284 | } else { 285 | ret.push('X'); 286 | } 287 | 288 | // Second letter depends on channelling capabilities 289 | if self.has_ionospheric_data() { 290 | ret.push('Z') // Dual Freq / Multi channel 291 | } else if self.single_channel() { 292 | ret.push('S') // Single Freq / Channel 293 | } else { 294 | ret.push('M') // Single Freq / Multi Channel 295 | } 296 | 297 | // LAB / Agency 298 | if let Some(custom_lab) = custom_lab { 299 | let size = std::cmp::min(custom_lab.len(), 2); 300 | ret.push_str(&custom_lab[0..size]); 301 | } else { 302 | let size = std::cmp::min(self.header.station.len(), 2); 303 | ret.push_str(&self.header.station[0..size]); 304 | } 305 | 306 | // GNSS RX / SN 307 | if let Some(custom_id) = custom_id { 308 | let size = std::cmp::min(custom_id.len(), 2); 309 | ret.push_str(&custom_id[..size]); 310 | } else { 311 | let size = std::cmp::min(self.header.receiver.serial_number.len(), 2); 312 | ret.push_str(&self.header.receiver.serial_number[..size]); 313 | } 314 | 315 | if let Some(epoch) = self.first_epoch() { 316 | let mjd = epoch.to_mjd_utc_days(); 317 | ret.push_str(&format!("{:02.3}", (mjd / 1000.0))); 318 | } else { 319 | ret.push_str("dd.ddd"); 320 | } 321 | 322 | ret 323 | } 324 | 325 | /// Parse [CGGTTS] from a local file. 326 | /// Advanced CGGTTS files generated from modern GNSS 327 | /// receivers may describe the ionospheric delay compensation: 328 | /// ``` 329 | /// use cggtts::prelude::CGGTTS; 330 | /// 331 | /// let cggtts = CGGTTS::from_file("data/CGGTTS/GZGTR560.258") 332 | /// .unwrap(); 333 | /// 334 | /// if let Some(track) = cggtts.tracks.first() { 335 | /// assert_eq!(track.has_ionospheric_data(), true); 336 | /// if let Some(iono) = track.iono { 337 | /// let (msio, smsi, isg) = (iono.msio, iono.smsi, iono.isg); 338 | /// } 339 | /// } 340 | ///``` 341 | pub fn from_file>(path: P) -> Result { 342 | let fd = File::open(path).unwrap_or_else(|e| panic!("File open error: {}", e)); 343 | 344 | let mut reader = BufReader::new(fd); 345 | Self::parse(&mut reader) 346 | } 347 | 348 | /// Parse a new [CGGTTS] from any [Read]able interface. 349 | /// This will fail on: 350 | /// - Any critical standard violation 351 | /// - If file revision is not 2E (latest) 352 | /// - If following [Track]s do not contain the same [Constellation] 353 | pub fn parse(reader: &mut BufReader) -> Result { 354 | // Parse header section 355 | let header = Header::parse(reader)?; 356 | 357 | // Parse tracks: 358 | // consumes all remaning lines and attempt parsing on each new line. 359 | // Line CRC is internally verified for each line. 360 | // We abort if Constellation content is not constant, as per standard conventions. 361 | let mut tracks = Vec::with_capacity(16); 362 | let lines = reader.lines(); 363 | 364 | let mut constellation = Option::::None; 365 | 366 | for line in lines { 367 | if line.is_err() { 368 | continue; 369 | } 370 | 371 | let line = line.unwrap(); 372 | 373 | if let Ok(track) = Track::from_str(&line) { 374 | // constellation content verification 375 | if let Some(constellation) = &constellation { 376 | if track.sv.constellation != *constellation { 377 | return Err(ParsingError::MixedConstellation); 378 | } 379 | } else { 380 | constellation = Some(track.sv.constellation); 381 | } 382 | 383 | tracks.push(track); 384 | } 385 | } 386 | 387 | Ok(Self { header, tracks }) 388 | } 389 | 390 | /// Parse [CGGTTS] from gzip compressed local path. 391 | #[cfg(feature = "flate2")] 392 | #[cfg_attr(docsrs, doc(cfg(feature = "flate2")))] 393 | pub fn from_gzip_file>(path: P) -> Result { 394 | let fd = File::open(path).unwrap_or_else(|e| panic!("File open error: {}", e)); 395 | 396 | let reader = GzDecoder::new(fd); 397 | 398 | let mut reader = BufReader::new(reader); 399 | Self::parse(&mut reader) 400 | } 401 | 402 | /// Format [CGGTTS] following standard specifications. 403 | /// 404 | /// To produce valid advanced CGGTTS, one should specify: 405 | /// - IMS [Hardware] 406 | /// - ionospheric parameters 407 | /// - System delay definitions, per signal carrier 408 | /// 409 | /// ``` 410 | /// use std::io::Write; 411 | /// 412 | /// use cggtts::prelude::{ 413 | /// CGGTTS, 414 | /// Header, 415 | /// Hardware, Coordinates, 416 | /// Track, TrackData, 417 | /// SV, Epoch, Duration, 418 | /// CommonViewClass, 419 | /// }; 420 | /// 421 | /// let rcvr = Hardware::default() 422 | /// .with_manufacturer("SEPTENTRIO") 423 | /// .with_model("POLARRx5") 424 | /// .with_serial_number("#12345") 425 | /// .with_release_year(2023) 426 | /// .with_release_version("v1"); 427 | /// 428 | /// // form a header 429 | /// let header = Header::default() 430 | /// .with_station("AJACFR") 431 | /// .with_receiver_hardware(rcvr); 432 | /// 433 | /// // Although CGGTTS intends ITRF high precision 434 | /// // frames, you may use whatever you can or need 435 | /// let header = header 436 | /// .with_reference_frame("ITRF"); 437 | /// 438 | /// // It is best practice to specify the APC coordinates 439 | /// // (obviously in previous reference frame) 440 | /// let header = header 441 | /// .with_apc_coordinates(Coordinates { 442 | /// x: 1.0_f64, 443 | /// y: 2.0_f64, 444 | /// z: 3.0_f64, 445 | /// }); 446 | /// 447 | /// // Create a [CGGTTS] 448 | /// let mut cggtts = CGGTTS::default() 449 | /// .with_header(header); 450 | /// 451 | /// // Tracking context 452 | /// let epoch = Epoch::default(); 453 | /// let sv = SV::default(); 454 | /// let (elevation, azimuth) = (0.0_f64, 0.0_f64); 455 | /// let duration = Duration::from_seconds(780.0); 456 | /// 457 | /// // receiver channel being used 458 | /// let rcvr_channel = 0_u8; 459 | /// 460 | /// // TrackData is always mandatory (for each track) 461 | /// let data = TrackData { 462 | /// refsv: 0.0_f64, 463 | /// srsv: 0.0_f64, 464 | /// refsys: 0.0_f64, 465 | /// srsys: 0.0_f64, 466 | /// dsg: 0.0_f64, 467 | /// ioe: 0_u16, 468 | /// smdt: 0.0_f64, 469 | /// mdtr: 0.0_f64, 470 | /// mdio: 0.0_f64, 471 | /// smdi: 0.0_f64, 472 | /// }; 473 | /// 474 | /// // option 1: track resulting from a single SV observation 475 | /// let track = Track::new( 476 | /// sv, 477 | /// epoch, 478 | /// duration, 479 | /// CommonViewClass::SingleChannel, 480 | /// elevation, 481 | /// azimuth, 482 | /// data, 483 | /// None, 484 | /// rcvr_channel, // receiver channel 485 | /// "L1C", 486 | /// ); 487 | /// 488 | /// cggtts.tracks.push(track); 489 | /// 490 | /// // option 2: track resulting from multi channel SV observation 491 | /// let track = Track::new( 492 | /// sv, 493 | /// epoch, 494 | /// duration, 495 | /// CommonViewClass::MultiChannel, 496 | /// elevation, 497 | /// azimuth, 498 | /// data, 499 | /// None, 500 | /// rcvr_channel, // receiver channel 501 | /// "L1C", 502 | /// ); 503 | /// 504 | /// cggtts.tracks.push(track); 505 | /// 506 | /// // option 3: when working with Glonass, use the dedicated method 507 | /// let track = Track::new_glonass( 508 | /// sv, 509 | /// epoch, 510 | /// duration, 511 | /// CommonViewClass::SingleChannel, 512 | /// elevation, 513 | /// azimuth, 514 | /// data, 515 | /// None, 516 | /// rcvr_channel, // receiver channel 517 | /// 1, // FDMA channel 518 | /// "C1P", 519 | /// ); 520 | /// 521 | /// // produce CGGTTS 522 | /// cggtts.to_file("/tmp/test.txt") 523 | /// .unwrap(); 524 | /// ``` 525 | pub fn format(&self, writer: &mut BufWriter) -> Result<(), FormattingError> { 526 | const TRACK_LABELS_WITH_IONOSPHERIC_DATA: &str = 527 | "SAT CL MJD STTIME TRKL ELV AZTH REFSV SRSV REFSYS SRSYS DSG IOE MDTR SMDT MDIO SMDI MSIO SMSI ISG FR HC FRC CK"; 528 | 529 | const UNIT_LABELS_WITH_IONOSPHERIC : &str = " hhmmss s .1dg .1dg .1ns .1ps/s .1ns .1ps/s .1ns .1ns.1ps/s.1ns.1ps/s.1ns.1ps/s.1ns"; 530 | 531 | const TRACK_LABELS_WITHOUT_IONOSPHERIC_DATA: &str = 532 | "SAT CL MJD STTIME TRKL ELV AZTH REFSV SRSV REFSYS SRSYS DSG IOE MDTR SMDT MDIO SMDI FR HC FRC CK"; 533 | 534 | const UNIT_LABELS_WITHOUT_IONOSPHERIC :&str = " hhmmss s .1dg .1dg .1ns .1ps/s .1ns .1ps/s .1ns .1ns.1ps/s.1ns.1ps/s"; 535 | 536 | // create local (tiny) Utf-8 buffer 537 | let mut buf = Utf8Buffer::new(1024); 538 | 539 | // format header 540 | self.header.format(writer, &mut buf)?; 541 | 542 | // BLANK at end of header section 543 | write!(writer, "\n")?; 544 | 545 | // format track labels 546 | if self.has_ionospheric_data() { 547 | writeln!(writer, "{}", TRACK_LABELS_WITH_IONOSPHERIC_DATA)?; 548 | writeln!(writer, "{}", UNIT_LABELS_WITH_IONOSPHERIC,)?; 549 | } else { 550 | writeln!(writer, "{}", TRACK_LABELS_WITHOUT_IONOSPHERIC_DATA)?; 551 | writeln!(writer, "{}", UNIT_LABELS_WITHOUT_IONOSPHERIC)?; 552 | } 553 | 554 | // format all tracks 555 | for track in self.tracks.iter() { 556 | track.format(writer, &mut buf)?; 557 | write!(writer, "\n")?; 558 | } 559 | 560 | Ok(()) 561 | } 562 | 563 | /// Writes this [CGGTTS] into readable local file 564 | pub fn to_file>(&self, path: P) -> Result<(), FormattingError> { 565 | let fd = File::create(path)?; 566 | let mut writer = BufWriter::new(fd); 567 | self.format(&mut writer) 568 | } 569 | 570 | /// Writes this [CGGTTS] into gzip compressed local file 571 | #[cfg(feature = "flate2")] 572 | pub fn to_gzip_file>(&self, path: P) -> Result<(), FormattingError> { 573 | let fd = File::create(path)?; 574 | let compression = GzCompression::new(5); 575 | let mut writer = BufWriter::new(GzEncoder::new(fd, compression)); 576 | self.format(&mut writer) 577 | } 578 | 579 | /// Returns a new [CGGTTS] ready to track in [TimeScale::UTC]. 580 | /// This is the most (most) general use case, for the simple reason 581 | /// that UTC is a worldwide constant, hence, allows worldwide common-view. 582 | /// You can use our other method for exotic contexts. 583 | /// NB: use this prior solving any [Track]s, otherwise 584 | /// it will corrupt previously solved content, because 585 | /// it does not perform the time shift for you. 586 | pub fn with_utc_reference_time(&self) -> Self { 587 | let mut s = self.clone(); 588 | s.header = s.header.with_reference_time(TimeScale::UTC.into()); 589 | s 590 | } 591 | 592 | /// Returns a new [CGGTTS] ready to track in custom UTC-replica. 593 | /// Use this method when setting up a [CGGTTS] production context. 594 | /// NB(1): we differentiate UTC-replica (unofficial or local UTC) 595 | /// from custom reference time system (exotic or private), 596 | /// for which you have [Self::with_custom_reference_time]. 597 | /// NB(2): use this prior solving any [Track]s, otherwise 598 | /// it will corrupt previously solved content, because 599 | /// it does not perform the time shift for you. 600 | /// ## Inputs 601 | /// - name: name of your UTC replica (also referred to, as UTCk). 602 | pub fn with_utc_replica_reference_time(&self, name: &str) -> Self { 603 | let mut s = self.clone(); 604 | s.header = s 605 | .header 606 | .with_reference_time(ReferenceTime::UTCk(name.to_string())); 607 | s 608 | } 609 | 610 | /// Returns a new [CGGTTS] ready to track in [TimeScale::TAI]. 611 | /// Use this method when setting up a [CGGTTS] production context. 612 | /// NB: use this prior solving any [Track]s, otherwise 613 | /// it will corrupt previously solved content, because 614 | /// it does not perform the time shift for you. 615 | pub fn with_tai_reference_time(&self) -> Self { 616 | let mut s = self.clone(); 617 | s.header = s.header.with_reference_time(TimeScale::TAI.into()); 618 | s 619 | } 620 | 621 | /// Returns a new [CGGTTS] ready to track in custom timescale 622 | /// (either exotic, privately owned..). 623 | /// Use this method when setting up a [CGGTTS] production context. 624 | /// NB(1): we differentiate custom reference time systems from 625 | /// UTC-replica (unofficial or local UTC), 626 | /// for which you have [Self::with_utc_replica_reference_time]. 627 | /// NB(2): use this prior solving any [Track]s, otherwise 628 | /// it will corrupt previously solved content, because 629 | /// it does not perform the time shift for you. 630 | /// ## Inputs 631 | /// - name: name of your custom timescale 632 | pub fn with_custom_reference_time(&self, name: &str) -> Self { 633 | let mut s = self.clone(); 634 | s.header = s 635 | .header 636 | .with_reference_time(ReferenceTime::UTCk(name.to_string())); 637 | s 638 | } 639 | 640 | /// Copies and returns new [CGGTTS] with updated [Header] section. 641 | pub fn with_header(&self, header: Header) -> Self { 642 | let mut s = self.clone(); 643 | s.header = header; 644 | s 645 | } 646 | 647 | /// Copies and returns new [CGGTTS] with updated [Track] list. 648 | pub fn with_tracks(&self, tracks: Vec) -> Self { 649 | let mut s = self.clone(); 650 | s.tracks = tracks; 651 | s 652 | } 653 | } 654 | -------------------------------------------------------------------------------- /src/processing.rs: -------------------------------------------------------------------------------- 1 | //! Set of methods to compute CGGTTS data and 2 | //! produce tracks 3 | 4 | /// Speed of light in [m/s] 5 | const SPEED_OF_LIGHT: f64 = 300_000_000.0_f64; 6 | 7 | /// SAGNAC correction associated with Earth rotation 8 | const SAGNAC_CORRECTION: f64 = 0.0_f64; 9 | 10 | /// Refractivity Index @ seal level 11 | const NS: f64 = 324.8_f64; 12 | 13 | #[derive(Debug, PartialEq, Copy, Clone)] 14 | pub struct Vec3D { 15 | x: f64, 16 | y: f64, 17 | z: f64, 18 | } 19 | 20 | impl Default for Vec3D { 21 | fn default() -> Self { 22 | Self { 23 | x: 0.0, 24 | y: 0.0, 25 | z: 0.0, 26 | } 27 | } 28 | } 29 | 30 | impl Vec3D { 31 | pub fn norm(&self) -> f64 { 32 | (self.x.powf(2.0) + self.y.powf(2.0) + self.z.powf(2.0)).sqrt() 33 | } 34 | } 35 | 36 | impl std::ops::Sub for Vec3D { 37 | type Output = Vec3D; 38 | fn sub(self, rhs: Vec3D) -> Vec3D { 39 | Vec3D { 40 | x: self.x - rhs.x, 41 | y: self.y - rhs.y, 42 | z: self.z - rhs.z, 43 | } 44 | } 45 | } 46 | 47 | pub enum Policy { 48 | /// Simple straight forward processing, 49 | /// see [p6: Data processing paragraph] 50 | Simple, 51 | /// Use n tap smoothing. 52 | /// This feature is not needed when using a 53 | /// modern GNSS receiver 54 | Smoothing(u32), 55 | } 56 | 57 | pub struct Params { 58 | /// Raw measurements 59 | pr: f64, 60 | /// Current elevation [°] 61 | e: f64, 62 | /// Current altitude [km] 63 | h: f64, 64 | /// Current Sv vector 65 | x_sat: Vec3D, 66 | /// Broadcast satellite clock offset 67 | t_sat: f64, 68 | /// reference timescale 69 | t_ref: f64, 70 | /// Current Rcvr vector 71 | x_rec: Vec3D, 72 | /// Carrier dependent delay 73 | delay: f64, 74 | /// RF delay 75 | rf_delay: f64, 76 | /// REF delay 77 | ref_delay: f64, 78 | /// Group delay 79 | grp_delay: f64, 80 | } 81 | 82 | /// Computes dn constant 83 | fn dn() -> f64 { 84 | -7.32 * (0.005577 * NS).exp() 85 | } 86 | 87 | fn nslog() -> f64 { 88 | (NS + dn() / 105.0).ln() 89 | } 90 | 91 | /// Computes R_h quantity [eq(8)] Tropospheric delay at zenith, 92 | /// from space vehicule altitude in [km] 93 | fn r_h(altitude: f64) -> f64 { 94 | let dn = dn(); 95 | let nslog = nslog(); 96 | if altitude < 1.0 { 97 | (2162.0 + NS * (1.0 - altitude) + 0.5 * dn * (1.0 - altitude.powf(2.0))) * 10E-3 98 | / SPEED_OF_LIGHT 99 | } else { 100 | let frac = (NS + dn) / nslog; 101 | let e_1 = (-nslog).exp(); 102 | let e_2 = (0.125 * (1.0 - altitude) * nslog).exp(); 103 | (732.0 - (8.0 * frac * (e_1 - e_2))) * 10E-3 / SPEED_OF_LIGHT 104 | } 105 | } 106 | 107 | /// Computes f_e 108 | /// - e: elevation [°] 109 | fn f_e(e: f64) -> f64 { 110 | 1.0 / (e.sin() + 0.00143 / (e.tan() + 0.0455)) 111 | } 112 | 113 | /// Relativistic delay 114 | fn dt_rel() -> f64 { 115 | 0.0 116 | } 117 | 118 | /// Ionospheric delay 119 | fn dt_iono() -> f64 { 120 | 0.0 121 | } 122 | 123 | /// Inputs: 124 | /// - pr: raw measurement 125 | /// - x_sat: current Sv vector 126 | /// - x_rec: rcvr estimate 127 | /// - h: altitude in km 128 | /// - e: elevation in ° 129 | /// 130 | /// Returns 131 | /// - dt_sat : [eq(2)] 132 | /// - dt_ref : [eq(7)] 133 | /// - dt_tropo : [eq(6)] 134 | /// - dt_iono : [eq(5)] 135 | pub fn process(data: Params) -> (f64, f64, f64) { 136 | // compensation 137 | let p = data.pr - SPEED_OF_LIGHT * (data.delay + data.rf_delay - data.ref_delay); 138 | let fe = f_e(data.e); 139 | let rh = r_h(data.h); 140 | let dt_tropo = fe * rh; 141 | let d_tclk_tsat = 1.0 / SPEED_OF_LIGHT 142 | * (p - (data.x_sat - data.x_rec).norm() - SAGNAC_CORRECTION) 143 | + dt_rel() 144 | - dt_iono() 145 | - dt_tropo 146 | - data.grp_delay; 147 | let d_tclk_tref = d_tclk_tsat + data.t_sat - data.t_ref; 148 | (d_tclk_tsat, d_tclk_tref, dt_tropo) 149 | } 150 | 151 | /* 152 | /// Computes f(elevation) [eq(7)] neded by NATO hydrostatic model 153 | fn f_elev (elevation: f64) -> { 154 | 1.0 / (0.000143 / (e.tan() +0.0455) + e.sin()) 155 | } 156 | 157 | /// Computes delta troposphere using NATO hydrostatic model [eq(6)] 158 | fn d_tropo (elevation: f64, altitude: f64) -> { 159 | f_elev(elevation) * R_h(altitude) 160 | } 161 | 162 | /// Call this once per cycle 163 | /// to process a new symbol. 164 | /// Compensations & computations are then performed internally 165 | /// 166 | /// # Input: 167 | /// - pr: raw pseudo range 168 | /// - x_sat: 3D vehicule position estimate in IRTF system (must be IRTF!) 169 | /// - x_rec: 3D receiver position estimate in IRTF system (must be IRTF!) 170 | /// - dt_rel_corr : relativistic clock correction for space vehicule 171 | /// redshift along its orbit 172 | /// - iono_dt: carrier dependent ionospheric delay 173 | /// - dtropo: troposphere induced delay 174 | /// - grp_delay: broadcast group delay 175 | pub fn process (&mut self, pr: f64) { 176 | let p = symbol - SPEED_OF_LIGHT * (self.delay.value() + self.cab_delay - self.ref_delay); 177 | self.buffer.push(p) 178 | } 179 | 180 | pub fn run (&mut self, elevation: f64, 181 | x_sat: (f64,f64,f64), x_rec: (f64,f64,f64), dt_rel_corr: f64, 182 | iono_dt: f64, dtropo: f64, grp_delay: f64) 183 | { 184 | } 185 | 186 | pub fn next() 187 | self.buffer.push(p); 188 | dt = 189 | } 190 | 191 | /* 192 | pub struct Scheduler { 193 | now: chrono::NaiveDateTime; 194 | pub trk_duration: std::time::Duration, 195 | } 196 | 197 | impl Iterator for Scheduler { 198 | type Item = bool; 199 | } 200 | 201 | pub struct Scheduler { 202 | /// TrackGen policy 203 | pub processing : Policy, 204 | /// Current work date 205 | day: chrono::NaiveDate, 206 | /// should match BIPM recommendations, 207 | /// but custom values can be used (shortest tracking in particular) 208 | trk_duration: std::time::Duration, 209 | /// Scheduled events for today 210 | events: Vec, 211 | /// System delays 212 | /// Only single frequency generation supported @ the moment 213 | delay: delay::SystemDelay, 214 | /// Internal data buffer 215 | p : Vec, 216 | } 217 | 218 | impl Scheduler { 219 | /// Builds a new measurement scheduler, 220 | /// Inputs: 221 | /// day: optionnal work day, otherwise uses `now()` 222 | /// 223 | /// trk_duration: optionnal custom tracking duration, 224 | /// defaults to `BIPM_RECOMMENDED_TRACKING` 225 | pub fn new (day: chrono::NaiveDate, trk_duration: std::time::Duration) -> Self { 226 | let day = day.unwrap_or(chrono::Utc::now().naive_utc().date()); 227 | let duration = trk_duration.unwrap_or(BIPM_RECOMMENDED_TRACKING); 228 | //let events = Scheduler::events(day, duration); 229 | Self { 230 | day, 231 | trk_duration, 232 | events: Vec::new(), 233 | } 234 | } 235 | 236 | /* 237 | /// Returns scheduled measurements for given day, 238 | /// if date is not provided, we use now() 239 | pub fn scheduled_events (&self, date: Option) -> Vec { 240 | let mut res : Vec = Vec::new(); 241 | 242 | /// Call this once day has changed to reset internal FSM 243 | pub fn new_day (&mut self) { 244 | //self.day = chrono::Utc::now().naive_utc().date(); 245 | //self.events = Scheduler::events(self.day, self.duration); 246 | } 247 | 248 | /// Updates tracking duration to new value 249 | pub fn update_trk_duration (&mut self, new: std::time::Duration) { 250 | self.duration = new 251 | } 252 | 253 | /// Returns scheduled measurements for given day, 254 | /// if date is not provided, we use now() 255 | pub fn events (&self, date: Option) -> Vec { 256 | /*let mut res : Vec = Vec::new(); 257 | >>>>>>> Stashed changes 258 | let mjd_ref = ModifiedJulianDay::new(REFERENCE_MJD).inner(); 259 | let date = date.unwrap_or(chrono::Utc::now().naive_utc().date()); 260 | let mjd = ModifiedJulianDay::from(date).inner(); 261 | for i in 1..self.tracks_in_24h()-1 { 262 | let offset = Scheduler::time_ref(self.n) as i32 - 4*(mjd_ref - mjd)/60; 263 | if offset > 0 { 264 | let h = offset / 3600; 265 | let m = (offset - h*3600)/60; 266 | let s = offset -h*3600 - m*60; 267 | res.push( 268 | chrono::NaiveDate::from_ymd(date.year(), date.month(), date.day()) 269 | .and_hms(h as u32 ,m as u32,s as u32)); 270 | } 271 | } 272 | res*/ 273 | Vec::new() 274 | } 275 | */ 276 | /// Returns duration (time interval) between given date 277 | /// and next scheduled measurement 278 | pub fn time_to_next (&self, datetime: chrono::NaiveDateTime) -> std::time::Duration { 279 | //let offset = Scheduler::time_ref(self.n); 280 | std::time::Duration::from_secs(10) 281 | } 282 | 283 | /// Returns offset in seconds during the course of `MJD_REF` 284 | /// reference Modified Julian Day (defined in standards), 285 | /// for given nth observation within that day. 286 | /// 287 | /// Input: 288 | /// - observation: observation counter 289 | fn time_ref (observation: u32) -> u32 { 290 | 60 * 2 + (observation -1)*16*60 291 | } 292 | 293 | /// Returns number of measurements to perform within 24hours 294 | fn tracks_in_24h (&self) -> u64 { 295 | 24 * 3600 / self.duration.as_secs() 296 | } 297 | } 298 | 299 | #[cfg(test)] 300 | mod test { 301 | use super::*; 302 | use chrono::{NaiveDate, NaiveDateTime}; 303 | #[test] 304 | fn test_scheduler_basic() { 305 | let t0 = chrono::NaiveDate::from_ymd(2022, 07, 05) 306 | .and_hms(00, 00, 00); 307 | let scheduler = Scheduler::new(Some(t0), None); 308 | } 309 | }*/ 310 | */ 311 | -------------------------------------------------------------------------------- /src/scheduler/calendar.rs: -------------------------------------------------------------------------------- 1 | //! Common View Planification table 2 | use crate::scheduler::period::{CommonViewPeriod, BIPM_REFERENCE_MJD}; 3 | use hifitime::prelude::{Duration, Epoch, TimeScale, Unit}; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | #[error("an integral number of cv-periods must fit within a day")] 9 | UnalignedCvPeriod, 10 | } 11 | 12 | /// [CommonViewCalendar] is a serie of evenly spaced [CommonViewPeriod]s. 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub struct CommonViewCalendar { 15 | /// Reference [Epoch]. In historical CGGTTS, this is 16 | /// MJD 50_722 + 2'. 17 | reference_epoch: Epoch, 18 | /// Reference [Epoch] rounded to midnight that day 19 | reference_epoch_mjd_midnight: u32, 20 | /// Holds the possible number of nanoseconds offset from Reference [Epoch] midnight 21 | reference_epoch_midnight_offset_nanos: i128, 22 | /// Number of integral [CommonViewPeriod]s per day 23 | periods_per_day: u16, 24 | /// Abitrary Daily offset. In historical CGGTTS, this is -4' 25 | /// to remain aligned with GPS sideral period. 26 | daily_offset: Duration, 27 | /// [CommonViewPeriod] specifications. 28 | period: CommonViewPeriod, 29 | } 30 | 31 | impl CommonViewCalendar { 32 | /// Design a new [CommonViewCalendar] to plan your common view measurements 33 | /// and CGGTTS realizations. 34 | /// 35 | /// ## Input 36 | /// - reference_epoch: reference [Epoch] used in the scheduling process. 37 | /// In [Self::bipm], this is MJD 50_722 00:00:02. 38 | /// 39 | /// - period: [CommonViewPeriod] specifications. 40 | /// The total [CommonViewPeriod] must be a perfect multiple of a day, 41 | /// we do not support a fractional number of daily periods. 42 | /// In [Self::bipm], this is [CommonViewPeriod::bipm_common_view_period]. 43 | pub fn new(reference_epoch: Epoch, period: CommonViewPeriod) -> Result { 44 | let total_duration = period.total_duration().to_seconds(); 45 | let one_day = Duration::from_days(1.0).to_seconds(); 46 | 47 | let r = one_day / total_duration; 48 | let periods_per_day = r.floor() as u16; 49 | 50 | if r.fract() == 0.0 { 51 | let reference_mjd_midnight = reference_epoch.round(1.0 * Unit::Day); 52 | 53 | let reference_epoch_midnight_offset_nanos = 54 | (reference_epoch - reference_mjd_midnight).total_nanoseconds(); 55 | 56 | Ok(Self { 57 | period, 58 | periods_per_day, 59 | reference_epoch, 60 | daily_offset: Duration::ZERO, 61 | reference_epoch_mjd_midnight: { 62 | reference_mjd_midnight.to_mjd_utc_days().floor() as u32 63 | }, 64 | reference_epoch_midnight_offset_nanos, 65 | }) 66 | } else { 67 | Err(Error::UnalignedCvPeriod) 68 | } 69 | } 70 | 71 | /// Builds the standardized [CommonViewCalendar] 72 | /// following the CGGTTS historical specifications: 73 | /// 74 | /// - aligned to 50_722 MJD + 2' 75 | /// - -4' daily offset to remain aligned to GPS sideral time 76 | /// - 16' periods made of 13' data collection following 77 | /// a 3' warmup phase that is repeated at the beginning of each period. 78 | /// 79 | /// ``` 80 | /// use cggtts::prelude::CommonViewCalendar; 81 | /// 82 | /// let bipm_calendar = CommonViewCalendar::bipm(); 83 | /// assert_eq!(bipm_calendar.periods_per_day(), 90); 84 | /// ``` 85 | pub fn bipm() -> Self { 86 | Self::bipm_unaliged_gps_sideral().with_daily_offset(Duration::from_seconds(-240.0)) 87 | } 88 | 89 | /// Creates the standardized [CommonViewCalendar] 90 | /// following the CGGTTS historical specifications, except that 91 | /// it is not aligned to GPS sideral time. 92 | /// 93 | /// - aligned to 50_722 MJD + 2' 94 | /// - 16' periods made of 13' data collection following 95 | /// a 3' warmup phase that is repeated at the beginning of each period. 96 | /// 97 | /// ``` 98 | /// use cggtts::prelude::CommonViewCalendar; 99 | /// 100 | /// let bipm_calendar = CommonViewCalendar::bipm_unaliged_gps_sideral(); 101 | /// assert_eq!(bipm_calendar.periods_per_day(), 90); 102 | /// ``` 103 | pub fn bipm_unaliged_gps_sideral() -> Self { 104 | let period = CommonViewPeriod::bipm_common_view_period(); 105 | 106 | let reference_epoch = Epoch::from_mjd_utc(BIPM_REFERENCE_MJD as f64) + 120.0 * Unit::Second; 107 | 108 | Self::new(reference_epoch, period).unwrap() 109 | } 110 | 111 | /// Returns the total number of complete [CommonViewPeriod]s per day. 112 | pub const fn periods_per_day(&self) -> u16 { 113 | self.periods_per_day 114 | } 115 | 116 | /// Returns the total duration from the [CommonViewPeriod] specifications. 117 | pub fn total_period_duration(&self) -> Duration { 118 | self.period.total_duration() 119 | } 120 | 121 | /// Add a Daily offset to this [CommonViewCalendar]. 122 | /// A default [CommonViewCalendar] is calculated from the original reference point. 123 | /// It is possible to add (either positive or negative) offset to apply each day. 124 | /// Strict CGGTTS uses a -4' offset to remain aligned to GPS sideral time. 125 | pub fn with_daily_offset(&self, offset: Duration) -> Self { 126 | let mut s = self.clone(); 127 | s.daily_offset = offset; 128 | s 129 | } 130 | 131 | /// Returns true if a daily offset is defined 132 | fn has_daily_offset(&self) -> bool { 133 | self.daily_offset != Duration::ZERO 134 | } 135 | 136 | // Returns offset (in nanoseconds) of the i-th CV period starting point 137 | // for that MJD. 138 | fn period_start_offset_nanos(&self, mjd: u32, ith: u16) -> i128 { 139 | // compute offset to reference epoch 140 | let total_duration_nanos = self.period.total_duration().total_nanoseconds(); 141 | let mut offset_nanos = ith as i128 * total_duration_nanos; 142 | 143 | // offset from midnight (if any) 144 | offset_nanos += self.reference_epoch_midnight_offset_nanos; 145 | 146 | // daily shift (if any) 147 | if self.has_daily_offset() { 148 | let days_offset = mjd as i128 - self.reference_epoch_mjd_midnight as i128; 149 | offset_nanos += self.daily_offset.total_nanoseconds() * days_offset as i128; 150 | } 151 | 152 | offset_nanos %= total_duration_nanos; 153 | 154 | if offset_nanos.is_negative() { 155 | offset_nanos += total_duration_nanos; 156 | } 157 | 158 | offset_nanos 159 | } 160 | 161 | // Returns daily offset (in nanoseconds) of the very first period of that MJD. 162 | fn first_start_offset_nanos(&self, mjd: u32) -> i128 { 163 | self.period_start_offset_nanos(mjd, 0) 164 | } 165 | 166 | /// Returns datetime (as [Epoch]) of next [CommonViewPeriod] after 167 | /// specified [Epoch]. Although CGGTTS is scheduled in and aligned 168 | /// to [TimeScale::UTC], we tolerate other timescales here. 169 | pub fn next_period_start_after(&self, t: Epoch) -> Epoch { 170 | let ts = t.time_scale; 171 | let utc = ts == TimeScale::UTC; 172 | let period_duration = self.period.total_duration(); 173 | 174 | let t_utc = if utc { 175 | t 176 | } else { 177 | t.to_time_scale(TimeScale::UTC) 178 | }; 179 | 180 | // determine time to next midnight 181 | let mjd = t_utc.to_mjd_utc_days().floor() as u32; 182 | 183 | let mjd_midnight = Epoch::from_mjd_utc(mjd as f64); 184 | let mjd_next_midnight = Epoch::from_mjd_utc((mjd + 1) as f64); 185 | 186 | // offset of first period of that day 187 | let offset_nanos = self.first_start_offset_nanos(mjd) as f64; 188 | 189 | // first track of day 190 | let t0_utc = mjd_midnight + offset_nanos as f64 * Unit::Nanosecond; 191 | 192 | // last track of day 193 | let tn_utc = t0_utc 194 | + ((self.periods_per_day - 1) as i128 * period_duration.total_nanoseconds()) as f64 195 | * Unit::Nanosecond; 196 | 197 | let t_utc = if t_utc < t0_utc { 198 | // Propose first track of day 199 | t0_utc 200 | } else if t_utc == t0_utc { 201 | // Propose second track of day 202 | t0_utc + period_duration 203 | } else if t_utc > tn_utc { 204 | // Propose first track of day +1 205 | mjd_next_midnight + self.first_start_offset_nanos(mjd + 1) as f64 * Unit::Nanosecond 206 | } else { 207 | // compute period index 208 | let mut i = (t_utc - t0_utc).total_nanoseconds() as f64; 209 | i /= period_duration.total_nanoseconds() as f64; 210 | 211 | // Handles case where we're at the edge of a period 212 | let i_ceiled = i.ceil(); 213 | 214 | if i_ceiled == i { 215 | t0_utc 216 | + ((i.ceil() as i128 + 1) * period_duration.total_nanoseconds()) as f64 217 | * Unit::Nanosecond 218 | } else { 219 | t0_utc 220 | + ((i.ceil() as i128) * period_duration.total_nanoseconds()) as f64 221 | * Unit::Nanosecond 222 | } 223 | }; 224 | 225 | if utc { 226 | t_utc 227 | } else { 228 | t_utc.to_time_scale(ts) 229 | } 230 | } 231 | 232 | /// Returns datetime (as [Epoch]) of next active data collection 233 | /// after specified [Epoch]. Although CGGTTS is scheduled in and aligned 234 | /// to [TimeScale::UTC], we tolerate other timescales here. 235 | pub fn next_data_collection_after(&self, t: Epoch) -> Epoch { 236 | let mut next_t = self.next_period_start_after(t); 237 | if self.period.setup_duration == Duration::ZERO { 238 | next_t += self.period.setup_duration; 239 | } 240 | next_t 241 | } 242 | 243 | /// Returns remaining time (as [Duration]) until start of next 244 | /// [CommonViewPeriod] after specified [Epoch]. 245 | pub fn time_to_next_period(&self, t: Epoch) -> Duration { 246 | let next_start = self.next_period_start_after(t); 247 | t - next_start 248 | } 249 | 250 | /// Returns remaining time (as [Duration]) until start of next 251 | /// active data collection, after specified [Epoch]. 252 | pub fn time_to_next_data_collection(&self, t: Epoch) -> Duration { 253 | let mut dt = self.time_to_next_period(t); 254 | if self.period.setup_duration == Duration::ZERO { 255 | dt += self.period.setup_duration; 256 | } 257 | dt 258 | } 259 | } 260 | 261 | #[cfg(test)] 262 | mod test { 263 | 264 | use crate::{ 265 | prelude::{Duration, Epoch}, 266 | scheduler::{calendar::CommonViewCalendar, period::BIPM_REFERENCE_MJD}, 267 | }; 268 | 269 | #[test] 270 | fn test_bipm() { 271 | const MJD0_OFFSET_NANOS: i128 = 120_000_000_000; 272 | const DAILY_OFFSET_NANOS: i128 = -240_000_000_000; 273 | const PERIOD_DURATION_NANOS_128: i128 = 960_000_000_000; 274 | 275 | let calendar = CommonViewCalendar::bipm(); 276 | 277 | assert_eq!(calendar.periods_per_day, 90); 278 | assert_eq!(calendar.daily_offset.to_seconds(), -240.0); 279 | assert_eq!(calendar.reference_epoch_mjd_midnight, BIPM_REFERENCE_MJD); 280 | assert_eq!( 281 | calendar.reference_epoch_midnight_offset_nanos, 282 | MJD0_OFFSET_NANOS 283 | ); 284 | 285 | // MJD (ith=0): 2' to midnight (like the infamous song) 286 | assert_eq!( 287 | calendar.first_start_offset_nanos(BIPM_REFERENCE_MJD), 288 | MJD0_OFFSET_NANOS 289 | ); 290 | 291 | // Test all tracks for MJD0 292 | for ith_track in 0..90 { 293 | assert_eq!( 294 | calendar.period_start_offset_nanos(BIPM_REFERENCE_MJD, ith_track), 295 | (MJD0_OFFSET_NANOS + ith_track as i128 * PERIOD_DURATION_NANOS_128) 296 | % PERIOD_DURATION_NANOS_128, 297 | "failed for MJD0 | track={}", 298 | ith_track 299 | ); 300 | } 301 | 302 | // Test for MJD-1..-89 (days prior MJD0) 303 | for mjd_offset in 1..90 { 304 | let daily_offset_nanos = MJD0_OFFSET_NANOS - mjd_offset as i128 * DAILY_OFFSET_NANOS; 305 | let mjd = BIPM_REFERENCE_MJD - mjd_offset; 306 | 307 | // Test all periods for that day 308 | for i in 0..90 { 309 | assert_eq!( 310 | calendar.period_start_offset_nanos(mjd, i), 311 | (daily_offset_nanos + i as i128 * PERIOD_DURATION_NANOS_128) 312 | % PERIOD_DURATION_NANOS_128, 313 | "failed for MJD0 -{} | period={}", 314 | mjd_offset, 315 | i, 316 | ); 317 | } 318 | } 319 | 320 | // Test for MJD+1..+89 (days after MJD0) 321 | for mjd_offset in 1..90 { 322 | let daily_offset_nanos = MJD0_OFFSET_NANOS + mjd_offset as i128 * DAILY_OFFSET_NANOS; 323 | let mjd = BIPM_REFERENCE_MJD + mjd_offset; 324 | 325 | // Test all periods for that day 326 | for i in 0..90 { 327 | let mut expected = (daily_offset_nanos - i as i128 * PERIOD_DURATION_NANOS_128) 328 | % PERIOD_DURATION_NANOS_128; 329 | 330 | if expected.is_negative() { 331 | expected += PERIOD_DURATION_NANOS_128; 332 | } 333 | 334 | assert_eq!( 335 | calendar.period_start_offset_nanos(mjd, i), 336 | expected, 337 | "failed for MJD0 +{} | period={}", 338 | mjd_offset, 339 | i, 340 | ); 341 | } 342 | } 343 | 344 | // test a few verified values 345 | for (mjd, t0_nanos) in [ 346 | (50_721, 6 * 60 * 1_000_000_000), 347 | (50_722, 2 * 60 * 1_000_000_000), 348 | (50_723, 14 * 60 * 1_000_000_000), 349 | (50_724, 10 * 60 * 1_000_000_000), 350 | (59_507, 14 * 60 * 1_000_000_000), 351 | (59_508, 10 * 60 * 1_000_000_000), 352 | (59_509, 6 * 60 * 1_000_000_000), 353 | (59_510, 2 * 60 * 1_000_000_000), 354 | ] { 355 | assert_eq!( 356 | calendar.first_start_offset_nanos(mjd), 357 | t0_nanos, 358 | "failed for mdj={}", 359 | mjd, 360 | ); 361 | } 362 | 363 | // Test a few verified values 364 | for (i, (t, expected)) in [ 365 | ( 366 | //0 367 | Epoch::from_mjd_utc(50_722.0), 368 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 369 | ), 370 | ( 371 | //1 372 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(1.0), 373 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 374 | ), 375 | ( 376 | //2 377 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(2.0), 378 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 379 | ), 380 | ( 381 | //3 382 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(10.0), 383 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 384 | ), 385 | ( 386 | //4 387 | Epoch::from_mjd_utc(50_722.0) - Duration::from_seconds(1.0), 388 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 389 | ), 390 | ( 391 | //5 392 | Epoch::from_mjd_utc(50_722.0) - Duration::from_seconds(10.0), 393 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 394 | ), 395 | ( 396 | //6 397 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(119.0), 398 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 399 | ), 400 | ( 401 | //7 402 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 403 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 404 | ), 405 | ( 406 | //8 407 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(121.0), 408 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 409 | ), 410 | ( 411 | //9 412 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 959.0), 413 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 414 | ), 415 | ( 416 | //10 417 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 418 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 419 | ), 420 | ( 421 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 961.0), 422 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 423 | ), 424 | ( 425 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0 - 1.0), 426 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 427 | ), 428 | ( 429 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 430 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 3.0 * 960.0), 431 | ), 432 | ( 433 | Epoch::from_mjd_utc(59_506.0), 434 | Epoch::from_mjd_utc(59_506.0) + Duration::from_seconds(2.0 * 60.0), 435 | ), 436 | ( 437 | //15 438 | Epoch::from_mjd_utc(59_507.0), 439 | Epoch::from_mjd_utc(59_507.0) + Duration::from_seconds(14.0 * 60.0), 440 | ), 441 | ( 442 | Epoch::from_mjd_utc(59_508.0), 443 | Epoch::from_mjd_utc(59_508.0) + Duration::from_seconds(10.0 * 60.0), 444 | ), 445 | ( 446 | Epoch::from_mjd_utc(59_509.0), 447 | Epoch::from_mjd_utc(59_509.0) + Duration::from_seconds(6.0 * 60.0), 448 | ), 449 | ] 450 | .iter() 451 | .enumerate() 452 | { 453 | assert_eq!( 454 | calendar.next_period_start_after(*t), 455 | *expected, 456 | "failed for i={}/t={:?}", 457 | i, 458 | t 459 | ); 460 | } 461 | } 462 | 463 | #[test] 464 | fn test_bipm_not_sideral_gps() { 465 | const MJD0_OFFSET_NANOS: i128 = 120_000_000_000; 466 | const PERIOD_DURATION_NANOS_128: i128 = 960_000_000_000; 467 | 468 | let calendar = CommonViewCalendar::bipm_unaliged_gps_sideral(); 469 | 470 | assert_eq!(calendar.periods_per_day, 90); 471 | assert_eq!(calendar.daily_offset.to_seconds(), 0.0); 472 | assert_eq!(calendar.reference_epoch_mjd_midnight, BIPM_REFERENCE_MJD); 473 | assert_eq!( 474 | calendar.reference_epoch_midnight_offset_nanos, 475 | MJD0_OFFSET_NANOS 476 | ); 477 | 478 | // MJD0 (ith=0): 2' to midnight (like the infamous song) 479 | assert_eq!( 480 | calendar.first_start_offset_nanos(BIPM_REFERENCE_MJD), 481 | MJD0_OFFSET_NANOS 482 | ); 483 | 484 | // Test all Periods for MJD0 485 | for i in 0..90 { 486 | assert_eq!( 487 | calendar.period_start_offset_nanos(BIPM_REFERENCE_MJD, i), 488 | (MJD0_OFFSET_NANOS + i as i128 * PERIOD_DURATION_NANOS_128) 489 | % PERIOD_DURATION_NANOS_128, 490 | "failed for MJD0 | period={}", 491 | i 492 | ); 493 | } 494 | 495 | // Test for MJD-1..-89 (days prior MJD0) 496 | for mjd_offset in 1..90 { 497 | let daily_offset_nanos = 120_000_000_000; 498 | 499 | let mjd = BIPM_REFERENCE_MJD - mjd_offset; 500 | 501 | // Test all Periods for that day 502 | for i in 0..90 { 503 | assert_eq!( 504 | calendar.period_start_offset_nanos(mjd, i), 505 | (daily_offset_nanos + i as i128 * PERIOD_DURATION_NANOS_128) 506 | % PERIOD_DURATION_NANOS_128, 507 | "failed for MJD -{} | period={}", 508 | mjd_offset, 509 | i, 510 | ); 511 | } 512 | } 513 | 514 | // Test for MJD+1..89 (days after MJD0) 515 | for mjd_offset in 1..90 { 516 | let daily_offset_nanos = 120_000_000_000; 517 | 518 | let mjd = BIPM_REFERENCE_MJD + mjd_offset; 519 | 520 | // Test all tracks for that day 521 | for i in 0..90 { 522 | let mut expected = (daily_offset_nanos - i as i128 * PERIOD_DURATION_NANOS_128) 523 | % PERIOD_DURATION_NANOS_128; 524 | 525 | if expected.is_negative() { 526 | expected += PERIOD_DURATION_NANOS_128; 527 | } 528 | 529 | assert_eq!( 530 | calendar.period_start_offset_nanos(mjd, i), 531 | expected, 532 | "failed for MJD +{} | period={}", 533 | mjd_offset, 534 | i, 535 | ); 536 | } 537 | } 538 | 539 | // Test a few verified values 540 | for (i, (t, expected)) in [ 541 | ( 542 | //0 543 | Epoch::from_mjd_utc(50_722.0), 544 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 545 | ), 546 | ( 547 | //1 548 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(1.0), 549 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 550 | ), 551 | ( 552 | //2 553 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(2.0), 554 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 555 | ), 556 | ( 557 | //3 558 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(10.0), 559 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 560 | ), 561 | ( 562 | //4 563 | Epoch::from_mjd_utc(50_722.0) - Duration::from_seconds(1.0), 564 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 565 | ), 566 | ( 567 | //5 568 | Epoch::from_mjd_utc(50_722.0) - Duration::from_seconds(10.0), 569 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 570 | ), 571 | ( 572 | //6 573 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(119.0), 574 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 575 | ), 576 | ( 577 | //7 578 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0), 579 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 580 | ), 581 | ( 582 | //8 583 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(121.0), 584 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 585 | ), 586 | ( 587 | //9 588 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 959.0), 589 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 590 | ), 591 | ( 592 | //10 593 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 960.0), 594 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 595 | ), 596 | ( 597 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 961.0), 598 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 599 | ), 600 | ( 601 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0 - 1.0), 602 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 603 | ), 604 | ( 605 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 2.0 * 960.0), 606 | Epoch::from_mjd_utc(50_722.0) + Duration::from_seconds(120.0 + 3.0 * 960.0), 607 | ), 608 | ] 609 | .iter() 610 | .enumerate() 611 | { 612 | assert_eq!( 613 | calendar.next_period_start_after(*t), 614 | *expected, 615 | "failed for i={}/t={:?}", 616 | i, 617 | t 618 | ); 619 | } 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /src/scheduler/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common View Planification utilities 2 | pub mod calendar; 3 | pub mod period; 4 | -------------------------------------------------------------------------------- /src/scheduler/period.rs: -------------------------------------------------------------------------------- 1 | //! Common View Period definition 2 | use crate::prelude::Duration; 3 | 4 | /// Standard setup duration (in seconds), as per BIPM specifications. 5 | pub(crate) const BIPM_SETUP_DURATION_SECONDS: u32 = 180; 6 | 7 | /// Standard tracking duration (in seconds), as per BIPM specifications 8 | const BIPM_TRACKING_DURATION_SECONDS: u32 = 780; 9 | 10 | /// Reference MJD used in Common View tracking 11 | pub(crate) const BIPM_REFERENCE_MJD: u32 = 50_722; 12 | 13 | /// [CommonViewPeriod] describes the period of satellite 14 | /// tracking and common view realizations. 15 | #[derive(Debug, Clone, PartialEq)] 16 | pub struct CommonViewPeriod { 17 | /// Setup duration, may serve as a warmup [Duration] at the beginning 18 | /// of each period. Historically, this was a 3' duration that is still 19 | /// in use in strict CGTTTS 2E collection (which is arbitrary). 20 | pub setup_duration: Duration, 21 | /// Active tracking [Duration]. 22 | /// In strict CGGTTS 2E collection, is is set to 13'. 23 | pub tracking_duration: Duration, 24 | } 25 | 26 | impl Default for CommonViewPeriod { 27 | /// Creates a default [CommonViewPeriod] of 13' tracking, 28 | /// and no dead time. 29 | fn default() -> Self { 30 | Self { 31 | setup_duration: Duration::ZERO, 32 | tracking_duration: Duration::from_seconds(BIPM_TRACKING_DURATION_SECONDS as f64), 33 | } 34 | } 35 | } 36 | 37 | impl CommonViewPeriod { 38 | /// Creates a [CommonViewPeriod] as per historical 39 | /// BIPM Common View specifications. 40 | pub fn bipm_common_view_period() -> Self { 41 | Self::default().with_setup_duration_s(BIPM_SETUP_DURATION_SECONDS as f64) 42 | } 43 | 44 | /// Returns total period [Duration]. 45 | /// ``` 46 | /// use cggtts::prelude::CommonViewPeriod; 47 | /// 48 | /// let bipm = CommonViewPeriod::bipm_common_view_period(); 49 | /// assert_eq!(bipm.total_duration().to_seconds(), 960.0); 50 | /// ``` 51 | pub fn total_duration(&self) -> Duration { 52 | self.setup_duration + self.tracking_duration 53 | } 54 | 55 | /// Returns a new [CommonViewPeriod] with desired setup [Duration] 56 | /// for which data should not be collected (at the beginning of each period) 57 | pub fn with_setup_duration(&self, setup_duration: Duration) -> Self { 58 | let mut s = self.clone(); 59 | s.setup_duration = setup_duration; 60 | s 61 | } 62 | 63 | /// Returns a new [CommonViewPeriod] with desired setup duration in seconds, 64 | /// for which data should not be collected (at the beginning of each period) 65 | pub fn with_setup_duration_s(&self, setup_s: f64) -> Self { 66 | let mut s = self.clone(); 67 | s.setup_duration = Duration::from_seconds(setup_s); 68 | s 69 | } 70 | 71 | /// Returns a new [CommonViewPeriod] with desired tracking [Duration] 72 | /// for which data should be collected (at the end of each period, after possible 73 | /// setup [Duration]). 74 | pub fn with_tracking_duration(&self, tracking_duration: Duration) -> Self { 75 | let mut s = self.clone(); 76 | s.tracking_duration = tracking_duration; 77 | s 78 | } 79 | 80 | /// Returns a new [CommonViewPeriod] with desired tracking duration (in seconds) 81 | /// for which data should be collected (at the end of each period, after possible 82 | /// setup [Duration]). 83 | pub fn with_tracking_duration_s(&self, tracking_s: f64) -> Self { 84 | let mut s = self.clone(); 85 | s.tracking_duration = Duration::from_seconds(tracking_s); 86 | s 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod test { 92 | use crate::prelude::CommonViewPeriod; 93 | 94 | #[test] 95 | fn bipm_specifications() { 96 | let cv = CommonViewPeriod::bipm_common_view_period(); 97 | assert_eq!(cv.total_duration().to_seconds(), 960.0); 98 | assert_eq!(cv.setup_duration.to_seconds(), 180.0); 99 | assert_eq!(cv.tracking_duration.to_seconds(), 780.0); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod parser; 2 | mod toolkit; 3 | -------------------------------------------------------------------------------- /src/tests/parser.rs: -------------------------------------------------------------------------------- 1 | mod test { 2 | 3 | use hifitime::Unit; 4 | 5 | use crate::{ 6 | header::CalibrationID, 7 | prelude::CGGTTS, 8 | tests::toolkit::{random_name, track_dut_model_comparison}, 9 | track::CommonViewClass, 10 | }; 11 | use std::{ 12 | fs::{read_dir, remove_file}, 13 | path::{Path, PathBuf}, 14 | }; 15 | 16 | #[test] 17 | fn parse_dataset() { 18 | let dir: PathBuf = PathBuf::new() 19 | .join(env!("CARGO_MANIFEST_DIR")) 20 | .join("data/CGGTTS"); 21 | 22 | for entry in read_dir(dir).unwrap() { 23 | let entry = entry.unwrap(); 24 | let path = entry.path(); 25 | let is_hidden = path.file_name().unwrap().to_str().unwrap().starts_with('.'); 26 | if is_hidden { 27 | continue; 28 | } 29 | 30 | let cggtts = CGGTTS::from_file(&path) 31 | .unwrap_or_else(|e| panic!("failed to parse {}: {}", path.display(), e)); 32 | 33 | // Dump then parse back 34 | let file_name = random_name(8); 35 | 36 | cggtts.to_file(&file_name).unwrap(); 37 | 38 | let parsed = CGGTTS::from_file(&file_name) 39 | .unwrap_or_else(|e| panic!("failed to parse back \"{}\": {}", file_name, e)); 40 | 41 | assert_eq!(parsed.tracks.len(), cggtts.tracks.len()); 42 | 43 | for (dut, model) in parsed.tracks_iter().zip(cggtts.tracks_iter()) { 44 | track_dut_model_comparison(dut, model); 45 | } 46 | 47 | // remove generated file 48 | let _ = std::fs::remove_file(&file_name); 49 | } 50 | } 51 | 52 | #[test] 53 | fn ezgtr60_258() { 54 | let path = Path::new(env!("CARGO_MANIFEST_DIR")) 55 | .join("data/CGGTTS") 56 | .join("EZGTR60.258"); 57 | 58 | let fullpath = path.to_string_lossy().to_string(); 59 | 60 | let cggtts = CGGTTS::from_file(&fullpath).unwrap(); 61 | 62 | assert_eq!(cggtts.header.receiver.manufacturer, "GTR51"); 63 | 64 | let ims = cggtts.header.ims_hardware.as_ref().unwrap(); 65 | assert_eq!(ims.manufacturer, "GTR51"); 66 | 67 | assert!(cggtts.is_galileo_cggtts()); 68 | 69 | assert_eq!( 70 | cggtts.header.delay.calibration_id, 71 | Some(CalibrationID { 72 | process_id: 1015, 73 | year: 2021, 74 | }) 75 | ); 76 | 77 | let mut tests_passed = 0; 78 | 79 | for (nth, track) in cggtts.tracks_iter().enumerate() { 80 | match nth { 81 | 0 => { 82 | assert_eq!(track.sv.to_string(), "E03"); 83 | assert_eq!(track.class, CommonViewClass::MultiChannel); 84 | assert_eq!(track.epoch.to_mjd_utc(Unit::Day).floor() as u16, 60258); 85 | assert_eq!(track.duration.to_seconds() as u16, 780); 86 | assert!(track.follows_bipm_tracking()); 87 | assert!((track.elevation_deg - 13.9).abs() < 0.01); 88 | assert!((track.azimuth_deg - 54.8).abs() < 0.01); 89 | assert!((track.data.refsv - 723788.0 * 0.1E-9) < 1E-10); 90 | assert!((track.data.srsv - 14.0 * 0.1E-12) < 1E-12); 91 | assert!((track.data.refsys - -302.0 * 0.1E-9) < 1E-10); 92 | assert!((track.data.srsys - -14.0 * 0.1E-12) < 1E-12); 93 | assert!((track.data.dsg - 2.0 * 0.1E-9).abs() < 1E-10); 94 | assert_eq!(track.data.ioe, 76); 95 | 96 | assert!((track.data.mdtr - 325.0 * 0.1E-9).abs() < 1E-10); 97 | assert!((track.data.smdt - -36.0 * 0.1E-12).abs() < 1E-12); 98 | assert!((track.data.mdio - 32.0 * 0.1E-9).abs() < 1E-10); 99 | assert!((track.data.smdi - -3.0 * 0.1E-12).abs() < 1E-12); 100 | 101 | let iono = track.iono.unwrap(); 102 | assert!((iono.msio - 20.0 * 0.1E-9).abs() < 1E-10); 103 | assert!((iono.smsi - 20.0 * 0.1E-12).abs() < 1E-12); 104 | assert!((iono.isg - 3.0 * 0.1E-9).abs() < 1E-10); 105 | 106 | assert_eq!(track.frc, "E1"); 107 | tests_passed += 1; 108 | }, 109 | _ => {}, 110 | } 111 | } 112 | 113 | assert_eq!(tests_passed, 1); 114 | 115 | // test filename generator 116 | assert_eq!( 117 | cggtts.standardized_file_name(Some("GT"), Some("R")), 118 | "EZGTR60.258" 119 | ); 120 | 121 | // format (dump) then parse back 122 | let file_name = random_name(8); 123 | cggtts.to_file(&file_name).unwrap(); 124 | 125 | let parsed = CGGTTS::from_file(&file_name) 126 | .unwrap_or_else(|e| panic!("failed to parse back CGGTTS \"{}\": {}", file_name, e)); 127 | 128 | assert_eq!(parsed.tracks.len(), cggtts.tracks.len()); 129 | 130 | for (dut, model) in parsed.tracks_iter().zip(cggtts.tracks_iter()) { 131 | track_dut_model_comparison(dut, model); 132 | } 133 | 134 | let _ = remove_file(&file_name); 135 | } 136 | 137 | #[test] 138 | fn gzgtr5_60_258() { 139 | let path = Path::new(env!("CARGO_MANIFEST_DIR")) 140 | .join("data/CGGTTS/") 141 | .join("GZGTR560.258"); 142 | 143 | let cggtts = CGGTTS::from_file(&path).unwrap(); 144 | assert_eq!(cggtts.header.receiver.manufacturer, "GTR51"); 145 | 146 | let ims = cggtts 147 | .header 148 | .ims_hardware 149 | .as_ref() 150 | .expect("failed to parse \"IMS=\""); 151 | 152 | assert_eq!(ims.manufacturer, "GTR51"); 153 | 154 | assert!(cggtts.is_gps_cggtts()); 155 | 156 | assert_eq!( 157 | cggtts.header.delay.calibration_id, 158 | Some(CalibrationID { 159 | process_id: 1015, 160 | year: 2021, 161 | }) 162 | ); 163 | 164 | let mut tests_passed = 0; 165 | 166 | for (nth, track) in cggtts.tracks_iter().enumerate() { 167 | match nth { 168 | 0 => { 169 | assert_eq!(track.sv.to_string(), "G08"); 170 | assert_eq!(track.class, CommonViewClass::MultiChannel); 171 | assert_eq!(track.epoch.to_mjd_utc(Unit::Day).floor() as u16, 60258); 172 | assert_eq!(track.duration.to_seconds() as u16, 780); 173 | assert!(track.follows_bipm_tracking()); 174 | assert!((track.elevation_deg - 24.5).abs() < 0.01); 175 | assert!((track.azimuth_deg - 295.4).abs() < 0.01); 176 | assert!((track.data.refsv - 1513042.0 * 0.1E-9) < 1E-10); 177 | assert!((track.data.srsv - 28.0 * 0.1E-12) < 1E-12); 178 | assert!((track.data.refsys - -280.0 * 0.1E-9) < 1E-10); 179 | assert!((track.data.srsys - 10.0 * 0.1E-12) < 1E-12); 180 | assert!((track.data.dsg - 3.0 * 0.1E-9).abs() < 1E-10); 181 | assert_eq!(track.data.ioe, 42); 182 | 183 | assert!((track.data.mdtr - 192.0 * 0.1E-9).abs() < 1E-10); 184 | assert!((track.data.smdt - -49.0 * 0.1E-12).abs() < 1E-12); 185 | assert!((track.data.mdio - 99.0 * 0.1E-9).abs() < 1E-10); 186 | assert!((track.data.smdi - -14.0 * 0.1E-12).abs() < 1E-12); 187 | 188 | let iono = track.iono.unwrap(); 189 | assert!((iono.msio - 57.0 * 0.1E-9).abs() < 1E-10); 190 | assert!((iono.smsi - -29.0 * 0.1E-12).abs() < 1E-12); 191 | assert!((iono.isg - 5.0 * 0.1E-9).abs() < 1E-10); 192 | 193 | assert_eq!(track.frc, "L1C"); 194 | tests_passed += 1; 195 | }, 196 | _ => {}, 197 | } 198 | } 199 | assert_eq!(tests_passed, 1); 200 | 201 | // test filename generator 202 | assert_eq!( 203 | cggtts.standardized_file_name(Some("GT"), Some("R5")), 204 | "GZGTR560.258" 205 | ); 206 | 207 | // format (dump) then parse back 208 | let file_name = random_name(8); 209 | cggtts.to_file(&file_name).unwrap(); 210 | 211 | let parsed = CGGTTS::from_file(&file_name) 212 | .unwrap_or_else(|e| panic!("failed to parse back CGGTTS \"{}\": {}", file_name, e)); 213 | 214 | assert_eq!(parsed.tracks.len(), cggtts.tracks.len()); 215 | 216 | for (dut, model) in parsed.tracks_iter().zip(cggtts.tracks_iter()) { 217 | track_dut_model_comparison(dut, model); 218 | } 219 | 220 | let _ = remove_file(&file_name); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/tests/toolkit.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::{Epoch, Track, TrackData, CGGTTS}; 2 | use rand::{distributions::Alphanumeric, Rng}; 3 | 4 | pub fn cggtts_dut_model_comparison(dut: &CGGTTS, model: &CGGTTS) { 5 | assert_eq!(dut.header.version, model.header.version, "wrong version"); 6 | 7 | assert_eq!( 8 | dut.header.revision_date, model.header.revision_date, 9 | "wrong release date" 10 | ); 11 | 12 | assert_eq!( 13 | dut.header.station, model.header.station, 14 | "invalid station name" 15 | ); 16 | 17 | assert_eq!( 18 | dut.header.receiver, model.header.receiver, 19 | "invalid receiver data" 20 | ); 21 | 22 | assert_eq!( 23 | dut.header.nb_channels, model.header.nb_channels, 24 | "invalid receiver channels" 25 | ); 26 | 27 | assert_eq!( 28 | dut.header.reference_time, model.header.reference_time, 29 | "wrong reference time" 30 | ); 31 | 32 | assert_eq!( 33 | dut.header.apc_coordinates, model.header.apc_coordinates, 34 | "wrong apc coordinates" 35 | ); 36 | 37 | assert_eq!( 38 | dut.header.comments, model.header.comments, 39 | "wrong comments content" 40 | ); 41 | 42 | assert_eq!(dut.header.delay, model.header.delay, "wrong delay values"); 43 | 44 | // Tracks comparison 45 | assert!( 46 | dut.tracks.len() >= model.tracks.len(), 47 | "dut is missing some tracks" 48 | ); 49 | 50 | assert!( 51 | dut.tracks.len() <= model.tracks.len(), 52 | "dut has too many tracks" 53 | ); 54 | 55 | assert_eq!( 56 | dut.tracks.len(), 57 | model.tracks.len(), 58 | "wrong amount of tracks" 59 | ); 60 | 61 | for (dut_trk, model_trk) in dut.tracks.iter().zip(model.tracks.iter()) { 62 | track_dut_model_comparison(dut_trk, model_trk); 63 | } 64 | } 65 | 66 | pub fn track_dut_model_comparison(dut_trk: &Track, model_trk: &Track) { 67 | assert_eq!(dut_trk.epoch, model_trk.epoch, "bad track epoch"); 68 | assert_eq!( 69 | dut_trk.class, model_trk.class, 70 | "bad common view class @ {:?}", 71 | dut_trk.epoch 72 | ); 73 | 74 | assert_eq!( 75 | dut_trk.duration, model_trk.duration, 76 | "bad tracking duration @ {:?}", 77 | dut_trk.epoch 78 | ); 79 | assert_eq!( 80 | dut_trk.sv, model_trk.sv, 81 | "bad sv description @ {:?}", 82 | dut_trk.epoch 83 | ); 84 | 85 | assert_eq!( 86 | dut_trk.elevation_deg, model_trk.elevation_deg, 87 | "bad sv elevation @ {:?}", 88 | dut_trk.epoch 89 | ); 90 | 91 | assert_eq!( 92 | dut_trk.azimuth_deg, model_trk.azimuth_deg, 93 | "bad sv azimuth @ {:?}", 94 | dut_trk.epoch 95 | ); 96 | 97 | assert_eq!( 98 | dut_trk.hc, model_trk.hc, 99 | "bad hardware channel @ {:?}", 100 | dut_trk.epoch 101 | ); 102 | assert_eq!( 103 | dut_trk.fdma_channel, model_trk.fdma_channel, 104 | "invalid glonass FDMA channel @ {:?}", 105 | dut_trk.epoch 106 | ); 107 | assert_eq!( 108 | dut_trk.frc, model_trk.frc, 109 | "bad carrier code @ {:?}", 110 | dut_trk.epoch 111 | ); 112 | 113 | trk_data_cmp(dut_trk.epoch, &dut_trk.data, &model_trk.data); 114 | } 115 | 116 | pub fn trk_data_cmp(t: Epoch, dut: &TrackData, model: &TrackData) { 117 | assert_eq!(dut.ioe, model.ioe, "bad IOE @ {:?}", t); 118 | assert!( 119 | (dut.refsv - model.refsv).abs() < 1E-11, 120 | "REFSV {}/{}", 121 | dut.refsv, 122 | model.refsv 123 | ); 124 | 125 | //assert!((dut.refsv - model.refsv).abs() < 1.0E-9, "bad REFSV @ {:?}"); 126 | //assert!( 127 | // (dut.srsv - model.srsv).abs() < 1.0E-9, 128 | // "bad SRSV @ {:?} : {} vs {}", 129 | // t, 130 | // dut.srsv, 131 | // model.srsv 132 | //); 133 | //assert!((dut.refsys - model.refsys).abs() < 1.0E-9, "bad REFSYS @ {:?}", t); 134 | //assert!( 135 | // (dut.srsys - model.srsys).abs() < 1.0E-9, 136 | // "bad SRSYS @ {:?}: {} {}", 137 | // t, 138 | // dut.srsys, 139 | // model.srsys 140 | //); 141 | //assert!((dut.dsg - model.dsg).abs() < 1.0E-9, "bad DSG @ {:?}", t); 142 | //assert!((dut.mdtr - model.mdtr).abs() < 1.0E-9, "bad MDTR @ {:?}", t); 143 | //assert!((dut.smdt - model.smdt).abs() < 1.0E-9, "bad SMDT @ {:?}", t); 144 | //assert!((dut.mdio - model.mdio).abs() < 1.0E-9, "bad MDIO @ {:?}", t); 145 | //assert!((dut.smdi - model.smdi).abs() < 1.0E-9, "bad SMDI @ {:?}", t); 146 | } 147 | 148 | /// Generates a random name, used in file production testing 149 | pub fn random_name(size: usize) -> String { 150 | rand::thread_rng() 151 | .sample_iter(&Alphanumeric) 152 | .take(size) 153 | .map(char::from) 154 | .collect() 155 | } 156 | -------------------------------------------------------------------------------- /src/track/class.rs: -------------------------------------------------------------------------------- 1 | /// Describes whether this common view is based on a unique 2 | /// or a combination of SV 3 | use crate::track::Error; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(PartialEq, Default, Clone, Copy, Debug)] 9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 10 | pub enum CommonViewClass { 11 | /// Single Channel 12 | #[default] 13 | SingleChannel, 14 | /// Multi Channel 15 | MultiChannel, 16 | } 17 | 18 | impl std::fmt::Display for CommonViewClass { 19 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | match self { 21 | Self::SingleChannel => write!(f, "Single Channel"), 22 | Self::MultiChannel => write!(f, "Multi Channel"), 23 | } 24 | } 25 | } 26 | 27 | impl std::fmt::UpperHex for CommonViewClass { 28 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 29 | match self { 30 | CommonViewClass::MultiChannel => write!(fmt, "FF"), 31 | CommonViewClass::SingleChannel => write!(fmt, "99"), 32 | } 33 | } 34 | } 35 | 36 | impl std::str::FromStr for CommonViewClass { 37 | type Err = Error; 38 | fn from_str(s: &str) -> Result { 39 | if s.eq("FF") { 40 | Ok(Self::MultiChannel) 41 | } else if s.eq("99") { 42 | Ok(Self::SingleChannel) 43 | } else { 44 | Err(Error::UnknownClass) 45 | } 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod test { 51 | use super::CommonViewClass; 52 | use std::str::FromStr; 53 | #[test] 54 | fn cv_class() { 55 | assert_eq!(format!("{:X}", CommonViewClass::MultiChannel), "FF"); 56 | assert_eq!(format!("{:X}", CommonViewClass::SingleChannel), "99"); 57 | assert_eq!( 58 | CommonViewClass::from_str("FF"), 59 | Ok(CommonViewClass::MultiChannel) 60 | ); 61 | assert_eq!( 62 | CommonViewClass::from_str("FF"), 63 | Ok(CommonViewClass::MultiChannel) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/track/formatting.rs: -------------------------------------------------------------------------------- 1 | use crate::{buffer::Utf8Buffer, errors::FormattingError, prelude::Track}; 2 | 3 | use std::io::{BufWriter, Write}; 4 | 5 | use std::cmp::{max as cmp_max, min as cmp_min}; 6 | 7 | fn fmt_saturated(nb: T, sat: T, padding: usize) -> String { 8 | format!("{:>padding$}", std::cmp::min(nb, sat)) 9 | } 10 | 11 | fn fmt_saturated_f64(nb: f64, scaling: f64, sat: i64, padding: usize) -> String { 12 | let scaled = (nb * scaling).round() as i64; 13 | if scaled.is_negative() { 14 | format!( 15 | "{:>padding$}", 16 | cmp_max(scaled, -sat / 10), 17 | padding = padding 18 | ) // remove 1 digit for sign 19 | } else { 20 | format!("{:>padding$}", cmp_min(scaled, sat), padding = padding) 21 | } 22 | } 23 | 24 | impl Track { 25 | /// Format [Track] into mutable [BufWriter]. 26 | /// Requires a pre-allocated [Utf8Buffer]. 27 | pub fn format( 28 | &self, 29 | writer: &mut BufWriter, 30 | buffer: &mut Utf8Buffer, 31 | ) -> Result<(), FormattingError> { 32 | // start by clearing buffer from past residues 33 | buffer.clear(); 34 | 35 | buffer.push_str(&format!("{} {:X} ", self.sv, self.class)); 36 | 37 | buffer.push_str(&format!( 38 | "{} ", 39 | fmt_saturated_f64(self.epoch.to_mjd_utc_days().floor(), 1.0, 99999, 4) 40 | )); 41 | 42 | let (_, _, _, h, m, s, _) = self.epoch.to_gregorian_utc(); 43 | buffer.push_str(&format!("{:02}{:02}{:02} ", h, m, s)); 44 | 45 | buffer.push_str(&format!( 46 | "{} ", 47 | fmt_saturated(self.duration.to_seconds() as u64, 9999, 4) 48 | )); 49 | 50 | buffer.push_str(&format!( 51 | "{} ", 52 | fmt_saturated_f64(self.elevation_deg, 10.0, 999, 3) 53 | )); 54 | 55 | buffer.push_str(&format!( 56 | "{} ", 57 | fmt_saturated_f64(self.azimuth_deg, 10.0, 9999, 4) 58 | )); 59 | 60 | buffer.push_str(&format!( 61 | "{} ", 62 | fmt_saturated_f64(self.data.refsv, 1E10, 99_999_999_999, 11) 63 | )); 64 | 65 | buffer.push_str(&format!( 66 | "{} ", 67 | fmt_saturated_f64(self.data.srsv, 1E13, 999_999, 6) 68 | )); 69 | 70 | buffer.push_str(&format!( 71 | "{} ", 72 | fmt_saturated_f64(self.data.refsys, 1E10, 99_999_999_999, 11) 73 | )); 74 | 75 | buffer.push_str(&format!( 76 | "{} ", 77 | fmt_saturated_f64(self.data.srsys, 1E13, 999_999, 6) 78 | )); 79 | 80 | buffer.push_str(&format!( 81 | "{} ", 82 | fmt_saturated_f64(self.data.dsg, 1E10, 9_999, 4) 83 | )); 84 | 85 | buffer.push_str(&format!("{} ", fmt_saturated(self.data.ioe, 999, 3))); 86 | 87 | buffer.push_str(&format!( 88 | "{} ", 89 | fmt_saturated_f64(self.data.mdtr, 1E10, 9_999, 4) 90 | )); 91 | 92 | buffer.push_str(&format!( 93 | "{} ", 94 | fmt_saturated_f64(self.data.smdt, 1E13, 9_999, 4) 95 | )); 96 | 97 | buffer.push_str(&format!( 98 | "{} ", 99 | fmt_saturated_f64(self.data.mdio, 1E10, 9_999, 4) 100 | )); 101 | 102 | buffer.push_str(&format!( 103 | "{} ", 104 | fmt_saturated_f64(self.data.smdi, 1E13, 9_999, 4) 105 | )); 106 | 107 | if let Some(iono) = self.iono { 108 | buffer.push_str(&format!( 109 | "{} {} {} ", 110 | fmt_saturated_f64(iono.msio, 1E10, 9_999, 4), 111 | fmt_saturated_f64(iono.smsi, 1E13, 999_999, 4), 112 | fmt_saturated_f64(iono.isg, 1E10, 9_999, 3), 113 | )); 114 | } 115 | 116 | if let Some(fdma) = &self.fdma_channel { 117 | buffer.push_str(&format!("{:2} ", fdma)); 118 | } else { 119 | buffer.push_str(" 0 "); 120 | } 121 | 122 | buffer.push_str(&format!( 123 | "{:2} {:>frc_padding$} ", 124 | self.hc, 125 | self.frc, 126 | frc_padding = 3 127 | )); 128 | 129 | // ready to proceed to calculation 130 | let crc = buffer.calculate_crc(); 131 | 132 | // append CRC 133 | buffer.push_str(&format!("{:02X}", crc)); 134 | 135 | // interprate 136 | let utf8 = buffer.to_utf8_ascii()?; // we will never format bad Utf8 137 | 138 | // forward to user buffer 139 | write!(writer, "{}", &utf8)?; 140 | 141 | Ok(()) 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod test { 147 | use crate::buffer::Utf8Buffer; 148 | use crate::track::Track; 149 | use std::io::BufWriter; 150 | use std::str::FromStr; 151 | 152 | #[test] 153 | fn track_crc_formatting() { 154 | let mut buf = Utf8Buffer::new(1024); 155 | let mut user_buf = BufWriter::new(Utf8Buffer::new(1024)); 156 | 157 | let track = Track::from_str( 158 | "E03 FF 60258 001000 780 139 548 +723788 +14 -302 -14 2 076 325 -36 32 -3 20 +20 3 0 0 E1 A5" 159 | ) 160 | .unwrap(); 161 | 162 | track.format(&mut user_buf, &mut buf).unwrap(); 163 | 164 | let inner = user_buf.into_inner().unwrap_or_else(|_| panic!("oops")); 165 | let ascii_utf8 = inner.to_utf8_ascii().expect("generated invalid utf-8!"); 166 | 167 | assert_eq!( 168 | ascii_utf8, 169 | "E03 FF 60258 001000 780 139 548 723788 14 -302 -14 2 76 325 -36 32 -3 20 20 3 0 0 E1 74", 170 | ); 171 | 172 | // 3 letter modern Frequency modulation Code 173 | let mut buf = Utf8Buffer::new(1024); 174 | let mut user_buf = BufWriter::new(Utf8Buffer::new(1024)); 175 | 176 | let track = Track::from_str( 177 | "E08 FF 60258 002600 780 142 988 1745615 40 -233 -19 4 79 321 -96 73 -14 116 -53 13 0 0 E5a 84" 178 | ).unwrap(); 179 | 180 | track.format(&mut user_buf, &mut buf).unwrap(); 181 | 182 | let inner = user_buf.into_inner().unwrap_or_else(|_| panic!("oops")); 183 | let ascii_utf8 = inner.to_utf8_ascii().expect("generated invalid utf-8!"); 184 | 185 | assert_eq!( 186 | ascii_utf8, 187 | "E08 FF 60258 002600 780 142 988 1745615 40 -233 -19 4 79 321 -96 73 -14 116 -53 13 0 0 E5a 30" 188 | ); 189 | 190 | // 3 letter modern Frequency modulation Code (bis) 191 | let mut buf = Utf8Buffer::new(1024); 192 | let mut user_buf = BufWriter::new(Utf8Buffer::new(1024)); 193 | 194 | let track = Track::from_str( 195 | "E03 FF 60258 001000 780 139 548 724092 28 2 1 2 76 325 -36 54 -6 34 35 5 0 0 E5b 77" 196 | ).unwrap(); 197 | 198 | track.format(&mut user_buf, &mut buf).unwrap(); 199 | 200 | let inner = user_buf.into_inner().unwrap_or_else(|_| panic!("oops")); 201 | let ascii_utf8 = inner.to_utf8_ascii().expect("generated invalid utf-8!"); 202 | 203 | assert_eq!( 204 | ascii_utf8, 205 | "E03 FF 60258 001000 780 139 548 724092 28 2 1 2 76 325 -36 54 -6 34 35 5 0 0 E5b 77" 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/track/glonass.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::track::Error; 5 | 6 | /// Describes Glonass Frequency channel, 7 | /// in case this `Track` was estimated using Glonass 8 | #[derive(Debug, Default, PartialEq, Copy, Clone)] 9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 10 | pub enum GlonassChannel { 11 | /// Default value when not using Glonass constellation 12 | #[default] 13 | Unknown, 14 | /// Glonass Frequency channel number 15 | ChanNum(u8), 16 | } 17 | 18 | impl std::fmt::Display for GlonassChannel { 19 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | match self { 21 | GlonassChannel::Unknown => write!(fmt, "00"), 22 | GlonassChannel::ChanNum(c) => write!(fmt, "{:02X}", c), 23 | } 24 | } 25 | } 26 | 27 | impl std::str::FromStr for GlonassChannel { 28 | type Err = Error; 29 | fn from_str(s: &str) -> Result { 30 | let ch = s 31 | .trim() 32 | .parse::() 33 | .map_err(|_| Error::FieldParsing(String::from("FR")))?; 34 | if ch == 0 { 35 | Ok(Self::Unknown) 36 | } else { 37 | Ok(Self::ChanNum(ch)) 38 | } 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::track::GlonassChannel; 45 | #[test] 46 | fn glonass_chx() { 47 | for (value, expected) in [ 48 | (GlonassChannel::Unknown, "00"), 49 | (GlonassChannel::ChanNum(1), "01"), 50 | (GlonassChannel::ChanNum(9), "09"), 51 | (GlonassChannel::ChanNum(10), "0A"), 52 | (GlonassChannel::ChanNum(11), "0B"), 53 | ] { 54 | assert_eq!(value.to_string(), expected); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/track/mod.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | mod class; 4 | mod formatting; 5 | 6 | pub use class::CommonViewClass; 7 | 8 | use gnss::prelude::{Constellation, SV}; 9 | use hifitime::{Duration, Epoch, Unit}; 10 | 11 | #[cfg(feature = "serde")] 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[cfg(docsrs)] 15 | use crate::prelude::TimeScale; 16 | 17 | const TRACK_WITH_IONOSPHERIC: usize = 24; 18 | const TRACK_WITHOUT_IONOSPHERIC: usize = 21; 19 | 20 | /// A Track is a CGGTTS measurement 21 | #[derive(Debug, Default, PartialEq, Clone)] 22 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 23 | pub struct Track { 24 | /// Common View Class 25 | pub class: CommonViewClass, 26 | /// [Epoch] of this track 27 | pub epoch: Epoch, 28 | /// Tracking [Duration] 29 | pub duration: Duration, 30 | /// SV tracked during this realization 31 | pub sv: SV, 32 | /// [SV] elevation in degrees (at track midpoint, in case of complex 33 | /// track collection and fitting algorithm), in degrees. 34 | pub elevation_deg: f64, 35 | /// [SV] azimuth in degrees (at track midpoint, in case of complex 36 | /// track collection and fitting algorithm), in degrees. 37 | pub azimuth_deg: f64, 38 | /// Track data 39 | pub data: TrackData, 40 | /// Optionnal Ionospheric compensation terms 41 | pub iono: Option, 42 | /// Glonass FDMA channel [1:24] that only applies to 43 | /// [Track]s solved by tracking [Constellation::Glonass]. 44 | pub fdma_channel: Option, 45 | /// Hardware / receiver channel [0:99], 0 if Unknown 46 | pub hc: u8, 47 | /// Carrier frequency standard 3 letter code, 48 | /// refer to RINEX specifications for meaning 49 | pub frc: String, 50 | } 51 | 52 | #[derive(Error, Debug, PartialEq)] 53 | pub enum Error { 54 | #[error("invalid track format")] 55 | InvalidFormat, 56 | #[error("invalid sttime field format")] 57 | InvalidTrkTimeFormat, 58 | #[error("unknown common view class")] 59 | UnknownClass, 60 | #[error("failed to parse sv")] 61 | SVParsing(#[from] gnss::sv::ParsingError), 62 | #[error("failed to parse \"{0}\" field")] 63 | FieldParsing(String), 64 | #[error("missing \"{0}\" field")] 65 | MissingField(String), 66 | #[error("checksum error")] 67 | CrcError(#[from] crate::errors::CrcError), 68 | } 69 | 70 | /// Track data 71 | #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] 72 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 73 | pub struct TrackData { 74 | /// REFSV 75 | pub refsv: f64, 76 | /// SRSV 77 | pub srsv: f64, 78 | /// REFSYS 79 | pub refsys: f64, 80 | /// SRSYS 81 | pub srsys: f64, 82 | /// Data signma (`DSG`) : RMS residuals to linear fit 83 | pub dsg: f64, 84 | /// Issue of Ephemeris (`IOE`), 85 | /// Three-digit decimal code indicating the ephemeris used for the computation. 86 | /// As no IOE is associated with the GLONASS navigation messages, the values 1-96 have to be 87 | /// used to indicate the date of the ephemeris used, given by the number of the quarter of an hour in 88 | /// the day, starting at 1=00h00m00s. 89 | /// For BeiDou, IOE will report the integer hour in the date of the ephemeris (Time of Clock). 90 | pub ioe: u16, 91 | /// Modeled tropospheric delay 92 | pub mdtr: f64, 93 | /// Slope of the modeled tropospheric delay 94 | pub smdt: f64, 95 | /// Modeled ionospheric delay 96 | pub mdio: f64, 97 | /// Slope of the modeled ionospheric delay 98 | pub smdi: f64, 99 | } 100 | 101 | /// Ionospheric Data are attached to a CGGTTS track 102 | /// when generated in dual frequency contexts. 103 | #[derive(Copy, Clone, PartialEq, Debug)] 104 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 105 | pub struct IonosphericData { 106 | /// Measured ionospheric delay 107 | /// corresponding to the solution E in section 2.3.3. 108 | pub msio: f64, 109 | /// Slope of the measured ionospheric delay 110 | /// corresponding to the solution E in section 2.3.3. 111 | pub smsi: f64, 112 | /// Root-mean-square of the residuals 113 | /// corresponding to the solution E in section2.3.3 114 | pub isg: f64, 115 | } 116 | 117 | impl Track { 118 | /// Builds a new CGGTTS [Track]. To follow CGGTTS guidelines, 119 | /// it is important to use an [Epoch] expressed in [Timescale::UTC]. 120 | /// Prefer [Track::new_glonass] when working with [SV] from this constellation. 121 | /// 122 | /// ## Inputs 123 | /// - sv: [SV] that was tracked 124 | /// - utc_epoch: [Epoch] in [Timescale::UTC] 125 | /// - class: [CommonViewClass] this new [Track] corresponds to 126 | /// - elevation_deg: elevation (at mid point, in case of complex 127 | /// track collection and fitting algorithm) in degrees 128 | /// - azimuth_deg: azimuth (at mid point, in case of complex 129 | /// track collection and fitting algorithm) in degrees 130 | /// - data: actual [TrackData] 131 | /// - ionosphere: possible [IonosphericData] compatible 132 | /// with modern GNSS receivers 133 | /// - rcvr_channel: (ideally) channel number used 134 | /// by receiver when tracking this solution. Tie to "0" 135 | /// when not known. 136 | /// - frc: (ideally) RINEx like carrier/modulation frequency 137 | /// code. For example "C1" would be (old) pseudo range on L1 frequency. 138 | /// And "C1C" is the modern equivalent, that fully describe the modulation. 139 | pub fn new( 140 | sv: SV, 141 | utc_epoch: Epoch, 142 | duration: Duration, 143 | class: CommonViewClass, 144 | elevation_deg: f64, 145 | azimuth_deg: f64, 146 | data: TrackData, 147 | iono: Option, 148 | rcvr_channel: u8, 149 | frc: &str, 150 | ) -> Self { 151 | Self { 152 | sv, 153 | epoch: utc_epoch, 154 | class, 155 | duration, 156 | elevation_deg, 157 | azimuth_deg, 158 | data, 159 | iono, 160 | fdma_channel: None, 161 | hc: rcvr_channel, 162 | frc: frc.to_string(), 163 | } 164 | } 165 | 166 | /// Builds new CGGTTS [Track] from single Glonass SV realization. 167 | /// Epoch should be expressed in UTC for this operation to be valid. 168 | /// 169 | /// ## Inputs 170 | /// - sv: [SV] that was tracked 171 | /// - utc_epoch: [Epoch] in [Timescale::UTC] 172 | /// - class: [CommonViewClass] this new [Track] corresponds to 173 | /// - elevation_deg: elevation (at mid point, in case of complex 174 | /// track collection and fitting algorithm) in degrees 175 | /// - azimuth_deg: azimuth (at mid point, in case of complex 176 | /// track collection and fitting algorithm) in degrees 177 | /// - data: actual [TrackData] 178 | /// - ionosphere: possible [IonosphericData] compatible 179 | /// with modern GNSS receivers 180 | /// - fdma_channel: (ideally) FDMA channel used 181 | /// in the tracking process. Should be > 0 and < 25 for correct CGGTTS. 182 | /// - frc: (ideally) RINEx like carrier/modulation frequency 183 | /// code. For example "C1" would be (old) pseudo range on L1 frequency. 184 | /// And "C1C" is the modern equivalent, that fully describe the modulation. 185 | pub fn new_glonass( 186 | sv: SV, 187 | utc_epoch: Epoch, 188 | duration: Duration, 189 | class: CommonViewClass, 190 | elevation_deg: f64, 191 | azimuth_deg: f64, 192 | data: TrackData, 193 | iono: Option, 194 | rcvr_channel: u8, 195 | fdma_channel: u8, 196 | frc: &str, 197 | ) -> Self { 198 | Self { 199 | sv, 200 | epoch: utc_epoch, 201 | duration, 202 | class, 203 | elevation_deg, 204 | azimuth_deg, 205 | data, 206 | iono, 207 | fdma_channel: Some(fdma_channel), 208 | hc: rcvr_channel, 209 | frc: frc.to_string(), 210 | } 211 | } 212 | 213 | /// Returns true if this [Track]ed the following [Constellation]. 214 | pub fn uses_constellation(&self, c: Constellation) -> bool { 215 | self.sv.constellation == c 216 | } 217 | 218 | /// Returns True if this [Track] seems compatible with the [CommonViewPeriod] 219 | /// recommended by BIPM. This cannot be a complete confirmation, 220 | /// because only the receiver that generated this data knows 221 | /// if the [Track] collection and fitting was implemented correctly. 222 | pub fn follows_bipm_tracking(&self) -> bool { 223 | self.duration == Duration::from_seconds(780.0) 224 | } 225 | 226 | /// Returns a [Track] with desired [SV]. 227 | pub fn with_sv(&self, sv: SV) -> Self { 228 | let mut t = self.clone(); 229 | t.sv = sv; 230 | t 231 | } 232 | 233 | /// Returns a [Track] with desired elevation (at mid point in the fitting collection 234 | /// algorithm), in degrees. 235 | pub fn with_elevation_deg(&self, elevation_deg: f64) -> Self { 236 | let mut t = self.clone(); 237 | t.elevation_deg = elevation_deg; 238 | t 239 | } 240 | 241 | /// Returns a [Track] with desired azimuth (at mid point in the fitting collection 242 | /// algorithm), in degrees. 243 | pub fn with_azimuth_deg(&self, azimuth_deg: f64) -> Self { 244 | let mut t = self.clone(); 245 | t.azimuth_deg = azimuth_deg; 246 | t 247 | } 248 | 249 | /// Returns a `Track` with desired Frequency carrier code 250 | pub fn with_carrier_code(&self, code: &str) -> Self { 251 | let mut t = self.clone(); 252 | t.frc = code.to_string(); 253 | t 254 | } 255 | /// Returns true if Self comes with Ionospheric parameter estimates 256 | pub fn has_ionospheric_data(&self) -> bool { 257 | self.iono.is_some() 258 | } 259 | } 260 | 261 | fn parse_data(items: &mut std::str::SplitAsciiWhitespace<'_>) -> Result { 262 | let refsv = items 263 | .next() 264 | .ok_or(Error::MissingField(String::from("REFSV")))? 265 | .parse::() 266 | .map_err(|_| Error::FieldParsing(String::from("REFSV")))? 267 | * 1E-10; 268 | 269 | let srsv = items 270 | .next() 271 | .ok_or(Error::MissingField(String::from("SRSV")))? 272 | .parse::() 273 | .map_err(|_| Error::FieldParsing(String::from("SRSV")))? 274 | * 1E-13; 275 | 276 | let refsys = items 277 | .next() 278 | .ok_or(Error::MissingField(String::from("REFSYS")))? 279 | .parse::() 280 | .map_err(|_| Error::FieldParsing(String::from("REFSYS")))? 281 | * 1E-10; 282 | 283 | let srsys = items 284 | .next() 285 | .ok_or(Error::MissingField(String::from("SRSYS")))? 286 | .parse::() 287 | .map_err(|_| Error::FieldParsing(String::from("SRSYS")))? 288 | * 1E-13; 289 | 290 | let dsg = items 291 | .next() 292 | .ok_or(Error::MissingField(String::from("DSG")))? 293 | .parse::() 294 | .map_err(|_| Error::FieldParsing(String::from("DSG")))? 295 | * 1E-10; 296 | 297 | let ioe = items 298 | .next() 299 | .ok_or(Error::MissingField(String::from("IOE")))? 300 | .parse::() 301 | .map_err(|_| Error::FieldParsing(String::from("IOE")))?; 302 | 303 | let mdtr = items 304 | .next() 305 | .ok_or(Error::MissingField(String::from("MDTR")))? 306 | .parse::() 307 | .map_err(|_| Error::FieldParsing(String::from("MDTR")))? 308 | * 1E-10; 309 | 310 | let smdt = items 311 | .next() 312 | .ok_or(Error::MissingField(String::from("SMDT")))? 313 | .parse::() 314 | .map_err(|_| Error::FieldParsing(String::from("SMDT")))? 315 | * 1E-13; 316 | 317 | let mdio = items 318 | .next() 319 | .ok_or(Error::MissingField(String::from("MDIO")))? 320 | .parse::() 321 | .map_err(|_| Error::FieldParsing(String::from("MDIO")))? 322 | * 1E-10; 323 | 324 | let smdi = items 325 | .next() 326 | .ok_or(Error::MissingField(String::from("SMDI")))? 327 | .parse::() 328 | .map_err(|_| Error::FieldParsing(String::from("SMDI")))? 329 | * 1E-13; 330 | 331 | Ok(TrackData { 332 | refsv, 333 | srsv, 334 | refsys, 335 | srsys, 336 | dsg, 337 | ioe, 338 | mdtr, 339 | smdt, 340 | mdio, 341 | smdi, 342 | }) 343 | } 344 | 345 | fn parse_without_iono( 346 | items: &mut std::str::SplitAsciiWhitespace<'_>, 347 | ) -> Result<(TrackData, Option), Error> { 348 | let data = parse_data(items)?; 349 | Ok((data, None)) 350 | } 351 | 352 | fn parse_with_iono( 353 | items: &mut std::str::SplitAsciiWhitespace<'_>, 354 | ) -> Result<(TrackData, Option), Error> { 355 | let data = parse_data(items)?; 356 | 357 | let msio = items 358 | .next() 359 | .ok_or(Error::MissingField(String::from("MSIO")))? 360 | .parse::() 361 | .map_err(|_| Error::FieldParsing(String::from("MSIO")))? 362 | * 0.1E-9; 363 | 364 | let smsi = items 365 | .next() 366 | .ok_or(Error::MissingField(String::from("SMSI")))? 367 | .parse::() 368 | .map_err(|_| Error::FieldParsing(String::from("SMSI")))? 369 | * 0.1E-12; 370 | 371 | let isg = items 372 | .next() 373 | .ok_or(Error::MissingField(String::from("ISG")))? 374 | .parse::() 375 | .map_err(|_| Error::FieldParsing(String::from("ISG")))? 376 | * 0.1E-9; 377 | 378 | Ok((data, Some(IonosphericData { msio, smsi, isg }))) 379 | } 380 | 381 | impl std::str::FromStr for Track { 382 | type Err = Error; 383 | /* 384 | * Builds a Track from given str description 385 | */ 386 | fn from_str(line: &str) -> Result { 387 | let cleanedup = String::from(line.trim()); 388 | let _epoch = Epoch::default(); 389 | let mut items = cleanedup.split_ascii_whitespace(); 390 | 391 | let nb_items = items.clone().count(); 392 | 393 | let sv = SV::from_str( 394 | items 395 | .next() 396 | .ok_or(Error::MissingField(String::from("SV")))?, 397 | )?; 398 | 399 | let class = CommonViewClass::from_str( 400 | items 401 | .next() 402 | .ok_or(Error::MissingField(String::from("CL")))? 403 | .trim(), 404 | )?; 405 | 406 | let mjd = items 407 | .next() 408 | .ok_or(Error::MissingField(String::from("MJD")))? 409 | .parse::() 410 | .map_err(|_| Error::FieldParsing(String::from("MJD")))?; 411 | 412 | let trk_sttime = items 413 | .next() 414 | .ok_or(Error::MissingField(String::from("STTIME")))?; 415 | 416 | if trk_sttime.len() < 6 { 417 | return Err(Error::InvalidTrkTimeFormat); 418 | } 419 | 420 | let h = trk_sttime[0..2] 421 | .parse::() 422 | .map_err(|_| Error::FieldParsing(String::from("STTIME:%H")))?; 423 | 424 | let m = trk_sttime[2..4] 425 | .parse::() 426 | .map_err(|_| Error::FieldParsing(String::from("STTIME:%M")))?; 427 | 428 | let s = trk_sttime[4..6] 429 | .parse::() 430 | .map_err(|_| Error::FieldParsing(String::from("STTIME:%S")))?; 431 | 432 | let mut epoch = Epoch::from_mjd_utc(mjd as f64); 433 | epoch += (h as f64) * Unit::Hour; 434 | epoch += (m as f64) * Unit::Minute; 435 | epoch += (s as f64) * Unit::Second; 436 | 437 | let duration = Duration::from_seconds( 438 | items 439 | .next() 440 | .ok_or(Error::MissingField(String::from("STTIME")))? 441 | .parse::() 442 | .map_err(|_| Error::FieldParsing(String::from("STTIME")))?, 443 | ); 444 | 445 | let elevation_deg = items 446 | .next() 447 | .ok_or(Error::MissingField(String::from("ELV")))? 448 | .parse::() 449 | .map_err(|_| Error::FieldParsing(String::from("ELV")))? 450 | * 0.1; 451 | 452 | let azimuth_deg = items 453 | .next() 454 | .ok_or(Error::MissingField(String::from("AZTH")))? 455 | .parse::() 456 | .map_err(|_| Error::FieldParsing(String::from("AZTH")))? 457 | * 0.1; 458 | 459 | let (data, iono) = match nb_items { 460 | TRACK_WITH_IONOSPHERIC => parse_with_iono(&mut items)?, 461 | TRACK_WITHOUT_IONOSPHERIC => parse_without_iono(&mut items)?, 462 | _ => { 463 | return Err(Error::InvalidFormat); 464 | }, 465 | }; 466 | 467 | let fr = items 468 | .next() 469 | .ok_or(Error::MissingField(String::from("fr")))? 470 | .parse::() 471 | .map_err(|_| Error::FieldParsing(String::from("fr")))?; 472 | 473 | let hc = items 474 | .next() 475 | .ok_or(Error::MissingField(String::from("hc")))? 476 | .parse::() 477 | .map_err(|_| Error::FieldParsing(String::from("hc")))?; 478 | 479 | let frc: String = items 480 | .next() 481 | .ok_or(Error::MissingField(String::from("frc")))? 482 | .parse() 483 | .map_err(|_| Error::FieldParsing(String::from("frc")))?; 484 | 485 | // checksum 486 | let ck = items 487 | .next() 488 | .ok_or(Error::MissingField(String::from("ck")))?; 489 | 490 | let _ck = 491 | u8::from_str_radix(ck, 16).map_err(|_| Error::FieldParsing(String::from("ck")))?; 492 | 493 | // let cksum = calc_crc(&line.split_at(end_pos - 1).0)?; 494 | 495 | // verification 496 | /*if cksum != ck { 497 | println!("GOT {} EXPECT {}", ck, cksum); 498 | return Err(Error::ChecksumError(cksum, ck)) 499 | }*/ 500 | 501 | Ok(Track { 502 | sv, 503 | class, 504 | epoch, 505 | duration, 506 | elevation_deg, 507 | azimuth_deg, 508 | data, 509 | iono, 510 | hc, 511 | frc, 512 | fdma_channel: if fr == 0 { None } else { Some(fr) }, 513 | }) 514 | } 515 | } 516 | 517 | #[cfg(test)] 518 | mod tests { 519 | use crate::prelude::*; 520 | use gnss::prelude::{Constellation, SV}; 521 | use hifitime::Duration; 522 | use std::str::FromStr; 523 | #[test] 524 | fn track_parsing() { 525 | let content = 526 | "G99 99 59568 001000 0780 099 0099 +9999999999 +99999 +1536 +181 26 999 9999 +999 9999 +999 00 00 L1C D3"; 527 | let track = Track::from_str(content); 528 | assert!(track.is_ok()); 529 | let track = track.unwrap(); 530 | assert_eq!( 531 | track.sv, 532 | SV { 533 | constellation: Constellation::GPS, 534 | prn: 99 535 | } 536 | ); 537 | assert_eq!(track.class, CommonViewClass::SingleChannel); 538 | assert!(track.follows_bipm_tracking()); 539 | assert_eq!(track.duration, Duration::from_seconds(780.0)); 540 | assert!(!track.has_ionospheric_data()); 541 | assert_eq!(track.elevation_deg, 9.9); 542 | assert_eq!(track.azimuth_deg, 9.9); 543 | assert!(track.fdma_channel.is_none()); 544 | assert!((track.data.dsg - 2.5E-9).abs() < 1E-6); 545 | assert!((track.data.srsys - 2.83E-11).abs() < 1E-6); 546 | assert_eq!(track.hc, 0); 547 | assert_eq!(track.frc, "L1C"); 548 | 549 | let content = 550 | "G99 99 59563 001400 0780 099 0099 +9999999999 +99999 +1588 +1027 27 999 9999 +999 9999 +999 00 00 L1C EA"; 551 | let track = Track::from_str(content); 552 | assert!(track.is_ok()); 553 | let track = track.unwrap(); 554 | assert_eq!( 555 | track.sv, 556 | SV { 557 | constellation: Constellation::GPS, 558 | prn: 99 559 | } 560 | ); 561 | assert_eq!(track.class, CommonViewClass::SingleChannel); 562 | assert!(track.follows_bipm_tracking()); 563 | assert_eq!(track.duration, Duration::from_seconds(780.0)); 564 | assert!(!track.has_ionospheric_data()); 565 | assert_eq!(track.elevation_deg, 9.9); 566 | assert_eq!(track.azimuth_deg, 9.9); 567 | assert!(track.fdma_channel.is_none()); 568 | assert_eq!(track.hc, 0); 569 | assert_eq!(track.frc, "L1C"); 570 | 571 | let content = 572 | "G99 99 59563 232200 0780 099 0099 +9999999999 +99999 +1529 -507 23 999 9999 +999 9999 +999 00 00 L1C D9"; 573 | let track = Track::from_str(content); 574 | assert!(track.is_ok()); 575 | let track = track.unwrap(); 576 | assert_eq!(track.class, CommonViewClass::SingleChannel); 577 | assert!(track.follows_bipm_tracking()); 578 | assert_eq!(track.duration, Duration::from_seconds(780.0)); 579 | assert!(!track.has_ionospheric_data()); 580 | assert_eq!(track.elevation_deg, 9.9); 581 | assert_eq!(track.azimuth_deg, 9.9); 582 | assert!(track.fdma_channel.is_none()); 583 | assert_eq!(track.hc, 0); 584 | assert_eq!(track.frc, "L1C"); 585 | 586 | let content = 587 | "G99 99 59567 001400 0780 099 0099 +9999999999 +99999 +1561 -151 27 999 9999 +999 9999 +999 00 00 L1C D4"; 588 | let track = Track::from_str(content); 589 | assert!(track.is_ok()); 590 | let track = track.unwrap(); 591 | assert_eq!( 592 | track.sv, 593 | SV { 594 | constellation: Constellation::GPS, 595 | prn: 99 596 | } 597 | ); 598 | assert_eq!(track.class, CommonViewClass::SingleChannel); 599 | //assert_eq!(track.trktime 043400) 600 | assert!(track.follows_bipm_tracking()); 601 | assert_eq!(track.duration, Duration::from_seconds(780.0)); 602 | assert!(!track.has_ionospheric_data()); 603 | assert_eq!(track.elevation_deg, 9.9); 604 | assert_eq!(track.azimuth_deg, 9.9); 605 | assert!(track.fdma_channel.is_none()); 606 | assert_eq!(track.hc, 0); 607 | assert_eq!(track.frc, "L1C"); 608 | } 609 | 610 | #[test] 611 | fn parser_ionospheric() { 612 | let content = 613 | "R24 FF 57000 000600 0780 347 0394 +1186342 +0 163 +0 40 2 141 +22 23 -1 23 -1 29 +2 0 L3P EF"; 614 | let track = Track::from_str(content); 615 | //assert_eq!(track.is_ok(), true); 616 | let track = track.unwrap(); 617 | assert_eq!(track.class, CommonViewClass::MultiChannel); 618 | assert!(track.follows_bipm_tracking()); 619 | assert_eq!(track.duration, Duration::from_seconds(780.0)); 620 | assert!(track.has_ionospheric_data()); 621 | let iono = track.iono.unwrap(); 622 | assert_eq!(iono.msio, 23.0E-10); 623 | assert_eq!(iono.smsi, -1.0E-13); 624 | assert_eq!(iono.isg, 29.0E-10); 625 | assert_eq!(track.elevation_deg, 34.7); 626 | assert!((track.azimuth_deg - 39.4).abs() < 1E-6); 627 | assert_eq!(track.fdma_channel, Some(2)); 628 | assert_eq!(track.hc, 0); 629 | assert_eq!(track.frc, "L3P"); 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /src/track/parsing.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtk-rs/cggtts/6360dde78a6719bf7d44b0fbdd4b020326cc1498/src/track/parsing.rs -------------------------------------------------------------------------------- /src/tracker/fit.rs: -------------------------------------------------------------------------------- 1 | //! Satellite track fitting 2 | use hifitime::Unit; 3 | use log::debug; 4 | use polyfit_rs::polyfit_rs::polyfit; 5 | use thiserror::Error; 6 | 7 | use crate::prelude::{Duration, Epoch, FittedData, SV}; 8 | 9 | /// CGGTTS track formation errors 10 | #[derive(Debug, Clone, Error)] 11 | pub enum FitError { 12 | /// Unknown satellite (not tracked at all) 13 | #[error("unknown satellite (not tracked at all)")] 14 | UnknownSatellite, 15 | /// At least two symbols are required to fit 16 | #[error("track fitting requires at least 3 observations")] 17 | NotEnoughSymbols, 18 | /// Linear regression failure. Either extreme values 19 | /// encountered or data gaps are present. 20 | #[error("linear regression failure")] 21 | LinearRegressionFailure, 22 | } 23 | 24 | /// [SVTracker] is used to track an individual [SV]. 25 | #[derive(Default, Debug, Clone)] 26 | pub struct SVTracker { 27 | /// SV being tracked 28 | sv: SV, 29 | /// Symbols counter 30 | size: usize, 31 | /// Sampling gap tolerance 32 | gap_tolerance: Option, 33 | /// First Epoch of this fit 34 | t0: Option, 35 | /// Previous Epoch (for internal logic) 36 | prev_t: Option, 37 | /// Internal buffer 38 | buffer: Vec, 39 | } 40 | 41 | /// [Observation] you need to provide to attempt a CGGTTS fit. 42 | #[derive(Debug, Default, Clone)] 43 | pub struct Observation { 44 | /// Epoch of [Observation] 45 | pub epoch: Epoch, 46 | /// Satellite onboard clock offset to local clock 47 | pub refsv: f64, 48 | /// Satellite onboard clock offset to timescale 49 | pub refsys: f64, 50 | /// Modeled Tropospheric Delay in seconds of propagation delay 51 | pub mdtr: f64, 52 | /// Modeled Ionospheric Delay in seconds of propagation delay 53 | pub mdio: f64, 54 | /// Possible measured Ionospheric Delay in seconds of propagation delay 55 | pub msio: Option, 56 | /// Elevation in degrees 57 | pub elevation: f64, 58 | /// Azimuth in degrees 59 | pub azimuth: f64, 60 | } 61 | 62 | impl SVTracker { 63 | /// Allocate a new [SVTracker] for that particular satellite. 64 | /// 65 | /// ## Input 66 | /// - satellite: [SV] 67 | pub fn new(satellite: SV) -> Self { 68 | Self { 69 | size: 0, 70 | t0: None, 71 | prev_t: None, 72 | sv: satellite, 73 | gap_tolerance: None, 74 | buffer: Vec::with_capacity(16), 75 | } 76 | } 77 | 78 | /// Define a new [SVTracker] with desired observation gap tolerance. 79 | pub fn with_gap_tolerance(&self, tolerance: Duration) -> Self { 80 | let mut s = self.clone(); 81 | s.gap_tolerance = Some(tolerance); 82 | s 83 | } 84 | 85 | /// Feed new [Observation] at t [Epoch] of observation (sampling). 86 | /// Although CGGTTS works in UTC internally, we accept any timescale here. 87 | /// Samples must be provided in chronological order. 88 | /// If you provide MSIO, you are expected to provide it at very single epoch, 89 | /// like any other fields, in order to obtain valid results. 90 | /// 91 | /// ## Input 92 | /// - data: [Observation] 93 | pub fn new_observation(&mut self, data: Observation) { 94 | if let Some(past_t) = self.prev_t { 95 | if let Some(tolerance) = self.gap_tolerance { 96 | let dt = data.epoch - past_t; 97 | if dt > tolerance { 98 | debug!("{}({}) - {} data gap", data.epoch, self.sv, dt); 99 | self.size = 0; 100 | self.buffer.clear(); 101 | } 102 | } 103 | } 104 | 105 | if self.t0.is_none() { 106 | self.t0 = Some(data.epoch); 107 | } 108 | 109 | self.prev_t = Some(data.epoch); 110 | self.buffer.push(data); 111 | self.size += 1; 112 | } 113 | 114 | /// Manual reset of the internal buffer. 115 | pub fn reset(&mut self) { 116 | self.prev_t = None; 117 | self.size = 0; 118 | self.buffer.clear(); 119 | } 120 | 121 | /// True if at least one measurement is currently latched and may contribute to a fit. 122 | pub fn not_empty(&self) -> bool { 123 | self.size > 0 124 | } 125 | 126 | /// Apply fit algorithm over internal buffer. 127 | /// You manage the buffer content and sampling and are responsible 128 | /// for the [FittedData] you may obtain. The requirement being at least 3 129 | /// symbols must have been buffered. 130 | pub fn fit(&mut self) -> Result { 131 | // Request 3 symbols at least 132 | if self.size < 3 { 133 | return Err(FitError::NotEnoughSymbols); 134 | } 135 | 136 | let midpoint = if self.size % 2 == 0 { 137 | self.size / 2 - 1 138 | } else { 139 | (self.size + 1) / 2 - 1 140 | }; 141 | 142 | // Retrieve information @ mid point 143 | let t0 = self.buffer[0].epoch; 144 | let t_mid = self.buffer[midpoint].epoch; 145 | let t_mid_s = t_mid.duration.to_unit(Unit::Second); 146 | let t_last = self.buffer[self.size - 1].epoch; 147 | 148 | let azim_mid = self.buffer[midpoint].azimuth; 149 | let elev_mid = self.buffer[midpoint].elevation; 150 | 151 | let mut fitted = FittedData::default(); 152 | 153 | fitted.sv = self.sv; 154 | fitted.first_t = t0; 155 | fitted.duration = t_last - t0; 156 | fitted.midtrack = t_mid; 157 | fitted.azimuth_deg = azim_mid; 158 | fitted.elevation_deg = elev_mid; 159 | 160 | // retrieve x_s 161 | let x_s = self 162 | .buffer 163 | .iter() 164 | .map(|data| data.epoch.duration.to_unit(Unit::Second)) 165 | .collect::>(); 166 | 167 | // REFSV 168 | let fit = polyfit( 169 | &x_s, 170 | self.buffer 171 | .iter() 172 | .map(|data| data.refsv) 173 | .collect::>() 174 | .as_slice(), 175 | 1, 176 | ) 177 | .or(Err(FitError::LinearRegressionFailure))?; 178 | 179 | let (srsv, srsv_b) = (fit[1], fit[0]); 180 | let refsv = srsv * t_mid_s + srsv_b; 181 | 182 | // REFSYS 183 | let fit = polyfit( 184 | &x_s, 185 | self.buffer 186 | .iter() 187 | .map(|data| data.refsys) 188 | .collect::>() 189 | .as_slice(), 190 | 1, 191 | ) 192 | .or(Err(FitError::LinearRegressionFailure))?; 193 | 194 | let (srsys, srsys_b) = (fit[1], fit[0]); 195 | let refsys_fit = srsys * t_mid_s + srsys_b; 196 | 197 | // DSG 198 | let mut dsg = 0.0_f64; 199 | for obs in self.buffer.iter() { 200 | dsg += (obs.refsys - refsys_fit).powi(2); 201 | } 202 | dsg /= self.size as f64; 203 | dsg = dsg.sqrt(); 204 | 205 | // MDTR 206 | let fit = polyfit( 207 | &x_s, 208 | self.buffer 209 | .iter() 210 | .map(|data| data.mdtr) 211 | .collect::>() 212 | .as_slice(), 213 | 1, 214 | ) 215 | .or(Err(FitError::LinearRegressionFailure))?; 216 | 217 | let (smdt, smdt_b) = (fit[1], fit[0]); 218 | let mdtr = smdt * t_mid_s + smdt_b; 219 | 220 | // MDIO 221 | let fit = polyfit( 222 | &x_s, 223 | self.buffer 224 | .iter() 225 | .map(|data| data.mdio) 226 | .collect::>() 227 | .as_slice(), 228 | 1, 229 | ) 230 | .or(Err(FitError::LinearRegressionFailure))?; 231 | 232 | let (smdi, smdi_b) = (fit[1], fit[0]); 233 | let mdio = smdi * t_mid_s + smdi_b; 234 | 235 | // MSIO 236 | let msio = self 237 | .buffer 238 | .iter() 239 | .filter_map(|data| { 240 | if let Some(msio) = data.msio { 241 | Some(msio) 242 | } else { 243 | None 244 | } 245 | }) 246 | .collect::>(); 247 | 248 | let msio_len = msio.len(); 249 | 250 | if msio_len > 0 { 251 | let fit = polyfit(&x_s, &msio, 1).or(Err(FitError::LinearRegressionFailure))?; 252 | 253 | let (smsi, smsi_b) = (fit[1], fit[0]); 254 | let msio_fit = smsi * t_mid_s + smsi_b; 255 | 256 | // ISG 257 | let mut isg = 0.0_f64; 258 | for i in 0..msio_len { 259 | isg += (msio_fit - msio[i]).powi(2); 260 | } 261 | isg /= self.size as f64; 262 | isg = isg.sqrt(); 263 | 264 | fitted.isg = Some(isg); 265 | fitted.msio_s = Some(msio_fit); 266 | fitted.smsi_s_s = Some(smsi); 267 | } 268 | 269 | fitted.srsv_s_s = srsv; 270 | fitted.refsv_s = refsv; 271 | fitted.srsys_s_s = srsys; 272 | fitted.refsys_s = refsys_fit; 273 | fitted.dsg = dsg; 274 | fitted.mdtr_s = mdtr; 275 | fitted.smdt_s_s = smdt; 276 | fitted.mdio_s = mdio; 277 | fitted.smdi_s_s = smdi; 278 | 279 | // reset for next time 280 | self.t0 = None; 281 | self.buffer.clear(); 282 | self.size = 0; 283 | 284 | Ok(fitted) 285 | } 286 | 287 | fn has_msio(&self) -> bool { 288 | self.buffer 289 | .iter() 290 | .filter(|data| data.msio.is_some()) 291 | .count() 292 | > 0 293 | } 294 | } 295 | 296 | #[cfg(test)] 297 | mod test { 298 | use crate::prelude::{Duration, Epoch, Observation, SVTracker, SV}; 299 | use std::str::FromStr; 300 | 301 | #[test] 302 | fn tracker_no_gap_x3() { 303 | let g01 = SV::from_str("G01").unwrap(); 304 | let mut tracker = SVTracker::new(g01); 305 | 306 | for obs in [ 307 | Observation { 308 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 309 | refsv: 1.0, 310 | refsys: 2.0, 311 | mdtr: 3.0, 312 | mdio: 4.0, 313 | msio: None, 314 | elevation: 6.0, 315 | azimuth: 7.0, 316 | }, 317 | Observation { 318 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 319 | refsv: 1.1, 320 | refsys: 2.1, 321 | mdtr: 3.1, 322 | mdio: 4.1, 323 | msio: None, 324 | elevation: 6.1, 325 | azimuth: 7.1, 326 | }, 327 | ] { 328 | tracker.new_observation(obs); 329 | } 330 | 331 | assert!(tracker.fit().is_err()); 332 | 333 | tracker.new_observation(Observation { 334 | epoch: Epoch::from_str("2020-01-01T00:01:00 UTC").unwrap(), 335 | refsv: 1.2, 336 | refsys: 2.2, 337 | mdtr: 3.2, 338 | mdio: 4.2, 339 | msio: None, 340 | elevation: 6.2, 341 | azimuth: 7.2, 342 | }); 343 | 344 | let fitted = tracker.fit().unwrap(); 345 | 346 | assert_eq!(fitted.sv, g01); 347 | assert_eq!(fitted.duration, Duration::from_seconds(60.0)); 348 | 349 | assert_eq!( 350 | fitted.first_t, 351 | Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap() 352 | ); 353 | 354 | assert_eq!( 355 | fitted.midtrack, 356 | Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap() 357 | ); 358 | 359 | assert_eq!(fitted.elevation_deg, 6.1); 360 | assert_eq!(fitted.azimuth_deg, 7.1); 361 | } 362 | 363 | #[test] 364 | fn tracker_no_gap_x4() { 365 | let g01 = SV::from_str("G01").unwrap(); 366 | let mut tracker = SVTracker::new(g01); 367 | 368 | for obs in [ 369 | Observation { 370 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 371 | refsv: 1.0, 372 | refsys: 2.0, 373 | mdtr: 3.0, 374 | mdio: 4.0, 375 | msio: None, 376 | elevation: 6.0, 377 | azimuth: 7.0, 378 | }, 379 | Observation { 380 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 381 | refsv: 1.1, 382 | refsys: 2.1, 383 | mdtr: 3.1, 384 | mdio: 4.1, 385 | msio: None, 386 | elevation: 6.1, 387 | azimuth: 7.1, 388 | }, 389 | ] { 390 | tracker.new_observation(obs); 391 | } 392 | 393 | assert!(tracker.fit().is_err()); 394 | 395 | tracker.new_observation(Observation { 396 | epoch: Epoch::from_str("2020-01-01T00:01:00 UTC").unwrap(), 397 | refsv: 1.2, 398 | refsys: 2.2, 399 | mdtr: 3.2, 400 | mdio: 4.2, 401 | msio: None, 402 | elevation: 6.2, 403 | azimuth: 7.2, 404 | }); 405 | 406 | let fitted = tracker.fit().unwrap(); 407 | 408 | assert_eq!(fitted.sv, g01); 409 | assert_eq!(fitted.duration, Duration::from_seconds(60.0)); 410 | 411 | assert_eq!( 412 | fitted.first_t, 413 | Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap() 414 | ); 415 | 416 | assert_eq!( 417 | fitted.midtrack, 418 | Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap() 419 | ); 420 | 421 | assert_eq!(fitted.elevation_deg, 6.1); 422 | assert_eq!(fitted.azimuth_deg, 7.1); 423 | } 424 | 425 | #[test] 426 | fn tracker_30s_15s_ok() { 427 | let g01 = SV::from_str("G01").unwrap(); 428 | let dt_30s = Duration::from_str("30 s").unwrap(); 429 | 430 | let mut tracker = SVTracker::new(g01).with_gap_tolerance(dt_30s); 431 | 432 | for obs in [ 433 | Observation { 434 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 435 | refsv: 1.0, 436 | refsys: 2.0, 437 | mdtr: 3.0, 438 | mdio: 4.0, 439 | msio: None, 440 | elevation: 6.0, 441 | azimuth: 7.0, 442 | }, 443 | Observation { 444 | epoch: Epoch::from_str("2020-01-01T00:00:15 UTC").unwrap(), 445 | refsv: 1.1, 446 | refsys: 2.1, 447 | mdtr: 3.1, 448 | mdio: 4.1, 449 | msio: None, 450 | elevation: 6.1, 451 | azimuth: 7.1, 452 | }, 453 | Observation { 454 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 455 | refsv: 1.1, 456 | refsys: 2.1, 457 | mdtr: 3.1, 458 | mdio: 4.1, 459 | msio: None, 460 | elevation: 6.1, 461 | azimuth: 7.1, 462 | }, 463 | Observation { 464 | epoch: Epoch::from_str("2020-01-01T00:00:45 UTC").unwrap(), 465 | refsv: 1.1, 466 | refsys: 2.1, 467 | mdtr: 3.1, 468 | mdio: 4.1, 469 | msio: None, 470 | elevation: 6.1, 471 | azimuth: 7.1, 472 | }, 473 | ] { 474 | tracker.new_observation(obs); 475 | } 476 | 477 | assert!(tracker.fit().is_ok()); 478 | } 479 | 480 | #[test] 481 | fn tracker_30s_30s_ok() { 482 | let g01 = SV::from_str("G01").unwrap(); 483 | let dt_30s = Duration::from_str("30 s").unwrap(); 484 | 485 | let mut tracker = SVTracker::new(g01).with_gap_tolerance(dt_30s); 486 | 487 | for obs in [ 488 | Observation { 489 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 490 | refsv: 1.0, 491 | refsys: 2.0, 492 | mdtr: 3.0, 493 | mdio: 4.0, 494 | msio: None, 495 | elevation: 6.0, 496 | azimuth: 7.0, 497 | }, 498 | Observation { 499 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 500 | refsv: 1.1, 501 | refsys: 2.1, 502 | mdtr: 3.1, 503 | mdio: 4.1, 504 | msio: None, 505 | elevation: 6.1, 506 | azimuth: 7.1, 507 | }, 508 | Observation { 509 | epoch: Epoch::from_str("2020-01-01T00:01:00 UTC").unwrap(), 510 | refsv: 1.1, 511 | refsys: 2.1, 512 | mdtr: 3.1, 513 | mdio: 4.1, 514 | msio: None, 515 | elevation: 6.1, 516 | azimuth: 7.1, 517 | }, 518 | Observation { 519 | epoch: Epoch::from_str("2020-01-01T00:01:30 UTC").unwrap(), 520 | refsv: 1.1, 521 | refsys: 2.1, 522 | mdtr: 3.1, 523 | mdio: 4.1, 524 | msio: None, 525 | elevation: 6.1, 526 | azimuth: 7.1, 527 | }, 528 | ] { 529 | tracker.new_observation(obs); 530 | } 531 | 532 | assert!(tracker.fit().is_ok()); 533 | 534 | for obs in [ 535 | Observation { 536 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 537 | refsv: 1.0, 538 | refsys: 2.0, 539 | mdtr: 3.0, 540 | mdio: 4.0, 541 | msio: None, 542 | elevation: 6.0, 543 | azimuth: 7.0, 544 | }, 545 | Observation { 546 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 547 | refsv: 1.1, 548 | refsys: 2.1, 549 | mdtr: 3.1, 550 | mdio: 4.1, 551 | msio: None, 552 | elevation: 6.1, 553 | azimuth: 7.1, 554 | }, 555 | Observation { 556 | epoch: Epoch::from_str("2020-01-01T00:01:00 UTC").unwrap(), 557 | refsv: 1.1, 558 | refsys: 2.1, 559 | mdtr: 3.1, 560 | mdio: 4.1, 561 | msio: None, 562 | elevation: 6.1, 563 | azimuth: 7.1, 564 | }, 565 | Observation { 566 | epoch: Epoch::from_str("2020-01-01T00:01:15 UTC").unwrap(), 567 | refsv: 1.1, 568 | refsys: 2.1, 569 | mdtr: 3.1, 570 | mdio: 4.1, 571 | msio: None, 572 | elevation: 6.1, 573 | azimuth: 7.1, 574 | }, 575 | ] { 576 | tracker.new_observation(obs); 577 | } 578 | 579 | assert!(tracker.fit().is_ok()); 580 | } 581 | 582 | #[test] 583 | fn tracker_30s_nok() { 584 | let g01 = SV::from_str("G01").unwrap(); 585 | let dt_30s = Duration::from_str("30 s").unwrap(); 586 | 587 | let mut tracker = SVTracker::new(g01).with_gap_tolerance(dt_30s); 588 | 589 | for obs in [ 590 | Observation { 591 | epoch: Epoch::from_str("2020-01-01T00:00:00 UTC").unwrap(), 592 | refsv: 1.0, 593 | refsys: 2.0, 594 | mdtr: 3.0, 595 | mdio: 4.0, 596 | msio: None, 597 | elevation: 6.0, 598 | azimuth: 7.0, 599 | }, 600 | Observation { 601 | epoch: Epoch::from_str("2020-01-01T00:00:30 UTC").unwrap(), 602 | refsv: 1.1, 603 | refsys: 2.1, 604 | mdtr: 3.1, 605 | mdio: 4.1, 606 | msio: None, 607 | elevation: 6.1, 608 | azimuth: 7.1, 609 | }, 610 | Observation { 611 | epoch: Epoch::from_str("2020-01-01T00:01:00 UTC").unwrap(), 612 | refsv: 1.1, 613 | refsys: 2.1, 614 | mdtr: 3.1, 615 | mdio: 4.1, 616 | msio: None, 617 | elevation: 6.1, 618 | azimuth: 7.1, 619 | }, 620 | Observation { 621 | epoch: Epoch::from_str("2020-01-01T00:01:31 UTC").unwrap(), 622 | refsv: 1.1, 623 | refsys: 2.1, 624 | mdtr: 3.1, 625 | mdio: 4.1, 626 | msio: None, 627 | elevation: 6.1, 628 | azimuth: 7.1, 629 | }, 630 | ] { 631 | tracker.new_observation(obs); 632 | } 633 | 634 | assert!(tracker.fit().is_err()); 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/tracker/fitted.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::{CommonViewClass, Duration, Epoch, IonosphericData, Track, TrackData, SV}; 2 | 3 | /// [FittedData] resulting from running the fit algorithm over many [Observation]s. 4 | #[derive(Debug, Copy, Default, Clone)] 5 | pub struct FittedData { 6 | /// [SV] that was used 7 | pub sv: SV, 8 | /// Fit time window duration 9 | pub duration: Duration, 10 | /// Fit start time 11 | pub first_t: Epoch, 12 | /// [Epoch] at midtrack 13 | pub midtrack: Epoch, 14 | /// Satellite elevation at midtrack (in degrees) 15 | pub elevation_deg: f64, 16 | /// Satellite azimuth at midtrack (in degrees) 17 | pub azimuth_deg: f64, 18 | /// Satellite onboard clock offset to local clock 19 | pub refsv_s: f64, 20 | /// REFSV derivative (s/s) 21 | pub srsv_s_s: f64, 22 | /// Satellite onboard clock offset to timescale 23 | pub refsys_s: f64, 24 | /// REFSYS derivative (s/s) 25 | pub srsys_s_s: f64, 26 | /// DSG: REFSYS Root Mean Square 27 | pub dsg: f64, 28 | /// MDTR: modeled troposphere delay (s) 29 | pub mdtr_s: f64, 30 | /// SMDT: MDTR derivative (s/s) 31 | pub smdt_s_s: f64, 32 | /// MDIO: modeled ionosphere delay (s) 33 | pub mdio_s: f64, 34 | /// SMDI: MDIO derivative (s/s) 35 | pub smdi_s_s: f64, 36 | /// Possible MSIO: measured ionosphere delay (s) 37 | pub msio_s: Option, 38 | /// Possible MSIO derivative (s/s) 39 | pub smsi_s_s: Option, 40 | /// Possible ISG: MSIO Root Mean Square 41 | pub isg: Option, 42 | } 43 | 44 | impl FittedData { 45 | /// Form a new CGGTTS [Track] from this [FittedData], 46 | /// ready to be formatted. 47 | /// ## Input 48 | /// - class: [CommonViewClass] 49 | /// - data: serves many purposes. 50 | /// Most often (GPS, Gal, QZSS) it is the Issue of Ephemeris. 51 | /// For Glonass, it should be between 1-96 as the date of ephemeris as 52 | /// daily quarters of hours, starting at 1 for 00:00:00 midnight. 53 | /// For BeiDou, the hour of clock, between 0-23 should be used. 54 | /// - rinex_code: RINEX code. 55 | pub fn to_track(&self, class: CommonViewClass, data: u16, rinex_code: &str) -> Track { 56 | Track { 57 | class, 58 | epoch: self.first_t, 59 | duration: self.duration, 60 | sv: self.sv, 61 | azimuth_deg: self.azimuth_deg, 62 | elevation_deg: self.elevation_deg, 63 | fdma_channel: None, 64 | data: TrackData { 65 | ioe: data, 66 | refsv: self.refsv_s, 67 | srsv: self.srsv_s_s, 68 | refsys: self.refsys_s, 69 | srsys: self.srsys_s_s, 70 | dsg: self.dsg, 71 | mdtr: self.mdtr_s, 72 | smdt: self.smdt_s_s, 73 | mdio: self.mdio_s, 74 | smdi: self.smdi_s_s, 75 | }, 76 | iono: if let Some(msio_s) = self.msio_s { 77 | Some(IonosphericData { 78 | msio: msio_s, 79 | smsi: self.smsi_s_s.unwrap(), 80 | isg: self.isg.unwrap(), 81 | }) 82 | } else { 83 | None 84 | }, 85 | // TODO 86 | hc: 0, 87 | frc: rinex_code.to_string(), 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/tracker/mod.rs: -------------------------------------------------------------------------------- 1 | mod fit; 2 | mod fitted; 3 | 4 | pub use fit::{FitError, Observation, SVTracker}; 5 | pub use fitted::FittedData; 6 | 7 | use crate::prelude::{Duration, SV}; 8 | 9 | use std::{collections::hash_map::Keys, collections::HashMap}; 10 | 11 | /// [SkyTracker] is used to track all Satellite vehicles 12 | /// in sight during a common view period and eventually collect CGGTTS. 13 | #[derive(Default, Debug, Clone)] 14 | pub struct SkyTracker { 15 | /// Internal buffer 16 | trackers: HashMap, 17 | /// Gap tolerance 18 | gap_tolerance: Option, 19 | } 20 | 21 | impl SkyTracker { 22 | /// Allocate new [SkyTracker] 23 | pub fn new() -> Self { 24 | Self { 25 | trackers: HashMap::with_capacity(8), 26 | gap_tolerance: None, 27 | } 28 | } 29 | 30 | /// List of Satellites currently tracker 31 | pub fn satellites(&self) -> Keys<'_, SV, SVTracker> { 32 | self.trackers.keys() 33 | } 34 | 35 | /// Define a [SkyTracker] with desired observation gap tolerance. 36 | pub fn with_gap_tolerance(&self, tolerance: Duration) -> Self { 37 | let mut s = self.clone(); 38 | s.gap_tolerance = Some(tolerance); 39 | s 40 | } 41 | 42 | /// Provide new [Observation] for that particular satellite. 43 | pub fn new_observation(&mut self, satellite: SV, data: Observation) { 44 | if let Some(tracker) = self.trackers.get_mut(&satellite) { 45 | tracker.new_observation(data); 46 | } else { 47 | let mut new = SVTracker::new(satellite); 48 | if let Some(tolerance) = self.gap_tolerance { 49 | new = new.with_gap_tolerance(tolerance); 50 | } 51 | new.new_observation(data); 52 | self.trackers.insert(satellite, new); 53 | } 54 | } 55 | 56 | /// Attempt new satellite track fitting. 57 | /// ## Input 58 | /// - satellite: [SV] that must have been tracked. 59 | /// That tracking duration might have to respect external requirements. 60 | /// ## Output 61 | /// - fitted: [FittedData] that you can turn into a valid 62 | /// CGGTTS track if you provide a little more information. 63 | pub fn track_fit(&mut self, satellite: SV) -> Result { 64 | if let Some(sv) = self.trackers.get_mut(&satellite) { 65 | sv.fit() 66 | } else { 67 | Err(FitError::UnknownSatellite) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord, Hash)] 7 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 8 | pub enum Version { 9 | #[default] 10 | Version2E, 11 | } 12 | 13 | impl std::str::FromStr for Version { 14 | type Err = Error; 15 | fn from_str(s: &str) -> Result { 16 | if s.eq("2E") { 17 | Ok(Self::Version2E) 18 | } else { 19 | Err(Error::NonSupportedRevision(s.to_string())) 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------