├── .gitignore ├── rust-toolchain ├── devtools-frontend ├── .eslintignore ├── .gitignore ├── crates │ ├── opslang-wasm │ │ ├── .gitignore │ │ ├── src │ │ │ ├── util.rs │ │ │ ├── free_variables.rs │ │ │ ├── value.rs │ │ │ └── union_value.rs │ │ ├── tests │ │ │ └── web.rs │ │ ├── js │ │ │ └── union.js │ │ ├── Cargo.toml │ │ └── build.rs │ └── Cargo.toml ├── postcss.config.js ├── .prettierignore ├── src │ ├── lib.rs │ ├── index.css │ ├── error.ts │ ├── components │ │ ├── Top.tsx │ │ ├── Layout.tsx │ │ └── TelemetryView.tsx │ ├── client.ts │ ├── proto │ │ ├── tmtc_generic_c2a.client.ts │ │ └── broker.client.ts │ ├── tree.ts │ ├── main.tsx │ └── worker.ts ├── tailwind.config.js ├── index.html ├── Cargo.toml ├── .eslintrc.yml ├── vite.config.ts ├── tsconfig.json ├── package.json └── build.rs ├── gaia-ccsds-c2a ├── src │ ├── access.rs │ ├── lib.rs │ ├── ccsds_c2a.rs │ ├── ccsds_c2a │ │ ├── tc.rs │ │ ├── aos │ │ │ ├── transfer_frame.rs │ │ │ ├── virtual_channel.rs │ │ │ └── space_packet.rs │ │ ├── aos.rs │ │ └── tc │ │ │ ├── segment.rs │ │ │ ├── transfer_frame.rs │ │ │ └── space_packet.rs │ ├── ccsds.rs │ ├── ccsds │ │ ├── tc.rs │ │ ├── aos.rs │ │ ├── tc │ │ │ ├── segment.rs │ │ │ ├── clcw.rs │ │ │ ├── sync_and_channel_coding.rs │ │ │ ├── transfer_frame.rs │ │ │ └── cltu.rs │ │ ├── aos │ │ │ ├── sync_and_channel_coding.rs │ │ │ ├── virtual_channel.rs │ │ │ ├── transfer_frame.rs │ │ │ └── m_pdu.rs │ │ └── space_packet.rs │ └── access │ │ ├── tlm.rs │ │ ├── tlm │ │ └── converter.rs │ │ ├── cmd.rs │ │ └── cmd │ │ └── schema.rs └── Cargo.toml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── actionlint.yml │ ├── devtools.yml │ ├── rust.yml │ ├── rustdoc.yml │ └── release.yml ├── CODEOWNERS ├── gaia-tmtc ├── src │ ├── lib.rs │ ├── tco_tmiv │ │ ├── mod.rs │ │ ├── tco.rs │ │ └── tmiv.rs │ ├── command.rs │ ├── recorder.rs │ ├── broker.rs │ ├── telemetry.rs │ └── handler.rs └── Cargo.toml ├── tmtc-c2a ├── src │ ├── registry.rs │ ├── lib.rs │ ├── satconfig.rs │ ├── devtools_server.rs │ ├── proto.rs │ ├── tmiv.rs │ ├── tco.rs │ ├── kble_gs.rs │ ├── registry │ │ ├── tlm.rs │ │ └── cmd.rs │ ├── main.rs │ └── satellite.rs ├── about.toml ├── build.rs ├── Cargo.toml └── proto │ └── tmtc_generic_c2a.proto ├── .cargo └── config.toml ├── README.md ├── gaia-stub ├── Cargo.toml ├── build.rs ├── proto │ ├── recorder.proto │ ├── tco_tmiv.proto │ └── broker.proto └── src │ └── lib.rs ├── structpack ├── src │ ├── value.rs │ ├── macros.rs │ ├── lib.rs │ ├── util.rs │ └── helper_impl.rs └── Cargo.toml ├── Cargo.toml └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.82.0" 3 | -------------------------------------------------------------------------------- /devtools-frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /src/proto/ 3 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/access.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod tlm; 3 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod access; 2 | pub mod ccsds; 3 | pub mod ccsds_c2a; 4 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a.rs: -------------------------------------------------------------------------------- 1 | /// CCSDS のうち、C2A Core 定義の部分 2 | pub mod aos; 3 | pub mod tc; 4 | -------------------------------------------------------------------------------- /devtools-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .DS_Store 3 | /dist/ 4 | /dist-ssr/ 5 | *.local 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## 概要 3 | 4 | ## 変更の意図や背景 5 | 6 | ## 発端となる Issue 7 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /devtools-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/tc.rs: -------------------------------------------------------------------------------- 1 | pub mod segment; 2 | pub mod space_packet; 3 | pub mod transfer_frame; 4 | pub use space_packet::SpacePacket; 5 | -------------------------------------------------------------------------------- /devtools-frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | /dist/ 3 | /node_modules/ 4 | /deploy/ 5 | /src/proto/ 6 | /crates/*/target 7 | /crates/*/pkg 8 | -------------------------------------------------------------------------------- /devtools-frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rust_embed::RustEmbed; 2 | 3 | #[derive(RustEmbed)] 4 | #[folder = "$OUT_DIR/devtools_dist"] 5 | pub struct Assets; 6 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds.rs: -------------------------------------------------------------------------------- 1 | /// CCSDS 標準の部分(C2A 定義を含まない) 2 | pub mod aos; 3 | pub mod space_packet; 4 | pub mod tc; 5 | 6 | pub use space_packet::SpacePacket; 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # project owner 2 | * @KOBA789 3 | 4 | # CI/CD team 5 | .github/workflows/* @KOBA789 @sksat 6 | 7 | # Renovate team 8 | renovate.json @KOBA789 @sksat 9 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/aos/transfer_frame.rs: -------------------------------------------------------------------------------- 1 | use crate::ccsds::{self, tc::clcw::CLCW}; 2 | 3 | pub type TransferFrame = ccsds::aos::TransferFrame; 4 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc.rs: -------------------------------------------------------------------------------- 1 | pub mod clcw; 2 | pub mod cltu; 3 | pub mod segment; 4 | pub mod sync_and_channel_coding; 5 | pub mod transfer_frame; 6 | pub use sync_and_channel_coding::SyncAndChannelCoding; 7 | -------------------------------------------------------------------------------- /gaia-tmtc/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod broker; 2 | pub mod command; 3 | pub mod handler; 4 | pub mod recorder; 5 | pub mod telemetry; 6 | 7 | pub use handler::{BeforeHook, BeforeHookLayer, Handle, Hook, Layer}; 8 | pub mod tco_tmiv; 9 | -------------------------------------------------------------------------------- /devtools-frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{ts,tsx}", "./*.html"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/aos.rs: -------------------------------------------------------------------------------- 1 | pub mod space_packet; 2 | pub mod transfer_frame; 3 | pub mod virtual_channel; 4 | pub use space_packet::SpacePacket; 5 | pub use transfer_frame::TransferFrame; 6 | pub use virtual_channel::VirtualChannel; 7 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/aos.rs: -------------------------------------------------------------------------------- 1 | pub mod m_pdu; 2 | pub mod sync_and_channel_coding; 3 | pub mod transfer_frame; 4 | pub mod virtual_channel; 5 | 6 | pub use sync_and_channel_coding::SyncAndChannelCoding; 7 | pub use transfer_frame::TransferFrame; 8 | -------------------------------------------------------------------------------- /tmtc-c2a/src/registry.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | mod tlm; 3 | 4 | pub use cmd::FatCommandSchema; 5 | pub use cmd::Registry as CommandRegistry; 6 | pub use tlm::Registry as TelemetryRegistry; 7 | pub use tlm::{FatTelemetrySchema, FieldMetadata, TelemetrySchema}; 8 | -------------------------------------------------------------------------------- /tmtc-c2a/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod satconfig; 2 | pub use satconfig::Satconfig; 3 | pub mod kble_gs; 4 | pub mod proto; 5 | pub mod registry; 6 | pub mod satellite; 7 | mod tco; 8 | mod tmiv; 9 | 10 | #[cfg(feature = "devtools")] 11 | pub mod devtools_server; 12 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/src/util.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_macros)] 2 | macro_rules! log { 3 | ( $( $t:tt )* ) => { 4 | web_sys::console::log_1(&format!( $( $t )* ).into()); 5 | } 6 | } 7 | #[allow(unused_imports)] 8 | pub(crate) use log; 9 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/access/tlm.rs: -------------------------------------------------------------------------------- 1 | pub mod converter; 2 | pub mod schema; 3 | 4 | #[derive(Debug, Clone, PartialEq, PartialOrd)] 5 | pub enum FieldValue { 6 | Double(f64), 7 | Integer(i64), 8 | Constant(String), 9 | Bytes(Vec), 10 | } 11 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # for local testing 2 | [patch.crates-io] 3 | structpack = { path = "structpack" } 4 | gaia-stub = { path = "gaia-stub" } 5 | gaia-ccsds-c2a = { path = "gaia-ccsds-c2a" } 6 | gaia-tmtc = { path = "gaia-tmtc" } 7 | c2a-devtools-frontend = { path = "devtools-frontend" } 8 | -------------------------------------------------------------------------------- /devtools-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import "@blueprintjs/core/lib/css/blueprint.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | .highlight-domain:not(:has(.highlight-domain:hover)):hover { 8 | @apply bg-slate-800; 9 | } 10 | 11 | .column-fill-auto { 12 | column-fill: auto; 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gaia 2 | > A command and control system for C2A-based satellites 3 | 4 | ## Features 5 | - Communicates with a C2A-based satellite through [kble](https://github.com/arkedge/kble) 6 | - Runs as a gRPC server and provides a gRPC API for interacting with the satellite 7 | - Provides low-level libraries to make custom protocol implementation easier 8 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /devtools-frontend/src/error.ts: -------------------------------------------------------------------------------- 1 | export class FriendlyError extends Error { 2 | public details?: string; 3 | public cause?: Error; 4 | constructor(message: string, options?: { details?: string; cause?: Error }) { 5 | super(message, { cause: options?.cause }); 6 | this.name = "FriendlyError"; 7 | this.details = options?.details; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /devtools-frontend/src/components/Top.tsx: -------------------------------------------------------------------------------- 1 | import { NonIdealState } from "@blueprintjs/core"; 2 | import { IconNames } from "@blueprintjs/icons"; 3 | import React from "react"; 4 | 5 | export const Top: React.FC = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /tmtc-c2a/about.toml: -------------------------------------------------------------------------------- 1 | accepted = [ 2 | "Apache-2.0", 3 | "MIT", 4 | "Unicode-DFS-2016", 5 | "MPL-2.0", 6 | "BSD-2-Clause", 7 | "BSD-3-Clause", 8 | "ISC", 9 | "OpenSSL", 10 | ] 11 | 12 | workarounds = [ 13 | "ring", 14 | "rustls", 15 | "tonic", 16 | "sentry", 17 | "prost", 18 | "chrono", 19 | "bitvec", 20 | ] 21 | -------------------------------------------------------------------------------- /devtools-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | C2A DevTools 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /devtools-frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c2a-devtools-frontend" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | edition = "2021" 8 | 9 | [dependencies] 10 | rust-embed = { version = "8.3.0", features = ["interpolate-folder-path", "debug-embed"] } 11 | opslang-wasm = { path = "crates/opslang-wasm", version = "1.0" } 12 | -------------------------------------------------------------------------------- /gaia-stub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gaia-stub" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | tonic = "0.11" 13 | prost = "0.12" 14 | prost-types = "0.12" 15 | 16 | [build-dependencies] 17 | tonic-build = "0.11" 18 | -------------------------------------------------------------------------------- /structpack/src/value.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub enum IntegralValue { 3 | I8(i8), 4 | I16(i16), 5 | I32(i32), 6 | I64(i64), 7 | U8(u8), 8 | U16(u16), 9 | U32(u32), 10 | U64(u64), 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub enum FloatingValue { 15 | F32(f32), 16 | F64(f64), 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub enum NumericValue { 21 | Integral(IntegralValue), 22 | Floating(FloatingValue), 23 | } 24 | -------------------------------------------------------------------------------- /devtools-frontend/crates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "opslang-wasm", 6 | ] 7 | 8 | [workspace.package] 9 | version = "1.2.0" 10 | description = "A command and control system for C2A-based satellites" 11 | repository = "https://github.com/arkedge/gaia" 12 | license = "MPL-2.0" 13 | 14 | # profile config should be set in workspace root 15 | [profile.release] 16 | # Tell `rustc` to optimize for small code size. 17 | opt-level = "s" 18 | -------------------------------------------------------------------------------- /tmtc-c2a/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | fn main() { 4 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 5 | 6 | tonic_build::configure() 7 | .file_descriptor_set_path(out_dir.join("tmtc_generic_c2a.bin")) 8 | .compile(&["./proto/tmtc_generic_c2a.proto"], &["./proto"]) 9 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 10 | 11 | #[cfg(feature = "bin")] 12 | notalawyer_build::build(); 13 | } 14 | -------------------------------------------------------------------------------- /gaia-tmtc/src/tco_tmiv/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub mod tco; 4 | pub mod tmiv; 5 | 6 | pub use gaia_stub::tco_tmiv::{Tco, Tmiv, *}; 7 | 8 | #[derive(Debug, Serialize, Deserialize, Clone)] 9 | pub struct TcoTmivSchema { 10 | pub tco: Vec, 11 | pub tmiv: Vec, 12 | } 13 | 14 | impl TcoTmivSchema { 15 | pub fn new(tco: Vec, tmiv: Vec) -> Self { 16 | Self { tco, tmiv } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog / actionlint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/**' 7 | 8 | jobs: 9 | actionlint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: reviewdog/action-actionlint@a5524e1c19e62881d79c1f1b9b6f09f16356e281 # v1 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | reporter: github-pr-review 17 | fail_on_error: true 18 | -------------------------------------------------------------------------------- /structpack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "structpack" 3 | version.workspace = true 4 | license.workspace = true 5 | edition = "2021" 6 | description = "Dynamic bit-field accessor" 7 | documentation = "https://docs.rs/structpack" 8 | repository = "https://github.com/arkedge/gaia" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | anyhow = { version = "1", features = ["backtrace"] } 14 | bitvec = "1" 15 | funty = "2" 16 | serde = { version = "1.0", features = ["derive"] } 17 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/aos/virtual_channel.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::ccsds::aos::{m_pdu::Defragmenter, virtual_channel::Synchronizer}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct VirtualChannel { 7 | pub synchronizer: Synchronizer, 8 | pub defragmenter: Defragmenter, 9 | } 10 | 11 | #[derive(Debug, Default)] 12 | pub struct Demuxer { 13 | channels: HashMap, 14 | } 15 | 16 | impl Demuxer { 17 | pub fn demux(&mut self, vcid: u8) -> &mut VirtualChannel { 18 | self.channels.entry(vcid).or_default() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /devtools-frontend/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es2020: true 3 | browser: true 4 | node: true 5 | extends: 6 | - "eslint:recommended" 7 | - "plugin:react/recommended" 8 | - "plugin:@typescript-eslint/eslint-recommended" 9 | - "prettier" 10 | parser: "@typescript-eslint/parser" 11 | plugins: 12 | - "@typescript-eslint" 13 | - "react-hooks" 14 | parserOptions: 15 | sourceType: module 16 | settings: 17 | react: 18 | version: detect 19 | rules: 20 | no-unused-vars: "off" 21 | react/prop-types: "off" 22 | react-hooks/rules-of-hooks: "error" 23 | react-hooks/exhaustive-deps: "warn" 24 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc/segment.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use modular_bitfield_msb::prelude::*; 4 | use zerocopy::{AsBytes, FromBytes, Unaligned}; 5 | 6 | #[bitfield(bytes = 1)] 7 | #[derive(Debug, Default, Clone, FromBytes, AsBytes, Unaligned)] 8 | #[repr(C)] 9 | pub struct Header { 10 | pub sequence_flag: SequenceFlag, 11 | pub map_id: B6, 12 | } 13 | 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitfieldSpecifier)] 15 | #[bits = 2] 16 | pub enum SequenceFlag { 17 | Continuing = 0b00, 18 | First = 0b01, 19 | Last = 0b10, 20 | NoSegmentation = 0b11, 21 | } 22 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gaia-ccsds-c2a" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | edition = "2021" 7 | license = "MPL-2.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { version = "1", features = ["backtrace"] } 13 | bitvec = "1" 14 | crc = "3" 15 | funty = "2" 16 | modular-bitfield-msb = "0.11" 17 | serde = { version = "1.0", features = ["derive"] } 18 | zerocopy = "0.6" 19 | tlmcmddb = "=2.5.1" 20 | structpack.workspace = true 21 | async-trait = "0.1" 22 | -------------------------------------------------------------------------------- /gaia-stub/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | fn main() { 4 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 5 | tonic_build::configure() 6 | .file_descriptor_set_path(out_dir.join("broker_descriptor.bin")) 7 | .compile(&["proto/broker.proto"], &["proto"]) 8 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 9 | tonic_build::configure() 10 | .file_descriptor_set_path(out_dir.join("recorder_descriptor.bin")) 11 | .compile(&["proto/recorder.proto"], &["proto"]) 12 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 13 | } 14 | -------------------------------------------------------------------------------- /gaia-stub/proto/recorder.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package recorder; 4 | 5 | import "tco_tmiv.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | service Recorder { 9 | rpc PostCommand(PostCommandRequest) returns (PostCommandResponse); 10 | rpc PostTelemetry(PostTelemetryRequest) returns (PostTelemetryResponse); 11 | } 12 | 13 | message PostCommandRequest { 14 | tco_tmiv.Tco tco = 1; 15 | google.protobuf.Timestamp timestamp = 2; 16 | } 17 | 18 | message PostCommandResponse { 19 | } 20 | 21 | message PostTelemetryRequest { 22 | tco_tmiv.Tmiv tmiv = 1; 23 | } 24 | 25 | message PostTelemetryResponse { 26 | } 27 | -------------------------------------------------------------------------------- /gaia-tmtc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gaia-tmtc" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | futures = "0.3" 14 | async-trait = "0.1" 15 | chrono = "0.4" 16 | tracing = "0.1" 17 | tokio = { version = "1", features = ["sync"] } 18 | tokio-stream = { version = "0.1", features = ["sync"] } 19 | tonic = "0.11" 20 | prost-types = "0.12" 21 | gaia-stub.workspace = true 22 | serde = { version = "1", features = ["derive"] } 23 | -------------------------------------------------------------------------------- /devtools-frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: [ 7 | { 8 | find: "@crate/", 9 | // wasmpackでrustソースから作られたpkgの場所を指定する 10 | // cargo build の際はbuild.rsによってDEVTOOLS_CRATE_ROOTが指定される 11 | // yarn dev している場合はrustソースのディレクトリに直接pkgを配置し、DEVTOOLS_CRATE_ROOTは指定されない 12 | replacement: (process.env.DEVTOOLS_CRATE_ROOT ?? "/crates") + "/", 13 | }, 14 | ], 15 | }, 16 | base: "/devtools/", 17 | plugins: [react()], 18 | server: { 19 | hmr: {}, 20 | }, 21 | define: { 22 | "process.env": {}, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /devtools-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@crate/*": ["crates/*"] 6 | }, 7 | "target": "ESNext", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], 9 | "types": ["vite/client"], 10 | "allowJs": false, 11 | "skipLibCheck": false, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": ["./src"] 24 | } 25 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/aos/sync_and_channel_coding.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use zerocopy::Unaligned; 3 | 4 | use super::TransferFrame; 5 | 6 | #[async_trait::async_trait] 7 | pub trait SyncAndChannelCoding { 8 | async fn receive(&mut self) -> Result; 9 | } 10 | 11 | pub struct TransferFrameBuffer { 12 | bytes: Vec, 13 | } 14 | 15 | impl TransferFrameBuffer { 16 | pub fn new(bytes: Vec) -> Self { 17 | Self { bytes } 18 | } 19 | 20 | pub fn transfer_frame(&self) -> Option> { 21 | TransferFrame::<_, T>::new(self.bytes.as_slice()) 22 | } 23 | 24 | pub fn into_inner(self) -> Vec { 25 | self.bytes 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc/clcw.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use modular_bitfield_msb::prelude::*; 4 | use zerocopy::{AsBytes, FromBytes, Unaligned}; 5 | 6 | #[bitfield(bytes = 4)] 7 | #[derive(Debug, Default, Clone, FromBytes, AsBytes, Unaligned)] 8 | #[repr(C)] 9 | pub struct CLCW { 10 | pub control_word_type: B1, 11 | pub clcw_version_number: B2, 12 | pub status_field: B3, 13 | pub cop_in_effect: B2, 14 | pub virtual_channel_identification: B6, 15 | #[skip] 16 | __: B2, 17 | pub no_rf_available: B1, 18 | pub no_bit_lock: B1, 19 | pub lockout: B1, 20 | pub wait: B1, 21 | pub retransmit: B1, 22 | pub farm_b_counter: B2, 23 | #[skip] 24 | __: B1, 25 | pub report_value: B8, 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "gaia-stub", 6 | "structpack", 7 | "gaia-tmtc", 8 | "gaia-ccsds-c2a", 9 | "tmtc-c2a", 10 | "devtools-frontend", 11 | ] 12 | 13 | exclude = [ 14 | # WASM 用の別の cargo workspace であるため 15 | "devtools-frontend/crates", 16 | # ビルド時はdevtools_frontend/crates が OUT_DIR 以下にコピーされた後wasm-pack buildされるため 17 | "target", 18 | ] 19 | 20 | [workspace.package] 21 | version = "1.2.0" 22 | description = "A command and control system for C2A-based satellites" 23 | repository = "https://github.com/arkedge/gaia" 24 | license = "MPL-2.0" 25 | 26 | [workspace.dependencies] 27 | structpack = "1.2" 28 | gaia-stub = "1.2" 29 | gaia-ccsds-c2a = "1.2" 30 | gaia-tmtc = "1.2" 31 | c2a-devtools-frontend = "1.2" 32 | -------------------------------------------------------------------------------- /gaia-stub/proto/tco_tmiv.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tco_tmiv; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message Tco { 8 | string name = 1; 9 | repeated TcoParam params = 2; 10 | } 11 | 12 | message TcoParam { 13 | string name = 1; 14 | oneof value { 15 | int64 integer = 2; 16 | double double = 3; 17 | bytes bytes = 4; 18 | } 19 | } 20 | 21 | message Tmiv { 22 | string name = 1; 23 | uint64 plugin_received_time = 2; 24 | repeated TmivField fields = 3; 25 | google.protobuf.Timestamp timestamp = 4; 26 | } 27 | 28 | message TmivField { 29 | string name = 1; 30 | oneof value { 31 | string string = 2; 32 | double double = 3; 33 | int64 integer = 4; 34 | string enum = 5; 35 | bytes bytes = 6; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gaia-stub/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod broker { 2 | tonic::include_proto!("broker"); 3 | 4 | pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("broker_descriptor"); 5 | } 6 | 7 | pub mod recorder { 8 | tonic::include_proto!("recorder"); 9 | 10 | pub const FILE_DESCRIPTOR_SET: &[u8] = 11 | tonic::include_file_descriptor_set!("recorder_descriptor"); 12 | } 13 | 14 | pub mod tco_tmiv { 15 | tonic::include_proto!("tco_tmiv"); 16 | 17 | pub mod tmiv { 18 | pub fn get_timestamp(tmiv: &super::Tmiv, pseudo_nanos: i32) -> prost_types::Timestamp { 19 | tmiv.timestamp.clone().unwrap_or(prost_types::Timestamp { 20 | seconds: tmiv.plugin_received_time as i64, 21 | nanos: pseudo_nanos, 22 | }) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/aos/virtual_channel.rs: -------------------------------------------------------------------------------- 1 | use super::transfer_frame::FrameCount; 2 | 3 | #[derive(Debug, Default)] 4 | pub struct Synchronizer { 5 | counter: Option, 6 | } 7 | 8 | impl Synchronizer { 9 | pub fn next(&mut self, frame_count: FrameCount) -> Result<(), FrameCount> { 10 | if let Some(counter) = self.counter { 11 | let is_contiguous = frame_count.is_next_to(counter); 12 | self.counter = Some(frame_count); 13 | if is_contiguous { 14 | Ok(()) 15 | } else { 16 | Err(counter.next()) 17 | } 18 | } else { 19 | self.counter = Some(frame_count); 20 | Ok(()) 21 | } 22 | } 23 | 24 | pub fn reset(&mut self) { 25 | self.counter = None; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tmtc-c2a/src/satconfig.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Clone, Deserialize)] 6 | pub struct Satconfig { 7 | pub aos_scid: u16, 8 | pub tc_scid: u16, 9 | pub tlm_apid_map: HashMap, 10 | pub cmd_apid_map: HashMap, 11 | pub tlm_channel_map: TelemetryChannelMap, 12 | pub cmd_prefix_map: CommandPrefixMap, 13 | } 14 | 15 | pub type TelemetryChannelMap = HashMap; 16 | 17 | #[derive(Debug, Clone, Deserialize)] 18 | pub struct TelemetryChannel { 19 | pub destination_flag_mask: u8, 20 | } 21 | 22 | pub type CommandPrefixMap = HashMap>; 23 | 24 | #[derive(Debug, Clone, Deserialize)] 25 | pub struct CommandSubsystem { 26 | pub has_time_indicator: bool, 27 | pub destination_type: u8, 28 | pub execution_type: u8, 29 | } 30 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc/sync_and_channel_coding.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[async_trait::async_trait] 4 | pub trait SyncAndChannelCoding { 5 | async fn transmit( 6 | &mut self, 7 | scid: u16, 8 | vcid: u8, 9 | frame_type: FrameType, 10 | sequence_number: u8, 11 | data_field: &[u8], 12 | ) -> Result<()>; 13 | } 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 16 | pub enum FrameType { 17 | TypeAD, 18 | TypeBD, 19 | TypeBC, 20 | } 21 | 22 | impl FrameType { 23 | pub fn bypass_flag(&self) -> bool { 24 | match self { 25 | FrameType::TypeAD => false, 26 | FrameType::TypeBD => true, 27 | FrameType::TypeBC => true, 28 | } 29 | } 30 | 31 | pub fn control_command_flag(&self) -> bool { 32 | match self { 33 | FrameType::TypeAD => false, 34 | FrameType::TypeBD => false, 35 | FrameType::TypeBC => true, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/js/union.js: -------------------------------------------------------------------------------- 1 | const asKind = (kind) => { 2 | return (obj) => { 3 | if (obj.kind == kind) { 4 | return obj.value; 5 | } else { 6 | return undefined; 7 | } 8 | }; 9 | }; 10 | 11 | export const asInt = asKind("integer"); 12 | export const asDouble = asKind("double"); 13 | export const asBool = asKind("bool"); 14 | export const asArray = asKind("array"); 15 | export const asString = asKind("string"); 16 | export const asDuration = asKind("duration"); 17 | export const asDateTime = asKind("datetime"); 18 | 19 | const make = (kind) => { 20 | return (value) => { 21 | return { 22 | kind, 23 | value, 24 | }; 25 | }; 26 | }; 27 | 28 | export const makeInt = make("integer"); 29 | export const makeDouble = make("double"); 30 | export const makeBool = make("bool"); 31 | export const makeArray = make("array"); 32 | export const makeString = make("string"); 33 | export const makeDuration = make("duration"); 34 | export const makeDateTime = make("datetime"); 35 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opslang-wasm" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | edition = "2021" 8 | 9 | links = "opslang-wasm" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [features] 15 | default = ["console_error_panic_hook"] 16 | 17 | [dependencies] 18 | wasm-bindgen = "0.2" 19 | wasm-bindgen-futures = "0.4" 20 | web-sys = { version = "0.3.69", features = ["console"] } 21 | opslang-ast = "0.2.1" 22 | opslang-parser = "0.2.1" 23 | async-recursion = "1.1.0" 24 | 25 | # The `console_error_panic_hook` crate provides better debugging of panics by 26 | # logging them with `console.error`. This is great for development, but requires 27 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 28 | # code size when deploying. 29 | console_error_panic_hook = { version = "0.1.7", optional = true } 30 | chrono = "0.4.38" 31 | regex = "1.10.4" 32 | 33 | [dev-dependencies] 34 | wasm-bindgen-test = "0.3.42" 35 | -------------------------------------------------------------------------------- /gaia-tmtc/src/command.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use async_trait::async_trait; 5 | use gaia_stub::tco_tmiv::Tco; 6 | 7 | use super::Hook; 8 | use crate::tco_tmiv::tco; 9 | 10 | #[derive(Clone)] 11 | pub struct SanitizeHook { 12 | schema_set: Arc, 13 | } 14 | 15 | impl SanitizeHook { 16 | pub fn new(schema_set: impl Into>) -> Self { 17 | Self { 18 | schema_set: schema_set.into(), 19 | } 20 | } 21 | 22 | fn sanitize(&self, input: &Tco) -> Result { 23 | let sanitized = self 24 | .schema_set 25 | .sanitize(input) 26 | .map_err(|msg| anyhow!("TCO validation error: {}", msg))?; 27 | Ok(sanitized) 28 | } 29 | } 30 | 31 | #[async_trait] 32 | impl Hook> for SanitizeHook { 33 | type Output = Arc; 34 | 35 | async fn hook(&mut self, tco: Arc) -> Result { 36 | let sanitized = self.sanitize(&tco)?; 37 | Ok(Arc::new(sanitized)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tmtc-c2a/src/devtools_server.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::{header, StatusCode, Uri}, 3 | response::{Html, IntoResponse, Response}, 4 | }; 5 | use c2a_devtools_frontend::Assets; 6 | 7 | static INDEX_HTML: &str = "index.html"; 8 | 9 | pub async fn serve(uri: Uri) -> Response { 10 | let path = uri.path().trim_start_matches('/'); 11 | 12 | if path.is_empty() || path == INDEX_HTML { 13 | return index_html().await; 14 | } 15 | 16 | match Assets::get(path) { 17 | Some(content) => { 18 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 19 | 20 | ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() 21 | } 22 | None => index_html().await, 23 | } 24 | } 25 | 26 | async fn index_html() -> Response { 27 | match Assets::get(INDEX_HTML) { 28 | Some(content) => Html(content.data).into_response(), 29 | None => not_found().await, 30 | } 31 | } 32 | 33 | async fn not_found() -> Response { 34 | (StatusCode::NOT_FOUND, "404").into_response() 35 | } 36 | -------------------------------------------------------------------------------- /structpack/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! define_casting_integral { 2 | ($ty:ident, $variant:ident) => { 3 | impl TryFrom<$crate::IntegralValue> for $ty { 4 | type Error = anyhow::Error; 5 | 6 | fn try_from(value: $crate::IntegralValue) -> Result { 7 | Ok(match value { 8 | $crate::IntegralValue::I8(v) => v.try_into()?, 9 | $crate::IntegralValue::I16(v) => v.try_into()?, 10 | $crate::IntegralValue::I32(v) => v.try_into()?, 11 | $crate::IntegralValue::I64(v) => v.try_into()?, 12 | $crate::IntegralValue::U8(v) => v.try_into()?, 13 | $crate::IntegralValue::U16(v) => v.try_into()?, 14 | $crate::IntegralValue::U32(v) => v.try_into()?, 15 | $crate::IntegralValue::U64(v) => v.try_into()?, 16 | }) 17 | } 18 | } 19 | 20 | impl From<$ty> for $crate::IntegralValue { 21 | fn from(v: $ty) -> Self { 22 | Self::$variant(v) 23 | } 24 | } 25 | }; 26 | } 27 | 28 | pub(crate) use define_casting_integral; 29 | -------------------------------------------------------------------------------- /structpack/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod field; 2 | mod helper_impl; 3 | mod macros; 4 | pub mod util; 5 | mod value; 6 | 7 | use std::marker::PhantomData; 8 | 9 | pub use field::{ 10 | FloatingField, GenericFloatingField, GenericIntegralField, IntegralField, NumericField, 11 | SizedField, 12 | }; 13 | pub use value::{FloatingValue, IntegralValue, NumericValue}; 14 | 15 | #[derive(Default)] 16 | pub struct SizedBuilder { 17 | fields: E, 18 | bit_len: usize, 19 | _field: PhantomData<*const F>, 20 | } 21 | 22 | impl SizedBuilder { 23 | pub fn bit_len(&self) -> usize { 24 | self.bit_len 25 | } 26 | 27 | pub fn byte_len(&self) -> usize { 28 | if self.bit_len() == 0 { 29 | 0 30 | } else { 31 | (self.bit_len() - 1) / 8 + 1 32 | } 33 | } 34 | 35 | pub fn build(self) -> E { 36 | self.fields 37 | } 38 | } 39 | 40 | impl Extend for SizedBuilder 41 | where 42 | E: Extend, 43 | F: SizedField, 44 | { 45 | fn extend>(&mut self, iter: T) { 46 | for field in iter { 47 | self.bit_len = self.bit_len.max(field.last_bit_exclusive()); 48 | self.fields.extend([field]); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gaia-stub/proto/broker.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package broker; 4 | 5 | import "tco_tmiv.proto"; 6 | 7 | service Broker { 8 | rpc PostCommand(PostCommandRequest) returns (PostCommandResponse); 9 | rpc OpenTelemetryStream(TelemetryStreamRequest) returns (stream TelemetryStreamResponse); 10 | rpc GetLastReceivedTelemetry(GetLastReceivedTelemetryRequest) returns (GetLastReceivedTelemetryResponse); 11 | 12 | rpc OpenCommandStream(stream CommandStreamRequest) returns (stream CommandStreamResponse); 13 | rpc PostTelemetry(PostTelemetryRequest) returns (PostTelemetryResponse); 14 | } 15 | 16 | message PostCommandRequest { 17 | tco_tmiv.Tco tco = 3; 18 | } 19 | 20 | message PostCommandResponse { 21 | // TODO: 22 | } 23 | 24 | message CommandStreamRequest { 25 | } 26 | 27 | message CommandStreamResponse { 28 | string tco_json = 1; 29 | tco_tmiv.Tco tco = 2; 30 | } 31 | 32 | message PostTelemetryRequest { 33 | string tmiv_json = 1; 34 | tco_tmiv.Tmiv tmiv = 2; 35 | } 36 | 37 | message PostTelemetryResponse { 38 | // TODO: 39 | } 40 | 41 | message TelemetryStreamRequest { 42 | } 43 | 44 | message TelemetryStreamResponse { 45 | tco_tmiv.Tmiv tmiv = 3; 46 | } 47 | 48 | message GetLastReceivedTelemetryRequest { 49 | string telemetry_name = 1; 50 | } 51 | 52 | message GetLastReceivedTelemetryResponse { 53 | tco_tmiv.Tmiv tmiv = 1; 54 | } 55 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/tc/segment.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use zerocopy::{ByteSlice, ByteSliceMut, LayoutVerified}; 7 | 8 | use crate::ccsds::tc::segment::*; 9 | 10 | pub struct Builder { 11 | header: LayoutVerified, 12 | body: B, 13 | } 14 | 15 | impl Builder 16 | where 17 | B: ByteSlice, 18 | { 19 | pub fn new(bytes: B) -> Option { 20 | let (header, body) = LayoutVerified::new_unaligned_from_prefix(bytes)?; 21 | Some(Self { header, body }) 22 | } 23 | } 24 | 25 | impl Builder 26 | where 27 | B: ByteSliceMut, 28 | { 29 | pub fn use_default(&mut self) { 30 | self.set_map_id(0b10); 31 | self.set_sequence_flag(SequenceFlag::NoSegmentation); 32 | } 33 | 34 | pub fn body_mut(&mut self) -> &mut B { 35 | &mut self.body 36 | } 37 | 38 | pub fn finish(self, body_len: usize) -> usize { 39 | mem::size_of::
() + body_len 40 | } 41 | } 42 | 43 | impl Deref for Builder 44 | where 45 | B: ByteSlice, 46 | { 47 | type Target = Header; 48 | 49 | fn deref(&self) -> &Self::Target { 50 | &self.header 51 | } 52 | } 53 | 54 | impl DerefMut for Builder 55 | where 56 | B: ByteSliceMut, 57 | { 58 | fn deref_mut(&mut self) -> &mut Self::Target { 59 | &mut self.header 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /devtools-frontend/src/client.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerResponse, WorkerRpcService } from "./worker"; 2 | 3 | export const buildClient = ( 4 | worker: SharedWorker, 5 | ): S => { 6 | const methodFactory = (proc: keyof S) => { 7 | return (...args: any[]) => { 8 | const channel = new MessageChannel(); 9 | const callback = channel.port2; 10 | worker.port.postMessage( 11 | { 12 | callback, 13 | proc, 14 | args, 15 | }, 16 | [callback], 17 | ); 18 | return new Promise((resolve, reject) => { 19 | const handleResponse = ( 20 | e: MessageEvent[keyof S]>, 21 | ) => { 22 | channel.port1.removeEventListener("message", handleResponse); 23 | const response = e.data; 24 | if ("value" in response) { 25 | resolve(response.value); 26 | } else if ("error" in response) { 27 | reject(response.error); 28 | } 29 | }; 30 | channel.port1.addEventListener("message", handleResponse); 31 | channel.port1.start(); 32 | }); 33 | }; 34 | }; 35 | worker.port.start(); 36 | 37 | return new Proxy( 38 | {}, 39 | { 40 | get(_target, prop) { 41 | if (typeof prop === "symbol") { 42 | return void 0; 43 | } 44 | return methodFactory(prop); 45 | }, 46 | }, 47 | ) as S; 48 | }; 49 | -------------------------------------------------------------------------------- /.github/workflows/devtools.yml: -------------------------------------------------------------------------------- 1 | name: DevTools 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | devtools: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | submodules: recursive 19 | 20 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 21 | with: 22 | node-version: 21 23 | 24 | - name: Get Rust toolchain 25 | id: toolchain 26 | run: | 27 | awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" 28 | 29 | - uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 # v1 30 | with: 31 | toolchain: ${{ steps.toolchain.outputs.toolchain }} 32 | 33 | - name: cache dependencies 34 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 35 | 36 | - name: Install wasm-pack 37 | run: cargo install wasm-pack --locked 38 | 39 | - name: install pnpm 40 | working-directory: devtools-frontend 41 | run: corepack enable 42 | 43 | - name: install frontend dependencies 44 | working-directory: devtools-frontend 45 | run: pnpm install 46 | 47 | - name: lint 48 | working-directory: devtools-frontend 49 | run: pnpm lint 50 | 51 | - name: build 52 | working-directory: devtools-frontend 53 | run: pnpm build 54 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::Command; 3 | use std::{env, path::PathBuf}; 4 | 5 | fn main() { 6 | // no build logic on wasm32 build (cargo build called from wasm-pack) 7 | let target = env::var("TARGET").unwrap(); 8 | if target.contains("wasm32") { 9 | return; 10 | } 11 | 12 | // Just called cargo build (target may be x86_64 or aarch64) 13 | // But, now we build wasm by wasm-pack 14 | 15 | let out_dir = env::var("OUT_DIR").unwrap(); 16 | 17 | // pass dist directory path to dependents crate (via package.links) 18 | // it can be used as DEP_OPSLANG_WASM_OUT_DIR in dependents build.rs 19 | println!("cargo:out_dir={}", out_dir); 20 | 21 | // Of course we think we should copy source dir into $OUT_DIR 22 | // & build(wasm-pack build) in $OUT_DIR, 23 | // we can't be happy with cargo-metadata called from wasm-pack build 24 | // (from cargo build only.not from cargo package) 25 | let out_dir = PathBuf::from(out_dir); 26 | 27 | // Let's go 28 | let status = Command::new("wasm-pack") 29 | .arg("build") 30 | .arg("--weak-refs") 31 | .arg("--target") 32 | .arg("web") 33 | .arg("--release") 34 | .arg("--out-dir") 35 | .arg(out_dir) 36 | .status() 37 | .expect("failed to execute wasm-pack"); 38 | assert!(status.success(), "failed to wasm-pack build"); 39 | 40 | // wasm-pack build (cargo build --target wasm32) generates Cargo.lock 41 | // On cargo build, it's fine. 42 | // On cargo package, it cause a catastrophe!!! (it can't be exists in source directory) 43 | fs::remove_file("Cargo.lock").unwrap_or(()); 44 | } 45 | -------------------------------------------------------------------------------- /tmtc-c2a/src/proto.rs: -------------------------------------------------------------------------------- 1 | pub mod tmtc_generic_c2a { 2 | use crate::registry::{CommandRegistry, TelemetryRegistry}; 3 | 4 | tonic::include_proto!("tmtc_generic_c2a"); 5 | 6 | pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("tmtc_generic_c2a"); 7 | 8 | pub struct Service { 9 | satellite_schema: SatelliteSchema, 10 | } 11 | 12 | impl Service { 13 | pub fn new( 14 | tlm_registry: &TelemetryRegistry, 15 | cmd_registry: &CommandRegistry, 16 | ) -> anyhow::Result { 17 | let telemetry_channels = tlm_registry.build_telemetry_channel_schema_map(); 18 | let telemetry_components = tlm_registry.build_telemetry_component_schema_map(); 19 | let command_prefixes = cmd_registry.build_command_prefix_schema_map(); 20 | let command_components = cmd_registry.build_command_component_schema_map(); 21 | let satellite_schema = SatelliteSchema { 22 | telemetry_channels, 23 | telemetry_components, 24 | command_prefixes, 25 | command_components, 26 | }; 27 | Ok(Self { satellite_schema }) 28 | } 29 | } 30 | 31 | #[tonic::async_trait] 32 | impl tmtc_generic_c2a_server::TmtcGenericC2a for Service { 33 | async fn get_satellite_schema( 34 | &self, 35 | _request: tonic::Request, 36 | ) -> Result, tonic::Status> { 37 | Ok(tonic::Response::new(GetSateliteSchemaResponse { 38 | satellite_schema: Some(self.satellite_schema.clone()), 39 | })) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/access/tlm/converter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Status { 5 | map: HashMap, 6 | default_label: String, 7 | } 8 | 9 | impl Status { 10 | pub fn new(map: HashMap, default_label: String) -> Self { 11 | Self { map, default_label } 12 | } 13 | 14 | pub fn convert(&self, value: i64) -> String { 15 | self.map.get(&value).unwrap_or(&self.default_label).clone() 16 | } 17 | } 18 | 19 | #[derive(Debug, Clone, PartialEq)] 20 | pub struct Polynomial { 21 | a: [f64; 6], 22 | } 23 | 24 | impl Polynomial { 25 | pub fn new(a: [f64; 6]) -> Self { 26 | Self { a } 27 | } 28 | } 29 | 30 | impl Polynomial { 31 | pub fn convert(&self, x: f64) -> f64 { 32 | self.a 33 | .iter() 34 | .enumerate() 35 | .fold(0f64, |acc, (i, a)| acc + (a * x.powi(i as i32))) 36 | } 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | pub enum Integral { 41 | Status(Status), 42 | Polynomial(Polynomial), 43 | } 44 | 45 | impl From for Status { 46 | fn from(db: tlmcmddb::tlm::conversion::Status) -> Self { 47 | let map = db.variants.into_iter().map(|v| (v.key, v.value)).collect(); 48 | Self::new(map, db.default_value.unwrap_or_else(|| "OTHER".to_string())) 49 | } 50 | } 51 | 52 | impl From for Polynomial { 53 | fn from( 54 | tlmcmddb::tlm::conversion::Polynomial { 55 | a0, 56 | a1, 57 | a2, 58 | a3, 59 | a4, 60 | a5, 61 | }: tlmcmddb::tlm::conversion::Polynomial, 62 | ) -> Self { 63 | Self::new([a0, a1, a2, a3, a4, a5]) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gaia-tmtc/src/recorder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use gaia_stub::{ 6 | recorder::recorder_client::RecorderClient, 7 | tco_tmiv::{Tco, Tmiv}, 8 | }; 9 | use prost_types::Timestamp; 10 | use tonic::transport::Channel; 11 | use tracing::error; 12 | 13 | use super::Hook; 14 | 15 | pub use gaia_stub::recorder::*; 16 | 17 | #[derive(Clone)] 18 | pub struct RecordHook { 19 | recorder_client: RecorderClient, 20 | } 21 | 22 | impl RecordHook { 23 | pub fn new(recorder_client: RecorderClient) -> Self { 24 | Self { recorder_client } 25 | } 26 | } 27 | 28 | #[async_trait] 29 | impl Hook> for RecordHook { 30 | type Output = Arc; 31 | 32 | async fn hook(&mut self, tco: Arc) -> Result { 33 | let now = chrono::Utc::now().naive_utc(); 34 | let timestamp = Timestamp { 35 | seconds: now.and_utc().timestamp(), 36 | nanos: now.and_utc().timestamp_subsec_nanos() as i32, 37 | }; 38 | self.recorder_client 39 | .post_command(PostCommandRequest { 40 | tco: Some(tco.as_ref().clone()), 41 | timestamp: Some(timestamp), 42 | }) 43 | .await?; 44 | Ok(tco) 45 | } 46 | } 47 | 48 | #[async_trait] 49 | impl Hook> for RecordHook { 50 | type Output = Arc; 51 | 52 | async fn hook(&mut self, tmiv: Arc) -> Result { 53 | let ret = self 54 | .recorder_client 55 | .post_telemetry(PostTelemetryRequest { 56 | tmiv: Some(tmiv.as_ref().clone()), 57 | }) 58 | .await; 59 | if let Err(e) = ret { 60 | error!("failed to record TMIV: {}", e); 61 | } 62 | Ok(tmiv) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tmtc-c2a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tmtc-c2a" 3 | version.workspace = true 4 | description.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | funty = "2" 13 | anyhow = { version = "1", features = ["backtrace"] } 14 | async-trait = "0.1" 15 | chrono = "0.4" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | clap = { version = "4", features = ["derive", "env"] } 19 | futures = "0.3" 20 | tracing = "0.1" 21 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 22 | http = "0.2" 23 | tower = "0.4" 24 | tower-http = { version = "0.4", features = ["trace", "cors"] } 25 | tokio = { version = "1", features = ["full"] } 26 | prost = "0.12" 27 | prost-types = "0.12" 28 | tonic = { version = "0.11", features = ["tls", "tls-roots-common", "tls-webpki-roots"] } 29 | tonic-health = "0.11" 30 | tonic-reflection = "0.11" 31 | tonic-web = "0.11" 32 | axum = { version = "0.6", default-features = false, features = ["http1", "tokio"] } 33 | mime_guess = "2.0.4" 34 | sentry = { version = "0.34", default-features = false, features = ["backtrace", "contexts", "panic", "rustls", "reqwest"] } 35 | sentry-tracing = "0.34" 36 | tlmcmddb = "2.5.1" 37 | structpack.workspace = true 38 | gaia-ccsds-c2a.workspace = true 39 | gaia-tmtc.workspace = true 40 | c2a-devtools-frontend = { workspace = true, optional = true } 41 | kble-socket = { version = "0.4.0", features = ["tungstenite"] } 42 | tokio-tungstenite = "0.20.1" 43 | itertools = "0.12.1" 44 | notalawyer = "0.1.0" 45 | notalawyer-clap = "0.1.0" 46 | 47 | [build-dependencies] 48 | tonic-build = "0.11" 49 | notalawyer-build = { version = "0.1.0", optional = true } 50 | 51 | [features] 52 | default = ["bin", "devtools"] 53 | bin = ["dep:notalawyer-build"] 54 | devtools = ["dep:c2a-devtools-frontend"] 55 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/access/cmd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, ensure, Result}; 2 | 3 | pub mod schema; 4 | 5 | pub struct Writer<'b, I> { 6 | iter: I, 7 | static_size: usize, 8 | has_trailer: bool, 9 | bytes: &'b mut [u8], 10 | } 11 | 12 | impl<'b, I> Writer<'b, I> { 13 | pub fn new(iter: I, static_size: usize, has_trailer: bool, bytes: &'b mut [u8]) -> Self { 14 | Self { 15 | iter, 16 | static_size, 17 | has_trailer, 18 | bytes, 19 | } 20 | } 21 | } 22 | 23 | impl<'a, 'b, I, F> Writer<'b, I> 24 | where 25 | I: Iterator, 26 | F: structpack::SizedField + 'static, 27 | { 28 | pub fn write(&mut self, value: F::Value<'_>) -> Result<()> { 29 | let field = self 30 | .iter 31 | .next() 32 | .ok_or_else(|| anyhow!("all fields have been written"))?; 33 | field.write(self.bytes, value)?; 34 | Ok(()) 35 | } 36 | 37 | fn verify_completion(&mut self) -> Result<()> { 38 | ensure!( 39 | self.iter.next().is_none(), 40 | "some parameters have not been written yet" 41 | ); 42 | Ok(()) 43 | } 44 | 45 | /// Returns total (static + trailer) len 46 | pub fn write_trailer_and_finish(mut self, trailer: &[u8]) -> Result { 47 | ensure!( 48 | self.has_trailer, 49 | "this command has no trailer(RAW) parameter" 50 | ); 51 | self.verify_completion()?; 52 | let buf = self 53 | .bytes 54 | .get_mut(self.static_size..self.static_size + trailer.len()) 55 | .ok_or_else(|| anyhow!("trailer is too long for the buffer"))?; 56 | buf.copy_from_slice(trailer); 57 | Ok(self.static_size + trailer.len()) 58 | } 59 | 60 | pub fn finish(mut self) -> Result { 61 | self.verify_completion()?; 62 | Ok(self.static_size) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/aos/space_packet.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use std::mem; 4 | 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, ByteSlice, FromBytes, LayoutVerified, Unaligned}; 7 | 8 | use crate::ccsds; 9 | pub use crate::ccsds::space_packet::*; 10 | 11 | #[bitfield(bytes = 20)] 12 | #[derive(Debug, Clone, FromBytes, AsBytes, Unaligned, Default)] 13 | #[repr(C)] 14 | pub struct SecondaryHeader { 15 | pub version_number: B8, 16 | pub board_time: B32, 17 | pub telemetry_id: B8, 18 | pub global_time_bits: B64, 19 | pub on_board_subnetwork_time: B32, 20 | pub destination_flags: B8, 21 | pub data_recorder_partition: B8, 22 | } 23 | 24 | impl SecondaryHeader { 25 | pub const SIZE: usize = mem::size_of::(); 26 | 27 | const HOUSEKEEPING: u8 = 0b00000001; 28 | const MISSION: u8 = 0b00000010; 29 | #[deprecated = "this is not a generic method. it's better to build a proper routing mechanism using destination_flags directly"] 30 | pub fn is_realtime(&self) -> bool { 31 | self.destination_flags() & (Self::HOUSEKEEPING | Self::MISSION) != 0 32 | } 33 | 34 | pub fn global_time(&self) -> f64 { 35 | f64::from_bits(self.global_time_bits()) 36 | } 37 | } 38 | 39 | #[derive(Debug)] 40 | pub struct SpacePacket { 41 | pub primary_header: LayoutVerified, 42 | pub secondary_header: LayoutVerified, 43 | pub user_data: B, 44 | } 45 | 46 | impl SpacePacket 47 | where 48 | B: ByteSlice, 49 | { 50 | pub fn from_generic(generic: ccsds::SpacePacket) -> Option { 51 | let (secondary_header, user_data) = 52 | LayoutVerified::<_, SecondaryHeader>::new_unaligned_from_prefix(generic.packet_data)?; 53 | Some(Self { 54 | primary_header: generic.primary_header, 55 | secondary_header, 56 | user_data, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | permissions: 9 | id-token: write 10 | contents: read 11 | checks: write 12 | pull-requests: write 13 | 14 | env: 15 | CARGO_INCREMENTAL: 0 16 | 17 | jobs: 18 | rust: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | submodules: recursive 25 | 26 | # for devtools-frontend 27 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 28 | with: 29 | node-version: 21 30 | 31 | - name: Install Protoc 32 | uses: arduino/setup-protoc@v1 33 | with: 34 | version: '3.x' 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Get Rust toolchain 38 | id: toolchain 39 | run: | 40 | awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" 41 | 42 | - uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 # v1 43 | with: 44 | toolchain: ${{ steps.toolchain.outputs.toolchain }} 45 | components: clippy, rustfmt 46 | 47 | - name: cache dependencies 48 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 49 | 50 | - name: Install cargo-about 51 | run: cargo install cargo-about@0.6.4 --locked 52 | 53 | - name: Install wasm-pack 54 | run: cargo install wasm-pack --locked 55 | 56 | - name: reviewdog / clippy 57 | uses: sksat/action-clippy@87e08e0c289f2654fe702b0aaf88c2f1027a3e57 # v1.1.1 58 | with: 59 | reporter: github-pr-review 60 | clippy_flags: --locked 61 | 62 | - name: format 63 | run: | 64 | cargo fmt --all -- --check 65 | 66 | - name: unit test 67 | run: | 68 | cargo test --locked 69 | -------------------------------------------------------------------------------- /structpack/src/util.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::SizedField; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct FieldWithMetadata { 7 | pub metadata: M, 8 | pub field: F, 9 | } 10 | 11 | impl SizedField for FieldWithMetadata 12 | where 13 | F: SizedField, 14 | { 15 | type Value<'a> = F::Value<'a>; 16 | 17 | fn read<'a>(&self, bytes: &'a [u8]) -> anyhow::Result> { 18 | self.field.read(bytes) 19 | } 20 | 21 | fn write(&self, bytes: &mut [u8], value: Self::Value<'_>) -> anyhow::Result<()> { 22 | self.field.write(bytes, value) 23 | } 24 | 25 | fn last_bit_exclusive(&self) -> usize { 26 | self.field.last_bit_exclusive() 27 | } 28 | 29 | fn bit_len(&self) -> usize { 30 | self.field.bit_len() 31 | } 32 | } 33 | 34 | impl SizedField for (M, F) 35 | where 36 | F: SizedField, 37 | { 38 | type Value<'a> = F::Value<'a>; 39 | 40 | fn read<'a>(&self, bytes: &'a [u8]) -> anyhow::Result> { 41 | self.1.read(bytes) 42 | } 43 | 44 | fn write(&self, bytes: &mut [u8], value: Self::Value<'_>) -> anyhow::Result<()> { 45 | self.1.write(bytes, value) 46 | } 47 | 48 | fn last_bit_exclusive(&self) -> usize { 49 | self.1.last_bit_exclusive() 50 | } 51 | 52 | fn bit_len(&self) -> usize { 53 | self.1.bit_len() 54 | } 55 | } 56 | 57 | impl SizedField for (T, U, F) 58 | where 59 | F: SizedField, 60 | { 61 | type Value<'a> = F::Value<'a>; 62 | 63 | fn read<'a>(&self, bytes: &'a [u8]) -> anyhow::Result> { 64 | self.2.read(bytes) 65 | } 66 | 67 | fn write(&self, bytes: &mut [u8], value: Self::Value<'_>) -> anyhow::Result<()> { 68 | self.2.write(bytes, value) 69 | } 70 | 71 | fn last_bit_exclusive(&self) -> usize { 72 | self.2.last_bit_exclusive() 73 | } 74 | 75 | fn bit_len(&self) -> usize { 76 | self.2.bit_len() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /devtools-frontend/src/proto/tmtc_generic_c2a.client.ts: -------------------------------------------------------------------------------- 1 | // @generated by protobuf-ts 2.9.1 2 | // @generated from protobuf file "tmtc_generic_c2a.proto" (package "tmtc_generic_c2a", syntax proto3) 3 | // tslint:disable 4 | import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; 5 | import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; 6 | import { TmtcGenericC2a } from "./tmtc_generic_c2a"; 7 | import { stackIntercept } from "@protobuf-ts/runtime-rpc"; 8 | import type { GetSateliteSchemaResponse } from "./tmtc_generic_c2a"; 9 | import type { GetSatelliteSchemaRequest } from "./tmtc_generic_c2a"; 10 | import type { UnaryCall } from "@protobuf-ts/runtime-rpc"; 11 | import type { RpcOptions } from "@protobuf-ts/runtime-rpc"; 12 | /** 13 | * @generated from protobuf service tmtc_generic_c2a.TmtcGenericC2a 14 | */ 15 | export interface ITmtcGenericC2aClient { 16 | /** 17 | * @generated from protobuf rpc: GetSatelliteSchema(tmtc_generic_c2a.GetSatelliteSchemaRequest) returns (tmtc_generic_c2a.GetSateliteSchemaResponse); 18 | */ 19 | getSatelliteSchema(input: GetSatelliteSchemaRequest, options?: RpcOptions): UnaryCall; 20 | } 21 | /** 22 | * @generated from protobuf service tmtc_generic_c2a.TmtcGenericC2a 23 | */ 24 | export class TmtcGenericC2aClient implements ITmtcGenericC2aClient, ServiceInfo { 25 | typeName = TmtcGenericC2a.typeName; 26 | methods = TmtcGenericC2a.methods; 27 | options = TmtcGenericC2a.options; 28 | constructor(private readonly _transport: RpcTransport) { 29 | } 30 | /** 31 | * @generated from protobuf rpc: GetSatelliteSchema(tmtc_generic_c2a.GetSatelliteSchemaRequest) returns (tmtc_generic_c2a.GetSateliteSchemaResponse); 32 | */ 33 | getSatelliteSchema(input: GetSatelliteSchemaRequest, options?: RpcOptions): UnaryCall { 34 | const method = this.methods[0], opt = this._transport.mergeOptions(options); 35 | return stackIntercept("unary", this._transport, method, opt, input); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>arkedge/renovate-config" 5 | ], 6 | "packageRules": [ 7 | { 8 | "groupName": "Typescript", 9 | "groupSlug": "typescript", 10 | "matchUpdateTypes": [ 11 | "patch" 12 | ], 13 | "matchDepTypes": [ 14 | "devDependencies" 15 | ], 16 | "automerge": false 17 | }, 18 | { 19 | "groupName": "Sentry", 20 | "groupSlug": "sentry", 21 | "matchPackageNames": [ 22 | "sentry", 23 | "sentry-tracing" 24 | ] 25 | }, 26 | { 27 | "groupName": "http-grpc", 28 | "groupSlug": "http-grpc", 29 | "matchPackageNames": [ 30 | "tonic", 31 | "tonic-build", 32 | "tonic-health", 33 | "tonic-reflection", 34 | "tonic-web", 35 | "prost", 36 | "prost-types", 37 | "prost-derive", 38 | "http", 39 | "axum", 40 | "tokio-tungstenite", 41 | "kble-socket", 42 | "tower", 43 | "tower-http", 44 | "http" 45 | ] 46 | }, 47 | { 48 | "groupName": "axum", 49 | "groupSlug": "axum", 50 | "matchPackageNames": [ 51 | "axum" 52 | ], 53 | "allowedVersions": "0.6.20" 54 | }, 55 | { 56 | "groupName": "tungstenite", 57 | "groupSlug": "tungstenite", 58 | "matchPackageNames": [ 59 | "tokio-tungstenite" 60 | ], 61 | "allowedVersions": "0.20.1" 62 | }, 63 | { 64 | "groupName": "http", 65 | "groupSlug": "http", 66 | "matchPackageNames": [ 67 | "http" 68 | ], 69 | "allowedVersions": "0.2.11" 70 | }, 71 | { 72 | "groupName": "tower-http", 73 | "groupSlug": "tower-http", 74 | "matchPackageNames": [ 75 | "tower-http" 76 | ], 77 | "allowedVersions": "0.4.4" 78 | }, 79 | { 80 | "groupName": "protobuf-ts", 81 | "groupSlug": "protobuf-ts", 82 | "matchPackageNames": [ 83 | "@protobuf-ts/runtime-rpc", 84 | "@protobuf-ts/runtime", 85 | "@protobuf-ts/grpcweb-transport" 86 | ] 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/src/free_variables.rs: -------------------------------------------------------------------------------- 1 | use opslang_ast::*; 2 | use std::collections::HashSet; 3 | 4 | pub struct Variables { 5 | pub variables: HashSet, 6 | pub telemetry_variables: HashSet, 7 | } 8 | 9 | impl Variables { 10 | pub fn empty() -> Self { 11 | Variables { 12 | variables: HashSet::new(), 13 | telemetry_variables: HashSet::new(), 14 | } 15 | } 16 | 17 | pub fn from_statement(stmt: &SingleStatement) -> Self { 18 | let mut vs = Variables::empty(); 19 | vs.stmt(stmt); 20 | vs 21 | } 22 | 23 | fn stmt(&mut self, stmt: &SingleStatement) { 24 | use SingleStatement::*; 25 | match stmt { 26 | Call(_) => (), 27 | Wait(w) => self.expr(&w.condition), 28 | Assert(c) => self.expr(&c.condition), 29 | AssertEq(c) => { 30 | self.expr(&c.left); 31 | self.expr(&c.right); 32 | if let Some(t) = &c.tolerance { 33 | self.expr(t) 34 | } 35 | } 36 | 37 | Command(cmd) => { 38 | for arg in &cmd.args { 39 | self.expr(arg); 40 | } 41 | } 42 | Let(l) => self.expr(&l.rhs), 43 | Print(p) => self.expr(&p.arg), 44 | Set(p) => self.expr(&p.expr), 45 | } 46 | } 47 | 48 | fn expr(&mut self, expr: &Expr) { 49 | match expr { 50 | Expr::Variable(VariablePath { raw }) => { 51 | self.variables.insert(raw.to_owned()); 52 | } 53 | Expr::TlmRef(VariablePath { raw }) => { 54 | self.telemetry_variables.insert(raw.to_owned()); 55 | } 56 | Expr::Literal(_) => {} 57 | Expr::UnOp(_, expr) => self.expr(expr), 58 | Expr::BinOp(_, lhs, rhs) => { 59 | self.expr(lhs); 60 | self.expr(rhs); 61 | } 62 | Expr::FunCall(fun, args) => { 63 | self.expr(fun); 64 | for arg in args { 65 | self.expr(arg); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc/transfer_frame.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use std::mem; 4 | 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, FromBytes, Unaligned}; 7 | 8 | #[bitfield(bytes = 5)] 9 | #[derive(Debug, Default, Clone, FromBytes, AsBytes, Unaligned)] 10 | #[repr(C)] 11 | pub struct PrimaryHeader { 12 | pub version_number: B2, 13 | pub bypass_flag: bool, 14 | pub control_command_flag: bool, 15 | #[skip] 16 | __: B2, 17 | pub scid: B10, 18 | pub vcid: B6, 19 | pub frame_length_raw: B10, 20 | pub frame_sequence_number: B8, 21 | } 22 | 23 | impl PrimaryHeader { 24 | pub const SIZE: usize = mem::size_of::(); 25 | 26 | pub fn frame_length_in_bytes(&self) -> usize { 27 | self.frame_length_raw() as usize + 1 28 | } 29 | 30 | pub fn set_frame_length_in_bytes(&mut self, frame_length_in_bytes: usize) { 31 | self.set_frame_length_raw((frame_length_in_bytes - 1) as u16) 32 | } 33 | } 34 | 35 | pub const FECF_CRC: crc::Crc = crc::Crc::::new(&crc::CRC_16_IBM_3740); 36 | 37 | pub const MAX_SIZE: usize = 1024; 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use zerocopy::LayoutVerified; 42 | 43 | use super::*; 44 | 45 | const CASE1: [u8; 5] = [0b01110010, 0b00011100, 0b10100110, 0b01100011, 0xDEu8]; 46 | 47 | #[test] 48 | fn test_read() { 49 | let ph = LayoutVerified::<_, PrimaryHeader>::new(CASE1.as_slice()).unwrap(); 50 | assert_eq!(1, ph.version_number()); 51 | assert!(ph.bypass_flag()); 52 | assert!(ph.control_command_flag()); 53 | assert_eq!(0b1000011100, ph.scid()); 54 | assert_eq!(0b101001, ph.vcid()); 55 | assert_eq!(0b1001100011, ph.frame_length_raw()); 56 | assert_eq!(0xDE, ph.frame_sequence_number()); 57 | } 58 | 59 | #[test] 60 | fn test_write() { 61 | let mut bytes = [0u8; 5]; 62 | let mut ph = LayoutVerified::<_, PrimaryHeader>::new(bytes.as_mut_slice()).unwrap(); 63 | ph.set_version_number(1); 64 | ph.set_bypass_flag(true); 65 | ph.set_control_command_flag(true); 66 | ph.set_scid(0b1000011100); 67 | ph.set_vcid(0b101001); 68 | ph.set_frame_length_raw(0b1001100011); 69 | ph.set_frame_sequence_number(0xDE); 70 | assert_eq!(CASE1, bytes); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /structpack/src/helper_impl.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, ensure}; 2 | 3 | use crate::{macros::define_casting_integral, FloatingValue, IntegralValue, NumericValue}; 4 | 5 | define_casting_integral!(i8, I8); 6 | define_casting_integral!(i16, I16); 7 | define_casting_integral!(i32, I32); 8 | define_casting_integral!(i64, I64); 9 | define_casting_integral!(u8, U8); 10 | define_casting_integral!(u16, U16); 11 | define_casting_integral!(u32, U32); 12 | define_casting_integral!(u64, U64); 13 | 14 | impl TryFrom for IntegralValue { 15 | type Error = anyhow::Error; 16 | 17 | fn try_from(value: NumericValue) -> Result { 18 | match value { 19 | NumericValue::Integral(i) => Ok(i), 20 | _ => Err(anyhow::anyhow!( 21 | "cannot convert floating point number to integral number" 22 | )), 23 | } 24 | } 25 | } 26 | 27 | impl From for FloatingValue { 28 | fn from(f: f32) -> Self { 29 | Self::F32(f) 30 | } 31 | } 32 | 33 | impl From for FloatingValue { 34 | fn from(f: f64) -> Self { 35 | Self::F64(f) 36 | } 37 | } 38 | 39 | impl TryFrom for f32 { 40 | type Error = anyhow::Error; 41 | 42 | fn try_from(value: FloatingValue) -> Result { 43 | match value { 44 | FloatingValue::F32(f) => Ok(f), 45 | FloatingValue::F64(d) => { 46 | let f = d as f32; 47 | ensure!(f.is_finite() || d.is_infinite(), "overflow: {} for f32", d); 48 | Ok(f) 49 | } 50 | } 51 | } 52 | } 53 | impl TryFrom for f64 { 54 | type Error = anyhow::Error; 55 | 56 | fn try_from(value: FloatingValue) -> Result { 57 | match value { 58 | FloatingValue::F32(f) => Ok(f as f64), 59 | FloatingValue::F64(d) => Ok(d), 60 | } 61 | } 62 | } 63 | 64 | impl TryFrom for FloatingValue { 65 | type Error = anyhow::Error; 66 | 67 | fn try_from(value: NumericValue) -> Result { 68 | match value { 69 | NumericValue::Floating(f) => Ok(f), 70 | _ => Err(anyhow!( 71 | "cannot convert integral number to floating point number" 72 | )), 73 | } 74 | } 75 | } 76 | 77 | impl From for NumericValue { 78 | fn from(i: IntegralValue) -> Self { 79 | Self::Integral(i) 80 | } 81 | } 82 | 83 | impl From for NumericValue { 84 | fn from(f: FloatingValue) -> Self { 85 | Self::Floating(f) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/rustdoc.yml: -------------------------------------------------------------------------------- 1 | name: rustdoc 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_INCREMENTAL: 0 10 | 11 | jobs: 12 | rustdoc: 13 | permissions: 14 | id-token: write 15 | contents: read 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: checkout all the submodules 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | submodules: recursive 24 | 25 | # for devtools-frontend 26 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 27 | with: 28 | node-version: 21 29 | 30 | - name: Install Protoc 31 | uses: arduino/setup-protoc@v1 32 | with: 33 | version: '3.x' 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Get Rust toolchain 37 | id: toolchain 38 | run: | 39 | awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" 40 | 41 | - uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 # v1 42 | with: 43 | toolchain: ${{ steps.toolchain.outputs.toolchain }} 44 | 45 | - name: cache dependencies 46 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 47 | 48 | - name: Install cargo-about 49 | run: cargo install cargo-about@0.6.4 --locked 50 | 51 | - name: Install wasm-pack 52 | run: cargo install wasm-pack --locked 53 | 54 | - run: rm -rf ./target/doc 55 | 56 | - run: cargo doc --all --no-deps 57 | env: 58 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 59 | 60 | - name: add index.html 61 | run: | 62 | cat > ./target/doc/index.html << EOS 63 | 64 | EOS 65 | 66 | - name: Fix file permissions 67 | shell: sh 68 | run: | 69 | chmod -c -R +rX "target/doc/" 70 | 71 | - name: Archive rustdoc 72 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 73 | with: 74 | path: target/doc/ 75 | 76 | deploy: 77 | if: github.ref == 'refs/heads/main' 78 | needs: rustdoc 79 | 80 | permissions: 81 | pages: write 82 | id-token: write 83 | 84 | environment: 85 | name: github-pages 86 | url: ${{ steps.deployment.outputs.page_url }} 87 | 88 | runs-on: ubuntu-latest 89 | 90 | steps: 91 | - name: Deploy to GitHub Pages 92 | id: deployment 93 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 94 | -------------------------------------------------------------------------------- /devtools-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c2a-devtools", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "codegen:proto:tco_tmiv": "protoc --ts_out src/proto --proto_path ../gaia-stub/proto ../gaia-stub/proto/tco_tmiv.proto", 6 | "codegen:proto:broker": "protoc --ts_out src/proto --proto_path ../gaia-stub/proto ../gaia-stub/proto/broker.proto", 7 | "codegen:proto:tmtc_generic_c2a": "protoc --ts_out src/proto --proto_path ../tmtc-c2a/proto ../tmtc-c2a/proto/tmtc_generic_c2a.proto", 8 | "codegen:proto": "run-p codegen:proto:*", 9 | "codegen": "run-s codegen:proto", 10 | "crate:build": "cd crates && wasm-pack build --weak-refs --target web --release", 11 | "crate:dev": "cd crates && cargo watch -s 'wasm-pack build --weak-refs --target web --dev' -C", 12 | "crate": "pnpm run crate:${MODE:-build}", 13 | "crates:opslang-wasm": "pnpm run crate opslang-wasm", 14 | "dev:crates": "MODE=dev run-p crates:*", 15 | "dev:vite": "vite --host", 16 | "dev": "run-p dev:*", 17 | "build:crates": "run-s crates:*", 18 | "build:vite": "vite build", 19 | "build": "run-s build:crates build:vite", 20 | "typecheck": "tsc", 21 | "lint:prettier": "prettier . --check", 22 | "lint:eslint": "eslint . --format stylish", 23 | "lint": "run-p lint:*", 24 | "fix:prettier": "pnpm run lint:prettier --write", 25 | "fix:eslint": "pnpm run lint:eslint --fix", 26 | "fix": "run-s fix:eslint fix:prettier" 27 | }, 28 | "dependencies": { 29 | "@blueprintjs/core": "5.10.5", 30 | "@blueprintjs/icons": "5.10.0", 31 | "@monaco-editor/react": "^4.5.0", 32 | "@protobuf-ts/grpcweb-transport": "^2.8.2", 33 | "@protobuf-ts/runtime": "^2.9.3", 34 | "@protobuf-ts/runtime-rpc": "^2.9.3", 35 | "monaco-editor": "^0.50.0", 36 | "react": "18.3.1", 37 | "react-dom": "18.3.1", 38 | "react-helmet-async": "^2.0.0", 39 | "react-resizable-panels": "^2.0.0", 40 | "react-router-dom": "^6.10.0", 41 | "tailwindcss": "3.4.14" 42 | }, 43 | "devDependencies": { 44 | "@protobuf-ts/plugin": "^2.8.2", 45 | "@types/react": "18.3.3", 46 | "@types/react-dom": "18.3.0", 47 | "@typescript-eslint/eslint-plugin": "7.15.0", 48 | "@typescript-eslint/parser": "7.15.0", 49 | "@vitejs/plugin-react": "4.3.1", 50 | "autoprefixer": "10.4.19", 51 | "eslint": "8.57.0", 52 | "eslint-config-prettier": "9.1.0", 53 | "eslint-config-react": "1.1.7", 54 | "eslint-plugin-react": "7.34.3", 55 | "eslint-plugin-react-hooks": "4.6.2", 56 | "npm-run-all2": "6.2.2", 57 | "postcss": "8.4.39", 58 | "prettier": "3.3.2", 59 | "typescript": "5.5.3", 60 | "vite": "5.4.19" 61 | }, 62 | "engines": { 63 | "node": "^21" 64 | }, 65 | "packageManager": "pnpm@9.8.0" 66 | } 67 | -------------------------------------------------------------------------------- /tmtc-c2a/src/tmiv.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use gaia_tmtc::tco_tmiv::{tmiv_field, TmivField}; 3 | 4 | use crate::registry::TelemetrySchema; 5 | 6 | pub struct FieldsBuilder<'a> { 7 | schema: &'a TelemetrySchema, 8 | } 9 | 10 | impl<'a> FieldsBuilder<'a> { 11 | pub fn new(schema: &'a TelemetrySchema) -> Self { 12 | Self { schema } 13 | } 14 | 15 | fn build_integral_fields(&self, fields: &mut Vec, bytes: &[u8]) -> Result<()> { 16 | for (name_pair, field_schema) in self.schema.integral_fields.iter() { 17 | let (raw, converted) = field_schema.read_from(bytes)?; 18 | use gaia_ccsds_c2a::access::tlm::FieldValue; 19 | let converted = match converted { 20 | FieldValue::Double(d) => tmiv_field::Value::Double(d), 21 | FieldValue::Integer(i) => tmiv_field::Value::Integer(i), 22 | FieldValue::Constant(e) => tmiv_field::Value::Enum(e), 23 | FieldValue::Bytes(b) => tmiv_field::Value::Bytes(b), 24 | }; 25 | fields.push(TmivField { 26 | name: name_pair.raw_name.to_string(), 27 | value: Some(tmiv_field::Value::Bytes(raw)), 28 | }); 29 | fields.push(TmivField { 30 | name: name_pair.converted_name.to_string(), 31 | value: Some(converted), 32 | }); 33 | } 34 | Ok(()) 35 | } 36 | 37 | fn build_floating_fields(&self, fields: &mut Vec, bytes: &[u8]) -> Result<()> { 38 | for (name_pair, field_schema) in self.schema.floating_fields.iter() { 39 | let (raw, converted) = field_schema.read_from(bytes)?; 40 | use gaia_ccsds_c2a::access::tlm::FieldValue; 41 | let converted = match converted { 42 | FieldValue::Double(d) => tmiv_field::Value::Double(d), 43 | FieldValue::Integer(i) => tmiv_field::Value::Integer(i), 44 | FieldValue::Constant(e) => tmiv_field::Value::Enum(e), 45 | FieldValue::Bytes(b) => tmiv_field::Value::Bytes(b), 46 | }; 47 | fields.push(TmivField { 48 | name: name_pair.raw_name.to_string(), 49 | value: Some(tmiv_field::Value::Bytes(raw)), 50 | }); 51 | fields.push(TmivField { 52 | name: name_pair.converted_name.to_string(), 53 | value: Some(converted), 54 | }); 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub fn build(&self, tmiv_fields: &mut Vec, space_packet_bytes: &[u8]) -> Result<()> { 60 | self.build_integral_fields(tmiv_fields, space_packet_bytes)?; 61 | self.build_floating_fields(tmiv_fields, space_packet_bytes)?; 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/tc/transfer_frame.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use zerocopy::{ByteSlice, ByteSliceMut, LayoutVerified}; 7 | 8 | pub use crate::ccsds::tc::transfer_frame::*; 9 | 10 | pub const TRAILER_SIZE: usize = 2; 11 | 12 | pub struct Builder { 13 | bare: B, 14 | } 15 | 16 | impl Builder 17 | where 18 | B: ByteSliceMut, 19 | { 20 | pub fn new(bytes: B) -> Option { 21 | if bytes.len() < TRAILER_SIZE { 22 | None 23 | } else { 24 | Some(Self { bare: bytes }) 25 | } 26 | } 27 | 28 | pub fn bare_mut(&mut self) -> Option> { 29 | let bare_len = self.bare.len(); 30 | BareBuilder::new(&mut self.bare[..bare_len - TRAILER_SIZE]) 31 | } 32 | 33 | pub fn finish(mut self, bare_len: usize) -> usize { 34 | let frame_len = bare_len + TRAILER_SIZE; 35 | assert!(frame_len <= self.bare.len()); 36 | let bare_bytes = &self.bare[..bare_len]; 37 | let fecw = FECF_CRC.checksum(bare_bytes); 38 | let fecw_bytes = &mut self.bare[bare_len..][..TRAILER_SIZE]; 39 | fecw_bytes.copy_from_slice(&fecw.to_be_bytes()); 40 | frame_len 41 | } 42 | } 43 | 44 | pub struct BareBuilder { 45 | primary_header: LayoutVerified, 46 | data_field: B, 47 | } 48 | 49 | impl BareBuilder 50 | where 51 | B: ByteSlice, 52 | { 53 | fn new(bytes: B) -> Option { 54 | let (primary_header, data_field) = LayoutVerified::new_unaligned_from_prefix(bytes)?; 55 | Some(Self { 56 | primary_header, 57 | data_field, 58 | }) 59 | } 60 | } 61 | 62 | impl BareBuilder 63 | where 64 | B: ByteSliceMut, 65 | { 66 | pub fn use_default(&mut self) { 67 | self.set_version_number(0); 68 | self.set_bypass_flag(true); // Type-Bx 69 | self.set_control_command_flag(false); // Type-xD 70 | self.set_vcid(0); 71 | } 72 | 73 | pub fn data_field_mut(&mut self) -> &mut B { 74 | &mut self.data_field 75 | } 76 | 77 | pub fn finish(mut self, data_field_len: usize) -> usize { 78 | let bare_len = mem::size_of::() + data_field_len; 79 | let frame_len = bare_len + TRAILER_SIZE; 80 | self.set_frame_length_in_bytes(frame_len); 81 | bare_len 82 | } 83 | } 84 | 85 | impl Deref for BareBuilder 86 | where 87 | B: ByteSlice, 88 | { 89 | type Target = PrimaryHeader; 90 | 91 | fn deref(&self) -> &Self::Target { 92 | &self.primary_header 93 | } 94 | } 95 | 96 | impl DerefMut for BareBuilder 97 | where 98 | B: ByteSliceMut, 99 | { 100 | fn deref_mut(&mut self) -> &mut Self::Target { 101 | &mut self.primary_header 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /devtools-frontend/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use std::{ 3 | env, fs, io, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | fn main() { 8 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 9 | 10 | println!("cargo:rerun-if-changed=package.json"); 11 | println!("cargo:rerun-if-changed=pnpm-lock.yaml"); 12 | println!("cargo:rerun-if-changed=index.html"); 13 | println!("cargo:rerun-if-changed=src/"); 14 | 15 | // copy frontend source into OUT_DIR 16 | let devtools_build_dir = out_dir.join("devtools_frontend"); 17 | copy_devtools_dir(".", &devtools_build_dir).unwrap(); 18 | 19 | let status = Command::new("corepack") 20 | .arg("enable") 21 | .current_dir(&devtools_build_dir) 22 | .status() 23 | .expect("failed to execute corepack"); 24 | assert!(status.success(), "failed to install pnpm via corepack"); 25 | 26 | let status = Command::new("pnpm") 27 | .arg("install") 28 | .current_dir(&devtools_build_dir) 29 | .status() 30 | .expect("failed to execute pnpm"); 31 | assert!(status.success(), "failed to install deps for frontend"); 32 | 33 | // parepare crate dir 34 | let crate_root_dir = out_dir.join("crate_root"); 35 | 36 | // copy opslang-wasm dist 37 | { 38 | let opslang_pkg_dir = env::var("DEP_OPSLANG_WASM_OUT_DIR").unwrap(); 39 | let opslang_dist_dir = crate_root_dir.join("opslang-wasm").join("pkg"); 40 | 41 | copy_devtools_dir(opslang_pkg_dir, opslang_dist_dir).unwrap(); 42 | } 43 | 44 | let devtools_out_dir = out_dir.join("devtools_dist"); 45 | let status = Command::new("pnpm") 46 | .current_dir(&devtools_build_dir) 47 | // vite.config.ts にwasmのビルド場所を教えるために環境変数を渡す 48 | .envs([("DEVTOOLS_CRATE_ROOT", crate_root_dir)]) 49 | .arg("run") 50 | .arg("build:vite") 51 | .arg("--outDir") 52 | .arg(&devtools_out_dir) 53 | .status() 54 | .expect("failed to execute yarn"); 55 | assert!(status.success(), "failed to build frontend"); 56 | } 57 | 58 | fn copy_devtools_dir(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { 59 | fs::create_dir_all(&dst)?; 60 | for entry in fs::read_dir(src)? { 61 | let entry = entry?; 62 | let ty = entry.file_type()?; 63 | if ty.is_dir() { 64 | if entry.file_name().to_str() == Some("node_modules") { 65 | continue; 66 | } 67 | // In `cargo package`, each crate source files are copied to 68 | // target/package/crate- & threre are target dir 69 | if entry.file_name().to_str() == Some("target") { 70 | continue; 71 | } 72 | copy_devtools_dir(entry.path(), dst.as_ref().join(entry.file_name()))?; 73 | } else { 74 | fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; 75 | } 76 | } 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /tmtc-c2a/proto/tmtc_generic_c2a.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tmtc_generic_c2a; 4 | 5 | service TmtcGenericC2a { 6 | rpc GetSatelliteSchema(GetSatelliteSchemaRequest) returns (GetSateliteSchemaResponse); 7 | } 8 | 9 | message GetSatelliteSchemaRequest { 10 | } 11 | 12 | message GetSateliteSchemaResponse { 13 | SatelliteSchema satellite_schema = 1; 14 | } 15 | 16 | message SatelliteSchema { 17 | map telemetry_channels = 1; 18 | map telemetry_components = 2; 19 | map command_prefixes = 3; 20 | map command_components = 4; 21 | } 22 | 23 | message TelemetryComponentSchema { 24 | TelemetryComponentSchemaMetadata metadata = 1; 25 | map telemetries = 2; 26 | } 27 | 28 | message TelemetryComponentSchemaMetadata { 29 | uint32 apid = 1; 30 | } 31 | 32 | message CommandComponentSchema { 33 | CommandComponentSchemaMetadata metadata = 1; 34 | map commands = 2; 35 | } 36 | 37 | message CommandComponentSchemaMetadata { 38 | uint32 apid = 1; 39 | } 40 | 41 | message CommandSchema { 42 | CommandSchemaMetadata metadata = 1; 43 | repeated CommandParameterSchema parameters = 2; 44 | } 45 | 46 | message CommandSchemaMetadata { 47 | uint32 id = 1; 48 | } 49 | 50 | message CommandParameterSchema { 51 | CommandParameterSchemaMetadata metadata = 1; 52 | CommandParameterDataType data_type = 2; 53 | } 54 | 55 | message CommandParameterSchemaMetadata { 56 | // TODO: string description = 1; 57 | } 58 | 59 | enum CommandParameterDataType { 60 | CMD_PARAMETER_INTEGER = 0; 61 | CMD_PARAMETER_DOUBLE = 1; 62 | CMD_PARAMETER_BYTES = 2; 63 | } 64 | 65 | message TelemetrySchema { 66 | TelemetrySchemaMetadata metadata = 1; 67 | repeated TelemetryFieldSchema fields = 2; 68 | } 69 | 70 | message TelemetrySchemaMetadata { 71 | uint32 id = 1; 72 | } 73 | 74 | message TelemetryFieldSchema { 75 | TelemetryFieldSchemaMetadata metadata = 1; 76 | string name = 2; 77 | // TODO: TelemetryFieldDataType data_type = 3; 78 | } 79 | 80 | message TelemetryFieldSchemaMetadata { 81 | // TODO: string description = 1; 82 | } 83 | 84 | message TelemetryChannelSchema { 85 | TelemetryChannelSchemaMetadata metadata = 1; 86 | } 87 | 88 | message TelemetryChannelSchemaMetadata { 89 | uint32 destination_flag_mask = 1; 90 | } 91 | 92 | message CommandPrefixSchema { 93 | CommandPrefixSchemaMetadata metadata = 1; 94 | map subsystems = 2; 95 | } 96 | 97 | message CommandPrefixSchemaMetadata { 98 | } 99 | 100 | message CommandSubsystemSchema { 101 | CommandSubsystemSchemaMetadata metadata = 1; 102 | bool has_time_indicator = 2; 103 | } 104 | 105 | message CommandSubsystemSchemaMetadata { 106 | uint32 destination_type = 1; 107 | uint32 execution_type = 2; 108 | } 109 | -------------------------------------------------------------------------------- /devtools-frontend/src/tree.ts: -------------------------------------------------------------------------------- 1 | export type TreeNamespace = Map>; 2 | export type TreeNode = 3 | | { type: "leaf"; value: T } 4 | | { type: "ns"; ns: TreeNamespace }; 5 | 6 | const makeLeaf = (value: T): TreeNode => ({ type: "leaf", value }); 7 | const makeNs = (ns: TreeNamespace): TreeNode => ({ type: "ns", ns }); 8 | 9 | const mapMapWithKey = ( 10 | map: Map, 11 | f: (key: K, value: T) => U, 12 | ): Map => { 13 | const result = new Map(); 14 | for (const [key, value] of map) { 15 | result.set(key, f(key, value)); 16 | } 17 | return result; 18 | }; 19 | 20 | const mapTreeRec = ( 21 | tree: TreeNode, 22 | path: string[], 23 | f: (path: string[], node: T) => U, 24 | ): TreeNode => { 25 | switch (tree.type) { 26 | case "leaf": 27 | return makeLeaf(f(path, tree.value)); 28 | case "ns": 29 | return makeNs( 30 | mapMapWithKey(tree.ns, (key, child) => 31 | mapTreeRec(child, [...path, key], f), 32 | ), 33 | ); 34 | } 35 | }; 36 | 37 | export const mapTree = ( 38 | tree: TreeNode, 39 | f: (path: string[], node: T) => U, 40 | ): TreeNode => { 41 | return mapTreeRec(tree, [], f); 42 | }; 43 | 44 | export const mapNamespace = ( 45 | ns: TreeNamespace, 46 | f: (path: string[], node: T) => U, 47 | ): TreeNamespace => { 48 | const tree = makeNs(ns); 49 | const mappedTree = mapTreeRec(tree, [], f); 50 | if (mappedTree.type === "ns") { 51 | return mappedTree.ns; 52 | } else { 53 | throw new Error("Impossible"); 54 | } 55 | }; 56 | 57 | export const singleton = (path: string[], value: T): TreeNode => { 58 | let tree: TreeNode = { type: "leaf", value }; 59 | for (const key of path.toReversed()) { 60 | const ns = new Map(); 61 | ns.set(key, tree); 62 | tree = { type: "ns", ns }; 63 | } 64 | return tree; 65 | }; 66 | 67 | export const add = ( 68 | tree: TreeNode, 69 | path: string[], 70 | value: T, 71 | ): TreeNode => { 72 | switch (tree.type) { 73 | case "leaf": 74 | if (path.length === 0) { 75 | tree.value = value; 76 | return tree; 77 | } else { 78 | const [pathHead, ...pathTail] = path; 79 | const ns = new Map(); 80 | ns.set("", tree); 81 | ns.set(pathHead, singleton(pathTail, value)); 82 | return { 83 | type: "ns", 84 | ns, 85 | }; 86 | } 87 | case "ns": 88 | addToNamespace(tree.ns, path, value); 89 | return tree; 90 | } 91 | }; 92 | 93 | export const addToNamespace = ( 94 | ns: TreeNamespace, 95 | path: string[], 96 | value: T, 97 | ): void => { 98 | if (path.length === 0) { 99 | ns.set("", { type: "leaf", value }); 100 | } else { 101 | const [pathHead, ...pathTail] = path; 102 | const child = ns.get(pathHead); 103 | if (child === undefined) { 104 | ns.set(pathHead, singleton(pathTail, value)); 105 | } else { 106 | ns.set(pathHead, add(child, pathTail, value)); 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /devtools-frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMClient from "react-dom/client"; 3 | import "./index.css"; 4 | import { 5 | LoaderFunction, 6 | RouterProvider, 7 | createBrowserRouter, 8 | useRouteError, 9 | } from "react-router-dom"; 10 | import { TelemetryView } from "./components/TelemetryView"; 11 | import { Layout } from "./components/Layout"; 12 | import { HelmetProvider } from "react-helmet-async"; 13 | import { Top } from "./components/Top"; 14 | import { Callout, FocusStyleManager, Intent } from "@blueprintjs/core"; 15 | import { CommandView } from "./components/CommandView"; 16 | import { OldCommandView } from "./components/OldCommandView"; 17 | import { buildClient } from "./client"; 18 | import type { GrpcClientService } from "./worker"; 19 | import { IconNames } from "@blueprintjs/icons"; 20 | import { FriendlyError } from "./error"; 21 | 22 | FocusStyleManager.onlyShowFocusOnTabs(); 23 | 24 | const root = ReactDOMClient.createRoot(document.getElementById("root")!); 25 | 26 | const clientLoader: LoaderFunction = async () => { 27 | const worker = new SharedWorker(new URL("./worker.ts", import.meta.url), { 28 | type: "module", 29 | /* @vite-ignore */ 30 | name: location.origin, 31 | }); 32 | const client = buildClient(worker); 33 | const { satelliteSchema } = await client.getSatelliteSchema().catch((err) => { 34 | throw new FriendlyError(`Failed to get satellite schema`, { 35 | cause: err, 36 | details: "Make sure that your tmtc-c2a is running.", 37 | }); 38 | })!; 39 | return { client, satelliteSchema }; 40 | }; 41 | 42 | const ErrorBoundary = () => { 43 | const error = useRouteError(); 44 | console.error(error); 45 | let title = "Error"; 46 | let description = `${error}`; 47 | if (error instanceof FriendlyError) { 48 | title = `${error.message}`; 49 | description = error.details ?? `${error.cause}`; 50 | } 51 | return ( 52 |
53 |
54 | 55 | {description} 56 | 57 |
58 |
59 | ); 60 | }; 61 | 62 | const router = createBrowserRouter( 63 | [ 64 | { 65 | path: "/", 66 | element: , 67 | loader: clientLoader, 68 | errorElement: , 69 | children: [ 70 | { 71 | path: "", 72 | element: , 73 | }, 74 | { 75 | path: "telemetries/:tmivName", 76 | element: , 77 | }, 78 | { 79 | path: "command_new", 80 | element: , 81 | }, 82 | { 83 | path: "command", 84 | element: , 85 | }, 86 | ], 87 | }, 88 | ], 89 | { 90 | basename: import.meta.env.BASE_URL, 91 | }, 92 | ); 93 | 94 | root.render( 95 | 96 | 97 | 98 | 99 | , 100 | ); 101 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/src/value.rs: -------------------------------------------------------------------------------- 1 | use crate::type_err; 2 | use crate::Result; 3 | 4 | #[derive(Debug)] 5 | pub(crate) enum Value { 6 | Integer(i64), 7 | Double(f64), 8 | Bool(bool), 9 | Array(Vec), 10 | String(String), 11 | Duration(chrono::Duration), 12 | DateTime(chrono::DateTime), 13 | } 14 | 15 | impl From for Value { 16 | fn from(v: i64) -> Self { 17 | Value::Integer(v) 18 | } 19 | } 20 | 21 | impl From for Value { 22 | fn from(v: f64) -> Self { 23 | Value::Double(v) 24 | } 25 | } 26 | 27 | impl From for Value { 28 | fn from(v: bool) -> Self { 29 | Value::Bool(v) 30 | } 31 | } 32 | 33 | impl From for Value { 34 | fn from(v: chrono::Duration) -> Self { 35 | Value::Duration(v) 36 | } 37 | } 38 | 39 | impl From> for Value { 40 | fn from(v: chrono::DateTime) -> Self { 41 | Value::DateTime(v) 42 | } 43 | } 44 | 45 | impl Value { 46 | pub fn type_name(&self) -> &'static str { 47 | use Value::*; 48 | match self { 49 | Integer(_) => "integer", 50 | Double(_) => "double", 51 | Bool(_) => "bool", 52 | Array(_) => "array", 53 | String(_) => "string", 54 | Duration(_) => "duration", 55 | DateTime(_) => "datetime", 56 | } 57 | } 58 | 59 | pub fn integer(&self) -> Result { 60 | self.cast() 61 | } 62 | 63 | pub fn double(&self) -> Result { 64 | self.cast() 65 | } 66 | 67 | pub fn bool(&self) -> Result { 68 | self.cast() 69 | } 70 | 71 | pub fn array(&self) -> Result<&Vec> { 72 | match self { 73 | Value::Array(x) => Ok(x), 74 | _ => type_err("array", self), 75 | } 76 | } 77 | 78 | pub fn string(&self) -> Result<&str> { 79 | match self { 80 | Value::String(x) => Ok(x), 81 | _ => type_err("string", self), 82 | } 83 | } 84 | 85 | pub fn duration(&self) -> Result { 86 | self.cast() 87 | } 88 | 89 | pub fn datetime(&self) -> Result> { 90 | self.cast() 91 | } 92 | } 93 | 94 | pub trait Castable { 95 | const TYPE_NAME: &'static str; 96 | fn from_value(v: &Value) -> Option 97 | where 98 | Self: Sized; 99 | } 100 | 101 | impl Value { 102 | pub fn cast(&self) -> Result { 103 | match T::from_value(self) { 104 | Some(x) => Ok(x), 105 | None => type_err(T::TYPE_NAME, self), 106 | } 107 | } 108 | } 109 | 110 | macro_rules! impl_castable { 111 | ($t:ty, $tyname:expr, $variant:ident) => { 112 | impl Castable for $t { 113 | const TYPE_NAME: &'static str = stringify!($tyname); 114 | fn from_value(v: &Value) -> Option { 115 | match v { 116 | Value::$variant(x) => Some(*x as $t), 117 | _ => None, 118 | } 119 | } 120 | } 121 | }; 122 | } 123 | 124 | impl_castable!(i64, "integer", Integer); 125 | impl_castable!(f64, "double", Double); 126 | impl_castable!(bool, "bool", Bool); 127 | impl_castable!(chrono::Duration, "duration", Duration); 128 | impl_castable!(chrono::DateTime, "datetime", DateTime); 129 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/aos/transfer_frame.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use std::{fmt::Display, mem}; 4 | 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, ByteSlice, FromBytes, LayoutVerified, Unaligned}; 7 | 8 | #[bitfield(bytes = 6)] 9 | #[derive(Debug, Default, Clone, FromBytes, AsBytes, Unaligned)] 10 | #[repr(C)] 11 | pub struct PrimaryHeader { 12 | pub version_number: B2, 13 | pub scid_low: B8, 14 | pub vcid: B6, 15 | pub frame_count_raw: B24, 16 | pub replay_flag: bool, 17 | #[skip] 18 | __: B1, 19 | scid_high: B2, 20 | #[skip] 21 | __: B4, 22 | } 23 | 24 | impl PrimaryHeader { 25 | pub const SIZE: usize = mem::size_of::(); 26 | } 27 | 28 | impl PrimaryHeader { 29 | pub fn frame_count(&self) -> FrameCount { 30 | FrameCount(self.frame_count_raw()) 31 | } 32 | 33 | pub fn set_frame_count(&mut self, FrameCount(raw): FrameCount) { 34 | self.set_frame_count_raw(raw); 35 | } 36 | 37 | pub fn scid(&self) -> u16 { 38 | self.scid_low() as u16 | (self.scid_high() as u16) << 8 39 | } 40 | 41 | pub fn set_scid(&mut self, scid: u16) { 42 | assert!(scid < 1024); 43 | self.set_scid_low(scid as u8); 44 | self.set_scid_high((scid >> 8) as u8); 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 49 | pub struct FrameCount(u32); 50 | impl FrameCount { 51 | const MAX: u32 = 0xFFFFFF; 52 | pub fn is_next_to(self, other: Self) -> bool { 53 | self == other.next() 54 | } 55 | 56 | #[must_use] 57 | pub fn next(self) -> Self { 58 | Self((self.0 + 1) & Self::MAX) 59 | } 60 | } 61 | 62 | impl Display for FrameCount { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | write!(f, "{}", self.0) 65 | } 66 | } 67 | 68 | pub struct TransferFrame { 69 | pub primary_header: LayoutVerified, 70 | pub data_unit_zone: B, 71 | pub trailer: LayoutVerified, 72 | } 73 | 74 | impl TransferFrame 75 | where 76 | B: ByteSlice, 77 | T: Unaligned, 78 | { 79 | pub fn new(bytes: B) -> Option { 80 | let (primary_header, tail) = LayoutVerified::new_unaligned_from_prefix(bytes)?; 81 | let (data_unit_zone, trailer) = LayoutVerified::new_unaligned_from_suffix(tail)?; 82 | Some(Self { 83 | primary_header, 84 | data_unit_zone, 85 | trailer, 86 | }) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use zerocopy::LayoutVerified; 93 | 94 | use super::*; 95 | 96 | const CASE1: [u8; 6] = [119, 129, 9, 226, 57, 0]; 97 | 98 | #[test] 99 | fn test_read() { 100 | let ph = LayoutVerified::<_, PrimaryHeader>::new(CASE1.as_slice()).unwrap(); 101 | assert_eq!(1, ph.version_number()); 102 | assert_eq!(0xDE, ph.scid()); 103 | assert_eq!(1, ph.vcid()); 104 | assert_eq!(647737, ph.frame_count_raw()); 105 | assert!(!ph.replay_flag()); 106 | } 107 | 108 | #[test] 109 | fn test_write() { 110 | let mut bytes = [0u8; 6]; 111 | let mut ph = LayoutVerified::<_, PrimaryHeader>::new(bytes.as_mut_slice()).unwrap(); 112 | ph.set_version_number(1); 113 | ph.set_scid(0xDE); 114 | ph.set_vcid(1); 115 | ph.set_frame_count_raw(647737); 116 | ph.set_replay_flag(false); 117 | assert_eq!(CASE1, bytes); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /devtools-frontend/crates/opslang-wasm/src/union_value.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | #[wasm_bindgen(typescript_custom_section)] 3 | const TS_SECTION_VALUE: &str = r#" 4 | type Value = 5 | {kind : "integer", value : bigint } | 6 | {kind : "double", value : number} | 7 | {kind : "bool", value : boolean } | 8 | {kind: "array", value : Value[] } | 9 | {kind: "string", value: string } | 10 | {kind: "duration", value: bigint } | 11 | {kind: "datetime", value: bigint } 12 | "#; 13 | 14 | #[wasm_bindgen(module = "/js/union.js")] 15 | extern "C" { 16 | #[wasm_bindgen(js_name = "Value", typescript_type = "Value")] 17 | pub type UnionValue; 18 | 19 | #[wasm_bindgen(js_name = "asInt")] 20 | fn as_int(_: &UnionValue) -> Option; 21 | #[wasm_bindgen(js_name = "asDouble")] 22 | fn as_double(_: &UnionValue) -> Option; 23 | #[wasm_bindgen(js_name = "asBool")] 24 | fn as_bool(_: &UnionValue) -> Option; 25 | #[wasm_bindgen(js_name = "asArray")] 26 | fn as_array(_: &UnionValue) -> Option>; 27 | #[wasm_bindgen(js_name = "asString")] 28 | fn as_string(_: &UnionValue) -> Option; 29 | #[wasm_bindgen(js_name = "asDuration")] 30 | fn as_duration(_: &UnionValue) -> Option; 31 | #[wasm_bindgen(js_name = "asDateTime")] 32 | fn as_datetime(_: &UnionValue) -> Option; 33 | 34 | #[wasm_bindgen(js_name = "makeInt")] 35 | fn make_int(_: i64) -> UnionValue; 36 | #[wasm_bindgen(js_name = "makeDouble")] 37 | fn make_double(_: f64) -> UnionValue; 38 | #[wasm_bindgen(js_name = "makeBool")] 39 | fn make_bool(_: bool) -> UnionValue; 40 | #[wasm_bindgen(js_name = "makeArray")] 41 | fn make_array(_: Vec) -> UnionValue; 42 | #[wasm_bindgen(js_name = "makeString")] 43 | fn make_string(_: std::string::String) -> UnionValue; 44 | #[wasm_bindgen(js_name = "makeDuration")] 45 | fn make_duration(_: i64) -> UnionValue; 46 | #[wasm_bindgen(js_name = "makeDateTime")] 47 | fn make_datetime(_: i64) -> UnionValue; 48 | } 49 | 50 | use crate::Value; 51 | use Value::*; 52 | 53 | impl From for Value { 54 | fn from(v: UnionValue) -> Value { 55 | if let Some(v) = as_int(&v) { 56 | Integer(v) 57 | } else if let Some(v) = as_double(&v) { 58 | Double(v) 59 | } else if let Some(v) = as_bool(&v) { 60 | Bool(v) 61 | } else if let Some(vs) = as_array(&v) { 62 | let vs = vs.into_iter().map(Into::into).collect(); 63 | Array(vs) 64 | } else if let Some(v) = as_string(&v) { 65 | String(v) 66 | } else if let Some(v) = as_duration(&v) { 67 | Duration(chrono::Duration::milliseconds(v)) 68 | } else if let Some(v) = as_datetime(&v) { 69 | DateTime(chrono::DateTime::UNIX_EPOCH + chrono::Duration::milliseconds(v)) 70 | } else { 71 | unreachable!() 72 | } 73 | } 74 | } 75 | 76 | impl From for UnionValue { 77 | fn from(v: Value) -> UnionValue { 78 | match v { 79 | Integer(v) => make_int(v), 80 | Double(v) => make_double(v), 81 | Bool(v) => make_bool(v), 82 | Array(vs) => { 83 | let vs = vs.into_iter().map(Into::into).collect(); 84 | make_array(vs) 85 | } 86 | String(v) => make_string(v), 87 | Duration(v) => make_duration(v.num_milliseconds()), 88 | DateTime(v) => make_datetime(v.timestamp_millis()), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tmtc-c2a/src/tco.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use gaia_ccsds_c2a::access::cmd::schema::CommandSchema; 5 | use gaia_tmtc::tco_tmiv::{tco_param, Tco}; 6 | use structpack::{FloatingValue, IntegralValue}; 7 | 8 | pub const PARAMETER_NAMES: [&str; 6] = ["param1", "param2", "param3", "param4", "param5", "param6"]; 9 | 10 | pub struct Reader<'a> { 11 | tco: &'a Tco, 12 | } 13 | 14 | impl<'a> Reader<'a> { 15 | pub fn new(tco: &'a Tco) -> Self { 16 | Self { tco } 17 | } 18 | pub fn get_value_by_name(&self, name: &str) -> Option<&'a tco_param::Value> { 19 | self.tco 20 | .params 21 | .iter() 22 | .find(|param| param.name == name) 23 | .and_then(|param| param.value.as_ref()) 24 | } 25 | 26 | pub fn get_reader_by_name(&self, name: &str) -> Option> { 27 | self.get_value_by_name(name) 28 | .map(|value| ValueReader { value }) 29 | } 30 | 31 | pub fn time_indicator(&self) -> Result { 32 | let value = self 33 | .get_reader_by_name("time_indicator") 34 | .ok_or_else(|| anyhow!("no time_indicator"))?; 35 | let integer = value.read_integer()?; 36 | Ok(integer as u32) 37 | } 38 | 39 | pub fn parameters(&self) -> Vec<&'a tco_param::Value> { 40 | let mut values = Vec::with_capacity(PARAMETER_NAMES.len()); 41 | for name in PARAMETER_NAMES.iter() { 42 | let Some(value) = self.get_value_by_name(name) else { 43 | break; 44 | }; 45 | values.push(value); 46 | } 47 | values 48 | } 49 | } 50 | 51 | pub struct ValueReader<'a> { 52 | value: &'a tco_param::Value, 53 | } 54 | 55 | impl<'a> ValueReader<'a> { 56 | pub fn read_integer(&self) -> Result { 57 | if let tco_param::Value::Integer(integer) = self.value { 58 | Ok(*integer) 59 | } else { 60 | Err(anyhow!("unexpected data type")) 61 | } 62 | } 63 | 64 | #[allow(unused)] 65 | pub fn read_double(&self) -> Result { 66 | if let tco_param::Value::Double(double) = self.value { 67 | Ok(*double) 68 | } else { 69 | Err(anyhow!("unexpected data type")) 70 | } 71 | } 72 | 73 | #[allow(unused)] 74 | pub fn read_bytes(&self) -> Result<&[u8]> { 75 | if let tco_param::Value::Bytes(bytes) = self.value { 76 | Ok(bytes) 77 | } else { 78 | Err(anyhow!("unexpected data type")) 79 | } 80 | } 81 | } 82 | 83 | pub struct ParameterListWriter<'a> { 84 | command_schema: &'a CommandSchema, 85 | } 86 | 87 | impl<'a> ParameterListWriter<'a> { 88 | pub fn new(command_schema: &'a CommandSchema) -> Self { 89 | Self { command_schema } 90 | } 91 | } 92 | 93 | impl<'a> ParameterListWriter<'a> { 94 | pub fn write_all

(&self, bytes: &mut [u8], parameters: P) -> Result 95 | where 96 | P: Iterator, 97 | P::Item: Deref, 98 | { 99 | let mut writer = self.command_schema.build_writer(bytes); 100 | for parameter in parameters { 101 | match parameter.deref() { 102 | tco_param::Value::Integer(i) => { 103 | writer.write(IntegralValue::from(*i).into())?; 104 | } 105 | tco_param::Value::Double(d) => { 106 | writer.write(FloatingValue::from(*d).into())?; 107 | } 108 | tco_param::Value::Bytes(b) => { 109 | return writer.write_trailer_and_finish(b); 110 | } 111 | } 112 | } 113 | writer.finish() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds_c2a/tc/space_packet.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use std::mem; 4 | 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, ByteSliceMut, FromBytes, LayoutVerified, Unaligned}; 7 | 8 | pub use crate::ccsds::space_packet::*; 9 | 10 | #[bitfield(bytes = 9)] 11 | #[derive(Debug, Clone, FromBytes, AsBytes, Unaligned)] 12 | #[repr(C)] 13 | pub struct SecondaryHeader { 14 | pub version_number: B8, 15 | pub command_type: B8, 16 | pub command_id: B16, 17 | pub destination_type: B4, 18 | pub execution_type: B4, 19 | pub time_indicator: B32, 20 | } 21 | 22 | impl SecondaryHeader { 23 | pub const SIZE: usize = mem::size_of::(); 24 | } 25 | 26 | impl Default for SecondaryHeader { 27 | fn default() -> Self { 28 | // https://github.com/ut-issl/c2a-core/blob/577e7cd148f8b5284c1b320866875fb076f52561/Docs/Core/communication.md#%E5%90%84%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%81%AE%E8%AA%AC%E6%98%8E-2 29 | SecondaryHeader::new().with_version_number(1) 30 | } 31 | } 32 | 33 | pub struct Builder { 34 | primary_header: LayoutVerified, 35 | secondary_header: LayoutVerified, 36 | user_data: B, 37 | } 38 | 39 | impl Builder 40 | where 41 | B: ByteSliceMut, 42 | { 43 | pub fn new(bytes: B) -> Option { 44 | let (primary_header, tail) = LayoutVerified::new_unaligned_from_prefix(bytes)?; 45 | let (secondary_header, user_data) = LayoutVerified::new_unaligned_from_prefix(tail)?; 46 | Some(Self { 47 | primary_header, 48 | secondary_header, 49 | user_data, 50 | }) 51 | } 52 | 53 | pub fn ph_mut(&mut self) -> &mut PrimaryHeader { 54 | &mut self.primary_header 55 | } 56 | 57 | pub fn sh_mut(&mut self) -> &mut SecondaryHeader { 58 | &mut self.secondary_header 59 | } 60 | 61 | pub fn user_data_mut(&mut self) -> &mut B { 62 | &mut self.user_data 63 | } 64 | 65 | pub fn use_default(&mut self) { 66 | let ph = self.ph_mut(); 67 | ph.set_packet_type(PacketType::Telecommand); 68 | ph.set_secondary_header_flag(true); 69 | ph.set_sequence_flag(SequenceFlag::Unsegmented); 70 | let sh = self.sh_mut(); 71 | sh.set_version_number(1); 72 | } 73 | 74 | pub fn finish(mut self, user_data_len: usize) -> usize { 75 | let packet_data_len = mem::size_of::() + user_data_len; 76 | self.ph_mut() 77 | .set_packet_data_length_in_bytes(packet_data_len); 78 | mem::size_of::() + packet_data_len 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn test_build_secondary_header() { 88 | let mut sh = SecondaryHeader::default(); 89 | sh.set_version_number(1); 90 | sh.set_command_type(0); 91 | sh.set_command_id(0xDEAD); 92 | sh.set_destination_type(1); 93 | sh.set_execution_type(6); 94 | sh.set_time_indicator(0xC001CAFE); 95 | let expected = [1u8, 0, 0xDE, 0xAD, 0b0001_0110, 0xC0, 0x01, 0xCA, 0xFE]; 96 | assert_eq!(sh.as_bytes(), expected); 97 | } 98 | 99 | #[test] 100 | fn test_parse_secondary_header() { 101 | let bytes = [1u8, 0, 0xDE, 0xAD, 0b0001_0110, 0xC0, 0x01, 0xCA, 0xFE]; 102 | let sh = SecondaryHeader::read_from(bytes.as_slice()).unwrap(); 103 | assert_eq!(sh.version_number(), 1); 104 | assert_eq!(sh.command_type(), 0); 105 | assert_eq!(sh.command_id(), 0xDEAD); 106 | assert_eq!(sh.destination_type(), 1); 107 | assert_eq!(sh.execution_type(), 6); 108 | assert_eq!(sh.time_indicator(), 0xC001CAFE); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /gaia-tmtc/src/tco_tmiv/tco.rs: -------------------------------------------------------------------------------- 1 | use gaia_stub::tco_tmiv::{tco_param, Tco, TcoParam}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 5 | pub enum DataType { 6 | INTEGER, 7 | DOUBLE, 8 | BYTES, 9 | } 10 | 11 | #[derive(Debug, Serialize, Deserialize, Clone)] 12 | pub struct Schema { 13 | pub name: String, 14 | pub params: Vec, 15 | } 16 | 17 | impl Schema { 18 | fn validate(&self, normalized_tco: &Tco) -> Result<(), String> { 19 | if self.params.len() != normalized_tco.params.len() { 20 | return Err(format!( 21 | "Mismatched the number of params: expected: {}, actual: {}", 22 | self.params.len(), 23 | normalized_tco.params.len() 24 | )); 25 | } 26 | for (idx, (param_schema, value)) in self 27 | .params 28 | .iter() 29 | .zip(normalized_tco.params.iter()) 30 | .enumerate() 31 | { 32 | param_schema 33 | .validate(value) 34 | .map_err(|e| format!("params[{idx}]: {e}"))?; 35 | } 36 | Ok(()) 37 | } 38 | 39 | fn normalize(&mut self) { 40 | self.params.sort_by(|a, b| a.name.cmp(&b.name)); 41 | } 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 45 | pub struct ParamSchema { 46 | pub name: String, 47 | pub data_type: DataType, 48 | } 49 | 50 | impl ParamSchema { 51 | fn validate_name(&self, name: &str) -> Result<(), String> { 52 | if self.name != *name { 53 | return Err(format!( 54 | "Mismatched param name: expected: {}, actual: {}", 55 | &self.name, name 56 | )); 57 | } 58 | Ok(()) 59 | } 60 | 61 | fn validate_value(&self, value: &tco_param::Value) -> Result<(), String> { 62 | match (&self.data_type, value) { 63 | (DataType::INTEGER, tco_param::Value::Integer(_)) 64 | | (DataType::DOUBLE, tco_param::Value::Double(_)) 65 | | (DataType::BYTES, tco_param::Value::Bytes(_)) => Ok(()), 66 | _ => Err("type mismatched".to_string()), 67 | } 68 | } 69 | 70 | fn validate(&self, TcoParam { name, value }: &TcoParam) -> Result<(), String> { 71 | self.validate_name(name)?; 72 | let value = value.as_ref().ok_or("no value")?; 73 | self.validate_value(value) 74 | .map_err(|e| format!("{name} is {e}"))?; 75 | Ok(()) 76 | } 77 | } 78 | 79 | #[derive(Debug)] 80 | pub struct SchemaSet { 81 | schemata: Vec, 82 | } 83 | 84 | impl SchemaSet { 85 | pub fn new(mut schemata: Vec) -> Self { 86 | schemata.sort_by(|a, b| a.name.cmp(&b.name)); 87 | for schema in &mut schemata { 88 | schema.normalize(); 89 | } 90 | Self::new_unchecked(schemata) 91 | } 92 | 93 | pub fn new_unchecked(schemata: Vec) -> Self { 94 | Self { schemata } 95 | } 96 | 97 | pub fn validate(&self, normalized_tco: &Tco) -> Result<(), String> { 98 | let schema = self 99 | .find_schema_by_name(&normalized_tco.name) 100 | .ok_or_else(|| format!("No matched schema for command {}", &normalized_tco.name))?; 101 | schema.validate(normalized_tco)?; 102 | Ok(()) 103 | } 104 | 105 | pub fn sanitize(&self, tco: &Tco) -> Result { 106 | let mut normalized_tco = tco.clone(); 107 | normalize_tco(&mut normalized_tco); 108 | self.validate(&normalized_tco)?; 109 | Ok(normalized_tco) 110 | } 111 | 112 | fn find_schema_by_name(&self, param_name: &str) -> Option<&Schema> { 113 | self.schemata 114 | .iter() 115 | .find(|schema| param_name == schema.name) 116 | } 117 | } 118 | 119 | fn normalize_tco(tco: &mut Tco) { 120 | tco.params.sort_by(|a, b| a.name.cmp(&b.name)) 121 | } 122 | -------------------------------------------------------------------------------- /gaia-tmtc/src/broker.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use futures::prelude::*; 5 | use gaia_stub::tco_tmiv::Tco; 6 | use tokio::sync::Mutex; 7 | use tokio_stream::wrappers::BroadcastStream; 8 | use tonic::{Request, Response, Status, Streaming}; 9 | 10 | use super::telemetry::{self, LastTmivStore}; 11 | 12 | pub use gaia_stub::broker::*; 13 | 14 | pub struct BrokerService { 15 | cmd_handler: Mutex, 16 | tlm_bus: telemetry::Bus, 17 | last_tmiv_store: Arc, 18 | } 19 | 20 | impl BrokerService { 21 | pub fn new( 22 | cmd_service: C, 23 | tlm_bus: telemetry::Bus, 24 | last_tmiv_store: Arc, 25 | ) -> Self { 26 | Self { 27 | cmd_handler: Mutex::new(cmd_service), 28 | tlm_bus, 29 | last_tmiv_store, 30 | } 31 | } 32 | } 33 | 34 | #[tonic::async_trait] 35 | impl broker_server::Broker for BrokerService 36 | where 37 | C: super::Handle> + Send + Sync + 'static, 38 | C::Response: Send + 'static, 39 | { 40 | type OpenCommandStreamStream = 41 | stream::BoxStream<'static, Result>; 42 | type OpenTelemetryStreamStream = 43 | stream::BoxStream<'static, Result>; 44 | 45 | #[tracing::instrument(skip(self))] 46 | async fn post_command( 47 | &self, 48 | request: Request, 49 | ) -> Result, tonic::Status> { 50 | let message = request.into_inner(); 51 | 52 | let tco = message 53 | .tco 54 | .ok_or_else(|| Status::invalid_argument("tco is required"))?; 55 | 56 | fn internal_error(e: E) -> Status { 57 | Status::internal(format!("{:?}", e)) 58 | } 59 | self.cmd_handler 60 | .lock() 61 | .await 62 | .handle(Arc::new(tco)) 63 | .await 64 | .map_err(internal_error)?; 65 | 66 | Ok(Response::new(PostCommandResponse {})) 67 | } 68 | 69 | #[tracing::instrument(skip(self))] 70 | async fn open_telemetry_stream( 71 | &self, 72 | _request: tonic::Request, 73 | ) -> Result, tonic::Status> { 74 | let rx = self.tlm_bus.subscribe(); 75 | let stream = BroadcastStream::new(rx) 76 | .map_ok(move |tmiv| TelemetryStreamResponse { 77 | tmiv: Some(tmiv.as_ref().clone()), 78 | }) 79 | .map_err(|_| Status::data_loss("stream was lagged")); 80 | Ok(Response::new(Box::pin(stream))) 81 | } 82 | 83 | #[tracing::instrument(skip(self))] 84 | async fn open_command_stream( 85 | &self, 86 | _request: Request>, 87 | ) -> Result, tonic::Status> { 88 | Err(tonic::Status::unimplemented("needless")) 89 | } 90 | 91 | #[tracing::instrument(skip(self))] 92 | async fn post_telemetry( 93 | &self, 94 | _request: tonic::Request, 95 | ) -> Result, tonic::Status> { 96 | Err(tonic::Status::unimplemented("needless")) 97 | } 98 | 99 | #[tracing::instrument(skip(self))] 100 | async fn get_last_received_telemetry( 101 | &self, 102 | request: Request, 103 | ) -> Result, Status> { 104 | let message = request.get_ref(); 105 | let tmiv = self 106 | .last_tmiv_store 107 | .get(&message.telemetry_name) 108 | .await 109 | .map_err(|_| Status::invalid_argument("invalid telemetry name"))?; 110 | if let Some(tmiv) = tmiv { 111 | Ok(Response::new(GetLastReceivedTelemetryResponse { 112 | tmiv: Some(tmiv.as_ref().clone()), 113 | })) 114 | } else { 115 | Err(Status::not_found("not received yet")) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /gaia-tmtc/src/telemetry.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | sync::Arc, 4 | }; 5 | 6 | use anyhow::{anyhow, ensure, Result}; 7 | use async_trait::async_trait; 8 | use gaia_stub::tco_tmiv::Tmiv; 9 | use tokio::sync::{broadcast, RwLock}; 10 | 11 | use super::{Handle, Hook}; 12 | use crate::tco_tmiv::tmiv; 13 | 14 | #[derive(Clone)] 15 | pub struct Bus { 16 | tmiv_tx: broadcast::Sender>, 17 | } 18 | 19 | impl Bus { 20 | pub fn new(capacity: usize) -> Self { 21 | let (tmiv_tx, _) = broadcast::channel(capacity); 22 | Self { tmiv_tx } 23 | } 24 | 25 | pub fn subscribe(&self) -> broadcast::Receiver> { 26 | self.tmiv_tx.subscribe() 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl Handle> for Bus { 32 | type Response = (); 33 | 34 | async fn handle(&mut self, tmiv: Arc) -> Result { 35 | // it's ok if there are no receivers 36 | // so just fire and forget 37 | self.tmiv_tx.send(tmiv).ok(); 38 | Ok(()) 39 | } 40 | } 41 | 42 | #[derive(Clone)] 43 | pub struct SanitizeHook { 44 | schema_set: Arc, 45 | } 46 | 47 | impl SanitizeHook { 48 | pub fn new(schema_set: impl Into>) -> Self { 49 | Self { 50 | schema_set: schema_set.into(), 51 | } 52 | } 53 | 54 | fn sanitize(&self, input: &Tmiv) -> Result { 55 | let sanitized = self 56 | .schema_set 57 | .sanitize(input) 58 | .map_err(|msg| anyhow!("TMIV validation error: {}", msg))?; 59 | Ok(sanitized) 60 | } 61 | } 62 | 63 | #[async_trait] 64 | impl Hook> for SanitizeHook { 65 | type Output = Arc; 66 | 67 | async fn hook(&mut self, tmiv: Arc) -> Result { 68 | let sanitized = self.sanitize(&tmiv)?; 69 | Ok(Arc::new(sanitized)) 70 | } 71 | } 72 | 73 | pub trait CheckTmivName { 74 | fn check_tmiv_name(&self, tmiv_name: &str) -> bool; 75 | } 76 | 77 | impl CheckTmivName for tmiv::SchemaSet { 78 | fn check_tmiv_name(&self, tmiv_name: &str) -> bool { 79 | self.find_schema_by_name(tmiv_name).is_some() 80 | } 81 | } 82 | 83 | impl CheckTmivName for HashSet { 84 | fn check_tmiv_name(&self, tmiv_name: &str) -> bool { 85 | self.contains(tmiv_name) 86 | } 87 | } 88 | 89 | pub struct LastTmivStore { 90 | check_tmiv_name: Box, 91 | map: RwLock>>, 92 | } 93 | 94 | impl LastTmivStore { 95 | pub fn new(check_tmiv_name: impl CheckTmivName + Send + Sync + 'static) -> Self { 96 | let check_tmiv_name = Box::new(check_tmiv_name); 97 | Self { 98 | check_tmiv_name, 99 | map: RwLock::new(HashMap::new()), 100 | } 101 | } 102 | 103 | fn is_valid(&self, telemetry_name: &str) -> bool { 104 | self.check_tmiv_name.check_tmiv_name(telemetry_name) 105 | } 106 | 107 | pub async fn set(&self, tmiv: Arc) { 108 | let mut map = self.map.write().await; 109 | map.insert(tmiv.name.clone(), tmiv); 110 | } 111 | 112 | pub async fn get(&self, telemetry_name: &str) -> Result>> { 113 | ensure!( 114 | self.is_valid(telemetry_name), 115 | "no such telemetry definition: {}", 116 | telemetry_name 117 | ); 118 | let map = self.map.read().await; 119 | if let Some(tmiv) = map.get(telemetry_name) { 120 | Ok(Some(tmiv.clone())) 121 | } else { 122 | Ok(None) 123 | } 124 | } 125 | } 126 | 127 | #[derive(Clone)] 128 | pub struct StoreLastTmivHook { 129 | store: Arc, 130 | } 131 | 132 | impl StoreLastTmivHook { 133 | pub fn new(store: Arc) -> Self { 134 | Self { store } 135 | } 136 | } 137 | 138 | #[async_trait] 139 | impl Hook> for StoreLastTmivHook { 140 | type Output = Arc; 141 | 142 | async fn hook(&mut self, tmiv: Arc) -> Result { 143 | self.store.set(tmiv.clone()).await; 144 | Ok(tmiv) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: ['v*'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | target: 16 | - x86_64-unknown-linux-musl 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | # for devtools-frontend 22 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 23 | with: 24 | node-version: 21 25 | 26 | - name: install apt depenedencies 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get install -y musl-tools 30 | 31 | - name: Install Protoc 32 | uses: arduino/setup-protoc@v1 33 | with: 34 | version: '3.x' 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Get Rust toolchain 38 | id: toolchain 39 | working-directory: . 40 | run: | 41 | awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" 42 | 43 | - uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 # v1 44 | with: 45 | toolchain: ${{ steps.toolchain.outputs.toolchain }} 46 | targets: ${{ matrix.target }} 47 | 48 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 49 | 50 | - name: install cargo-about 51 | run: | 52 | cargo install --locked cargo-about@0.6.4 53 | 54 | - name: Install wasm-pack 55 | run: | 56 | cargo install --locked wasm-pack 57 | 58 | - name: Build binaries 59 | run: | 60 | cargo build --target=${{ matrix.target }} --release --locked 61 | 62 | - name: Rename binaries 63 | run: | 64 | mkdir bin 65 | gaia_bins=("tmtc-c2a") 66 | for b in "${gaia_bins[@]}" ; do 67 | cp "./target/${{ matrix.target }}/release/${b}" "./bin/${b}-${{ matrix.target }}" 68 | done 69 | ls -lh ./bin 70 | 71 | - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 72 | with: 73 | name: release-executable-${{ matrix.target }} 74 | if-no-files-found: error 75 | path: ./bin/ 76 | 77 | package: 78 | runs-on: ubuntu-22.04 79 | 80 | steps: 81 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 82 | 83 | - name: install apt depenedencies 84 | run: | 85 | sudo apt-get update 86 | sudo apt-get install -y musl-tools 87 | 88 | - name: Install Protoc 89 | uses: arduino/setup-protoc@v1 90 | with: 91 | version: '3.x' 92 | repo-token: ${{ secrets.GITHUB_TOKEN }} 93 | 94 | - name: Get Rust toolchain 95 | id: toolchain 96 | working-directory: . 97 | run: | 98 | awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" 99 | 100 | - uses: dtolnay/rust-toolchain@888c2e1ea69ab0d4330cbf0af1ecc7b68f368cc1 # v1 101 | with: 102 | toolchain: ${{ steps.toolchain.outputs.toolchain }} 103 | 104 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 105 | 106 | - name: install cargo-about 107 | run: | 108 | cargo install --locked cargo-about@0.6.4 109 | 110 | - name: Install wasm-pack 111 | run: | 112 | cargo install --locked wasm-pack 113 | 114 | - name: package 115 | run: | 116 | cargo package 117 | 118 | # TODO: cargo publish 119 | 120 | release: 121 | name: Release 122 | needs: [ build, package ] 123 | permissions: 124 | contents: write 125 | 126 | runs-on: ubuntu-22.04 127 | 128 | steps: 129 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 130 | with: 131 | pattern: release-executable-* 132 | merge-multiple: true 133 | 134 | - run: | 135 | chmod +x tmtc-c2a* 136 | 137 | - run: ls -lh 138 | 139 | - name: Release to GitHub Release 140 | if: startsWith(github.ref, 'refs/tags/') 141 | uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 142 | with: 143 | draft: true 144 | fail_on_unmatched_files: true 145 | generate_release_notes: true 146 | files: | 147 | tmtc-c2a* 148 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/space_packet.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | 3 | use std::mem; 4 | 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, ByteSlice, FromBytes, LayoutVerified, Unaligned}; 7 | 8 | #[bitfield(bytes = 6)] 9 | #[derive(Debug, FromBytes, AsBytes, Unaligned, Default)] 10 | #[repr(C)] 11 | pub struct PrimaryHeader { 12 | pub version_number: B3, 13 | pub packet_type: PacketType, 14 | pub secondary_header_flag: bool, 15 | pub apid: B11, 16 | pub sequence_flag: SequenceFlag, 17 | pub sequence_count: B14, 18 | pub packet_data_length_raw: B16, 19 | } 20 | 21 | impl PrimaryHeader { 22 | pub const SIZE: usize = mem::size_of::(); 23 | 24 | pub fn packet_data_length_in_bytes(&self) -> usize { 25 | self.packet_data_length_raw() as usize + 1 26 | } 27 | 28 | pub fn set_packet_data_length_in_bytes(&mut self, packet_data_length_in_bytes: usize) { 29 | assert!(packet_data_length_in_bytes > 0); 30 | self.set_packet_data_length_raw(packet_data_length_in_bytes as u16 - 1); 31 | } 32 | 33 | pub fn is_idle_packet(&self) -> bool { 34 | // > 4.1.3.3.4.4 For Idle Packets the APID shall be ‘11111111111’, 35 | // > that is, ‘all ones’(see reference [4]). 36 | // ref: https://public.ccsds.org/Pubs/133x0b2e1.pdf 37 | const ALL_ONES_11BIT: u16 = 0b11111111111; 38 | self.apid() == ALL_ONES_11BIT 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct SpacePacket { 44 | pub primary_header: LayoutVerified, 45 | pub packet_data: B, 46 | } 47 | 48 | impl SpacePacket 49 | where 50 | B: ByteSlice, 51 | { 52 | pub fn new(bytes: B) -> Option<(SpacePacket, B)> { 53 | let (primary_header, tail) = 54 | LayoutVerified::<_, PrimaryHeader>::new_unaligned_from_prefix(bytes)?; 55 | let pd_size = primary_header.packet_data_length_in_bytes(); 56 | if tail.len() < pd_size { 57 | return None; 58 | } 59 | let (packet_data, trailer) = tail.split_at(pd_size); 60 | let space_packet = SpacePacket { 61 | primary_header, 62 | packet_data, 63 | }; 64 | debug_assert!(space_packet.packet_size().is_some()); 65 | Some((space_packet, trailer)) 66 | } 67 | 68 | /// returns None if the packet data length field in PH 69 | /// is not matched with the actual length of packet_data 70 | pub fn packet_size(&self) -> Option { 71 | let len_in_ph = self.primary_header.packet_data_length_in_bytes(); 72 | if self.packet_data.len() == len_in_ph { 73 | Some(PrimaryHeader::SIZE + len_in_ph) 74 | } else { 75 | None 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitfieldSpecifier)] 81 | #[bits = 1] 82 | pub enum PacketType { 83 | Telemetry = 0, 84 | Telecommand = 1, 85 | } 86 | 87 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitfieldSpecifier)] 88 | #[bits = 2] 89 | pub enum SequenceFlag { 90 | Continuation = 0b00, 91 | First = 0b01, 92 | Last = 0b10, 93 | Unsegmented = 0b11, 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_build_primary_header() { 102 | let mut ph = PrimaryHeader::default(); 103 | ph.set_version_number(6); 104 | ph.set_packet_type(PacketType::Telecommand); 105 | ph.set_secondary_header_flag(true); 106 | ph.set_apid(2000); 107 | ph.set_sequence_flag(SequenceFlag::First); 108 | ph.set_sequence_count(16000); 109 | ph.set_packet_data_length_in_bytes(0xABCD); 110 | let actual = ph.as_bytes(); 111 | let expected = [ 112 | 0b1101_1111, 113 | 0b1101_0000, 114 | 0b0111_1110, 115 | 0b1000_0000, 116 | 0xAB, 117 | 0xCC, 118 | ]; 119 | assert_eq!(actual, expected); 120 | } 121 | 122 | #[test] 123 | fn test_parse_primary_header() { 124 | let bytes = [ 125 | 0b1101_1111, 126 | 0b1101_0000, 127 | 0b0111_1110, 128 | 0b1000_0000, 129 | 0xAB, 130 | 0xCC, 131 | ]; 132 | let ph = PrimaryHeader::read_from(bytes.as_slice()).unwrap(); 133 | assert_eq!(ph.version_number(), 6); 134 | assert_eq!(ph.packet_type(), PacketType::Telecommand); 135 | assert!(ph.secondary_header_flag()); 136 | assert_eq!(ph.apid(), 2000); 137 | assert_eq!(ph.sequence_flag(), SequenceFlag::First); 138 | assert_eq!(ph.sequence_count(), 16000); 139 | assert_eq!(ph.packet_data_length_in_bytes(), 0xABCD); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /devtools-frontend/src/worker.ts: -------------------------------------------------------------------------------- 1 | import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; 2 | import { 3 | PostCommandRequest, 4 | PostCommandResponse, 5 | TelemetryStreamResponse, 6 | } from "./proto/broker"; 7 | import { BrokerClient } from "./proto/broker.client"; 8 | import { TmtcGenericC2aClient } from "./proto/tmtc_generic_c2a.client"; 9 | import { GetSateliteSchemaResponse } from "./proto/tmtc_generic_c2a"; 10 | import { Tmiv } from "./proto/tco_tmiv"; 11 | 12 | export default null; 13 | // eslint-disable-next-line no-var 14 | declare var self: SharedWorkerGlobalScope; 15 | 16 | export type GrpcClientService = { 17 | getSatelliteSchema(): Promise; 18 | postCommand(input: PostCommandRequest): Promise; 19 | openTelemetryStream(tmivName: string): Promise>; 20 | lastTelemetryValue(tmivName: string): Promise; 21 | }; 22 | 23 | export type WorkerRpcService = { 24 | [proc: string]: (...args: any) => Promise; 25 | }; 26 | 27 | type Values = T[keyof T]; 28 | 29 | export type WorkerRequest = Values<{ 30 | [Proc in keyof S]: { 31 | callback: MessagePort; 32 | proc: Proc; 33 | args: Parameters; 34 | }; 35 | }>; 36 | export type WorkerResponse = { 37 | [Proc in keyof S]: 38 | | { 39 | value: Awaited>; 40 | } 41 | | { 42 | error: string; 43 | }; 44 | }; 45 | 46 | const transport = new GrpcWebFetchTransport({ 47 | baseUrl: self.name, 48 | }); 49 | const brokerClient = new BrokerClient(transport); 50 | const tmtcGenericC2a = new TmtcGenericC2aClient(transport); 51 | 52 | const telemetryLastValues = new Map(); 53 | const telemetryBus = new EventTarget(); 54 | 55 | const startTelemetryStream = async () => { 56 | const { responses } = brokerClient.openTelemetryStream({}); 57 | for await (const { tmiv } of responses) { 58 | if (typeof tmiv === "undefined") { 59 | continue; 60 | } 61 | telemetryLastValues.set(tmiv.name, tmiv); 62 | telemetryBus.dispatchEvent(new CustomEvent(tmiv.name, { detail: tmiv })); 63 | } 64 | }; 65 | 66 | const server = { 67 | async getSatelliteSchema(): Promise { 68 | const { response } = await tmtcGenericC2a.getSatelliteSchema({}); 69 | return response; 70 | }, 71 | async postCommand(input: PostCommandRequest): Promise { 72 | const { response } = brokerClient.postCommand(input); 73 | return response; 74 | }, 75 | async openTelemetryStream(tmivName: string): Promise> { 76 | let handler: any; 77 | return new ReadableStream({ 78 | start(controller) { 79 | handler = (e: CustomEvent) => { 80 | controller.enqueue(e.detail); 81 | }; 82 | telemetryBus.addEventListener(tmivName, handler as any); 83 | const lastValue = telemetryLastValues.get(tmivName); 84 | if (typeof lastValue !== "undefined") { 85 | controller.enqueue(lastValue); 86 | } 87 | }, 88 | cancel() { 89 | telemetryBus.removeEventListener(tmivName, handler as any); 90 | }, 91 | }); 92 | }, 93 | async lastTelemetryValue(tmivName: string): Promise { 94 | return telemetryLastValues.get(tmivName); 95 | }, 96 | }; 97 | 98 | self.addEventListener("connect", (e) => { 99 | for (const port of e.ports) { 100 | port.addEventListener( 101 | "message", 102 | (e: MessageEvent>) => { 103 | // eslint-disable-next-line prefer-spread 104 | const promise = (server[e.data.proc] as any).apply( 105 | server, 106 | e.data.args, 107 | ) as Promise; 108 | const resolve = (value: any) => { 109 | if (value instanceof ReadableStream) { 110 | e.data.callback.postMessage( 111 | { 112 | value, 113 | }, 114 | [value], 115 | ); 116 | } else { 117 | e.data.callback.postMessage({ 118 | value, 119 | }); 120 | } 121 | }; 122 | const reject = (error: any) => { 123 | e.data.callback.postMessage({ 124 | error, 125 | }); 126 | }; 127 | promise.then(resolve, reject); 128 | }, 129 | ); 130 | port.start(); 131 | } 132 | }); 133 | (async () => { 134 | // eslint-disable-next-line no-constant-condition 135 | while (true) { 136 | try { 137 | await startTelemetryStream(); 138 | } catch (e) { 139 | console.error(e); 140 | } 141 | await new Promise((resolve) => setTimeout(resolve, 1000)); 142 | } 143 | })(); 144 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/tc/cltu.rs: -------------------------------------------------------------------------------- 1 | const RANDOMIZATION_TABLE: [u8; 255] = [ 2 | 0xFF, 0x39, 0x9E, 0x5A, 0x68, 0xE9, 0x06, 0xF5, 0x6C, 0x89, 0x2F, 0xA1, 0x31, 0x5E, 0x08, 0xC0, 3 | 0x52, 0xA8, 0xBB, 0xAE, 0x4E, 0xC2, 0xC7, 0xED, 0x66, 0xDC, 0x38, 0xD4, 0xF8, 0x86, 0x50, 0x3D, 4 | 0xFE, 0x73, 0x3C, 0xB4, 0xD1, 0xD2, 0x0D, 0xEA, 0xD9, 0x12, 0x5F, 0x42, 0x62, 0xBC, 0x11, 0x80, 5 | 0xA5, 0x51, 0x77, 0x5C, 0x9D, 0x85, 0x8F, 0xDA, 0xCD, 0xB8, 0x71, 0xA9, 0xF1, 0x0C, 0xA0, 0x7B, 6 | 0xFC, 0xE6, 0x79, 0x69, 0xA3, 0xA4, 0x1B, 0xD5, 0xB2, 0x24, 0xBE, 0x84, 0xC5, 0x78, 0x23, 0x01, 7 | 0x4A, 0xA2, 0xEE, 0xB9, 0x3B, 0x0B, 0x1F, 0xB5, 0x9B, 0x70, 0xE3, 0x53, 0xE2, 0x19, 0x40, 0xF7, 8 | 0xF9, 0xCC, 0xF2, 0xD3, 0x47, 0x48, 0x37, 0xAB, 0x64, 0x49, 0x7D, 0x09, 0x8A, 0xF0, 0x46, 0x02, 9 | 0x95, 0x45, 0xDD, 0x72, 0x76, 0x16, 0x3F, 0x6B, 0x36, 0xE1, 0xC6, 0xA7, 0xC4, 0x32, 0x81, 0xEF, 10 | 0xF3, 0x99, 0xE5, 0xA6, 0x8E, 0x90, 0x6F, 0x56, 0xC8, 0x92, 0xFA, 0x13, 0x15, 0xE0, 0x8C, 0x05, 11 | 0x2A, 0x8B, 0xBA, 0xE4, 0xEC, 0x2C, 0x7E, 0xD6, 0x6D, 0xC3, 0x8D, 0x4F, 0x88, 0x65, 0x03, 0xDF, 12 | 0xE7, 0x33, 0xCB, 0x4D, 0x1D, 0x20, 0xDE, 0xAD, 0x91, 0x25, 0xF4, 0x26, 0x2B, 0xC1, 0x18, 0x0A, 13 | 0x55, 0x17, 0x75, 0xC9, 0xD8, 0x58, 0xFD, 0xAC, 0xDB, 0x87, 0x1A, 0x9F, 0x10, 0xCA, 0x07, 0xBF, 14 | 0xCE, 0x67, 0x96, 0x9A, 0x3A, 0x41, 0xBD, 0x5B, 0x22, 0x4B, 0xE8, 0x4C, 0x57, 0x82, 0x30, 0x14, 15 | 0xAA, 0x2E, 0xEB, 0x93, 0xB0, 0xB1, 0xFB, 0x59, 0xB7, 0x0E, 0x35, 0x3E, 0x21, 0x94, 0x0F, 0x7F, 16 | 0x9C, 0xCF, 0x2D, 0x34, 0x74, 0x83, 0x7A, 0xB6, 0x44, 0x97, 0xD0, 0x98, 0xAF, 0x04, 0x60, 0x29, 17 | 0x54, 0x5D, 0xD7, 0x27, 0x61, 0x63, 0xF6, 0xB3, 0x6E, 0x1C, 0x6A, 0x7C, 0x43, 0x28, 0x1E, 18 | ]; 19 | 20 | pub fn bch_code(info_bytes: &[u8]) -> u8 { 21 | assert_eq!(info_bytes.len(), 7); 22 | let info_bits = info_bytes 23 | .iter() 24 | .flat_map(|octet| (0usize..8).map(move |shift| octet << shift & 0x80)); 25 | let ecc = info_bits.fold(0u8, |ecc, infobit| { 26 | let msb = ecc & 0x80; 27 | let maskbit = msb ^ infobit; 28 | let mask = maskbit | (maskbit >> 4) | (maskbit >> 6); 29 | (ecc << 1) ^ mask 30 | }); 31 | !ecc & !0b1 32 | } 33 | 34 | pub fn encode(tc_tf: &mut [u8]) -> Vec { 35 | let tf_len = tc_tf.len(); 36 | let randomized = tc_tf; 37 | randomize(randomized); 38 | let info_blocks = (tf_len + 1) / 7; 39 | let codewords_size = info_blocks * 8; 40 | let cltu_size = codewords_size + 2 + 8; 41 | let mut cltu = Vec::with_capacity(cltu_size); 42 | cltu.extend([0xEB, 0x90]); // START SEQUENCE 43 | for info_bytes in randomized.chunks(7) { 44 | if info_bytes.len() < 7 { 45 | let mut filled_info_bytes = [0x55; 7]; 46 | filled_info_bytes[..info_bytes.len()].copy_from_slice(info_bytes); 47 | let bch = bch_code(&filled_info_bytes); 48 | cltu.extend_from_slice(&filled_info_bytes); 49 | cltu.push(bch); 50 | } else { 51 | let bch = bch_code(info_bytes); 52 | cltu.extend_from_slice(info_bytes); 53 | cltu.push(bch); 54 | } 55 | } 56 | cltu.extend([0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0x79]); // TAIL SEQUENCE 57 | cltu 58 | } 59 | 60 | pub fn randomize(tc_tf: &mut [u8]) { 61 | for (byte, mask) in tc_tf.iter_mut().zip(RANDOMIZATION_TABLE.iter().cycle()) { 62 | *byte ^= mask; 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn test_bch() { 72 | let bch = bch_code(&[0xDCu8, 0x65, 0x9E, 0x4C, 0x68, 0x2B, 0x1C]); 73 | assert_eq!(bch, 0xCC); 74 | } 75 | 76 | #[test] 77 | fn test_randomize() { 78 | let mut bytes = vec![ 79 | 0x23, 0x5C, 0x00, 0x16, 0x00, 0xC2, 0x1A, 0x10, 0xC0, 0x00, 0x00, 0x08, 0x01, 0x02, 80 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x66, 81 | ]; 82 | randomize(&mut bytes); 83 | assert_eq!( 84 | bytes, 85 | &[ 86 | 0xDC, 0x65, 0x9E, 0x4C, 0x68, 0x2B, 0x1C, 0xE5, 0xAC, 0x89, 0x2F, 0xA9, 0x30, 0x5C, 87 | 0x08, 0xC0, 0x52, 0xA8, 0xBB, 0xAE, 0x4E, 0x3E, 0xA1 88 | ] 89 | ); 90 | } 91 | 92 | #[test] 93 | fn test_cltu() { 94 | let cltu = encode(&mut [ 95 | 0x23, 0x5C, 0x00, 0x16, 0x00, 0xC2, 0x1A, 0x10, 0xC0, 0x00, 0x00, 0x08, 0x01, 0x02, 96 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x66, 97 | ]); 98 | assert_eq!( 99 | cltu, 100 | &[ 101 | 0xEB, 0x90, 0xDC, 0x65, 0x9E, 0x4C, 0x68, 0x2B, 0x1C, 0xCC, 0xE5, 0xAC, 0x89, 0x2F, 102 | 0xA9, 0x30, 0x5C, 0x72, 0x08, 0xC0, 0x52, 0xA8, 0xBB, 0xAE, 0x4E, 0x2A, 0x3E, 0xA1, 103 | 0x55, 0x55, 0x55, 0x55, 0x55, 0x6E, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0x79 104 | ] 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /gaia-tmtc/src/tco_tmiv/tmiv.rs: -------------------------------------------------------------------------------- 1 | use gaia_stub::tco_tmiv::{tmiv_field, Tmiv, TmivField}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub use gaia_stub::tco_tmiv::tmiv::*; 5 | 6 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 7 | pub struct Variant { 8 | pub name: String, 9 | } 10 | 11 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 12 | pub enum DataType { 13 | INTEGER, 14 | DOUBLE, 15 | STRING, 16 | ENUM, 17 | #[serde(alias = "BASE64")] 18 | BYTES, 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 22 | pub struct Schema { 23 | pub name: String, 24 | pub fields: Vec, 25 | } 26 | 27 | impl Schema { 28 | fn validate(&self, normalized_tmiv: &Tmiv) -> Result<(), String> { 29 | if self.fields.len() != normalized_tmiv.fields.len() { 30 | return Err(format!( 31 | "Mismatched the number of fields: expected: {}, actual: {}", 32 | self.fields.len(), 33 | normalized_tmiv.fields.len() 34 | )); 35 | } 36 | for (idx, (field_schema, value)) in self 37 | .fields 38 | .iter() 39 | .zip(normalized_tmiv.fields.iter()) 40 | .enumerate() 41 | { 42 | field_schema 43 | .validate(value) 44 | .map_err(|e| format!("params[{idx}]: {e}"))?; 45 | } 46 | Ok(()) 47 | } 48 | 49 | fn normalize(&mut self) { 50 | self.fields.sort_by(|a, b| a.name.cmp(&b.name)); 51 | for field in &mut self.fields { 52 | field.normalize(); 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 58 | pub struct FieldSchema { 59 | pub name: String, 60 | pub data_type: DataType, 61 | pub variants: Vec, 62 | } 63 | 64 | impl FieldSchema { 65 | fn validate(&self, TmivField { name, value }: &TmivField) -> Result<(), String> { 66 | self.validate_name(name)?; 67 | let value = value.as_ref().ok_or("no value")?; 68 | self.validate_value(value) 69 | .map_err(|e| format!("{name} is {e}"))?; 70 | Ok(()) 71 | } 72 | 73 | fn validate_name(&self, name: &str) -> Result<(), String> { 74 | if self.name != *name { 75 | return Err(format!( 76 | "Mismatched param name: expected: {}, actual: {}", 77 | &self.name, name 78 | )); 79 | } 80 | Ok(()) 81 | } 82 | 83 | fn validate_value(&self, value: &tmiv_field::Value) -> Result<(), String> { 84 | match (&self.data_type, value) { 85 | (DataType::INTEGER, tmiv_field::Value::Integer(_)) 86 | | (DataType::DOUBLE, tmiv_field::Value::Double(_)) 87 | | (DataType::STRING, tmiv_field::Value::String(_)) 88 | | (DataType::BYTES, tmiv_field::Value::Bytes(_)) => Ok(()), 89 | (DataType::ENUM, tmiv_field::Value::Enum(variant)) => { 90 | self.validate_enum(variant)?; 91 | Ok(()) 92 | } 93 | _ => Err("mismatched type".to_string()), 94 | } 95 | } 96 | 97 | fn validate_enum(&self, value: &str) -> Result<(), String> { 98 | self.variants 99 | .iter() 100 | .any(|v| v.name == value) 101 | .then_some(()) 102 | .ok_or_else(|| format!("not a valid variant: {}", value)) 103 | } 104 | 105 | fn normalize(&mut self) { 106 | self.variants.sort_by(|a, b| a.name.cmp(&b.name)); 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone)] 111 | pub struct SchemaSet { 112 | schemata: Vec, 113 | } 114 | 115 | impl SchemaSet { 116 | pub fn new(mut schemata: Vec) -> Self { 117 | schemata.sort_by(|a, b| a.name.cmp(&b.name)); 118 | for schema in &mut schemata { 119 | schema.normalize(); 120 | } 121 | Self::new_unchecked(schemata) 122 | } 123 | 124 | pub fn new_unchecked(schemata: Vec) -> Self { 125 | Self { schemata } 126 | } 127 | 128 | pub fn sanitize(&self, tmiv: &Tmiv) -> Result { 129 | let mut normalized_tmiv = tmiv.clone(); 130 | normalize_tmiv(&mut normalized_tmiv); 131 | self.validate(&normalized_tmiv)?; 132 | Ok(normalized_tmiv) 133 | } 134 | 135 | pub fn validate(&self, normalized_tmiv: &Tmiv) -> Result<(), String> { 136 | let schema = self 137 | .find_schema_by_name(&normalized_tmiv.name) 138 | .ok_or_else(|| format!("No matched schema for telemetry {}", &normalized_tmiv.name))?; 139 | schema.validate(normalized_tmiv)?; 140 | Ok(()) 141 | } 142 | 143 | pub fn find_schema_by_name(&self, field_name: &str) -> Option<&Schema> { 144 | self.schemata 145 | .iter() 146 | .find(|schema| field_name == schema.name) 147 | } 148 | } 149 | 150 | fn normalize_tmiv(tmiv: &mut Tmiv) { 151 | tmiv.fields.sort_by(|a, b| a.name.cmp(&b.name)) 152 | } 153 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/access/cmd/schema.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{ensure, Result}; 2 | use funty::{Floating, Integral}; 3 | use structpack::{ 4 | FloatingField, GenericFloatingField, GenericIntegralField, IntegralField, NumericField, 5 | SizedField, 6 | }; 7 | use tlmcmddb::{cmd as cmddb, Component}; 8 | 9 | use super::Writer; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Metadata { 13 | pub component_name: String, 14 | pub command_name: String, 15 | pub cmd_id: u16, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct CommandSchema { 20 | pub sized_parameters: Vec, 21 | pub static_size: usize, 22 | pub has_trailer_parameter: bool, 23 | } 24 | 25 | impl CommandSchema { 26 | pub fn build_writer<'b>( 27 | &'b self, 28 | bytes: &'b mut [u8], 29 | ) -> Writer<'b, std::slice::Iter<'b, NumericField>> { 30 | Writer::new( 31 | self.sized_parameters.iter(), 32 | self.static_size, 33 | self.has_trailer_parameter, 34 | bytes, 35 | ) 36 | } 37 | } 38 | 39 | pub fn from_tlmcmddb(db: &tlmcmddb::Database) -> ComponentIter { 40 | ComponentIter { 41 | iter: db.components.iter(), 42 | } 43 | } 44 | 45 | pub struct ComponentIter<'a> { 46 | iter: std::slice::Iter<'a, Component>, 47 | } 48 | 49 | impl<'a> Iterator for ComponentIter<'a> { 50 | type Item = Iter<'a>; 51 | 52 | fn next(&mut self) -> Option { 53 | let component = self.iter.next()?; 54 | Some(Iter { 55 | name: &component.name, 56 | entries: component.cmd.entries.iter(), 57 | }) 58 | } 59 | } 60 | 61 | pub struct Iter<'a> { 62 | name: &'a str, 63 | entries: std::slice::Iter<'a, cmddb::Entry>, 64 | } 65 | 66 | impl<'a> Iterator for Iter<'a> { 67 | type Item = Result<(Metadata, CommandSchema)>; 68 | 69 | fn next(&mut self) -> Option { 70 | #[allow(clippy::never_loop)] 71 | loop { 72 | let cmddb::Entry::Command(command) = self.entries.next()? else { 73 | continue; 74 | }; 75 | let metadata = Metadata { 76 | component_name: self.name.to_string(), 77 | command_name: command.name.to_string(), 78 | cmd_id: command.code, 79 | }; 80 | return build_schema(command) 81 | .map(|schema| Some((metadata, schema))) 82 | .transpose(); 83 | } 84 | } 85 | } 86 | 87 | fn build_schema(db: &cmddb::Command) -> Result { 88 | let mut params_iter = db.parameters.iter(); 89 | let mut static_size_bits = 0; 90 | let mut sized_parameters = vec![]; 91 | let mut has_trailer_parameter = false; 92 | for parameter in params_iter.by_ref() { 93 | if let Some(field) = build_numeric_field(static_size_bits, parameter) { 94 | static_size_bits += field.bit_len(); 95 | sized_parameters.push(field); 96 | } else { 97 | // raw parameter is present 98 | has_trailer_parameter = true; 99 | break; 100 | } 101 | } 102 | ensure!( 103 | params_iter.next().is_none(), 104 | "trailer(RAW) parameter is valid only if at the last position" 105 | ); 106 | let static_size = if static_size_bits == 0 { 107 | 0 108 | } else { 109 | (static_size_bits - 1) / 8 + 1 110 | }; 111 | Ok(CommandSchema { 112 | sized_parameters, 113 | static_size, 114 | has_trailer_parameter, 115 | }) 116 | } 117 | 118 | fn build_numeric_field(offset: usize, parameter: &cmddb::Parameter) -> Option { 119 | match parameter.data_type { 120 | cmddb::DataType::Int8 => Some(NumericField::Integral(GenericIntegralField::I8( 121 | build_command_integral_field(offset, 8), 122 | ))), 123 | cmddb::DataType::Int16 => Some(NumericField::Integral(GenericIntegralField::I16( 124 | build_command_integral_field(offset, 16), 125 | ))), 126 | cmddb::DataType::Int32 => Some(NumericField::Integral(GenericIntegralField::I32( 127 | build_command_integral_field(offset, 32), 128 | ))), 129 | cmddb::DataType::Uint8 => Some(NumericField::Integral(GenericIntegralField::U8( 130 | build_command_integral_field(offset, 8), 131 | ))), 132 | cmddb::DataType::Uint16 => Some(NumericField::Integral(GenericIntegralField::U16( 133 | build_command_integral_field(offset, 16), 134 | ))), 135 | cmddb::DataType::Uint32 => Some(NumericField::Integral(GenericIntegralField::U32( 136 | build_command_integral_field(offset, 32), 137 | ))), 138 | cmddb::DataType::Float => Some(NumericField::Floating(GenericFloatingField::F32( 139 | build_command_floating_field(offset, 32), 140 | ))), 141 | cmddb::DataType::Double => Some(NumericField::Floating(GenericFloatingField::F64( 142 | build_command_floating_field(offset, 64), 143 | ))), 144 | cmddb::DataType::Raw => None, 145 | } 146 | } 147 | 148 | fn build_command_integral_field(offset: usize, len: usize) -> IntegralField { 149 | IntegralField::new(offset..offset + len).expect("never fails") 150 | } 151 | 152 | fn build_command_floating_field(offset: usize, len: usize) -> FloatingField { 153 | FloatingField::new(offset..offset + len).expect("never fails") 154 | } 155 | -------------------------------------------------------------------------------- /tmtc-c2a/src/kble_gs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, ensure, Result}; 2 | use futures::{SinkExt, TryStreamExt}; 3 | use gaia_ccsds_c2a::{ 4 | ccsds::{ 5 | aos, 6 | tc::{self, sync_and_channel_coding::FrameType}, 7 | }, 8 | ccsds_c2a, 9 | }; 10 | use tokio::{ 11 | net::{TcpListener, ToSocketAddrs}, 12 | sync::{broadcast, mpsc}, 13 | }; 14 | use tracing::{error, info}; 15 | 16 | pub fn new() -> (Link, Socket) { 17 | let (cmd_tx, cmd_rx) = mpsc::channel(1); 18 | let (tlm_tx, _) = broadcast::channel(10); 19 | let link = Link { 20 | cmd_tx, 21 | tlm_tx: tlm_tx.clone(), 22 | }; 23 | let socket = Socket { cmd_rx, tlm_tx }; 24 | (link, socket) 25 | } 26 | 27 | pub struct Socket { 28 | cmd_rx: mpsc::Receiver>, 29 | tlm_tx: broadcast::Sender>, 30 | } 31 | 32 | impl Socket { 33 | pub async fn serve(mut self, addr: impl ToSocketAddrs) -> Result<()> { 34 | let listener = TcpListener::bind(addr).await?; 35 | loop { 36 | let accept_fut = listener.accept(); 37 | let leak_fut = async { 38 | loop { 39 | self.cmd_rx.recv().await; 40 | } 41 | }; 42 | let (incoming, addr) = tokio::select! { 43 | accept = accept_fut => accept?, 44 | _ = leak_fut => unreachable!(), 45 | }; 46 | info!("accept kble connection from {addr}"); 47 | let wss = tokio_tungstenite::accept_async(incoming).await?; 48 | let (mut sink, mut stream) = kble_socket::from_tungstenite(wss); 49 | let uplink = async { 50 | loop { 51 | let cmd_bytes = self 52 | .cmd_rx 53 | .recv() 54 | .await 55 | .ok_or_else(|| anyhow!("command sender has gone"))?; 56 | sink.send(cmd_bytes.into()).await?; 57 | } 58 | }; 59 | let downlink = async { 60 | loop { 61 | let Some(tlm_bytes) = stream.try_next().await? else { 62 | break; 63 | }; 64 | self.tlm_tx.send(tlm_bytes.into())?; 65 | } 66 | anyhow::Ok(()) 67 | }; 68 | let res = tokio::select! { 69 | res = uplink => res, 70 | res = downlink => res, 71 | }; 72 | if let Err(e) = res { 73 | error!("kble socket error: {e}") 74 | } 75 | sink.close().await?; 76 | } 77 | } 78 | } 79 | 80 | pub struct Link { 81 | cmd_tx: mpsc::Sender>, 82 | tlm_tx: broadcast::Sender>, 83 | } 84 | 85 | impl Link { 86 | pub fn uplink(&self) -> Uplink { 87 | Uplink { 88 | cmd_tx: self.cmd_tx.clone(), 89 | } 90 | } 91 | 92 | pub fn downlink(&self) -> Downlink { 93 | Downlink { 94 | tlm_rx: self.tlm_tx.subscribe(), 95 | } 96 | } 97 | } 98 | 99 | #[derive(Debug)] 100 | pub struct Downlink { 101 | tlm_rx: broadcast::Receiver>, 102 | } 103 | 104 | #[async_trait::async_trait] 105 | impl aos::SyncAndChannelCoding for Downlink { 106 | async fn receive(&mut self) -> Result { 107 | loop { 108 | match self.tlm_rx.recv().await { 109 | Ok(bytes) => { 110 | return Ok(aos::sync_and_channel_coding::TransferFrameBuffer::new( 111 | bytes, 112 | )) 113 | } 114 | Err(broadcast::error::RecvError::Lagged(_)) => continue, // NOTE: should report data lost? 115 | Err(e) => { 116 | return Err(anyhow::Error::from(e) 117 | .context("failed to receive telemetry bytes from broadcast channel")) 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | #[derive(Debug, Clone)] 125 | pub struct Uplink { 126 | cmd_tx: mpsc::Sender>, 127 | } 128 | 129 | #[async_trait::async_trait] 130 | impl tc::SyncAndChannelCoding for Uplink { 131 | async fn transmit( 132 | &mut self, 133 | scid: u16, 134 | vcid: u8, 135 | frame_type: FrameType, 136 | sequence_number: u8, 137 | data: &[u8], 138 | ) -> Result<()> { 139 | let tf_bytes = build_tf(scid, vcid, frame_type, sequence_number, data)?; 140 | self.cmd_tx.send(tf_bytes).await?; 141 | Ok(()) 142 | } 143 | } 144 | 145 | fn build_tf( 146 | scid: u16, 147 | vcid: u8, 148 | frame_type: FrameType, 149 | sequence_number: u8, 150 | data: &[u8], 151 | ) -> Result> { 152 | let mut tf_bytes = vec![0u8; ccsds_c2a::tc::transfer_frame::MAX_SIZE]; 153 | let mut tf_fecw = ccsds_c2a::tc::transfer_frame::Builder::new(&mut *tf_bytes).unwrap(); 154 | let mut tf = tf_fecw.bare_mut().unwrap(); 155 | tf.set_scid(scid); 156 | tf.set_vcid(vcid); 157 | tf.set_bypass_flag(frame_type.bypass_flag()); 158 | tf.set_control_command_flag(frame_type.control_command_flag()); 159 | tf.set_frame_sequence_number(sequence_number); 160 | let data_field = tf.data_field_mut(); 161 | ensure!(data.len() <= data_field.len(), "too large data"); 162 | data_field[..data.len()].copy_from_slice(data); 163 | let bare_len = tf.finish(data.len()); 164 | let tf_len = tf_fecw.finish(bare_len); 165 | tf_bytes.truncate(tf_len); 166 | Ok(tf_bytes) 167 | } 168 | -------------------------------------------------------------------------------- /devtools-frontend/src/proto/broker.client.ts: -------------------------------------------------------------------------------- 1 | // @generated by protobuf-ts 2.9.1 2 | // @generated from protobuf file "broker.proto" (package "broker", syntax proto3) 3 | // tslint:disable 4 | import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; 5 | import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; 6 | import { Broker } from "./broker"; 7 | import type { PostTelemetryResponse } from "./broker"; 8 | import type { PostTelemetryRequest } from "./broker"; 9 | import type { CommandStreamResponse } from "./broker"; 10 | import type { CommandStreamRequest } from "./broker"; 11 | import type { DuplexStreamingCall } from "@protobuf-ts/runtime-rpc"; 12 | import type { GetLastReceivedTelemetryResponse } from "./broker"; 13 | import type { GetLastReceivedTelemetryRequest } from "./broker"; 14 | import type { TelemetryStreamResponse } from "./broker"; 15 | import type { TelemetryStreamRequest } from "./broker"; 16 | import type { ServerStreamingCall } from "@protobuf-ts/runtime-rpc"; 17 | import { stackIntercept } from "@protobuf-ts/runtime-rpc"; 18 | import type { PostCommandResponse } from "./broker"; 19 | import type { PostCommandRequest } from "./broker"; 20 | import type { UnaryCall } from "@protobuf-ts/runtime-rpc"; 21 | import type { RpcOptions } from "@protobuf-ts/runtime-rpc"; 22 | /** 23 | * @generated from protobuf service broker.Broker 24 | */ 25 | export interface IBrokerClient { 26 | /** 27 | * @generated from protobuf rpc: PostCommand(broker.PostCommandRequest) returns (broker.PostCommandResponse); 28 | */ 29 | postCommand(input: PostCommandRequest, options?: RpcOptions): UnaryCall; 30 | /** 31 | * @generated from protobuf rpc: OpenTelemetryStream(broker.TelemetryStreamRequest) returns (stream broker.TelemetryStreamResponse); 32 | */ 33 | openTelemetryStream(input: TelemetryStreamRequest, options?: RpcOptions): ServerStreamingCall; 34 | /** 35 | * @generated from protobuf rpc: GetLastReceivedTelemetry(broker.GetLastReceivedTelemetryRequest) returns (broker.GetLastReceivedTelemetryResponse); 36 | */ 37 | getLastReceivedTelemetry(input: GetLastReceivedTelemetryRequest, options?: RpcOptions): UnaryCall; 38 | /** 39 | * @generated from protobuf rpc: OpenCommandStream(stream broker.CommandStreamRequest) returns (stream broker.CommandStreamResponse); 40 | */ 41 | openCommandStream(options?: RpcOptions): DuplexStreamingCall; 42 | /** 43 | * @generated from protobuf rpc: PostTelemetry(broker.PostTelemetryRequest) returns (broker.PostTelemetryResponse); 44 | */ 45 | postTelemetry(input: PostTelemetryRequest, options?: RpcOptions): UnaryCall; 46 | } 47 | /** 48 | * @generated from protobuf service broker.Broker 49 | */ 50 | export class BrokerClient implements IBrokerClient, ServiceInfo { 51 | typeName = Broker.typeName; 52 | methods = Broker.methods; 53 | options = Broker.options; 54 | constructor(private readonly _transport: RpcTransport) { 55 | } 56 | /** 57 | * @generated from protobuf rpc: PostCommand(broker.PostCommandRequest) returns (broker.PostCommandResponse); 58 | */ 59 | postCommand(input: PostCommandRequest, options?: RpcOptions): UnaryCall { 60 | const method = this.methods[0], opt = this._transport.mergeOptions(options); 61 | return stackIntercept("unary", this._transport, method, opt, input); 62 | } 63 | /** 64 | * @generated from protobuf rpc: OpenTelemetryStream(broker.TelemetryStreamRequest) returns (stream broker.TelemetryStreamResponse); 65 | */ 66 | openTelemetryStream(input: TelemetryStreamRequest, options?: RpcOptions): ServerStreamingCall { 67 | const method = this.methods[1], opt = this._transport.mergeOptions(options); 68 | return stackIntercept("serverStreaming", this._transport, method, opt, input); 69 | } 70 | /** 71 | * @generated from protobuf rpc: GetLastReceivedTelemetry(broker.GetLastReceivedTelemetryRequest) returns (broker.GetLastReceivedTelemetryResponse); 72 | */ 73 | getLastReceivedTelemetry(input: GetLastReceivedTelemetryRequest, options?: RpcOptions): UnaryCall { 74 | const method = this.methods[2], opt = this._transport.mergeOptions(options); 75 | return stackIntercept("unary", this._transport, method, opt, input); 76 | } 77 | /** 78 | * @generated from protobuf rpc: OpenCommandStream(stream broker.CommandStreamRequest) returns (stream broker.CommandStreamResponse); 79 | */ 80 | openCommandStream(options?: RpcOptions): DuplexStreamingCall { 81 | const method = this.methods[3], opt = this._transport.mergeOptions(options); 82 | return stackIntercept("duplex", this._transport, method, opt); 83 | } 84 | /** 85 | * @generated from protobuf rpc: PostTelemetry(broker.PostTelemetryRequest) returns (broker.PostTelemetryResponse); 86 | */ 87 | postTelemetry(input: PostTelemetryRequest, options?: RpcOptions): UnaryCall { 88 | const method = this.methods[4], opt = this._transport.mergeOptions(options); 89 | return stackIntercept("unary", this._transport, method, opt, input); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /gaia-tmtc/src/handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use futures::future::BoxFuture; 4 | 5 | #[async_trait] 6 | pub trait Handle { 7 | type Response; 8 | async fn handle(&mut self, request: Request) -> Result; 9 | } 10 | 11 | pub trait Layer { 12 | type Handle; 13 | fn layer(self, handle: H) -> Self::Handle; 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct BeforeHookLayer { 18 | hook: K, 19 | } 20 | 21 | impl BeforeHookLayer { 22 | pub fn new(hook: K) -> Self { 23 | Self { hook } 24 | } 25 | } 26 | 27 | impl Layer for BeforeHookLayer { 28 | type Handle = BeforeHook; 29 | 30 | fn layer(self, inner: H) -> Self::Handle { 31 | BeforeHook { 32 | hook: self.hook, 33 | inner, 34 | } 35 | } 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | pub struct BeforeHook { 40 | hook: K, 41 | inner: H, 42 | } 43 | 44 | #[async_trait] 45 | impl Handle for BeforeHook 46 | where 47 | Q: Send + 'static, 48 | H: Handle + Send, 49 | K: Hook + Send, 50 | K::Output: Send, 51 | { 52 | type Response = H::Response; 53 | 54 | async fn handle(&mut self, request: Q) -> Result { 55 | let next_request = self.hook.hook(request).await?; 56 | self.inner.handle(next_request).await 57 | } 58 | } 59 | 60 | #[async_trait] 61 | pub trait Hook { 62 | type Output; 63 | async fn hook(&mut self, input: Input) -> Result; 64 | } 65 | 66 | #[derive(Clone, Debug)] 67 | pub struct Choice { 68 | first: X, 69 | second: Y, 70 | } 71 | 72 | #[async_trait] 73 | impl Handle for Choice 74 | where 75 | Q: Clone + Send + Sync + 'static, 76 | X: Handle> + Send, 77 | Y: Handle> + Send, 78 | { 79 | type Response = Option; 80 | 81 | async fn handle(&mut self, request: Q) -> Result { 82 | if let Some(ret) = self.first.handle(request.clone()).await? { 83 | return Ok(Some(ret)); 84 | } 85 | self.second.handle(request).await 86 | } 87 | } 88 | 89 | pub trait HandleChoiceExt: Handle { 90 | fn prepend(self, first: X) -> Choice 91 | where 92 | Self: Sized, 93 | { 94 | Choice { 95 | first, 96 | second: self, 97 | } 98 | } 99 | 100 | fn append(self, second: Y) -> Choice 101 | where 102 | Self: Sized, 103 | { 104 | Choice { 105 | first: self, 106 | second, 107 | } 108 | } 109 | } 110 | 111 | impl HandleChoiceExt for T where T: Handle {} 112 | 113 | #[derive(Debug, Clone, Default)] 114 | pub struct Identity; 115 | 116 | impl Layer for Identity { 117 | type Handle = H; 118 | 119 | fn layer(self, inner: H) -> Self::Handle { 120 | inner 121 | } 122 | } 123 | 124 | #[derive(Clone, Debug)] 125 | pub enum Either { 126 | A(A), 127 | B(B), 128 | } 129 | 130 | impl Layer for Either 131 | where 132 | A: Layer, 133 | B: Layer, 134 | { 135 | type Handle = Either; 136 | 137 | fn layer(self, inner: H) -> Self::Handle { 138 | match self { 139 | Either::A(a) => Either::A(a.layer(inner)), 140 | Either::B(b) => Either::B(b.layer(inner)), 141 | } 142 | } 143 | } 144 | 145 | impl Handle for Either 146 | where 147 | A: Handle, 148 | B: Handle, 149 | { 150 | type Response = A::Response; 151 | 152 | fn handle<'a: 's, 's>(&'a mut self, request: Q) -> BoxFuture<'s, Result> 153 | where 154 | Self: 's, 155 | { 156 | match self { 157 | Either::A(a) => a.handle(request), 158 | Either::B(b) => b.handle(request), 159 | } 160 | } 161 | } 162 | 163 | #[derive(Clone, Debug)] 164 | pub struct Stack { 165 | inner: I, 166 | outer: O, 167 | } 168 | 169 | impl Stack { 170 | pub fn new(inner: I, outer: O) -> Stack { 171 | Self { inner, outer } 172 | } 173 | } 174 | 175 | impl Layer for Stack 176 | where 177 | I: Layer, 178 | O: Layer, 179 | { 180 | type Handle = O::Handle; 181 | 182 | fn layer(self, handle: H) -> Self::Handle { 183 | self.outer.layer(self.inner.layer(handle)) 184 | } 185 | } 186 | 187 | #[derive(Clone, Debug)] 188 | pub struct Builder { 189 | layer: L, 190 | } 191 | 192 | impl Builder { 193 | pub fn new() -> Self { 194 | Self { layer: Identity } 195 | } 196 | } 197 | 198 | impl Default for Builder { 199 | fn default() -> Self { 200 | Self::new() 201 | } 202 | } 203 | 204 | impl Builder { 205 | pub fn layer(self, layer: I) -> Builder> { 206 | Builder { 207 | layer: Stack::new(layer, self.layer), 208 | } 209 | } 210 | 211 | pub fn option_layer(self, layer: Option) -> Builder, L>> { 212 | let inner = match layer { 213 | Some(layer) => Either::A(layer), 214 | None => Either::B(Identity), 215 | }; 216 | Builder { 217 | layer: Stack::new(inner, self.layer), 218 | } 219 | } 220 | 221 | pub fn before_hook(self, hook: K) -> Builder, L>> { 222 | let before_hook = BeforeHookLayer::new(hook); 223 | self.layer(before_hook) 224 | } 225 | 226 | pub fn build(self, handle: H) -> L::Handle 227 | where 228 | L: Layer, 229 | { 230 | self.layer.layer(handle) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /gaia-ccsds-c2a/src/ccsds/aos/m_pdu.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::identity_op)] 2 | use std::mem; 3 | 4 | use anyhow::{anyhow, ensure, Result}; 5 | use modular_bitfield_msb::prelude::*; 6 | use zerocopy::{AsBytes, FromBytes, LayoutVerified, Unaligned}; 7 | 8 | use crate::ccsds::space_packet::{self, SpacePacket}; 9 | 10 | #[bitfield(bytes = 2)] 11 | #[derive(Debug, Default, Clone, FromBytes, AsBytes, Unaligned)] 12 | #[repr(C)] 13 | pub struct Header { 14 | #[skip] 15 | __: B5, 16 | pub first_header_pointer_raw: B11, 17 | } 18 | 19 | impl Header { 20 | pub const SIZE: usize = mem::size_of::(); 21 | } 22 | 23 | impl Header { 24 | pub fn first_header_pointer(&self) -> FirstHeaderPointer { 25 | self.first_header_pointer_raw() 26 | .try_into() 27 | .expect("first_header_pointer_raw must be 11bits") 28 | } 29 | 30 | pub fn set_first_header_pointer(&mut self, first_header_pointer: FirstHeaderPointer) { 31 | self.set_first_header_pointer_raw(first_header_pointer.into()); 32 | } 33 | } 34 | 35 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 36 | pub enum FirstHeaderPointer { 37 | Pointer(u16), 38 | NoPacketStarts, 39 | IdleData, 40 | } 41 | 42 | impl FirstHeaderPointer { 43 | pub const ALL_ONES: u16 = 0b11111111111; 44 | pub const ALL_ONES_MINUS_ONE: u16 = 0b11111111110; 45 | } 46 | 47 | impl TryFrom for FirstHeaderPointer { 48 | type Error = anyhow::Error; 49 | 50 | fn try_from(raw: u16) -> Result { 51 | ensure!(raw <= Self::ALL_ONES, "too large first header pointer"); 52 | match raw { 53 | Self::ALL_ONES => Ok(Self::NoPacketStarts), 54 | Self::ALL_ONES_MINUS_ONE => Ok(Self::IdleData), 55 | pointer => Ok(Self::Pointer(pointer)), 56 | } 57 | } 58 | } 59 | 60 | impl From for u16 { 61 | fn from(value: FirstHeaderPointer) -> Self { 62 | match value { 63 | FirstHeaderPointer::Pointer(pointer) => pointer, 64 | FirstHeaderPointer::NoPacketStarts => FirstHeaderPointer::ALL_ONES, 65 | FirstHeaderPointer::IdleData => FirstHeaderPointer::ALL_ONES_MINUS_ONE, 66 | } 67 | } 68 | } 69 | 70 | #[derive(Debug, Default)] 71 | pub struct Defragmenter { 72 | buf: Vec, 73 | } 74 | 75 | impl Defragmenter { 76 | pub fn push(&mut self, m_pdu_bytes: &[u8]) -> Result { 77 | let (header, packet_zone) = 78 | LayoutVerified::<_, Header>::new_unaligned_from_prefix(m_pdu_bytes) 79 | .ok_or_else(|| anyhow!("given M_PDU is too small"))?; 80 | ensure!( 81 | packet_zone.len() > space_packet::PrimaryHeader::SIZE, 82 | "packet zone must be a Space Packet" 83 | ); 84 | if self.buf.is_empty() { 85 | if let FirstHeaderPointer::Pointer(pointer) = header.first_header_pointer() { 86 | let offset = pointer as usize; 87 | let first_packet_bytes = &packet_zone 88 | .get(offset..) 89 | .ok_or_else(|| anyhow!("invalid first header pointer"))?; 90 | self.buf.extend_from_slice(first_packet_bytes); 91 | Ok(true) 92 | } else { 93 | Ok(false) 94 | } 95 | } else { 96 | match header.first_header_pointer() { 97 | FirstHeaderPointer::Pointer(_) | FirstHeaderPointer::NoPacketStarts => { 98 | self.buf.extend_from_slice(packet_zone); 99 | Ok(true) 100 | } 101 | FirstHeaderPointer::IdleData => Ok(false), 102 | } 103 | } 104 | } 105 | 106 | #[deprecated] 107 | pub fn read(&self) -> Option> { 108 | let (_bytes, packet) = self.read_as_bytes_and_packet()?; 109 | Some(packet) 110 | } 111 | 112 | pub fn read_as_bytes_and_packet(&self) -> Option<(&[u8], SpacePacket<&'_ [u8]>)> { 113 | let buf = self.buf.as_slice(); 114 | let (packet, trailer) = SpacePacket::new(buf)?; 115 | let bytes = &buf[..buf.len() - trailer.len()]; 116 | Some((bytes, packet)) 117 | } 118 | 119 | pub fn advance(&mut self) -> usize { 120 | let Some((packet, _trailer)) = SpacePacket::new(self.buf.as_slice()) else { 121 | return 0; 122 | }; 123 | let size = packet.packet_size().expect( 124 | "packet_data.len() must be correct because it was constructed with ::new method", 125 | ); 126 | self.buf.drain(..size); 127 | size 128 | } 129 | 130 | pub fn reset(&mut self) { 131 | self.buf.clear(); 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use super::*; 138 | 139 | #[test] 140 | fn test() { 141 | let mut defrag = Defragmenter::default(); 142 | let m_pdu1 = { 143 | let mut bytes = [0u8; Header::SIZE + space_packet::PrimaryHeader::SIZE + 1]; 144 | let (mut m_pdu_hdr, pz) = 145 | LayoutVerified::<_, Header>::new_unaligned_from_prefix(bytes.as_mut_slice()) 146 | .unwrap(); 147 | m_pdu_hdr.set_first_header_pointer_raw(0); 148 | let (mut ph, ud) = 149 | LayoutVerified::<_, space_packet::PrimaryHeader>::new_unaligned_from_prefix(pz) 150 | .unwrap(); 151 | ph.set_packet_data_length_in_bytes(1); 152 | ud[0] = 0xde; 153 | bytes 154 | }; 155 | defrag.push(&m_pdu1).unwrap(); 156 | let packet = defrag.read_as_bytes_and_packet().unwrap().1; 157 | assert_eq!(1, packet.packet_data.len()); 158 | assert_eq!(0xDE, packet.packet_data[0]); 159 | let size = packet.packet_size().unwrap(); 160 | assert_eq!(defrag.advance(), size); 161 | assert!(defrag.read_as_bytes_and_packet().is_none()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /devtools-frontend/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Classes, Icon } from "@blueprintjs/core"; 2 | import React, { useMemo } from "react"; 3 | import { 4 | Link, 5 | NavLink, 6 | Outlet, 7 | useLoaderData, 8 | useOutletContext, 9 | useParams, 10 | } from "react-router-dom"; 11 | import { BrokerClient } from "../proto/broker.client"; 12 | import { SatelliteSchema } from "../proto/tmtc_generic_c2a"; 13 | import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; 14 | import { IconNames } from "@blueprintjs/icons"; 15 | import type { GrpcClientService } from "../worker"; 16 | 17 | type TelemetryMenuItem = { 18 | name: string; 19 | telemetryId: number; 20 | }; 21 | 22 | const formatU8Hex = (u8: number) => { 23 | return "0x" + `0${u8.toString(16)}`.slice(-2); 24 | }; 25 | 26 | type TelemetryListSidebarProps = { 27 | activeName: string | undefined; 28 | telemetryListItems: TelemetryMenuItem[]; 29 | }; 30 | const TelemetryListSidebar: React.FC = ({ 31 | activeName: tmivName, 32 | telemetryListItems, 33 | }) => { 34 | return ( 35 |

36 |
    37 |
  • 38 | 41 | `${Classes.MENU_ITEM} ${isActive ? Classes.ACTIVE : ""}` 42 | } 43 | > 44 | 47 | Command 48 | 49 | 50 | 51 | 52 | 53 |
  • 54 |
  • 55 | 58 | `${Classes.MENU_ITEM} ${isActive ? Classes.ACTIVE : ""}` 59 | } 60 | > 61 | 64 | Command (Experimental Opslang) 65 | 66 | 67 | 68 | 69 | 70 |
  • 71 |
  • 72 |
    Telemetry
    73 |
  • 74 |
75 |
    76 | {telemetryListItems.map((item) => { 77 | return ( 78 |
  • 79 | 85 | 88 | {item.name} 89 | 90 | 91 | {formatU8Hex(item.telemetryId)} 92 | 93 | 94 |
  • 95 | ); 96 | })} 97 |
98 |
99 | ); 100 | }; 101 | 102 | export const Layout = () => { 103 | const ctx = useLoaderData() as ClientContext; 104 | const params = useParams(); 105 | const tmivName = params["tmivName"]; 106 | 107 | const telemetryListItems = useMemo(() => { 108 | const items: TelemetryMenuItem[] = []; 109 | const channelNames = Object.keys(ctx.satelliteSchema.telemetryChannels); 110 | for (const [componentName, componentSchema] of Object.entries( 111 | ctx.satelliteSchema.telemetryComponents, 112 | )) { 113 | for (const [telemetryName, telemetrySchema] of Object.entries( 114 | componentSchema.telemetries, 115 | )) { 116 | for (const channelName of channelNames) { 117 | const name = `${channelName}.${componentName}.${telemetryName}`; 118 | const telemetryId = telemetrySchema.metadata!.id; 119 | items.push({ name, telemetryId }); 120 | } 121 | } 122 | } 123 | items.sort((a, b) => { 124 | // ad-hoc optimization 125 | const rtA = a.name.startsWith("RT."); 126 | const rtB = b.name.startsWith("RT."); 127 | if (rtA && !rtB) { 128 | return -1; 129 | } else if (!rtA && rtB) { 130 | return 1; 131 | } 132 | 133 | if (a.name > b.name) { 134 | return 1; 135 | } else if (a.name < b.name) { 136 | return -1; 137 | } else { 138 | return 0; 139 | } 140 | }); 141 | return items; 142 | }, [ 143 | ctx.satelliteSchema.telemetryChannels, 144 | ctx.satelliteSchema.telemetryComponents, 145 | ]); 146 | 147 | return ( 148 |
149 |
150 | 151 | 157 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
168 |
169 | ); 170 | }; 171 | 172 | export type ClientContext = { 173 | client: GrpcClientService; 174 | satelliteSchema: SatelliteSchema; 175 | }; 176 | 177 | export function useClient() { 178 | return useOutletContext(); 179 | } 180 | -------------------------------------------------------------------------------- /devtools-frontend/src/components/TelemetryView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 2 | import { TreeNamespace, addToNamespace, mapNamespace } from "../tree"; 3 | 4 | import { Tmiv, TmivField } from "../proto/tco_tmiv"; 5 | import { useClient } from "./Layout"; 6 | import { useParams } from "react-router-dom"; 7 | import { Helmet } from "react-helmet-async"; 8 | import { TelemetrySchema } from "../proto/tmtc_generic_c2a"; 9 | 10 | const buildTelemetryFieldTreeBlueprintFromSchema = ( 11 | tlm: TelemetrySchema, 12 | ): TreeNamespace => { 13 | const fieldNames = tlm.fields.map((f) => f.name); 14 | const root: TreeNamespace = new Map(); 15 | for (const fieldName of fieldNames) { 16 | const path = fieldName.split("."); 17 | addToNamespace(root, path, undefined); 18 | } 19 | return root; 20 | }; 21 | 22 | type TelemetryValuePair = { 23 | converted: TmivField["value"] | null; 24 | raw: TmivField["value"] | null; 25 | }; 26 | 27 | const buildTelemetryFieldTree = ( 28 | blueprint: TreeNamespace, 29 | fields: TmivField[], 30 | ): TreeNamespace => { 31 | const convertedFieldMap = new Map(); 32 | const rawFieldMap = new Map(); 33 | for (const field of fields) { 34 | if (field.name.endsWith("@RAW")) { 35 | const strippedName = field.name.slice(0, -4); 36 | rawFieldMap.set(strippedName, field.value); 37 | } else { 38 | convertedFieldMap.set(field.name, field.value); 39 | } 40 | } 41 | return mapNamespace(blueprint, (path, _key) => { 42 | const key = path.join("."); 43 | const converted = convertedFieldMap.get(key) ?? null; 44 | const raw = rawFieldMap.get(key) ?? null; 45 | return { converted, raw }; 46 | }); 47 | }; 48 | 49 | const prettyprintValue = (value: TmivField["value"] | null) => { 50 | if (value === null) { 51 | return "****"; 52 | } 53 | switch (value.oneofKind) { 54 | case "integer": 55 | return `${value.integer}`; 56 | case "bytes": 57 | return [...value.bytes] 58 | .map((x) => x.toString(16).padStart(2, "0")) 59 | .join(""); 60 | case "enum": 61 | return value.enum; 62 | case "double": 63 | return value.double.toFixed(3); 64 | case "string": 65 | return value.string; 66 | } 67 | }; 68 | 69 | type ValueCellProps = { 70 | name: string; 71 | value: TelemetryValuePair; 72 | }; 73 | const LeafCell: React.FC = ({ name, value }) => { 74 | return ( 75 |
76 | {name} 77 | 78 | 79 | {prettyprintValue(value.converted)} 80 | 81 |
82 | ); 83 | }; 84 | 85 | type NamespaceCellProps = { 86 | name: string; 87 | ns: TreeNamespace; 88 | }; 89 | const NamespaceCell: React.FC = ({ name, ns }) => { 90 | const [isOpen, setIsOpen] = useState(true); 91 | const handleClickHeading = useCallback(() => { 92 | setIsOpen(!isOpen); 93 | }, [isOpen]); 94 | return ( 95 |
96 |
97 | 111 |
112 |
113 | 114 |
115 |
116 | ); 117 | }; 118 | 119 | type NamespaceContentCellProps = { 120 | ns: TreeNamespace; 121 | }; 122 | const NamespaceContentCell: React.FC = ({ ns }) => { 123 | return ( 124 |
125 | {[...ns.entries()].map(([name, v]) => { 126 | switch (v.type) { 127 | case "leaf": 128 | return ; 129 | case "ns": 130 | return ; 131 | } 132 | })} 133 |
134 | ); 135 | }; 136 | 137 | type InlineNamespaceContentCellProps = { 138 | ns: TreeNamespace; 139 | }; 140 | const InlineNamespaceContentCell: React.FC = ({ 141 | ns, 142 | }) => { 143 | return ( 144 | <> 145 | {[...ns.entries()].map(([name, v]) => { 146 | switch (v.type) { 147 | case "leaf": 148 | return ( 149 | 150 | {name}: 151 | 152 | {prettyprintValue(v.value.converted)} 153 | 154 | 155 | ); 156 | case "ns": 157 | return null; 158 | } 159 | })} 160 | 161 | ); 162 | }; 163 | 164 | export const TelemetryView: React.FC = () => { 165 | const params = useParams(); 166 | const tmivName = params["tmivName"]!; 167 | const { 168 | client, 169 | satelliteSchema: { telemetryComponents }, 170 | } = useClient(); 171 | const [tmiv, setTmiv] = useState(null); 172 | useEffect(() => { 173 | setTmiv(null); 174 | const readerP = client 175 | .openTelemetryStream(tmivName) 176 | .then((stream) => stream.getReader()); 177 | let cancel; 178 | const cancelP = new Promise((resolve) => (cancel = resolve)); 179 | Promise.all([readerP, cancelP]).then(([reader]) => reader.cancel()); 180 | readerP.then(async (reader) => { 181 | // eslint-disable-next-line no-constant-condition 182 | while (true) { 183 | const next = await reader.read(); 184 | if (next.done) { 185 | break; 186 | } 187 | const tmiv = next.value; 188 | setTmiv(tmiv); 189 | } 190 | }); 191 | return cancel; 192 | }, [client, tmivName]); 193 | const telemetryDef = useMemo(() => { 194 | const [_channel, componentName, telemetryName] = tmivName.split("."); 195 | const [_c, componentDef] = Object.entries(telemetryComponents).find( 196 | ([name, _]) => name === componentName, 197 | )!; 198 | const [_t, telemetryDef] = Object.entries(componentDef.telemetries).find( 199 | ([name, _]) => name === telemetryName, 200 | )!; 201 | return telemetryDef; 202 | }, [telemetryComponents, tmivName]); 203 | const treeBlueprint = useMemo(() => { 204 | return buildTelemetryFieldTreeBlueprintFromSchema(telemetryDef!); 205 | }, [telemetryDef]); 206 | const tree = buildTelemetryFieldTree(treeBlueprint, tmiv?.fields ?? []); 207 | 208 | return ( 209 | <> 210 | 211 | {tmivName} 212 | 213 |
214 | 215 |
216 | 217 | ); 218 | }; 219 | -------------------------------------------------------------------------------- /tmtc-c2a/src/registry/tlm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | fmt::{self, Display}, 4 | }; 5 | 6 | use anyhow::{anyhow, Result}; 7 | use gaia_ccsds_c2a::access::tlm::schema::{ 8 | from_tlmcmddb, FieldSchema, FloatingFieldSchema, IntegralFieldSchema, 9 | }; 10 | use itertools::Itertools; 11 | 12 | use crate::{ 13 | proto::tmtc_generic_c2a::{self as proto}, 14 | satconfig, 15 | }; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct FatTelemetrySchema { 19 | component: String, 20 | telemetry: String, 21 | pub schema: TelemetrySchema, 22 | } 23 | 24 | impl FatTelemetrySchema { 25 | pub fn build_tmiv_name<'a>(&'a self, channel: &'a str) -> TmivName<'a> { 26 | TmivName { 27 | channel, 28 | component: &self.component, 29 | telemetry: &self.telemetry, 30 | } 31 | } 32 | } 33 | 34 | pub struct TmivName<'a> { 35 | channel: &'a str, 36 | component: &'a str, 37 | telemetry: &'a str, 38 | } 39 | 40 | impl<'a> Display for TmivName<'a> { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!(f, "{}.{}.{}", self.channel, self.component, self.telemetry) 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | pub struct TelemetrySchema { 48 | pub integral_fields: Vec<(FieldMetadata, IntegralFieldSchema)>, 49 | pub floating_fields: Vec<(FieldMetadata, FloatingFieldSchema)>, 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct FieldMetadata { 54 | order: usize, 55 | original_name: String, 56 | pub converted_name: String, 57 | pub raw_name: String, 58 | } 59 | 60 | #[derive(Debug, Clone)] 61 | pub struct Registry { 62 | channel_map: satconfig::TelemetryChannelMap, 63 | schema_map: HashMap<(u16, u8), FatTelemetrySchema>, 64 | } 65 | 66 | impl Registry { 67 | pub fn build_telemetry_channel_schema_map( 68 | &self, 69 | ) -> HashMap { 70 | self.channel_map 71 | .iter() 72 | .map(|(channel_name, ch)| { 73 | let channel_name = channel_name.to_string(); 74 | let telmetry_channel_schema = proto::TelemetryChannelSchema { 75 | metadata: Some(proto::TelemetryChannelSchemaMetadata { 76 | destination_flag_mask: ch.destination_flag_mask as u32, 77 | }), 78 | }; 79 | (channel_name, telmetry_channel_schema) 80 | }) 81 | .collect() 82 | } 83 | 84 | pub fn build_telemetry_component_schema_map( 85 | &self, 86 | ) -> HashMap { 87 | self.schema_map 88 | .iter() 89 | .map(|((apid, tlm_id), fat_tlm_schema)| { 90 | let fields = fat_tlm_schema 91 | .schema 92 | .integral_fields 93 | .iter() 94 | .map(|(m, _)| m) 95 | .chain(fat_tlm_schema.schema.floating_fields.iter().map(|(m, _)| m)) 96 | .sorted_by_key(|m| m.order) 97 | .map(|m| proto::TelemetryFieldSchema { 98 | metadata: Some(proto::TelemetryFieldSchemaMetadata {}), 99 | name: m.original_name.to_string(), 100 | }) 101 | .collect(); 102 | let telemetry_schema = proto::TelemetrySchema { 103 | metadata: Some(proto::TelemetrySchemaMetadata { id: *tlm_id as u32 }), 104 | fields, 105 | }; 106 | ( 107 | (fat_tlm_schema.component.as_str(), *apid), 108 | fat_tlm_schema.telemetry.as_str(), 109 | telemetry_schema, 110 | ) 111 | }) 112 | .sorted_by_key(|&((component_name, _), _, _)| component_name) 113 | .group_by(|&(key, _, _)| key) 114 | .into_iter() 115 | .map(|((component_name, apid), group)| { 116 | let metadata = proto::TelemetryComponentSchemaMetadata { apid: apid as u32 }; 117 | let telemetries: HashMap = group 118 | .map(|(_, telemetry_name, telemetry_schema)| { 119 | (telemetry_name.to_string(), telemetry_schema) 120 | }) 121 | .collect(); 122 | let component_name = component_name.to_string(); 123 | let telemetry_component_schema = proto::TelemetryComponentSchema { 124 | metadata: Some(metadata), 125 | telemetries, 126 | }; 127 | (component_name, telemetry_component_schema) 128 | }) 129 | .collect() 130 | } 131 | 132 | pub fn all_tmiv_names(&self) -> HashSet { 133 | self.channel_map 134 | .keys() 135 | .flat_map(|channel| { 136 | self.schema_map 137 | .values() 138 | .map(|schema| schema.build_tmiv_name(channel).to_string()) 139 | }) 140 | .collect() 141 | } 142 | 143 | pub fn find_channels(&self, destination_flags: u8) -> impl Iterator { 144 | self.channel_map.iter().filter_map(move |(name, ch)| { 145 | if ch.destination_flag_mask & destination_flags != 0 { 146 | Some(name.as_str()) 147 | } else { 148 | None 149 | } 150 | }) 151 | } 152 | 153 | pub fn lookup(&self, apid: u16, tlm_id: u8) -> Option<&FatTelemetrySchema> { 154 | let fat_schema = self.schema_map.get(&(apid, tlm_id))?; 155 | Some(fat_schema) 156 | } 157 | 158 | pub fn from_tlmcmddb_with_apid_map( 159 | db: &tlmcmddb::Database, 160 | apid_map: &HashMap, 161 | channel_map: satconfig::TelemetryChannelMap, 162 | ) -> Result { 163 | let mut rev_apid_map: HashMap<&str, Vec> = HashMap::new(); 164 | for (apid, component) in apid_map.iter() { 165 | let entry = rev_apid_map.entry(component.as_str()); 166 | entry 167 | .and_modify(|e| e.push(*apid)) 168 | .or_insert_with(|| vec![*apid]); 169 | } 170 | 171 | let mut schema_map = HashMap::new(); 172 | for (metadata, fields) in from_tlmcmddb(db).flatten() { 173 | let apids = rev_apid_map 174 | .get(metadata.component_name.as_str()) 175 | .ok_or_else(|| anyhow!("APID not defined for {}", metadata.component_name))?; 176 | let schema = build_telemetry_schema(fields)?; 177 | for apid in apids { 178 | let metadata = metadata.clone(); 179 | let schema = schema.clone(); 180 | schema_map.insert( 181 | (*apid, metadata.tlm_id), 182 | FatTelemetrySchema { 183 | component: metadata.component_name, 184 | telemetry: metadata.telemetry_name, 185 | schema, 186 | }, 187 | ); 188 | } 189 | } 190 | Ok(Self { 191 | channel_map, 192 | schema_map, 193 | }) 194 | } 195 | } 196 | 197 | fn build_telemetry_schema<'a>( 198 | iter: impl Iterator>, 199 | ) -> Result { 200 | let mut schema = TelemetrySchema { 201 | integral_fields: vec![], 202 | floating_fields: vec![], 203 | }; 204 | for (order, pair) in iter.enumerate() { 205 | let (field_name, field_schema) = pair?; 206 | let name_pair = build_field_metadata(order, field_name); 207 | match field_schema { 208 | FieldSchema::Integral(field_schema) => { 209 | schema.integral_fields.push((name_pair, field_schema)); 210 | } 211 | FieldSchema::Floating(field_schema) => { 212 | schema.floating_fields.push((name_pair, field_schema)); 213 | } 214 | } 215 | } 216 | Ok(schema) 217 | } 218 | 219 | fn build_field_metadata(order: usize, tlmdb_name: &str) -> FieldMetadata { 220 | FieldMetadata { 221 | order, 222 | original_name: tlmdb_name.to_string(), 223 | converted_name: tlmdb_name.to_string(), 224 | raw_name: format!("{tlmdb_name}@RAW"), 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /tmtc-c2a/src/registry/cmd.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::{self, Display}, 4 | str::FromStr, 5 | }; 6 | 7 | use anyhow::{anyhow, Result}; 8 | use gaia_ccsds_c2a::access::cmd::schema::{from_tlmcmddb, CommandSchema}; 9 | use itertools::Itertools; 10 | 11 | use crate::proto::tmtc_generic_c2a as proto; 12 | use crate::satconfig; 13 | 14 | struct TcoName { 15 | prefix: String, 16 | component: String, 17 | command: String, 18 | } 19 | 20 | impl Display for TcoName { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | write!(f, "{}.{}.{}", self.prefix, self.component, self.command) 23 | } 24 | } 25 | 26 | impl FromStr for TcoName { 27 | type Err = (); 28 | 29 | fn from_str(s: &str) -> Result { 30 | let mut parts = s.split('.'); 31 | let prefix = parts.next().ok_or(())?; 32 | let component = parts.next().ok_or(())?; 33 | let command = parts.next().ok_or(())?; 34 | if parts.next().is_some() { 35 | return Err(()); 36 | } 37 | Ok(Self { 38 | prefix: prefix.to_string(), 39 | component: component.to_string(), 40 | command: command.to_string(), 41 | }) 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct FatCommandSchema<'a> { 47 | pub apid: u16, 48 | pub command_id: u16, 49 | pub destination_type: u8, 50 | pub execution_type: u8, 51 | pub has_time_indicator: bool, 52 | pub schema: &'a CommandSchema, 53 | } 54 | 55 | #[derive(Debug, Clone)] 56 | struct CommandSchemaWithId { 57 | apid: u16, 58 | command_id: u16, 59 | schema: CommandSchema, 60 | } 61 | 62 | #[derive(Debug, Clone)] 63 | pub struct Registry { 64 | prefix_map: satconfig::CommandPrefixMap, 65 | schema_map: HashMap<(String, String), CommandSchemaWithId>, 66 | } 67 | 68 | impl Registry { 69 | pub fn build_command_prefix_schema_map(&self) -> HashMap { 70 | self.prefix_map 71 | .iter() 72 | .map(|(prefix, subsystem_map)| { 73 | let prefix = prefix.to_string(); 74 | let subsystems = subsystem_map 75 | .iter() 76 | .map(|(component_name, subsystem)| { 77 | let component_name = component_name.to_string(); 78 | let command_subsystem_schema = proto::CommandSubsystemSchema { 79 | metadata: Some(proto::CommandSubsystemSchemaMetadata { 80 | destination_type: subsystem.destination_type as u32, 81 | execution_type: subsystem.execution_type as u32, 82 | }), 83 | has_time_indicator: subsystem.has_time_indicator, 84 | }; 85 | (component_name, command_subsystem_schema) 86 | }) 87 | .collect(); 88 | let command_prefix_schema = proto::CommandPrefixSchema { 89 | metadata: Some(proto::CommandPrefixSchemaMetadata {}), 90 | subsystems, 91 | }; 92 | (prefix, command_prefix_schema) 93 | }) 94 | .collect() 95 | } 96 | 97 | pub fn build_command_component_schema_map( 98 | &self, 99 | ) -> HashMap { 100 | self.schema_map 101 | .iter() 102 | .sorted_by_key(|&((component_name, _), _)| component_name) 103 | .group_by(|&((component_name, _), schema_with_id)| { 104 | (component_name, schema_with_id.apid) 105 | }) 106 | .into_iter() 107 | .map(|((component_name, apid), group)| { 108 | let command_schema_map = group 109 | .map(|((_, command_name), schema_with_id)| { 110 | let trailer_parameter = if schema_with_id.schema.has_trailer_parameter { 111 | Some(proto::CommandParameterSchema { 112 | metadata: Some(proto::CommandParameterSchemaMetadata {}), 113 | data_type: proto::CommandParameterDataType::CmdParameterBytes 114 | .into(), 115 | }) 116 | } else { 117 | None 118 | }; 119 | let parameters = schema_with_id 120 | .schema 121 | .sized_parameters 122 | .iter() 123 | .map(|param| { 124 | let data_type = match param { 125 | structpack::NumericField::Integral(_) => { 126 | proto::CommandParameterDataType::CmdParameterInteger 127 | } 128 | structpack::NumericField::Floating(_) => { 129 | proto::CommandParameterDataType::CmdParameterDouble 130 | } 131 | }; 132 | proto::CommandParameterSchema { 133 | metadata: Some(proto::CommandParameterSchemaMetadata {}), 134 | data_type: data_type.into(), 135 | } 136 | }) 137 | .chain(trailer_parameter) 138 | .collect(); 139 | let command_name = command_name.to_string(); 140 | let command_schema = proto::CommandSchema { 141 | metadata: Some(proto::CommandSchemaMetadata { 142 | id: schema_with_id.command_id as u32, 143 | }), 144 | parameters, 145 | }; 146 | (command_name, command_schema) 147 | }) 148 | .collect(); 149 | let command_component_schema = proto::CommandComponentSchema { 150 | metadata: Some(proto::CommandComponentSchemaMetadata { apid: apid as u32 }), 151 | commands: command_schema_map, 152 | }; 153 | (component_name.to_string(), command_component_schema) 154 | }) 155 | .collect() 156 | } 157 | 158 | pub fn lookup(&self, tco_name: &str) -> Option { 159 | let TcoName { 160 | prefix, 161 | component, 162 | command, 163 | } = tco_name.parse().ok()?; 164 | let satconfig::CommandSubsystem { 165 | has_time_indicator, 166 | destination_type, 167 | execution_type, 168 | } = self.prefix_map.get(&prefix)?.get(&component)?; 169 | let CommandSchemaWithId { 170 | apid, 171 | command_id, 172 | schema, 173 | } = self.schema_map.get(&(component, command))?; 174 | Some(FatCommandSchema { 175 | apid: *apid, 176 | command_id: *command_id, 177 | destination_type: *destination_type, 178 | execution_type: *execution_type, 179 | has_time_indicator: *has_time_indicator, 180 | schema, 181 | }) 182 | } 183 | 184 | pub fn from_tlmcmddb_with_satconfig( 185 | db: &tlmcmddb::Database, 186 | apid_map: &HashMap, 187 | prefix_map: satconfig::CommandPrefixMap, 188 | ) -> Result { 189 | let schema_map = from_tlmcmddb(db) 190 | .flatten() 191 | .map(|schema| { 192 | let (metadata, schema) = schema?; 193 | let component = metadata.component_name; 194 | let cmddb_name = metadata.command_name; 195 | let apid = *apid_map 196 | .get(&component) 197 | .ok_or_else(|| anyhow!("APID is not defined for {component}"))?; 198 | let schema_with_id = CommandSchemaWithId { 199 | apid, 200 | command_id: metadata.cmd_id, 201 | schema, 202 | }; 203 | Ok(((component, cmddb_name), schema_with_id)) 204 | }) 205 | .collect::>()?; 206 | Ok(Self { 207 | schema_map, 208 | prefix_map, 209 | }) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tmtc-c2a/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 2 | use std::path::PathBuf; 3 | use std::sync::Arc; 4 | use std::{fs, io}; 5 | 6 | use anyhow::{Context, Result}; 7 | use axum::{error_handling::HandleError, response::Redirect, routing::get}; 8 | use clap::Parser; 9 | use gaia_tmtc::broker::broker_server::BrokerServer; 10 | use gaia_tmtc::recorder::recorder_client::RecorderClient; 11 | use gaia_tmtc::recorder::RecordHook; 12 | use gaia_tmtc::BeforeHookLayer; 13 | use gaia_tmtc::{ 14 | broker::{self, BrokerService}, 15 | handler, 16 | telemetry::{self, LastTmivStore}, 17 | }; 18 | use notalawyer_clap::*; 19 | use tmtc_c2a::proto::tmtc_generic_c2a::tmtc_generic_c2a_server::TmtcGenericC2aServer; 20 | use tonic::server::NamedService; 21 | use tonic::transport::{Channel, Server, Uri}; 22 | use tonic_health::server::HealthReporter; 23 | use tonic_web::GrpcWebLayer; 24 | use tower::ServiceBuilder; 25 | use tower_http::cors::CorsLayer; 26 | use tower_http::trace::TraceLayer; 27 | use tracing::metadata::LevelFilter; 28 | use tracing_subscriber::{prelude::*, EnvFilter}; 29 | 30 | #[cfg(feature = "devtools")] 31 | use tmtc_c2a::devtools_server; 32 | use tmtc_c2a::{kble_gs, proto, registry, satellite, Satconfig}; 33 | 34 | #[derive(Parser, Debug)] 35 | #[clap(author, version, about, long_about = None)] 36 | pub struct Args { 37 | #[clap(long, env, default_value_t = Ipv4Addr::UNSPECIFIED.into())] 38 | broker_addr: IpAddr, 39 | #[clap(long, env, default_value_t = 8900)] 40 | broker_port: u16, 41 | #[clap(long, env, default_value_t = Ipv4Addr::UNSPECIFIED.into())] 42 | kble_addr: IpAddr, 43 | #[clap(long, env, default_value_t = 8910)] 44 | kble_port: u16, 45 | #[clap(long, env, default_value_t = 1.0)] 46 | traces_sample_rate: f32, 47 | #[clap(long, env)] 48 | sentry_dsn: Option, 49 | #[clap(env, long)] 50 | tlmcmddb: PathBuf, 51 | #[clap(env, long)] 52 | satconfig: PathBuf, 53 | #[clap(env, long)] 54 | recorder_endpoint: Option, 55 | } 56 | 57 | impl Args { 58 | fn load_satconfig(&self) -> Result { 59 | let file = fs::OpenOptions::new().read(true).open(&self.satconfig)?; 60 | Ok(serde_json::from_reader(&file)?) 61 | } 62 | 63 | fn load_tlmcmddb(&self) -> Result { 64 | let file = fs::OpenOptions::new().read(true).open(&self.tlmcmddb)?; 65 | let rdr = io::BufReader::new(file); 66 | Ok(serde_json::from_reader(rdr)?) 67 | } 68 | } 69 | 70 | #[tokio::main] 71 | async fn main() -> Result<()> { 72 | let args = Args::parse_with_license_notice(include_notice!()); 73 | 74 | let _guard = sentry::init(sentry::ClientOptions { 75 | dsn: args.sentry_dsn.clone(), 76 | traces_sample_rate: args.traces_sample_rate, 77 | release: sentry::release_name!(), 78 | ..sentry::ClientOptions::default() 79 | }); 80 | 81 | tracing_subscriber::registry() 82 | .with(tracing_subscriber::fmt::layer().with_ansi(false)) 83 | .with(sentry_tracing::layer()) 84 | .with( 85 | EnvFilter::builder() 86 | .with_default_directive(LevelFilter::INFO.into()) 87 | .from_env_lossy(), 88 | ) 89 | .init(); 90 | 91 | let satconfig = args.load_satconfig().context("Loading satconf")?; 92 | let tlmcmddb = args.load_tlmcmddb().context("Loading tlmcmddb")?; 93 | let tlm_registry = registry::TelemetryRegistry::from_tlmcmddb_with_apid_map( 94 | &tlmcmddb, 95 | &satconfig.tlm_apid_map, 96 | satconfig.tlm_channel_map, 97 | )?; 98 | let cmd_registry = registry::CommandRegistry::from_tlmcmddb_with_satconfig( 99 | &tlmcmddb, 100 | &satconfig.cmd_apid_map, 101 | satconfig.cmd_prefix_map, 102 | )?; 103 | 104 | let recorder_client = if let Some(recorder_endpoint) = args.recorder_endpoint { 105 | let recorder_client_channel = Channel::builder(recorder_endpoint).connect().await?; 106 | let recorder_client = RecorderClient::new(recorder_client_channel); 107 | Some(recorder_client) 108 | } else { 109 | None 110 | }; 111 | let recorder_layer = recorder_client 112 | .map(RecordHook::new) 113 | .map(BeforeHookLayer::new); 114 | 115 | let tmtc_generic_c2a_service = 116 | proto::tmtc_generic_c2a::Service::new(&tlm_registry, &cmd_registry)?; 117 | 118 | let tlm_bus = telemetry::Bus::new(20); 119 | 120 | let all_tmiv_names = tlm_registry.all_tmiv_names(); 121 | let last_tmiv_store = Arc::new(LastTmivStore::new(all_tmiv_names)); 122 | let store_last_tmiv_hook = telemetry::StoreLastTmivHook::new(last_tmiv_store.clone()); 123 | let tlm_handler = handler::Builder::new() 124 | .before_hook(store_last_tmiv_hook) 125 | .option_layer(recorder_layer.clone()) 126 | .build(tlm_bus.clone()); 127 | 128 | let (link, socket) = kble_gs::new(); 129 | let kble_socket_fut = socket.serve((args.kble_addr, args.kble_port)); 130 | 131 | let (satellite_svc, sat_tlm_reporter) = satellite::new( 132 | satconfig.aos_scid, 133 | satconfig.tc_scid, 134 | tlm_registry, 135 | cmd_registry, 136 | link.downlink(), 137 | link.uplink(), 138 | ); 139 | let sat_tlm_reporter_task = sat_tlm_reporter.run(tlm_handler.clone()); 140 | 141 | let cmd_handler = handler::Builder::new() 142 | .option_layer(recorder_layer) 143 | .build(satellite_svc); 144 | 145 | // Constructing gRPC services 146 | let server_task = { 147 | let broker_service = BrokerService::new(cmd_handler, tlm_bus, last_tmiv_store); 148 | let broker_server = BrokerServer::new(broker_service); 149 | 150 | let tmtc_generic_c2a_server = TmtcGenericC2aServer::new(tmtc_generic_c2a_service); 151 | 152 | let (mut health_reporter, health_service) = tonic_health::server::health_reporter(); 153 | async fn set_serving(health_reporter: &mut HealthReporter, _: &S) { 154 | health_reporter.set_serving::().await; 155 | } 156 | set_serving(&mut health_reporter, &broker_server).await; 157 | set_serving(&mut health_reporter, &tmtc_generic_c2a_server).await; 158 | let grpc_web_layer = GrpcWebLayer::new(); 159 | let cors_layer = CorsLayer::new() 160 | .allow_methods([http::Method::GET, http::Method::POST]) 161 | .allow_headers(tower_http::cors::Any) 162 | .allow_origin(tower_http::cors::Any); 163 | let trace_layer = TraceLayer::new_for_grpc(); 164 | let layer = ServiceBuilder::new() 165 | .layer(trace_layer) 166 | .layer(cors_layer) 167 | .layer(grpc_web_layer); 168 | let reflection_service = tonic_reflection::server::Builder::configure() 169 | .register_encoded_file_descriptor_set(broker::FILE_DESCRIPTOR_SET) 170 | .register_encoded_file_descriptor_set(proto::tmtc_generic_c2a::FILE_DESCRIPTOR_SET) 171 | .build() 172 | .unwrap(); 173 | 174 | let socket_addr = SocketAddr::new(args.broker_addr, args.broker_port); 175 | tracing::info!(message = "starting broker", %socket_addr); 176 | 177 | let rpc_service = Server::builder() 178 | .layer(layer) 179 | .add_service(broker_server) 180 | .add_service(tmtc_generic_c2a_server) 181 | .add_service(health_service) 182 | .add_service(reflection_service) 183 | .into_service(); 184 | 185 | let app = axum::Router::new(); 186 | #[cfg(feature = "devtools")] 187 | let app = app.nest( 188 | "/devtools/", 189 | axum::Router::new().fallback(devtools_server::serve), 190 | ); 191 | let app = app 192 | .route("/", get(|| async { Redirect::to("/devtools/") })) 193 | .route("/devtools", get(|| async { Redirect::to("/devtools/") })) 194 | .fallback_service(HandleError::new(rpc_service, handle_rpc_error)); 195 | axum::Server::bind(&socket_addr).serve(app.into_make_service()) 196 | }; 197 | 198 | tokio::select! { 199 | ret = sat_tlm_reporter_task => Ok(ret?), 200 | ret = kble_socket_fut => Ok(ret?), 201 | ret = server_task => Ok(ret?), 202 | } 203 | } 204 | 205 | async fn handle_rpc_error( 206 | err: Box, 207 | ) -> impl axum::response::IntoResponse { 208 | ( 209 | axum::http::StatusCode::OK, 210 | [ 211 | ("content-type", "application/grpc".to_owned()), 212 | ("grpc-status", "13".to_owned()), 213 | ("content-type", format!("internal error: {err}")), 214 | ], 215 | ) 216 | } 217 | -------------------------------------------------------------------------------- /tmtc-c2a/src/satellite.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time}; 2 | 3 | use crate::{ 4 | registry::{CommandRegistry, FatCommandSchema, TelemetryRegistry}, 5 | tco::{self, ParameterListWriter}, 6 | tmiv, 7 | }; 8 | use anyhow::{anyhow, Result}; 9 | use async_trait::async_trait; 10 | use gaia_ccsds_c2a::{ 11 | ccsds::{self, aos, tc}, 12 | ccsds_c2a::{ 13 | self, 14 | aos::{virtual_channel::Demuxer, SpacePacket}, 15 | tc::{segment, space_packet}, 16 | }, 17 | }; 18 | use gaia_tmtc::{ 19 | tco_tmiv::{Tco, Tmiv}, 20 | Handle, 21 | }; 22 | use tracing::{debug, error, warn}; 23 | 24 | struct TmivBuilder { 25 | tlm_registry: TelemetryRegistry, 26 | } 27 | 28 | impl TmivBuilder { 29 | fn build( 30 | &self, 31 | plugin_received_time: time::SystemTime, 32 | space_packet_bytes: &[u8], 33 | space_packet: ccsds::SpacePacket<&[u8]>, 34 | ) -> Result> { 35 | let plugin_received_time_secs = plugin_received_time 36 | .duration_since(time::UNIX_EPOCH) 37 | .expect("incorrect system clock") 38 | .as_secs(); 39 | 40 | let space_packet = SpacePacket::from_generic(space_packet) 41 | .ok_or_else(|| anyhow!("space packet is too short"))?; 42 | let apid = space_packet.primary_header.apid(); 43 | let tlm_id = space_packet.secondary_header.telemetry_id(); 44 | let Some(telemetry) = self.tlm_registry.lookup(apid, tlm_id) else { 45 | return Err(anyhow!("unknown tlm_id: {tlm_id} from apid: {apid}")); 46 | }; 47 | let channels = self 48 | .tlm_registry 49 | .find_channels(space_packet.secondary_header.destination_flags()); 50 | let mut fields = vec![]; 51 | tmiv::FieldsBuilder::new(&telemetry.schema).build(&mut fields, space_packet_bytes)?; 52 | let tmivs = channels 53 | .map(|channel| { 54 | let name = telemetry.build_tmiv_name(channel); 55 | Tmiv { 56 | name: name.to_string(), 57 | plugin_received_time: plugin_received_time_secs, 58 | timestamp: Some(plugin_received_time.into()), 59 | fields: fields.clone(), 60 | } 61 | }) 62 | .collect(); 63 | Ok(tmivs) 64 | } 65 | } 66 | 67 | struct CommandContext<'a> { 68 | tc_scid: u16, 69 | fat_schema: FatCommandSchema<'a>, 70 | tco: &'a Tco, 71 | } 72 | 73 | impl<'a> CommandContext<'a> { 74 | fn build_tc_segment(&self, data_field_buf: &mut [u8]) -> Result { 75 | let mut segment = segment::Builder::new(data_field_buf).unwrap(); 76 | segment.use_default(); 77 | 78 | let space_packet_bytes = segment.body_mut(); 79 | let mut space_packet = space_packet::Builder::new(&mut space_packet_bytes[..]).unwrap(); 80 | let tco_reader = tco::Reader::new(self.tco); 81 | let params_writer = ParameterListWriter::new(self.fat_schema.schema); 82 | space_packet.use_default(); 83 | let ph = space_packet.ph_mut(); 84 | ph.set_version_number(0); // always zero 85 | ph.set_apid(self.fat_schema.apid); 86 | let sh = space_packet.sh_mut(); 87 | sh.set_command_id(self.fat_schema.command_id); 88 | sh.set_destination_type(self.fat_schema.destination_type); 89 | sh.set_execution_type(self.fat_schema.execution_type); 90 | if self.fat_schema.has_time_indicator { 91 | sh.set_time_indicator(tco_reader.time_indicator()?); 92 | } else { 93 | sh.set_time_indicator(0); 94 | } 95 | let user_data_len = params_writer.write_all( 96 | space_packet.user_data_mut(), 97 | tco_reader.parameters().into_iter(), 98 | )?; 99 | let space_packet_len = space_packet.finish(user_data_len); 100 | let segment_len = segment.finish(space_packet_len); 101 | Ok(segment_len) 102 | } 103 | 104 | async fn transmit_to(&self, sync_and_channel_coding: &mut T) -> Result<()> 105 | where 106 | T: tc::SyncAndChannelCoding, 107 | { 108 | let vcid = 0; // FIXME: make this configurable 109 | let frame_type = tc::sync_and_channel_coding::FrameType::TypeBD; 110 | let sequence_number = 0; // In case of Type-BD, it's always zero. 111 | let mut data_field = vec![0u8; 1017]; // FIXME: hard-coded max size 112 | let segment_len = self.build_tc_segment(&mut data_field)?; 113 | data_field.truncate(segment_len); 114 | sync_and_channel_coding 115 | .transmit(self.tc_scid, vcid, frame_type, sequence_number, &data_field) 116 | .await?; 117 | Ok(()) 118 | } 119 | } 120 | 121 | #[derive(Clone)] 122 | pub struct Service { 123 | sync_and_channel_coding: T, 124 | registry: Arc, 125 | tc_scid: u16, 126 | } 127 | 128 | impl Service 129 | where 130 | T: tc::SyncAndChannelCoding, 131 | { 132 | async fn try_handle_command(&mut self, tco: &Tco) -> Result { 133 | let Some(fat_schema) = self.registry.lookup(&tco.name) else { 134 | return Ok(false); 135 | }; 136 | let ctx = CommandContext { 137 | tc_scid: self.tc_scid, 138 | fat_schema, 139 | tco, 140 | }; 141 | ctx.transmit_to(&mut self.sync_and_channel_coding).await?; 142 | Ok(true) 143 | } 144 | } 145 | 146 | #[allow(clippy::too_many_arguments)] 147 | pub fn new( 148 | aos_scid: u16, 149 | tc_scid: u16, 150 | tlm_registry: TelemetryRegistry, 151 | cmd_registry: impl Into>, 152 | receiver: R, 153 | transmitter: T, 154 | ) -> (Service, TelemetryReporter) 155 | where 156 | T: tc::SyncAndChannelCoding, 157 | R: aos::SyncAndChannelCoding, 158 | { 159 | ( 160 | Service { 161 | tc_scid, 162 | sync_and_channel_coding: transmitter, 163 | registry: cmd_registry.into(), 164 | }, 165 | TelemetryReporter { 166 | aos_scid, 167 | receiver, 168 | tmiv_builder: TmivBuilder { tlm_registry }, 169 | }, 170 | ) 171 | } 172 | 173 | #[async_trait] 174 | impl Handle> for Service 175 | where 176 | T: tc::SyncAndChannelCoding + Clone + Send + Sync + 'static, 177 | { 178 | type Response = Option<()>; 179 | 180 | async fn handle(&mut self, tco: Arc) -> Result { 181 | Ok(self.try_handle_command(&tco).await?.then_some(())) 182 | } 183 | } 184 | 185 | pub struct TelemetryReporter { 186 | #[allow(unused)] 187 | aos_scid: u16, 188 | tmiv_builder: TmivBuilder, 189 | receiver: R, 190 | } 191 | 192 | impl TelemetryReporter 193 | where 194 | R: aos::SyncAndChannelCoding, 195 | { 196 | pub async fn run(mut self, mut tlm_handler: H) -> Result<()> 197 | where 198 | H: Handle, Response = ()>, 199 | { 200 | let mut demuxer = Demuxer::default(); 201 | loop { 202 | let tf_buf = self.receiver.receive().await?; 203 | let mut plugin_received_time = time::SystemTime::now(); 204 | let tf: Option> = tf_buf.transfer_frame(); 205 | let Some(tf) = tf else { 206 | let bytes = tf_buf.into_inner(); 207 | warn!( 208 | "transfer frame is too short ({} bytes): {:02x?}", 209 | bytes.len(), 210 | bytes 211 | ); 212 | continue; 213 | }; 214 | let incoming_scid = tf.primary_header.scid(); 215 | if incoming_scid != self.aos_scid { 216 | warn!("unknown SCID: {incoming_scid}"); 217 | continue; 218 | } 219 | let vcid = tf.primary_header.vcid(); 220 | let channel = demuxer.demux(vcid); 221 | let frame_count = tf.primary_header.frame_count(); 222 | if let Err(expected) = channel.synchronizer.next(frame_count) { 223 | warn!( 224 | %vcid, 225 | "some transfer frames has been dropped: expected frame count: {} but got {}", 226 | expected, frame_count, 227 | ); 228 | channel.defragmenter.reset(); 229 | } 230 | if let Err(e) = channel.defragmenter.push(tf.data_unit_zone) { 231 | warn!(%vcid, "malformed M_PDU: {}", e); 232 | channel.synchronizer.reset(); 233 | channel.defragmenter.reset(); 234 | continue; 235 | } 236 | while let Some((space_packet_bytes, space_packet)) = 237 | channel.defragmenter.read_as_bytes_and_packet() 238 | { 239 | if space_packet.primary_header.is_idle_packet() { 240 | debug!("skipping idle packet"); 241 | } else { 242 | match self.tmiv_builder.build( 243 | plugin_received_time, 244 | space_packet_bytes, 245 | space_packet, 246 | ) { 247 | Ok(tmivs) => { 248 | for tmiv in tmivs { 249 | if let Err(e) = tlm_handler.handle(Arc::new(tmiv)).await { 250 | error!("failed to handle telemetry: {:?}", e); 251 | } 252 | } 253 | } 254 | Err(e) => { 255 | warn!(%vcid, "failed to build TMIV from space packet: {}", e); 256 | channel.defragmenter.reset(); 257 | break; 258 | } 259 | }; 260 | // NOTE: workaround to avoid timestamp collision 261 | plugin_received_time += time::Duration::from_nanos(1); 262 | } 263 | channel.defragmenter.advance(); 264 | } 265 | } 266 | } 267 | } 268 | --------------------------------------------------------------------------------