From 17bd1ed9c69585e2787221843e64fe66f9202f8a Mon Sep 17 00:00:00 2001
From: KOBAYASHI Kazuhiro <kobayashi-kazuhiro@arkedgespace.com>
Date: Thu, 4 Jan 2024 17:36:16 +0900
Subject: [PATCH] devtools: use display info

---
 .../crates/wasm-interpolate/.gitignore        |  6 ++
 .../crates/wasm-interpolate/Cargo.toml        | 29 ++++++++++
 .../crates/wasm-interpolate/src/lib.rs        | 56 +++++++++++++++++++
 .../crates/wasm-interpolate/src/utils.rs      | 10 ++++
 tmtc-c2a/devtools_frontend/package.json       |  8 ++-
 .../src/components/TelemetryView.tsx          | 51 +++++++++++++----
 .../src/proto/tmtc_generic_c2a.ts             | 35 ++++++++++--
 7 files changed, 177 insertions(+), 18 deletions(-)
 create mode 100644 tmtc-c2a/devtools_frontend/crates/wasm-interpolate/.gitignore
 create mode 100644 tmtc-c2a/devtools_frontend/crates/wasm-interpolate/Cargo.toml
 create mode 100644 tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/lib.rs
 create mode 100644 tmtc-c2a/devtools_frontend/crates/wasm-interpolate/src/utils.rs

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..c500603e
--- /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.0.79"
+
+[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..97746758
--- /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<Self> {
+        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<String> {
+    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]
+pub fn format_value(format_string: &str, arg: &JsValue) -> Result<String, String> {
+    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 31754e22..b47c35fa 100644
--- a/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx
+++ b/tmtc-c2a/devtools_frontend/src/components/TelemetryView.tsx
@@ -7,25 +7,34 @@ import { useParams } from "react-router-dom";
 import { Helmet } from "react-helmet-async";
 import { TelemetrySchema } from "../proto/tmtc_generic_c2a";
 
+import initInterpolate, * as interpolate from "../../crates/wasm-interpolate/pkg";
+
+initInterpolate();
+
+type DisplayInfo = {
+  formatString: string;
+};
+
 const buildTelemetryFieldTreeBlueprintFromSchema = (
   tlm: TelemetrySchema,
-): TreeNamespace<undefined> => {
-  const fieldNames = tlm.fields.map((f) => f.name);
-  const root: TreeNamespace<undefined> = new Map();
-  for (const fieldName of fieldNames) {
-    const path = fieldName.split(".");
-    addToNamespace(root, path, undefined);
+): TreeNamespace<DisplayInfo> => {
+  const root: TreeNamespace<DisplayInfo> = 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: TreeNamespace<undefined>,
+  blueprint: TreeNamespace<DisplayInfo>,
   fields: TmivField[],
 ): TreeNamespace<TelemetryValuePair> => {
   const convertedFieldMap = new Map<string, TmivField["value"]>();
@@ -38,15 +47,33 @@ const buildTelemetryFieldTree = (
       convertedFieldMap.set(field.name, field.value);
     }
   }
-  return mapNamespace(blueprint, (path, _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) {
+    return "****";
+  }
+  try {
+    const ks = Object.keys(value).find((k) => k !== "oneofKind")!;
+    const rawValue = value[ks as keyof typeof value]!; //FIXME: ????
+    const interpolated = interpolate.format_value("{value:#0x}", rawValue);
+    return defaultPrettyPrint(value) + "/" + interpolated;
+  } catch (e) {
+    // TODO: show warning
+    return defaultPrettyPrint(value) + "!";
+  }
+};
+
+const defaultPrettyPrint = (value: TmivField["value"] | null) => {
   if (value === null) {
     return "****";
   }
@@ -76,7 +103,7 @@ const LeafCell: React.FC<ValueCellProps> = ({ name, value }) => {
       <span className="text-slate-300">{name}</span>
       <span className="min-w-[2ch]" />
       <span className="font-bold text-right">
-        {prettyprintValue(value.converted)}
+        {prettyprintValue(value.converted, value.displayInfo)}
       </span>
     </div>
   );
@@ -149,7 +176,7 @@ const InlineNamespaceContentCell: React.FC<InlineNamespaceContentCellProps> = ({
               <span className="ml-[0.5ch]" key={name}>
                 <span className="text-slate-300">{name}:</span>
                 <span className="font-bold">
-                  {prettyprintValue(v.value.converted)}
+                  {prettyprintValue(v.value.converted, v.value.displayInfo)}
                 </span>
               </span>
             );
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<TelemetryFieldSchemaMetadata> {
     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>): TelemetryFieldSchemaMetadata {
-        const message = {};
+        const message = { displayFormat: "" };
         globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
         if (value !== undefined)
             reflectionMergePartial<TelemetryFieldSchemaMetadata>(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);