diff --git a/Cargo.lock b/Cargo.lock index 62c71d14..550c93b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" dependencies = [ "backtrace", ] @@ -336,6 +336,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -870,6 +880,12 @@ dependencies = [ "hashbrown 0.14.2", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + [[package]] name = "ipnet" version = "2.9.0" @@ -893,9 +909,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1544,6 +1560,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1975,8 +1997,6 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tlmcmddb" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad369378cb3161705423ac55a49190816b6044296b576bdf9d7049dba5b9132" dependencies = [ "serde", ] @@ -2474,9 +2494,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2484,9 +2504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -2499,9 +2519,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -2511,9 +2531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2521,9 +2541,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -2534,9 +2554,46 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139bd73305d50e1c1c4333210c0db43d989395b64a237bd35c10ef3832a7f70c" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "70072aebfe5da66d2716002c729a14e4aec4da0e23cc2ea66323dac541c93928" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "wasm-interpolate" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "interpolator", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-test", +] [[package]] name = "web-sys" @@ -2711,3 +2768,7 @@ dependencies = [ "quote", "syn 2.0.39", ] + +[[patch.unused]] +name = "tlmcmddb-csv" +version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index da34022d..9e797ac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "gaia-tmtc", "gaia-ccsds-c2a", "tmtc-c2a", + "tmtc-c2a/devtools_frontend/crates/wasm-interpolate" ] diff --git a/gaia-ccsds-c2a/src/access/tlm/schema.rs b/gaia-ccsds-c2a/src/access/tlm/schema.rs index ef0ff76c..c060c58a 100644 --- a/gaia-ccsds-c2a/src/access/tlm/schema.rs +++ b/gaia-ccsds-c2a/src/access/tlm/schema.rs @@ -117,7 +117,7 @@ pub struct FieldIter<'a> { } impl<'a> Iterator for FieldIter<'a> { - type Item = Result<(&'a str, FieldSchema)>; + type Item = Result<(&'a str, &'a tlmdb::DisplayInfo, FieldSchema)>; fn next(&mut self) -> Option { let (obs, field, bit_range) = self.fields.next()?; @@ -131,10 +131,11 @@ fn build_field_schema( obs: tlmdb::OnboardSoftwareInfo, field: &tlmdb::Field, bit_range: Range, -) -> Result<(&str, FieldSchema)> { +) -> Result<(&str, &tlmdb::DisplayInfo, FieldSchema)> { let converter = build_integral_converter(&field.conversion_info); Ok(( &field.name, + &field.display_info, match obs.variable_type { tlmdb::VariableType::Int8 => FieldSchema::Integral(IntegralFieldSchema { converter, diff --git a/tmtc-c2a/build.rs b/tmtc-c2a/build.rs index 329c4d1a..17b3d7f3 100644 --- a/tmtc-c2a/build.rs +++ b/tmtc-c2a/build.rs @@ -1,6 +1,26 @@ use std::process::Command; use std::{env, path::PathBuf}; +fn wasm_packages_root() -> PathBuf { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let wasm_outdir = out_dir.join("wasm_packages"); + wasm_outdir +} + +fn wasm_pack(name: &str) { + let pkg_outdir = wasm_packages_root().join(name).join("pkg"); + let status = Command::new("yarn") + .current_dir("devtools_frontend") + .arg("run") + .arg("crate") + .arg(name) + .arg("--out-dir") + .arg(&pkg_outdir) + .status() + .expect("failed to build frontend"); + assert!(status.success()); +} + fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); tonic_build::configure() @@ -15,8 +35,12 @@ fn main() { .status() .expect("failed to build frontend"); assert!(status.success()); + + wasm_pack("wasm-interpolate"); let devtools_out_dir = out_dir.join("devtools_dist"); let status = Command::new("yarn") + // vite.config.ts にwasmのビルド場所を教えるために環境変数を渡す + .envs([("DEVTOOLS_CRATE_ROOT", wasm_packages_root())]) .current_dir("devtools_frontend") .arg("run") .arg("build:vite") diff --git a/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/.gitignore b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/.gitignore new file mode 100644 index 00000000..4e301317 --- /dev/null +++ b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/Cargo.toml b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/Cargo.toml new file mode 100644 index 00000000..1dd5f79a --- /dev/null +++ b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "wasm-interpolate" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.84" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } +interpolator = { version = "0.5.0", features = ["number"] } +js-sys = "0.3.66" +anyhow = "1" + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/lib.rs b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/lib.rs new file mode 100644 index 00000000..dbb7b70d --- /dev/null +++ b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/lib.rs @@ -0,0 +1,56 @@ +mod utils; + +use interpolator::{format, Formattable}; +use wasm_bindgen::prelude::*; + +use anyhow::{anyhow, Result}; +use js_sys::BigInt; +use std::collections::HashMap; + +enum Value { + I64(i64), + F64(f64), + String(String), +} + +impl Value { + pub fn formattable(&self) -> Formattable { + use Value::*; + match self { + I64(v) => Formattable::integer(v), + F64(v) => Formattable::float(v), + String(v) => Formattable::display(v), + } + } +} + +impl TryFrom<&JsValue> for Value { + type Error = anyhow::Error; + fn try_from(value: &JsValue) -> Result { + if value.is_bigint() { + let value = BigInt::new(&value) + .map_err(|_| anyhow!("not a bigint"))? + .try_into() + .map_err(|_| anyhow!("couldn't convert bigint to i64"))?; + Ok(Value::I64(value)) + } else if let Some(v) = value.as_f64() { + Ok(Value::F64(v)) + } else if let Some(s) = value.as_string() { + Ok(Value::String(s)) + } else { + Err(anyhow!("not a string, f64, or bigint")) + } + } +} + +pub fn format_value_inner(format_string: &str, arg: &JsValue) -> Result { + let arg = Value::try_from(arg)?; + let arg = arg.formattable(); + let args = HashMap::from([("value", arg)]); + format(format_string, &args).map_err(Into::into) +} + +#[wasm_bindgen(js_name = formatValue)] +pub fn format_value(format_string: &str, arg: &JsValue) -> Result { + format_value_inner(format_string, arg).map_err(|e| e.to_string()) +} diff --git a/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/utils.rs b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/utils.rs new file mode 100644 index 00000000..b1d7929d --- /dev/null +++ b/tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/tmtc-c2a/devtools_frontend/package.json b/tmtc-c2a/devtools_frontend/package.json index 49bc4562..5ace92d1 100644 --- a/tmtc-c2a/devtools_frontend/package.json +++ b/tmtc-c2a/devtools_frontend/package.json @@ -7,10 +7,16 @@ "codegen:proto:tmtc_generic_c2a": "protoc --ts_out src/proto --proto_path ../../tmtc-c2a/proto ../../tmtc-c2a/proto/tmtc_generic_c2a.proto", "codegen:proto": "run-p codegen:proto:*", "codegen": "run-s codegen:proto", + "crate:build": "cd crates && wasm-pack build --target web --release", + "crate:dev": "cd crates && cargo watch -s 'wasm-pack build --target web --dev' -C", + "crate": "yarn crate:${MODE:-build}", + "crates:wasm-interpolate": "yarn crate wasm-interpolate", + "dev:crates": "MODE=dev run-p crates:*", "dev:vite": "vite --host", "dev": "run-p dev:*", + "build:crates": "run-s crates:*", "build:vite": "vite build", - "build": "run-s build:vite", + "build": "run-s build:crates build:vite", "typecheck": "tsc", "lint:prettier": "prettier . --check", "lint:eslint": "eslint . --format stylish", diff --git a/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx b/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx index 9441b56e..9338613a 100644 --- a/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx +++ b/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx @@ -1,10 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { - TreeBlueprintNamespace, - TreeNamespace, - buildTree, - digTreeBlueprintNamespace, -} from "../tree"; +import { TreeNamespace, addToNamespace, mapNamespace } from "../tree"; import { Tmiv, TmivField } from "../proto/tco_tmiv"; import { useClient } from "./Layout"; @@ -12,26 +7,35 @@ import { useParams } from "react-router-dom"; import { Helmet } from "react-helmet-async"; import { TelemetrySchema } from "../proto/tmtc_generic_c2a"; -const buildTelemetryFieldTreeBlueprintFromSchema = (tlm: TelemetrySchema) => { - const fieldNames = tlm.fields.map((f) => f.name); - const root: TreeBlueprintNamespace = new Map(); - for (const fieldName of fieldNames) { - const path = fieldName.split("."); - const basename = path.pop()!; - const ns = digTreeBlueprintNamespace(root, path); - ns.set(basename, { type: "leaf", key: fieldName }); +import initInterpolate, * as interpolate from "@crate/wasm-interpolate/pkg"; + +initInterpolate(); + +type DisplayInfo = { + formatString: string; +}; + +const buildTelemetryFieldTreeBlueprintFromSchema = ( + tlm: TelemetrySchema +): TreeNamespace => { + const root: TreeNamespace = new Map(); + for (const field of tlm.fields) { + const path = field.name.split("."); + const formatString = field.metadata?.displayFormat ?? ""; + addToNamespace(root, path, { formatString }); } return root; }; type TelemetryValuePair = { + displayInfo: DisplayInfo; converted: TmivField["value"] | null; raw: TmivField["value"] | null; }; const buildTelemetryFieldTree = ( - blueprint: TreeBlueprintNamespace, - fields: TmivField[], + blueprint: TreeNamespace, + fields: TmivField[] ): TreeNamespace => { const convertedFieldMap = new Map(); const rawFieldMap = new Map(); @@ -43,14 +47,31 @@ const buildTelemetryFieldTree = ( convertedFieldMap.set(field.name, field.value); } } - return buildTree(blueprint, (key) => { + return mapNamespace(blueprint, (path, displayInfo) => { + const key = path.join("."); const converted = convertedFieldMap.get(key) ?? null; const raw = rawFieldMap.get(key) ?? null; - return { converted, raw }; + return { displayInfo, converted, raw }; }); }; -const prettyprintValue = (value: TmivField["value"] | null) => { +const prettyprintValue = ( + value: TmivField["value"] | null, + displayInfo: DisplayInfo +) => { + if (value === null || value.oneofKind === undefined || displayInfo.formatString === "") { + return defaultPrettyPrint(value); + } + try { + const v = (value as any)[value.oneofKind]; + return interpolate.formatValue(displayInfo.formatString, v); + } catch (e) { + // TODO: show warning + return defaultPrettyPrint(value) + "!"; + } +}; + +const defaultPrettyPrint = (value: TmivField["value"] | null) => { if (value === null) { return "****"; } @@ -80,7 +101,7 @@ const LeafCell: React.FC = ({ name, value }) => { {name} - {prettyprintValue(value.converted)} + {prettyprintValue(value.converted, value.displayInfo)} ); @@ -153,7 +174,7 @@ const InlineNamespaceContentCell: React.FC = ({ {name}: - {prettyprintValue(v.value.converted)} + {prettyprintValue(v.value.converted, v.value.displayInfo)} ); @@ -197,10 +218,10 @@ export const TelemetryView: React.FC = () => { const telemetryDef = useMemo(() => { const [_channel, componentName, telemetryName] = tmivName.split("."); const [_c, componentDef] = Object.entries(telemetryComponents).find( - ([name, _]) => name === componentName, + ([name, _]) => name === componentName )!; const [_t, telemetryDef] = Object.entries(componentDef.telemetries).find( - ([name, _]) => name === telemetryName, + ([name, _]) => name === telemetryName )!; return telemetryDef; }, [telemetryComponents, tmivName]); diff --git a/tmtc-c2a/devtools_frontend/src/proto/tmtc_generic_c2a.ts b/tmtc-c2a/devtools_frontend/src/proto/tmtc_generic_c2a.ts index 9f9a80ae..50920cf0 100644 --- a/tmtc-c2a/devtools_frontend/src/proto/tmtc_generic_c2a.ts +++ b/tmtc-c2a/devtools_frontend/src/proto/tmtc_generic_c2a.ts @@ -181,11 +181,15 @@ export interface TelemetryFieldSchema { name: string; // TODO: TelemetryFieldDataType data_type = 3; } /** - * TODO: string description = 1; - * * @generated from protobuf message tmtc_generic_c2a.TelemetryFieldSchemaMetadata */ export interface TelemetryFieldSchemaMetadata { + /** + * TODO: string description = 1; + * + * @generated from protobuf field: string display_format = 1; + */ + displayFormat: string; } /** * @generated from protobuf message tmtc_generic_c2a.TelemetryChannelSchema @@ -1070,19 +1074,40 @@ export const TelemetryFieldSchema = new TelemetryFieldSchema$Type(); // @generated message type with reflection information, may provide speed optimized methods class TelemetryFieldSchemaMetadata$Type extends MessageType { constructor() { - super("tmtc_generic_c2a.TelemetryFieldSchemaMetadata", []); + super("tmtc_generic_c2a.TelemetryFieldSchemaMetadata", [ + { no: 1, name: "display_format", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); } create(value?: PartialMessage): TelemetryFieldSchemaMetadata { - const message = {}; + const message = { displayFormat: "" }; globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this }); if (value !== undefined) reflectionMergePartial(this, message, value); return message; } internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TelemetryFieldSchemaMetadata): TelemetryFieldSchemaMetadata { - return target ?? this.create(); + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string display_format */ 1: + message.displayFormat = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; } internalBinaryWrite(message: TelemetryFieldSchemaMetadata, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string display_format = 1; */ + if (message.displayFormat !== "") + writer.tag(1, WireType.LengthDelimited).string(message.displayFormat); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/tmtc-c2a/devtools_frontend/src/tree.ts b/tmtc-c2a/devtools_frontend/src/tree.ts index d452084f..82169fda 100644 --- a/tmtc-c2a/devtools_frontend/src/tree.ts +++ b/tmtc-c2a/devtools_frontend/src/tree.ts @@ -1,56 +1,109 @@ -export type TreeBlueprintNamespace = Map; -export type TreeBlueprintNode = - | { type: "leaf"; key: string } - | { type: "ns"; ns: TreeBlueprintNamespace }; -export const digTreeBlueprintNamespace = ( - ns: TreeBlueprintNamespace, - path: string[], -): TreeBlueprintNamespace => { - if (path.length === 0) { - return ns; - } - const [pathHead, ...pathTail] = path; - const childNs = (() => { - if (ns.has(pathHead)) { - const node = ns.get(pathHead)!; - switch (node.type) { - case "ns": - return node.ns; - case "leaf": { - const childNs: TreeBlueprintNamespace = new Map(); - childNs.set("", node); - ns.set(pathHead, { type: "ns", ns: childNs }); - return childNs; - } - } - } else { - const childNs: TreeBlueprintNamespace = new Map(); - ns.set(pathHead, { type: "ns", ns: childNs }); - return childNs; - } - })(); - return digTreeBlueprintNamespace(childNs, pathTail); -}; - export type TreeNamespace = Map>; export type TreeNode = | { type: "leaf"; value: T } | { type: "ns"; ns: TreeNamespace }; -export const buildTree = ( - blueprint: TreeBlueprintNamespace, - getValue: (key: string) => T, -): TreeNamespace => { - const map = new Map(); - for (const [key, value] of blueprint) { - switch (value.type) { - case "leaf": - map.set(key, { type: "leaf", value: getValue(value.key) }); - break; - case "ns": - map.set(key, { type: "ns", ns: buildTree(value.ns, getValue) }); - break; +const makeLeaf = (value: T): TreeNode => ({ type: "leaf", value }); +const makeNs = (ns: TreeNamespace): TreeNode => ({ type: "ns", ns }); + +const mapMapWithKey = ( + map: Map, + f: (key: K, value: T) => U, +): Map => { + const result = new Map(); + for (const [key, value] of map) { + result.set(key, f(key, value)); + } + return result; +}; + +const mapTreeRec = ( + tree: TreeNode, + path: string[], + f: (path: string[], node: T) => U, +): TreeNode => { + switch (tree.type) { + case "leaf": + return makeLeaf(f(path, tree.value)); + case "ns": + return makeNs( + mapMapWithKey(tree.ns, (key, child) => + mapTreeRec(child, [...path, key], f), + ), + ); + } +}; + +export const mapTree = ( + tree: TreeNode, + f: (path: string[], node: T) => U, +): TreeNode => { + return mapTreeRec(tree, [], f); +}; + +export const mapNamespace = ( + ns: TreeNamespace, + f: (path: string[], node: T) => U, +): TreeNamespace => { + const tree = makeNs(ns); + const mappedTree = mapTreeRec(tree, [], f); + if (mappedTree.type === "ns") { + return mappedTree.ns; + } else { + throw new Error("Impossible"); + } +}; + +export const singleton = (path: string[], value: T): TreeNode => { + let tree: TreeNode = { type: "leaf", value }; + for (const key of path.toReversed()) { + const ns = new Map(); + ns.set(key, tree); + tree = { type: "ns", ns }; + } + return tree; +}; + +export const add = ( + tree: TreeNode, + path: string[], + value: T, +): TreeNode => { + switch (tree.type) { + case "leaf": + if (path.length === 0) { + tree.value = value; + return tree; + } else { + const [pathHead, ...pathTail] = path; + const ns = new Map(); + ns.set("", tree); + ns.set(pathHead, singleton(pathTail, value)); + return { + type: "ns", + ns, + }; + } + case "ns": + addToNamespace(tree.ns, path, value); + return tree; + } +}; + +export const addToNamespace = ( + ns: TreeNamespace, + path: string[], + value: T, +): void => { + if (path.length === 0) { + ns.set("", { type: "leaf", value }); + } else { + const [pathHead, ...pathTail] = path; + const child = ns.get(pathHead); + if (child === undefined) { + ns.set(pathHead, singleton(pathTail, value)); + } else { + ns.set(pathHead, add(child, pathTail, value)); } } - return map; }; diff --git a/tmtc-c2a/devtools_frontend/tsconfig.json b/tmtc-c2a/devtools_frontend/tsconfig.json index 0c020dfd..ec676f17 100644 --- a/tmtc-c2a/devtools_frontend/tsconfig.json +++ b/tmtc-c2a/devtools_frontend/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "baseUrl": "./", + "paths": { + "@crate/*": ["crates/*"] + }, "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], "types": ["vite/client"], diff --git a/tmtc-c2a/devtools_frontend/vite.config.ts b/tmtc-c2a/devtools_frontend/vite.config.ts index b212eb73..64d24954 100644 --- a/tmtc-c2a/devtools_frontend/vite.config.ts +++ b/tmtc-c2a/devtools_frontend/vite.config.ts @@ -3,7 +3,17 @@ import { defineConfig } from "vite"; import pluginRewriteAll from "vite-plugin-rewrite-all"; export default defineConfig({ - resolve: {}, + resolve: { + alias: [ + { + find: "@crate/", + // wasmpackでrustソースから作られたpkgの場所を指定する + // cargo build の際はbuild.rsによってDEVTOOLS_CRATE_ROOTが指定される + // yarn dev している場合はrustソースのディレクトリに直接pkgを配置し、DEVTOOLS_CRATE_ROOTは指定されない + replacement: (process.env.DEVTOOLS_CRATE_ROOT ?? "/crates") + "/", + }, + ], + }, base: "/devtools/", plugins: [react(), pluginRewriteAll()], server: { diff --git a/tmtc-c2a/proto/tmtc_generic_c2a.proto b/tmtc-c2a/proto/tmtc_generic_c2a.proto index 8b71d015..bd637972 100644 --- a/tmtc-c2a/proto/tmtc_generic_c2a.proto +++ b/tmtc-c2a/proto/tmtc_generic_c2a.proto @@ -79,6 +79,7 @@ message TelemetryFieldSchema { message TelemetryFieldSchemaMetadata { // TODO: string description = 1; + string display_format = 1; } message TelemetryChannelSchema { diff --git a/tmtc-c2a/src/registry/tlm.rs b/tmtc-c2a/src/registry/tlm.rs index 0a9ef520..e74f06bc 100644 --- a/tmtc-c2a/src/registry/tlm.rs +++ b/tmtc-c2a/src/registry/tlm.rs @@ -8,6 +8,7 @@ use gaia_ccsds_c2a::access::tlm::schema::{ from_tlmcmddb, FieldSchema, FloatingFieldSchema, IntegralFieldSchema, }; use itertools::Itertools; +use tlmcmddb::tlm::DisplayInfo; use crate::{ proto::tmtc_generic_c2a::{self as proto}, @@ -55,6 +56,7 @@ pub struct FieldMetadata { original_name: String, pub converted_name: String, pub raw_name: String, + display_format: Option, } #[derive(Debug, Clone)] @@ -95,7 +97,9 @@ impl Registry { .chain(fat_tlm_schema.schema.floating_fields.iter().map(|(m, _)| m)) .sorted_by_key(|m| m.order) .map(|m| proto::TelemetryFieldSchema { - metadata: Some(proto::TelemetryFieldSchemaMetadata {}), + metadata: Some(proto::TelemetryFieldSchemaMetadata { + display_format: m.display_format.clone().unwrap_or_default(), + }), name: m.original_name.to_string(), }) .collect(); @@ -195,15 +199,15 @@ impl Registry { } fn build_telemetry_schema<'a>( - iter: impl Iterator>, + iter: impl Iterator>, ) -> Result { let mut schema = TelemetrySchema { integral_fields: vec![], floating_fields: vec![], }; for (order, pair) in iter.enumerate() { - let (field_name, field_schema) = pair?; - let name_pair = build_field_metadata(order, field_name); + let (field_name, display_info, field_schema) = pair?; + let name_pair = build_field_metadata(order, field_name, display_info); match field_schema { FieldSchema::Integral(field_schema) => { schema.integral_fields.push((name_pair, field_schema)); @@ -216,11 +220,16 @@ fn build_telemetry_schema<'a>( Ok(schema) } -fn build_field_metadata(order: usize, tlmdb_name: &str) -> FieldMetadata { +fn build_field_metadata( + order: usize, + tlmdb_name: &str, + display_info: &DisplayInfo, +) -> FieldMetadata { FieldMetadata { order, original_name: tlmdb_name.to_string(), converted_name: tlmdb_name.to_string(), raw_name: format!("{tlmdb_name}@RAW"), + display_format: display_info.format.clone(), } }