summaryrefslogtreecommitdiffhomepage
path: root/static/js
diff options
context:
space:
mode:
authorPrivate Island Networks Inc <opensource@privateisland.tech>2026-03-03 15:56:53 -0500
committerPrivate Island Networks Inc <opensource@privateisland.tech>2026-03-03 15:56:53 -0500
commitab6ca080771b706a310ebfd8a4192841cdfef05c (patch)
treef9da21650402f17330d68bb7d6f86b031191ddb9 /static/js
initial commit of experimental code base for PI Explorer (PI-EXP)HEADmain
Diffstat (limited to 'static/js')
-rw-r--r--static/js/base.js200
-rw-r--r--static/js/controller.js93
-rw-r--r--static/js/lmmi.js39
-rw-r--r--static/js/main.js118
-rw-r--r--static/js/mdio.js36
-rw-r--r--static/js/mle.js35
-rw-r--r--static/js/modal.js120
-rw-r--r--static/js/ss.js302
-rw-r--r--static/js/ws.js147
9 files changed, 1090 insertions, 0 deletions
diff --git a/static/js/base.js b/static/js/base.js
new file mode 100644
index 0000000..2920c25
--- /dev/null
+++ b/static/js/base.js
@@ -0,0 +1,200 @@
+/*
+ *
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: base.js
+ *
+ */
+
+ "use strict";
+
+const STATUS = {
+ OK: 10,
+ FORM: 20,
+ DUPLICATE: 30,
+ REDIRECT: 100,
+ UNBOUND_FORM: 0,
+ ERROR_CSRF: -1,
+ ERROR_PARAMS: -2,
+ ERROR_SERVER: -3,
+ ERROR_COOKIES: -4,
+ ERROR_TAMPER: -5,
+};
+
+const PORT_PHY0 = 0;
+const PORT_PHY1 = 1;
+const PORT_PHY2 = 2;
+const PORT_PC = 10;
+
+const MSG_FORMAT_BASIC = 0;
+const MSG_FORMAT_CONTROLLER = 1;
+const MSG_FORMAT_MLE = 2;
+
+const MODE_IDLE = 0;
+const MODE_LIVE = 1;
+const MODE_FILE = 2;
+
+const MSG_TYPE_NULL = 0
+const MSG_TYPE_WRITE = 1;
+const MSG_TYPE_READ = 2;
+const MSG_TYPE_REPLY_SUCCESS = 3;
+const MSG_TYPE_REPLY_ERROR = 4;
+const MSG_TYPE_NOTIY = 5;
+const MSG_TYPE_MLE = 0X10;
+const MSG_MAP = { MSG_TYPE_NULL : "NULL", MSG_TYPE_WRITE : "Write", MSG_TYPE_READ : "Read",
+ MSG_TYPE_REPLY_SUCCESS : "Success", MSG_TYPE_REPLY_ERROR : "Error", MSG_TYPE_NOTIY : "Notify",
+ MSG_TYPE_MLE : "MLE"};
+
+/**
+ * Convert the server's response from JSON (string) to JS object form and
+ * gracefully handle an exception
+ */
+function getResponse(resp) {
+ try {
+ resp = JSON.parse(resp);
+ } catch (err) {
+ resp = {};
+ resp.d = "";
+ resp.r = MC_CONSTANTS.SERVER;
+ }
+
+ return resp;
+}
+
+function getString(obj) {
+ var result = "";
+ for (var key in obj) {
+ if (result) {
+ result += "&";
+ }
+ result += key + "=" + encodeURIComponent(obj[key]);
+ }
+ return result;
+}
+
+function getPrefix() {
+ return window.location.pathname.split('/')[1]; // eg., /todos/<function>
+}
+
+/**
+ * Alert the user depending on error returned by the server. Todo: consider
+ * using jqm popups instead of alert
+ */
+function processError(resp) {
+ $.loading('none');
+ if (resp.r === MC_CONSTANTS.COOKIES || resp.r === MC_CONSTANTS.CSRF) {
+ alert("Error saving your submission. Cookies may be blocked. Please modify your browser's settings and try again including refreshing this page");
+ } else if (resp.r === MC_CONSTANTS.TAMPER) {
+ alert("Our web server is in an inconsistent state with your browser. Please refresh your page and try again.");
+ } else {
+ alert("Server error. Sorry about the trouble. It's been logged, and our team will look into it");
+ }
+}
+
+
+function $(selector, el) {
+ if (!el) {
+ el = document;
+ }
+ return el.querySelector(selector);
+}
+
+$.loading = function(display) {
+ $('#mc-loading').style.display = display;
+};
+
+
+$.get = function(url, data, callback) {
+ var xhr = new XMLHttpRequest();
+
+ xhr.addEventListener("progress", updateProgress);
+ xhr.addEventListener("load", transferComplete);
+ xhr.addEventListener("error", transferFailed);
+ xhr.addEventListener("abort", transferCanceled);
+
+ if (data) {
+ url += "?" + getString(data);
+ }
+
+ xhr.open('GET', url, true);
+
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ xhr.send(null);
+
+ function updateProgress(event) {
+ // was:event.loaded / event.total
+ console.log('updateProgress');
+ }
+
+ function transferComplete(event) {
+ callback(event.target.response);
+ }
+
+ function transferFailed(event) {
+ console.log('transferFailed');
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+
+ function transferCanceled(event) {
+ console.log('transferCanceled');
+ }
+};
+
+$.post = function(url, data, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', url, true);
+
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ if (!(data instanceof FormData)) {
+ xhr.setRequestHeader("Content-type",
+ "application/x-www-form-urlencoded");
+ data = getString(data);
+ }
+
+ xhr.onreadystatechange = function(event) {
+ if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
+ callback(event.target.response);
+ }
+ };
+
+ xhr.send(data);
+};
+
+function $$(selector, el) {
+ if (!el) {
+ el = document;
+ }
+ return el.querySelectorAll(selector);
+ // Note: the returned object is a NodeList.
+ // If you'd like to convert it to a Array for convenience, use this instead:
+ // return Array.prototype.slice.call(el.querySelectorAll(selector));
+}
+
+// MDN: Assigning to an undeclared variable throws a ReferenceError
+function isStrictMode() {
+ try {
+ mc_doesNotExist = true;
+ }
+ catch(E) {
+ console.log(E.message);
+ return true;
+ }
+ return false;
+}
+
+
diff --git a/static/js/controller.js b/static/js/controller.js
new file mode 100644
index 0000000..a12362b
--- /dev/null
+++ b/static/js/controller.js
@@ -0,0 +1,93 @@
+/*
+ *
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: controller.js
+ *
+ *
+ */
+
+function ajaxData() {
+ var data = {};
+ return data;
+}
+
+
+function log_msg(msg) {
+ let msg_str = '';
+
+ msg_str += msg.type + ': ' + msg.address + ' ' + msg.data + '<br>';
+
+ return msg_str;
+}
+
+function postControllerMsg(event) {
+ $.loading('block');
+ event.preventDefault();
+ console.log("postControllerMsg");
+
+ let fd = new FormData($('#mc-cont_message'));
+ fd.append('dummy', 'foo')
+
+ $.post("/controller", fd, function(resp) {
+ $.loading('none');
+ resp = JSON.parse(resp);
+ if (resp.r == STATUS.OK) {
+ let log = document.getElementById('mc-log');
+ let height = 2 * resp.d.length;
+ for (let i=0; i < resp.d.length; i++) {
+ msg = resp.d[i];
+ if (msg.port == PORT_PC) {
+ log.innerHTML += '<div class="mc-log-row style=height:' + height.toString() + 'rem"><div class="mc-log-col-time">' + msg.time + '</div>' +
+ '<div class="mc-log-col-pc">' + log_msg(msg) + '</div>' +
+ '<div class="mc-log-col-phy0"></div>' +
+ '<div class="mc-log-col-phy1"></div></div>';
+ }
+ else if (msg.port == PORT_PHY0) {
+ log.innerHTML += '<div class="mc-log-row"><div class="mc-log-col-time">' + msg.time + '</div>' +
+ '<div class="mc-log-col-pc"></div>' +
+ '<div class="mc-log-col-phy0">' + log_msg(msg) +
+ '</div><div class="mc-log-col-phy1"></div></div>';
+ }
+ }
+ }
+
+ console.log("done");
+ });
+
+}
+
+/*
+The DOMContentLoaded event fires as soon as the HTML document has been fully parsed,
+which typically occurs long before images, stylesheets, and other external resources
+have loaded.
+*/
+window.addEventListener("DOMContentLoaded", function(e) {
+ var MsgForm = $('#mc-cont_message')
+ try {
+ // $('button[value=submit]').addEventListener('click', postControllerMsg);
+ MsgForm.addEventListener('submit', postControllerMsg);
+ } catch (event) {
+ }
+});
+
+/*
+The load event is essential for operations that require the entire web page to be fully loaded,
+including all dependent resources like images and stylesheets.
+This event ensures that every element is available for script manipulation.
+*/
+window.addEventListener("load", function(e) {
+ console.log("controller loaded");
+}); \ No newline at end of file
diff --git a/static/js/lmmi.js b/static/js/lmmi.js
new file mode 100644
index 0000000..5cad4e5
--- /dev/null
+++ b/static/js/lmmi.js
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: lmmi.js
+ *
+ *
+ */
+
+ "use strict";
+
+function ajaxData() {
+ var data = {};
+ return data;
+}
+
+/*
+The load event is essential for operations that require the entire web page to be fully loaded,
+including all dependent resources like images and stylesheets.
+This event ensures that every element is available for script manipulation.
+*/
+window.addEventListener("load", function(e) {
+
+ ss = spreadSheet('.mc-ss-main-panel');
+ $('#mc-ss-reload-all').addEventListener('click', ss.getNewTable);
+ console.log("lmmi loaded");
+}); \ No newline at end of file
diff --git a/static/js/main.js b/static/js/main.js
new file mode 100644
index 0000000..3caa3a7
--- /dev/null
+++ b/static/js/main.js
@@ -0,0 +1,118 @@
+/*
+ *
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: main.js
+ *
+ */
+
+
+"use strict";
+
+let selected_mode = MODE_LIVE;
+
+window.addEventListener("load", function(e) {
+ // re-initialize controls, as needed
+ /*
+ $('#mc-mode-live').checked=true;
+ $('#mc-filename').value = null; // clear any file that was previously selected
+
+ // set up event listeners
+ $('#mc-mode-live').addEventListener('change',wl_changeMode);
+ $('#mc-mode-file').addEventListener('change',wl_changeMode);
+ $('#mc-filename').addEventListener('change',wl_filename);
+ $('#mc-start').addEventListener('click', wl_start);
+ $('#mc-stop').addEventListener('click', wl_stop);
+ $('#mc-help').addEventListener('click', wl_help);
+ $('#mc-clear').addEventListener('click', wl_clear);
+ */
+ console.log("strict mode test: ",isStrictMode());
+});
+
+
+function wl_changeMode(event) {
+ event.preventDefault();
+ console.log('change mode: ',event.target.id);
+ if (event.target.id == "mc-mode-file") {
+ selected_mode = MODE_FILE;
+ }
+ else {
+ selected_mode = MODE_LIVE;
+ }
+}
+
+function wl_filename(event) {
+ event.preventDefault();
+ $.loading('block');
+
+ let data = { 'filename': event.target.files[0].name };
+
+ $.post("/filename", data, function(resp) {
+ console.log("filename");
+ });
+
+ $.loading('none');
+}
+
+function wl_start(event) {
+ event.preventDefault();
+ let data = { 'mode': selected_mode }
+ $.loading('block');
+
+ $.post("/start", data, function(resp) {
+ $('#mc-mode').innerText = "Running";
+ });
+ // await new Promise(r => setTimeout(r, 1000));
+ $.loading('none');
+}
+
+function wl_stop(event) {
+ event.preventDefault();
+ let data = { 'mode': MODE_IDLE }
+ $('#mc-mode').innerText = "Idle";
+ $.loading('block');
+ $.post("/stop", data, function(resp) {
+ $('#mc-mode').innerText = "Idle";
+ });
+ $.loading('none');
+}
+
+function wl_help(event) {
+ event.preventDefault();
+ m = modal('large');
+ /* jshint multistr: true */
+ m.display('<h2>GEODSS WebLogger Help:</h2> \
+ <img width=800px src="/static/fiber_tap.svg">');
+}
+
+function wl_clear(event) {
+ event.preventDefault();
+ let log = $('#mc-log');
+ while (log.firstChild) {
+ log.removeChild(log.firstChild);
+ }
+}
+
+function update_fw_version(fw_version) {
+ $('#mc-fw-version').innerText = fw_version.substring(2,4);
+}
+
+function update_fw_increment(fw_increment) {
+ $('#mc-fw-increment').innerText = fw_increment.substring(2,4);
+}
+
+function update_temperature(temperature) {
+ $('#mc-temperature').innerText = parseInt(temperature,16) + " \xB0F";
+}
diff --git a/static/js/mdio.js b/static/js/mdio.js
new file mode 100644
index 0000000..9185450
--- /dev/null
+++ b/static/js/mdio.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: mdio.js
+ *
+ */
+
+ "use strict";
+
+function ajaxData() {
+ var data = {};
+ return data;
+}
+
+/*
+The load event is essential for operations that require the entire web page to be fully loaded,
+including all dependent resources like images and stylesheets.
+This event ensures that every element is available for script manipulation.
+*/
+window.addEventListener("load", function(e) {
+ var ss = spreadSheet('.mc-ss-main-panel');
+ $('#mc-ss-reload-all').addEventListener('click', ss.getNewTable);
+ console.log("mdio loaded");
+}); \ No newline at end of file
diff --git a/static/js/mle.js b/static/js/mle.js
new file mode 100644
index 0000000..0a28665
--- /dev/null
+++ b/static/js/mle.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: mle.js
+ *
+ */
+
+ "use strict";
+
+function ajaxData() {
+ var data = {};
+ return data;
+}
+
+/*
+The load event is essential for operations that require the entire web page to be fully loaded,
+including all dependent resources like images and stylesheets.
+This event ensures that every element is available for script manipulation.
+*/
+window.addEventListener("load", function(e) {
+
+ console.log("mle loaded");
+}); \ No newline at end of file
diff --git a/static/js/modal.js b/static/js/modal.js
new file mode 100644
index 0000000..af4763b
--- /dev/null
+++ b/static/js/modal.js
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: modal.js
+ *
+ */
+
+"use strict";
+
+/*
+ * Modal TODO: stop using ids
+ * Only add and remove classes programmatically
+ *
+ */
+
+function Modal(size) {
+ var modal = this;
+ this.modalContainer = $('#mc-modal');
+ this.modalBody = $('#mc-modal-body', this.modalContainer);
+ this.modalBackdrop = $('#mc-modal-backdrop', this.modalContainer);
+ this.modalClose = $('#mc-modal-close', this.modalContainer);
+
+ this.modalBody.classList="";
+
+ if (size !== undefined) {
+ this.modalBody.classList.add('mc-' + size);
+ }
+
+ this.modalClose.addEventListener('click', function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ modal.modalContainer.style.display = 'none';
+ if ($('#mc-modal-content')) {
+ el = $('#mc-modal-content');
+ el.remove(); // IE supports removeNode
+ }
+ });
+ this.modalBackdrop.addEventListener('click', function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ });
+}
+
+Modal.prototype = {
+ constructor : Modal,
+ display : modal_display,
+ hide : modal_hide,
+ clean : modal_clean,
+};
+
+function modal(size) {
+ return new Modal(size);
+}
+
+function modal_display(content) {
+ var img, body, ratio, oldWidth, oldHeight;
+ this.clean();
+
+ if (content instanceof HTMLImageElement) {
+ img = content;
+ body = $('#mc-modal-body');
+
+ ratio = img.width / img.height;
+
+ /* todo use the bigger img dimension */
+ if (window.innerWidth - img.width < 20) {
+ img.width = window.innerWidth - 80;
+ img.height = img.width / ratio;
+ }
+
+ if (window.innerHeight - img.height < 20) {
+ img.height = window.innerHeight - 80;
+ }
+
+ body.style.top = String((window.innerHeight - img.height - 64) / 2) + 'px';
+ body.style.bottom = String((window.innerHeight - img.height - 64) / 2) + 'px';
+ body.style.left = String((window.innerWidth - img.width - 64) / 2) + 'px';
+ body.style.right = String((window.innerWidth - img.width - 64) / 2) + 'px';
+
+ el = document.createElement("div");
+ el.id = "mc-modal-content";
+ el.innerHTML = '<img src="' + img.src + '">';
+ this.modalBody.appendChild(el);
+
+ } else {
+ el = document.createElement("div");
+ el.id = "mc-modal-content";
+ el.innerHTML = content;
+ this.modalBody.appendChild(el);
+ var dateInputs = $$('.mc-date', this.modalBody);
+ dateInputs.forEach(function(val) {
+ val.addEventListener('click', setDate);
+ });
+ }
+
+ this.modalContainer.style.display = 'block';
+}
+
+function modal_hide() {
+ this.modalContainer.style.display = 'none';
+}
+
+function modal_clean() {
+ var el;
+ if ((el = $('#mc-modal-content', this.modalContainer))) {
+ el.remove();
+ }
+} \ No newline at end of file
diff --git a/static/js/ss.js b/static/js/ss.js
new file mode 100644
index 0000000..6f0310f
--- /dev/null
+++ b/static/js/ss.js
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: ss.js
+ *
+ */
+
+"use strict";
+
+/* constructor spreadsheet */
+function SpreadSheet(selector) {
+ var ss = this;
+ this.activeCell = null;
+ this.panel = $(selector);
+ this.panel.addEventListener('click', function(event){
+
+ if (event.target.classList.contains('mc-edit')) {
+ ss.editCell.call(ss, event);
+ }
+ else if (event.target.classList.contains('mc-icon') || event.target.nodeName==="IMG") {
+ ss.actions.call(ss, event);
+ }
+ else if (event.target.classList.contains('mc-sortable')) {
+ data = ajaxData();
+ data.sort=event.target.innerHTML.toLowerCase();
+ ss.getNewTable(event,data);
+ }
+ });
+ this.getNewTable = function(event, data) {
+ $.loading('block');
+ if (data == undefined) {
+ data = ajaxData();
+ }
+ $.get('/' + getPrefix(), data, function(resp) {
+ resp = JSON.parse(resp);
+ let values = resp.values;
+ console.log(resp.values);
+ cols=$$(".mc-values");
+ for (let i = 0; i < 16; i++) {
+ cols[i].textContent = "0x" + values[i].toString(16);
+ }
+ $.loading('none');
+ });
+ }
+}
+
+SpreadSheet.prototype = {
+ constructor: SpreadSheet,
+ actions: spreadSheet_actions,
+ editCell: spreadSheet_editCell,
+ newRow: spreadSheet_newRow
+};
+
+function spreadSheet(selector) {
+ return new SpreadSheet(selector);
+}
+
+function spreadSheet_actions(event) {
+ event.preventDefault(); // this fixed my href bug on 3/2/18!
+ $.loading('block');
+ var ss = this;
+ var link, icon, td, tr, id, action, txt, el, data, m;
+
+ if (event.target.nodeName==="IMG") {
+ icon = event.target;
+ link = icon.parentNode;
+ }
+ else {
+ link=event.target;
+ }
+
+ action=link.dataset.action;
+
+ td=link.parentNode.parentNode; // link is wrapped in a div and then a td cell
+ tr = td.parentElement;
+ id = tr.dataset.id;
+
+ txt = td.innerHTML;
+
+ // modal actions
+ if (action === 'edit' || action === 'cut' || action === 'notify' || action === 'modify') {
+
+ $.get('/' + getPrefix() + '/' + action + '/' + id, null,
+ function(resp) {
+ var resp = getResponse(resp);
+ $.loading('none');
+
+ if (action === 'edit') {
+ m = modal('large');
+ }
+ else {
+ m = modal();
+ }
+
+ if (resp.r === MC_CONSTANTS.REDIRECT) {
+ window.location.assign(resp.d);
+ } else if (resp.r === MC_CONSTANTS.NONE) {
+ m.display(resp.d)
+ $('button[value="submit"]', m.modalContainer)
+ .addEventListener('click', submitForm);
+ } else {
+ processError(resp);
+ }
+ })
+ }
+
+ // replace object actions
+ else if (action === "recalc") {
+ $.get('/' + getPrefix() + '/' + action + '/' + id, null, function(resp) {
+ var resp = getResponse(resp);
+ $.loading('none');
+ tr.innerHTML = resp.d;
+ if (resp.n) {
+ m = modal('large');
+ m.display(resp.n);
+ }
+ });
+ }
+
+ function submitForm(e) {
+ console.log('submit Form');
+ $.loading('block');
+ var form = $('form', m.modalContainer);
+ // var nodes = form.childNodes;
+ var data = new FormData(form);
+ // nodes.forEach(addToFormData, data);
+
+ $.post('/' + getPrefix() + '/' + action + '/' + id, data, function(resp) {
+ resp = getResponse(resp);
+ if (resp.r === MC_CONSTANTS.NONE) {
+ m.hide();
+ ss.getNewTable(e);
+ } else if (resp.r === MC_CONSTANTS.FORM) {
+ form.innerHTML=resp.d;
+ $('button[value="submit"]',m.modalContainer).addEventListener('click', submitForm );
+ } else {
+ processError(resp);
+ }
+ $.loading('none');
+ });
+ }
+}
+
+/* set up a handler for a new SpreadSheet item / row */
+function spreadSheet_newRow(selector) {
+ var ss = this;
+ var m = modal('large');
+ try{
+ $(selector).addEventListener('click',function(event) {
+ $.get('/' + getPrefix() + '/new/', null, function(resp) {
+
+ resp = getResponse(resp);
+ if (resp.r === MC_CONSTANTS.NONE) {
+ m.display(resp.d);
+ $('button[value="submit"]', m.modalContainer).addEventListener('click', submitForm);
+ } else {
+ processError(resp);
+ }
+ });
+ });
+ }
+ catch(event) {
+ console.log('spreadSheet_newRow problem')
+ }
+
+ function submitForm(e) {
+ $.loading('block');
+ var form = $('form', m.modalContainer);
+ var nodes = form.childNodes;
+ var data = new FormData();
+ nodes.forEach(addToFormData, data);
+
+ $.post('/' + getPrefix() + '/new/', data, function(resp) {
+ resp = getResponse(resp);
+ if (resp.r === MC_CONSTANTS.NONE) {
+ m.hide();
+ ss.getNewTable(e);
+ } else if (resp.r === MC_CONSTANTS.FORM) {
+ form.innerHTML=resp.d;
+ $('button[value="submit"]',m.modalContainer).addEventListener('click', submitForm );
+ } else {
+ processError(resp);
+ }
+ $.loading('none');
+ });
+ }
+}
+
+
+function spreadSheet_editCell(event) {
+ console.log('editCell');
+ var td=event.target;
+ var tr = td.parentElement;
+ var id = tr.dataset.id;
+ var type=td.dataset.type;
+ var field=td.dataset.field;
+ var txt = td.innerHTML;
+ var el, data;
+
+ if (type==="text") {
+ td.innerHTML="";
+ el = document.createElement("input");
+ el.addEventListener('keypress', keyHandler);
+ $('body').addEventListener('click',clickHandler);
+ // append the input
+ el.className="mc-ss-input"
+ el.value=txt;
+ td.appendChild(el);
+ el.focus();
+ }
+
+ /* TODO: do I need to be worried that the handler won't be set in time? */
+ else if (type==="date") {
+ var cal = calendar('.mc-cal');
+ cal.container.addEventListener("evtCalClose", calCloseHandler);
+ cal.open(td);
+ }
+
+ /* calendar has been closed */
+ function calCloseHandler(event) {
+ console.log('calCloseHandler');
+ cal.container.removeEventListener('evtCalClose',calCloseHandler);
+
+ if (event.detail.cancel) {
+ return;
+ }
+
+ $.loading('block');
+ data = {
+ id: id,
+ field: "date",
+ value: cal.getSelectedDate().toISOString(),
+ };
+
+ $.post('/' + getPrefix() + '/modify', data, function(resp) {
+ resp = getResponse(resp);
+ if(resp.r == MC_CONSTANTS.NONE) {
+ td.innerHTML=resp.d;
+ }
+ else {
+ processError(resp);
+ }
+ $.loading('none');
+ });
+ }
+
+ function keyHandler(event) {
+ console.log(keyHandler);
+ var el = event.target;
+ var key = event.keyCode;
+ var txt = el.value;
+
+ if (key==27) {
+ event.preventDefault();
+ el.remove();
+ td.innerHTML=txt;
+ }
+ else if (key==13) {
+ event.preventDefault();
+ var data = {
+ id: id,
+ field: field,
+ value: txt,
+ };
+
+ el.remove();
+ td.innerHTML=txt;
+
+ $.post('/' + getPrefix() + '/modify', data, function(response) {
+ console.log("upload text suceeded");
+ });
+ }
+ }
+
+ function clickHandler(event) {
+ console.log('clickHandler');
+ if(event.target.nodeName==="TD") {
+ return; // don't cancel out of a cell we just clicked
+ }
+ $('body').removeEventListener('click',clickHandler);
+ var children = td.childNodes;
+ children.forEach(function(val) {
+ val.remove();
+ })
+ td.innerHTML=txt;
+ }
+}
+
+
+ \ No newline at end of file
diff --git a/static/js/ws.js b/static/js/ws.js
new file mode 100644
index 0000000..bc814ff
--- /dev/null
+++ b/static/js/ws.js
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2026 Private Island Networks Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * file: ws.js
+ *
+ */
+
+"use strict";
+
+let socket = new WebSocket("ws://localhost:8010/logger");
+console.log("here we go");
+let state = "Wait";
+let log = document.getElementById('mc-log');
+
+function log_msg(msg) {
+ let msg_str = '';
+
+ if (msg.num_cmds > 1) {
+ console.log('multi commands');
+ }
+
+ for (let i = 0; i < msg.num_cmds; i++) {
+ if (msg.cmds[i][0].includes('RDREG') || msg.cmds[i][0].includes('GETTEMP')) {
+ msg_str += msg.cmds[i][0] + ' ' + msg.cmds[i][1] + '<br>';
+ }
+ else if (msg.cmds[i][0] == "" && (msg.cmds[i][1] == "PHY1_STATUS" || msg.cmds[i][1] == "PHY2_STATUS")) {
+ let val = parseInt(msg.cmds[i][2], 16) & 0x8;
+ if (val) {
+ msg_str += msg.cmds[i][1] + ': LOS = 1' + '<br>';
+ }
+ else {
+ msg_str += msg.cmds[i][1] + ': LOS = 0' + '<br>';
+ }
+ }
+ else {
+ msg_str += msg.cmds[i][0] + ' ' + msg.cmds[i][1] + ' ' + msg.cmds[i][2] + '<br>';
+ }
+ }
+
+ return msg_str;
+}
+
+// setInterval(function(){console.log(state)},1000);
+
+socket.onopen = function(e) {
+ console.log("[open] Connection established");
+ console.log("Sending to server");
+ socket.send("open"); // can send data as a string, Blob, or ArrayBuffer.
+};
+
+function update_mode(mode) {
+ if (mode == MODE_IDLE)
+ $('#mc-mode').innerText = "Idle";
+ else if (mode == MODE_LIVE || mode == MODE_FILE)
+ $('#mc-mode').innerText = "Running";
+ else
+ console.log("unsupported mode")
+}
+
+socket.onmessage = function(event) {
+ let num_msgs = 0;
+
+ let msg = JSON.parse(event.data);
+ if (msg.format == MSG_FORMAT_BASIC) {
+ if (typeof msg['mode'] !== "undefined") {
+ console.log("mode: ", msg['mode']);
+ update_mode(msg['mode']);
+ }
+ else if (typeof msg['fw_version'] !== "undefined") {
+ console.log("fw_version: ", msg['fw_version']);
+ update_fw_version(msg['fw_version']);
+ }
+ else if (typeof msg['fw_increment'] !== "undefined") {
+ console.log("fw_increment: ", msg['fw_increment']);
+ update_fw_increment(msg['fw_increment']);
+ }
+ else if (typeof msg['temperature'] !== "undefined") {
+ console.log("temperature: ", msg['temperature']);
+ update_temperature(msg['temperature']);
+ }
+ else
+ console.log("unsupported control");
+
+ return;
+ }
+
+ let height = 2 * msg.num_cmds;
+ if (height == 0) {
+ height = 2;
+ }
+
+ num_msgs += 1;
+ console.log("From server:", num_msgs, ': ', msg);
+
+ if (msg.port == PORT_PC) {
+ log.innerHTML += '<div class="mc-log-row style=height:' + height.toString() + 'rem"><div class="mc-log-col-time">' + msg.time + '</div>' +
+ '<div class="mc-log-col-pc">' + log_msg(msg) + '</div>' +
+ '<div class="mc-log-col-phy0"></div>' +
+ '<div class="mc-log-col-phy1"></div></div>';
+ }
+ else if (msg.port == PORT_PHY0) {
+ log.innerHTML += '<div class="mc-log-row"><div class="mc-log-col-time">' + msg.time + '</div>' +
+ '<div class="mc-log-col-pc"></div>' +
+ '<div class="mc-log-col-phy0">' + log_msg(msg) +
+ '</div><div class="mc-log-col-phy1"></div></div>';
+ }
+ else if (msg.port == PORT_PHY1) {
+ log.innerHTML += '<div class="mc-log-row"><div class="mc-log-col-time">' + msg.time + '</div>' +
+ '<div class="mc-log-col-pc"></div>' +
+ '<div class="mc-log-col-phy0"></div>' +
+ '<div class="mc-log-col-phy1">' + log_msg(msg) + '</div></div>';
+ }
+
+ // socket.send("request");
+ state = "Request"
+};
+
+socket.onclose = function(event) {
+ state = "Closed"
+ if (event.wasClean) {
+ console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
+ } else {
+ // e.g. server process killed or network down
+ // event.code is usually 1006 in this case
+ console.log('[close] Connection died');
+ }
+};
+
+socket.onerror = function(error) {
+ state = "Error"
+ console.log(`[error]`);
+};
+
+// When you've finished using the WebSocket connection,
+// call the WebSocket method close():

Highly Recommended Verilog Books