Add AOS Starter Status Webpage to Debugging Site

Signed-off-by: Niko Sohmers <nikolai@sohmers.com>
Change-Id: Ie6109a6551b9ba44fa025bbe59d71a6a853e11b8
diff --git a/aos/starter/BUILD b/aos/starter/BUILD
index a7d7e02..0ebe0f5 100644
--- a/aos/starter/BUILD
+++ b/aos/starter/BUILD
@@ -1,5 +1,6 @@
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("//aos:config.bzl", "aos_config")
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 
 exports_files(["roborio_irq_config.json"])
 
@@ -191,6 +192,14 @@
     deps = ["//aos/util:process_info_fbs"],
 )
 
+flatbuffer_ts_library(
+    name = "starter_ts_fbs",
+    srcs = ["starter.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = ["//aos/util:process_info_ts_fbs"],
+)
+
 static_flatbuffer(
     name = "kthread_fbs",
     srcs = ["kthread.fbs"],
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 599d477..206bd8f 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -1,6 +1,7 @@
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
 load("config_validator_macro.bzl", "config_validator_test")
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -373,6 +374,13 @@
     visibility = ["//visibility:public"],
 )
 
+flatbuffer_ts_library(
+    name = "process_info_ts_fbs",
+    srcs = ["process_info.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+)
+
 cc_library(
     name = "top",
     srcs = ["top.cc"],
diff --git a/y2024/BUILD b/y2024/BUILD
index 7568050..6687dac 100644
--- a/y2024/BUILD
+++ b/y2024/BUILD
@@ -340,6 +340,7 @@
         "//aos/network:log_web_proxy_main",
         "//y2024/www:field_main_bundle.min.js",
         "//y2024/www:files",
+        "//y2024/www:starter_main_bundle.min.js",
     ],
     target_compatible_with = ["@platforms//os:linux"],
 )
diff --git a/y2024/www/BUILD b/y2024/www/BUILD
index 5cb57c4..b6b6b9b 100644
--- a/y2024/www/BUILD
+++ b/y2024/www/BUILD
@@ -55,11 +55,38 @@
     ],
 )
 
+ts_project(
+    name = "starter_main",
+    srcs = [
+        "starter_handler.ts",
+        "starter_main.ts",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "//aos/network:connect_ts_fbs",
+        "//aos/network:message_bridge_client_ts_fbs",
+        "//aos/network/www:proxy",
+        "//aos/starter:starter_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+    ],
+)
+
+rollup_bundle(
+    name = "starter_main_bundle",
+    entry_point = "starter_main.ts",
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2024:__subpackages__"],
+    deps = [
+        ":starter_main",
+    ],
+)
+
 aos_downloader_dir(
     name = "www_files",
     srcs = [
         ":field_main_bundle.min.js",
         ":files",
+        ":starter_main_bundle.min.js",
         "//frc971/analysis:plot_index_bundle.min.js",
     ],
     dir = "www",
diff --git a/y2024/www/field.html b/y2024/www/field.html
index a8f28b1..ebb03e5 100644
--- a/y2024/www/field.html
+++ b/y2024/www/field.html
@@ -78,15 +78,15 @@
         </tr>
         <tr>
           <td>Transfer Beambreak</td>
-          <td id="transfer_beambreak">FALSE</td>
+          <td id="transfer_beambreak"> NA </td>
         </tr>
         <tr>
           <td>Extend Beambreak</td>
-          <td id="extend_beambreak">FALSE</td>
+          <td id="extend_beambreak"> NA </td>
         </tr>
         <tr>
           <td>Catapult Beambreak</td>
-          <td id="catapult_beambreak">FALSE</td>
+          <td id="catapult_beambreak"> NA </td>
         </tr>
         <tr>
           <th colspan="2">Subsytems At Position</th>
diff --git a/y2024/www/field_handler.ts b/y2024/www/field_handler.ts
index 6573e8d..7b5e0f4 100644
--- a/y2024/www/field_handler.ts
+++ b/y2024/www/field_handler.ts
@@ -479,12 +479,11 @@
 
   setBoolean(div: HTMLElement, triggered: boolean): void {
     div.innerHTML = ((triggered) ? "TRUE" : "FALSE")
+    div.className = '';
     if (triggered) {
-      div.classList.remove('false');
-      div.classList.add('true');
+      div.classList.add('lightgreen');
     } else {
-      div.classList.remove('true');
-      div.classList.add('false');
+      div.classList.add('lightcoral');
     }
   }
 
diff --git a/y2024/www/index.html b/y2024/www/index.html
index e4e185e..98ecf42 100644
--- a/y2024/www/index.html
+++ b/y2024/www/index.html
@@ -1,6 +1,7 @@
 <html>
   <body>
     <a href="field.html">Field Visualization</a><br>
-    <a href="plotter.html">Plots</a>
+    <a href="plotter.html">Plots</a><br>
+    <a href="starter.html">AOS Starter Status</a>
   </body>
 </html>
diff --git a/y2024/www/starter.html b/y2024/www/starter.html
new file mode 100644
index 0000000..9060332
--- /dev/null
+++ b/y2024/www/starter.html
@@ -0,0 +1,11 @@
+<html>
+  <head>
+    <script src="starter_main_bundle.min.js" defer></script>
+    <link rel="stylesheet" href="styles.css">
+  </head>
+  <body>
+    <div>
+      <div id="status_list"></div>
+    </div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/y2024/www/starter_handler.ts b/y2024/www/starter_handler.ts
new file mode 100644
index 0000000..2b953f3
--- /dev/null
+++ b/y2024/www/starter_handler.ts
@@ -0,0 +1,178 @@
+import {ByteBuffer} from 'flatbuffers'
+import {Connection} from '../../aos/network/www/proxy'
+import {Status, ApplicationStatus, State, LastStopReason, FileState} from '../../aos/starter/starter_generated'
+
+const NODES = ['/orin1', '/imu', '/roborio'];
+
+export class StarterHandler {
+  private statuses = new Map<string, ApplicationStatus[]>();
+
+  private statusList: HTMLElement =
+      (document.getElementById('status_list') as HTMLElement);
+
+  constructor(private readonly connection: Connection) {
+    for (const node in NODES) {
+      this.connection.addConfigHandler(() => {
+        this.connection.addHandler(
+          NODES[node] + '/aos', 'aos.starter.Status', (data) => {
+            this.handleStatus(data, NODES[node]);
+          });
+      });
+    }
+  }
+
+  private handleStatus(data: Uint8Array, node: string): void {
+    const fbBuffer = new ByteBuffer(data);
+    const status: Status = Status.getRootAsStatus(fbBuffer);
+
+    const temp: ApplicationStatus[] = new Array(status.statusesLength());
+
+    for (let i = 0; i < status.statusesLength(); i++) {
+      temp[i] = status.statuses(i);
+    }
+
+    this.statuses.set(node, temp);
+  }
+
+  setStateColor(div: HTMLElement): void {
+    div.className = '';
+    switch (div.innerHTML) {
+      case 'WAITING':
+        div.classList.add('yellow');
+        break;
+      case 'STARTING':
+        div.classList.add('yellowgreen');
+        break;
+      case 'RUNNING':
+        div.classList.add('lightgreen');
+        break;
+      case 'STOPPING':
+        div.classList.add('lightcoral');
+        break;
+      case 'STOPPED':
+        div.classList.add('lightcoral');
+        break;
+    }
+  }
+
+  setFileStateColor(div: HTMLElement): void {
+    div.className = '';
+    switch (div.innerHTML) {
+      case 'NOT_RUNNING':
+        div.classList.add('lightgreen');
+        break;
+      case 'NO_CHANGE':
+        div.classList.add('lightgreen');
+        break;
+      case 'CHANGED_DURING_STARTUP':
+        div.classList.add('lightcoral');
+        break;
+      case 'CHANGED':
+        div.classList.add('lightcoral');
+        break;
+    }
+  }
+
+  private populateStatusList() : void {
+    this.clearStatusList();
+
+    const ELEMENTS: string[] = [
+        'name',
+        'node',
+        'state',
+        'last_exit_code',
+        'pid',
+        // 'id',
+        'last_start_time',
+        'last_stop_reason',
+        // 'process_info',
+        // 'has_active_timing_report',
+        'file_state'
+    ];
+
+    // This is to add the field names as the top row
+    //---------------------------------------------------
+    const row = document.createElement('div');
+    row.className = "column_names";
+
+    for (const e in ELEMENTS) {
+      const element = document.createElement('div');
+      element.className = ELEMENTS[e];
+      element.innerHTML = ELEMENTS[e].toUpperCase();
+      element.style.fontWeight = 'bold';
+      row.appendChild(element);
+    }
+
+    this.statusList.appendChild(row);
+    //---------------------------------------------------
+
+    for (const node in NODES) {
+      const currentStatus = this.statuses.get(NODES[node]);
+
+      if (currentStatus) {
+        currentStatus.forEach(status => {
+          const row = document.createElement('div');
+          row.className = status.name();
+
+          for (const e in ELEMENTS) {
+            const element = document.createElement('div');
+            element.className = ELEMENTS[e];
+
+            switch (element.className) {
+              case 'name':
+                element.innerHTML = status.name();
+                break;
+              case 'node':
+                element.innerHTML = NODES[node];
+                break;
+              case 'state':
+                element.innerHTML = State[status.state()];
+                this.setStateColor(element);
+                break;
+              case 'last_exit_code':
+                element.innerHTML = status.lastExitCode().toString();
+                break;
+              case 'pid':
+                element.innerHTML = status.pid().toString();
+                break;
+              case 'last_start_time':
+                element.innerHTML = Number(status.lastStartTime() / 1000000000n).toString() + ' sec';
+                break;
+              case 'last_stop_reason':
+                element.innerHTML = LastStopReason[status.lastStopReason()];
+                break;
+              case 'file_state':
+                element.innerHTML = FileState[status.fileState()];
+                this.setFileStateColor(element);
+                break;
+              default:
+                element.innerHTML = "NA";
+                break;
+            }
+            row.appendChild(element);
+          }
+
+          this.statusList.appendChild(row);
+        })
+      }
+    }
+  }
+
+  private clearStatusList(): void {
+    if (!this.statusList) {
+        return;
+    }
+
+    while (this.statusList.firstChild) {
+        this.statusList.removeChild(this.statusList.firstChild);
+    }
+  }
+
+  draw(): void {
+    if (this.statuses) {
+      this.populateStatusList();
+    }
+
+    window.requestAnimationFrame(() => this.draw());
+  }
+}
diff --git a/y2024/www/starter_main.ts b/y2024/www/starter_main.ts
new file mode 100644
index 0000000..e9d23fa
--- /dev/null
+++ b/y2024/www/starter_main.ts
@@ -0,0 +1,11 @@
+import {Connection} from '../../aos/network/www/proxy';
+
+import {StarterHandler} from './starter_handler';
+
+const conn = new Connection();
+
+conn.connect();
+
+const starterHandler = new StarterHandler(conn);
+
+starterHandler.draw();
diff --git a/y2024/www/styles.css b/y2024/www/styles.css
index a11187b..c5e0409 100644
--- a/y2024/www/styles.css
+++ b/y2024/www/styles.css
@@ -142,10 +142,34 @@
   width: 100%;
 }
 
-.true {
+#status_list > div {
+  display: table-row;
+  padding: 3px;
+}
+
+#status_list > div > div {
+  display: table-cell;
+  padding: 10px;
+  text-align: left;
+}
+
+
+.yellow {
+  background-color: yellow;
+}
+
+.yellowgreen {
+  background-color: yellowgreen;
+}
+
+.lightgreen {
   background-color: LightGreen;
 }
 
-.false {
+.lightcoral {
+  background-color: lightcoral;
+}
+
+.red {
   background-color: red;
 }