From 5dc8819ee4b79b995d2bbadf0de76bbc6ed8d37d Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Wed, 1 Nov 2023 11:00:19 +0800 Subject: [PATCH] feat(hardware-testing): Gravimetric "daily-setup" script for confirming fixture stability and accuracy (#13876) --- hardware-testing/Makefile | 1 + .../hardware_testing/drivers/radwag/driver.py | 46 ++- .../drivers/radwag/responses.py | 12 + .../gravimetric/daily_setup.py | 277 ++++++++++++++++++ .../gravimetric/measurement/record.py | 39 ++- .../gravimetric/measurement/scale.py | 8 + .../scripts/gravimetric_rnd.py | 4 + .../hardware_testing/tools/plot/index.html | 25 +- .../hardware_testing/tools/plot/index.js | 64 +++- 9 files changed, 465 insertions(+), 11 deletions(-) create mode 100644 hardware-testing/hardware_testing/gravimetric/daily_setup.py diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 21e66d93491..1206e24b175 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -129,6 +129,7 @@ test-gravimetric-96: .PHONY: test-gravimetric test-gravimetric: -$(MAKE) apply-patches-gravimetric + $(python) -m hardware_testing.gravimetric.daily_setup --simulate $(MAKE) test-gravimetric-single $(MAKE) test-gravimetric-multi $(MAKE) test-gravimetric-96 diff --git a/hardware-testing/hardware_testing/drivers/radwag/driver.py b/hardware-testing/hardware_testing/drivers/radwag/driver.py index 3e9e25e696f..15f7e742a36 100644 --- a/hardware-testing/hardware_testing/drivers/radwag/driver.py +++ b/hardware-testing/hardware_testing/drivers/radwag/driver.py @@ -14,6 +14,8 @@ ) from .responses import RadwagResponse, RadwagResponseCodes, radwag_response_parse +from hardware_testing.data import get_testing_data_directory + class RadwagScaleBase(ABC): """Base class if Radwag scale driver.""" @@ -40,6 +42,11 @@ def read_serial_number(self) -> str: """Read the serial number.""" ... + @abstractmethod + def read_max_capacity(self) -> float: + """Read the max capacity.""" + ... + @abstractmethod def continuous_transmission(self, enable: bool) -> None: """Enable/disable continuous transmission.""" @@ -87,7 +94,8 @@ class RadwagScale(RadwagScaleBase): def __init__(self, connection: Serial) -> None: """Constructor.""" self._connection = connection - self._raw_log = open("/data/testing_data/scale_raw.txt", "w") + _raw_file_path = get_testing_data_directory() / "scale_raw.txt" + self._raw_log = open(_raw_file_path, "w") @classmethod def create( @@ -156,6 +164,22 @@ def disconnect(self) -> None: self._connection.close() self._raw_log.close() + def read_max_capacity(self) -> float: + """Read the max capacity.""" + cmd = RadwagCommand.GET_MAX_CAPACITY + res = self._write_command_and_read_response(cmd) + # NOTE: very annoying, different scales give different response codes + # where some will just not have a response code at all... + if len(res.response_list) == 3: + expected_code = RadwagResponseCodes.IN_PROGRESS + elif len(res.response_list) == 2: + expected_code = RadwagResponseCodes.NONE + else: + raise RuntimeError(f"unexpected reponse list: {res.response_list}") + assert res.code == expected_code, f"Unexpected response code: {res.code}" + assert res.message is not None + return float(res.message) + def read_serial_number(self) -> str: """Read serial number.""" cmd = RadwagCommand.GET_SERIAL_NUMBER @@ -220,6 +244,18 @@ def automatic_internal_adjustment(self, enable: bool) -> None: res.code == RadwagResponseCodes.CARRIED_OUT ), f"Unexpected response code: {res.code}" + def zero(self) -> None: + """Sero the scale.""" + cmd = RadwagCommand.ZERO + res = self._write_command_and_read_response(cmd) + assert ( + res.code == RadwagResponseCodes.IN_PROGRESS + ), f"Unexpected response code: {res.code}" + res = self._read_response(cmd, timeout=60) + assert ( + res.code == RadwagResponseCodes.CARRIED_OUT_AFTER_IN_PROGRESS + ), f"Unexpected response code: {res.code}" + def internal_adjustment(self) -> None: """Run internal adjustment.""" cmd = RadwagCommand.INTERNAL_ADJUST_PERFORMANCE @@ -269,6 +305,10 @@ def disconnect(self) -> None: """Disconnect.""" return + def read_max_capacity(self) -> float: + """Read the max capacity.""" + return 220.0 # :shrug: might as well simulate as low-rez scale + def read_serial_number(self) -> str: """Read serial number.""" return "radwag-sim-serial-num" @@ -297,6 +337,10 @@ def automatic_internal_adjustment(self, enable: bool) -> None: """Automatic internal adjustment.""" return + def zero(self) -> None: + """Zero.""" + return + def internal_adjustment(self) -> None: """Internal adjustment.""" return diff --git a/hardware-testing/hardware_testing/drivers/radwag/responses.py b/hardware-testing/hardware_testing/drivers/radwag/responses.py index 50af16688ca..3555b9f19da 100644 --- a/hardware-testing/hardware_testing/drivers/radwag/responses.py +++ b/hardware-testing/hardware_testing/drivers/radwag/responses.py @@ -98,10 +98,22 @@ def _on_serial_number( return data +def _on_max_capacity( + command: RadwagCommand, response_list: List[str] +) -> RadwagResponse: + data = RadwagResponse.build(command, response_list) + if 2 <= len(response_list) <= 3: + data.message = response_list[-1].replace('"', "") + else: + raise RuntimeError(f"unexpected response list: {response_list}") + return data + + HANDLERS = { RadwagCommand.GET_MEASUREMENT_BASIC_UNIT: _on_unstable_measurement, RadwagCommand.GET_MEASUREMENT_CURRENT_UNIT: _on_unstable_measurement, RadwagCommand.GET_SERIAL_NUMBER: _on_serial_number, + RadwagCommand.GET_MAX_CAPACITY: _on_max_capacity, } diff --git a/hardware-testing/hardware_testing/gravimetric/daily_setup.py b/hardware-testing/hardware_testing/gravimetric/daily_setup.py new file mode 100644 index 00000000000..4e3eb26ba0d --- /dev/null +++ b/hardware-testing/hardware_testing/gravimetric/daily_setup.py @@ -0,0 +1,277 @@ +"""Daily Setup.""" +import argparse +from time import time, sleep + +from opentrons.types import Point +from opentrons.hardware_control import SyncHardwareAPI +from opentrons.hardware_control.types import StatusBarState, OT3Mount + +from hardware_testing.data import create_run_id, create_datetime_string, ui +from hardware_testing.gravimetric.measurement.record import ( + GravimetricRecorder, + GravimetricRecorderConfig, +) +from hardware_testing.gravimetric.config import GANTRY_MAX_SPEED +from hardware_testing.gravimetric.measurement.scale import Scale # type: ignore[import] +from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.gravimetric.__main__ import API_LEVEL + +TEST_NAME = "gravimetric-daily-setup" + +STABLE_CHECK_SECONDS = 3.0 +STABLE_ATTEMPTS = 10 + +MAX_ALLOWED_ACCURACY_PERCENT_D = 0.01 # percent + +MOVE_X_MM = 450 +MOVE_Y_MM = 350 +MOVE_MINI_MM = 5 + +WALKING_SECONDS = 15 + +COLORS = { + "white": StatusBarState.IDLE, + "green": StatusBarState.RUNNING, + "yellow": StatusBarState.SOFTWARE_ERROR, + "red-flashing": StatusBarState.HARDWARE_ERROR, + "blue-pulsing": StatusBarState.PAUSED, + "green-pulsing": StatusBarState.RUN_COMPLETED, + "white-pulsing": StatusBarState.UPDATING, + "blue-quick": StatusBarState.ACTIVATION, + "green-quick": StatusBarState.CONFIRMATION, + "disco-quick": StatusBarState.DISCO, +} +COLOR_STATES = { + "idle": COLORS["white"], + "interact": COLORS["white-pulsing"], + "stable": COLORS["yellow"], + "walking": COLORS["blue-pulsing"], + "fail": COLORS["red-flashing"], + "pass": COLORS["green"], +} + + +def _get_real_weight() -> float: + try: + inp = input("enter weight's TRUE grams: ").strip() + return float(inp) + except ValueError as e: + print(e) + return _get_real_weight() + + +def _test_stability(recorder: GravimetricRecorder, hw: SyncHardwareAPI) -> None: + def _check_unstable_count(tag: str) -> None: + segment = recorder.recording.get_tagged_samples(tag) + stable_segment = segment.get_stable_samples() + num_unstable = len(segment) - len(stable_segment) + if num_unstable: + raise RuntimeError( + f"unstable samples during {tag} " + f"({num_unstable}x out of {len(segment)}x total)" + ) + + hw.set_status_bar_state(COLOR_STATES["stable"]) + + # BIG MOVES + tag = "BIG-MOVES" + with recorder.samples_of_tag(tag): + hw.move_rel( + OT3Mount.LEFT, Point(x=-MOVE_X_MM, y=-MOVE_Y_MM), speed=GANTRY_MAX_SPEED + ) + hw.move_rel(OT3Mount.LEFT, Point(y=MOVE_Y_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(y=-MOVE_Y_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(x=MOVE_X_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(x=-MOVE_X_MM), speed=GANTRY_MAX_SPEED) + _check_unstable_count(tag) + + # LITTLE MOVES + tag = "LITTLE-MOVES" + with recorder.samples_of_tag(tag): + for _ in range(5): + hw.move_rel(OT3Mount.LEFT, Point(y=MOVE_MINI_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(y=-MOVE_MINI_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(x=MOVE_MINI_MM), speed=GANTRY_MAX_SPEED) + hw.move_rel(OT3Mount.LEFT, Point(x=-MOVE_MINI_MM), speed=GANTRY_MAX_SPEED) + _check_unstable_count(tag) + + # GO BACK HOME + tag = "HOMING" + with recorder.samples_of_tag(tag): + hw.move_rel( + OT3Mount.LEFT, Point(x=MOVE_X_MM, y=MOVE_Y_MM), speed=GANTRY_MAX_SPEED + ) + _check_unstable_count(tag) + + hw.set_status_bar_state(COLOR_STATES["idle"]) + + # WALKING + ui.print_info( + "Instructions for next test:\n" + "\t 1) walk around robot\n" + "\t 2) move as if you were working normally" + ) + if not hw.is_simulator: + ui.get_user_ready("prepare to WALK") + tag = "WALKING" + with recorder.samples_of_tag(tag): + num_disco_cycles = int(WALKING_SECONDS / 5) + for _ in range(num_disco_cycles): + hw.set_status_bar_state(COLOR_STATES["walking"]) + if not hw.is_simulator: + sleep(5) + _check_unstable_count(tag) + + +def _wait_for_stability( + recorder: GravimetricRecorder, hw: SyncHardwareAPI, tag: str +) -> float: + prev_light_state = hw.get_status_bar_state() + hw.set_status_bar_state(COLOR_STATES["stable"]) + for i in range(STABLE_ATTEMPTS): + attempt = i + 1 + ui.print_info( + f"attempting {STABLE_CHECK_SECONDS} seconds of stability " + f"(attempt {attempt}/{STABLE_ATTEMPTS})" + ) + tag_detailed = f"{tag}-wait-for-stable-attempt-{attempt}" + with recorder.samples_of_tag(tag_detailed): + if hw.is_simulator: + # NOTE: give a bit of time during simulation, so some fake data can be stored + sleep(0.1) + else: + sleep(STABLE_CHECK_SECONDS) + segment = recorder.recording.get_tagged_samples(tag_detailed) + if hw.is_simulator and len(segment) == 1: + segment.append(segment[0]) + stable_only = segment.get_stable_samples() + if len(segment) == len(stable_only): + ui.print_info(f"stable after {attempt}x attempts") + hw.set_status_bar_state(prev_light_state) + return stable_only.average + raise RuntimeError( + f"unable to reach scale stability after {STABLE_ATTEMPTS}x attempts" + ) + + +def _run( + hw_api: SyncHardwareAPI, recorder: GravimetricRecorder, skip_stability: bool +) -> None: + ui.print_title("GRAVIMETRIC DAILY SETUP") + ui.print_info(f"Scale: {recorder.max_capacity}g (SN:{recorder.serial_number})") + + def _record() -> None: + recorder.set_tag(create_datetime_string()) + recorder.record(in_thread=True) + + def _zero() -> None: + hw_api.set_status_bar_state(COLOR_STATES["stable"]) + recorder.stop() + ui.print_info("zeroing scale...") + recorder.zero_scale() + _record() + hw_api.set_status_bar_state(COLOR_STATES["idle"]) + + def _calibrate() -> None: + hw_api.set_status_bar_state(COLOR_STATES["stable"]) + recorder.stop() + ui.print_info("calibrating scale, this may take up to 1 minute...") + ui.print_info("DO NOT MOVE NEAR SCALE UNTIL CALIBRATION IS COMPLETE!!") + recorder.calibrate_scale() + _record() + hw_api.set_status_bar_state(COLOR_STATES["idle"]) + + ui.print_header("SETUP SCALE") + _record() + hw_api.set_status_bar_state(COLOR_STATES["interact"]) + if not hw_api.is_simulator: + ui.get_user_ready("INSTALL Radwag's default weighing pan") + ui.get_user_ready("REMOVE all weights, vials, and labware from scale") + ui.get_user_ready("CLOSE door and step away from fixture") + hw_api.set_status_bar_state(COLOR_STATES["idle"]) + + ui.print_header("TEST STABILITY") + if not skip_stability: + hw_api.home() + _wait_for_stability(recorder, hw_api, tag="stability") + _test_stability(recorder, hw_api) + else: + ui.print_info("skipping") + + ui.print_header("ZERO SCALE") + if not hw_api.is_simulator: + ui.get_user_ready("about to ZERO the scale:") + _wait_for_stability(recorder, hw_api, tag="zero") + _zero() + + ui.print_header("CALIBRATE SCALE") + if hw_api.is_simulator or ui.get_user_answer("calibrate (ADJUST) this scale"): + if not hw_api.is_simulator: + ui.get_user_ready("about to CALIBRATE the scale:") + _wait_for_stability(recorder, hw_api, tag="calibrate") + _calibrate() + + ui.print_header("TEST ACCURACY") + if hw_api.is_simulator: + recorder.set_simulation_mass(0.0) + start_grams = _wait_for_stability(recorder, hw_api, tag="accuracy-start") + ui.print_info(f"start grams: {start_grams}") + weight_grams = 20 if recorder.max_capacity < 200 else 200 + if not hw_api.is_simulator: + real_weight = _get_real_weight() + hw_api.set_status_bar_state(COLOR_STATES["interact"]) + ui.get_user_ready(f"ADD {weight_grams} gram WEIGHT to scale") + ui.get_user_ready("CLOSE door and step away from fixture") + hw_api.set_status_bar_state(COLOR_STATES["idle"]) + else: + real_weight = float(weight_grams) + recorder.set_simulation_mass(float(weight_grams)) + ui.print_info(f"real grams: {real_weight}") + end_grams = _wait_for_stability(recorder, hw_api, tag="accuracy-end") + ui.print_info(f"end grams: {end_grams}") + found_grams = end_grams - start_grams + + # CALCULATE ACCURACY + accuracy_d = ((found_grams - real_weight) / real_weight) * 100.0 + ui.print_info(f"found weight: {found_grams} grams ({round(accuracy_d, 5)} %D)") + ui.print_info(f"%D must be less than {MAX_ALLOWED_ACCURACY_PERCENT_D} %") + if abs(accuracy_d) > MAX_ALLOWED_ACCURACY_PERCENT_D: + raise RuntimeError(f"accuracy failed: {accuracy_d} %D") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--skip-stability", action="store_true") + args = parser.parse_args() + _ctx = helpers.get_api_context( + API_LEVEL, # type: ignore[attr-defined] + is_simulating=args.simulate, + ) + _hw = workarounds.get_sync_hw_api(_ctx) + _hw.set_status_bar_state(COLOR_STATES["idle"]) + _rec = GravimetricRecorder( + GravimetricRecorderConfig( + test_name=TEST_NAME, + run_id=create_run_id(), + start_time=time(), + duration=0, + frequency=1000 if _hw.is_simulator else 5, + stable=False, + ), + scale=Scale.build(simulate=_hw.is_simulator), + simulate=_hw.is_simulator, + ) + try: + _run(_hw, _rec, args.skip_stability) + _hw.set_status_bar_state(COLOR_STATES["pass"]) + ui.print_header("Result: PASS") + except Exception as e: + _hw.set_status_bar_state(COLOR_STATES["fail"]) + ui.print_header("Result: FAIL") + raise e + finally: + if not args.simulate: + ui.get_user_ready("test done") + _rec.stop() + _hw.set_status_bar_state(COLOR_STATES["idle"]) diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/record.py b/hardware-testing/hardware_testing/gravimetric/measurement/record.py index 7e69e2ca4f3..d1e4ab7e4d4 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/record.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/record.py @@ -2,6 +2,7 @@ from contextlib import contextmanager from dataclasses import dataclass from statistics import stdev +from subprocess import Popen from threading import Thread, Event from time import sleep, time from typing import List, Optional, Callable, Generator @@ -10,6 +11,7 @@ dump_data_to_file, append_data_to_file, create_file_name, + ui, ) from .scale import Scale @@ -17,8 +19,7 @@ SLEEP_TIME_IN_RECORD_LOOP = 0.05 SLEEP_TIME_IN_RECORD_LOOP_SIMULATING = 0.01 -SERVER_PORT = 8080 -SERVER_CMD = "{0} -m hardware_testing.tools.plot --test-name gravimetric-ot3 --port {1}" +SERVER_CMD = "python3 -m hardware_testing.tools.plot" @dataclass @@ -216,6 +217,16 @@ def get_time_slice( f"(start={start}, duration={duration})" ) + def get_tagged_samples(self, tag: str) -> "GravimetricRecording": + """Get samples with given tag.""" + return GravimetricRecording( + [sample for sample in self if sample.tag and sample.tag == tag] + ) + + def get_stable_samples(self) -> "GravimetricRecording": + """Get stable samples.""" + return GravimetricRecording([sample for sample in self if sample.stable]) + class GravimetricRecorderConfig: """Recording config.""" @@ -281,9 +292,21 @@ def __init__( self._thread: Optional[Thread] = None self._sample_tag: str = "" self._scale_serial: str = "" + self._scale_max_capacity: float = 0.0 super().__init__() self.activate() + def _start_graph_server_process(self) -> None: + if self.is_simulator: + return + assert self._cfg.test_name + # NOTE: it's ok if this fails b/c prior process is already using port + # the server just needs to be running, doesn't matter when it started + Popen(f"nohup {SERVER_CMD} --test-name {self._cfg.test_name} &", shell=True) + if not self.is_simulator: + sleep(2) # small delay so nohup output isn't confusing + ui.get_user_ready("open WEBPAGE to port 8080") + @property def tag(self) -> str: """Tag.""" @@ -309,6 +332,11 @@ def scale(self) -> Scale: """Scale.""" return self._scale + @property + def max_capacity(self) -> float: + """Max capacity.""" + return self._scale_max_capacity + @property def serial_number(self) -> str: """Serial number.""" @@ -324,14 +352,15 @@ def add_simulation_mass(self, mass: float) -> None: def activate(self) -> None: """Activate.""" + self._start_graph_server_process() # Some Radwag settings cannot be controlled remotely. # Listed below are the things the must be done using the touchscreen: # 1) Set profile to USER # 2) Set screensaver to NONE self._scale.connect() self._scale.initialize() - self._scale.tare(0.0) self._scale_serial = self._scale.read_serial_number() + self._scale_max_capacity = self._scale.read_max_capacity() def deactivate(self) -> None: """Deactivate.""" @@ -371,6 +400,10 @@ def clear_sample_tag(self) -> None: """Clear the sample tag.""" self._sample_tag = "" + def zero_scale(self) -> None: + """Zero scale.""" + self._scale.zero() + def calibrate_scale(self) -> None: """Calibrate scale.""" self._scale.calibrate() diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py index 7dc5a31489d..e194a9a42b5 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py @@ -112,10 +112,18 @@ def tare(self, grams: float) -> None: """Tare.""" self._scale.set_tare(grams) + def read_max_capacity(self) -> float: + """Read max capacity.""" + return self._scale.read_max_capacity() + def read_serial_number(self) -> str: """Read serial number.""" return self._scale.read_serial_number() + def zero(self) -> None: + """Zero.""" + self._scale.zero() + def calibrate(self) -> None: """Calibrate.""" self._scale.internal_adjustment() diff --git a/hardware-testing/hardware_testing/scripts/gravimetric_rnd.py b/hardware-testing/hardware_testing/scripts/gravimetric_rnd.py index 1b352e26384..1ad0351df76 100644 --- a/hardware-testing/hardware_testing/scripts/gravimetric_rnd.py +++ b/hardware-testing/hardware_testing/scripts/gravimetric_rnd.py @@ -30,7 +30,11 @@ def _run(is_simulating: bool) -> None: scale=Scale.build(simulate=is_simulating), simulate=is_simulating, ) + print(f"Scale: {_rec.max_capacity}g (SN:{_rec.serial_number})") if CALIBRATE_SCALE: + input("Press ENTER to ZERO the scale:") + _rec.zero_scale() + input("Press ENTER to CALIBRATE the scale:") _rec.calibrate_scale() while True: input("Press ENTER to Record:") diff --git a/hardware-testing/hardware_testing/tools/plot/index.html b/hardware-testing/hardware_testing/tools/plot/index.html index 98ffafe4fb3..6b8389645be 100644 --- a/hardware-testing/hardware_testing/tools/plot/index.html +++ b/hardware-testing/hardware_testing/tools/plot/index.html @@ -15,7 +15,7 @@ border: solid 2px black; position: absolute; left: 5px; - top: 55px; + top: 105px; display: inline-block; } #testnamecontainer { @@ -24,13 +24,28 @@ top: 5px; display: inline-block; } + #buttoncontainer { + position: absolute; + border: 1px solid blue; + left: 8px; + top: 35px; + display: inline-block; + width: 500px; + height: 50px; + overflow-x: scroll; + white-space: nowrap; + } -

- Test Name: - -

+
+

+ Test Name: + +

+

+

+
diff --git a/hardware-testing/hardware_testing/tools/plot/index.js b/hardware-testing/hardware_testing/tools/plot/index.js index 72594e05fed..36bacd86e3f 100644 --- a/hardware-testing/hardware_testing/tools/plot/index.js +++ b/hardware-testing/hardware_testing/tools/plot/index.js @@ -1,3 +1,17 @@ +// NOTE: removed "gravimetric-" from string so buttons can be smaller +const testNames = [ + 'rnd', + 'daily-setup', + 'ot3-p50-multi', + 'ot3-p50-multi-50ul-tip-increment', + 'ot3-p50-single', + 'ot3-p1000-96', + 'ot3-p1000-multi', + 'ot3-p1000-multi-50ul-tip-increment', + 'ot3-p1000-multi-200ul-tip-increment', + 'ot3-p1000-multi-1000ul-tip-increment', + 'ot3-p1000-single', +] function getEmptyGravData() { return [ { @@ -104,7 +118,9 @@ function parseGravimetricCSV(CSVData, retData) { window.addEventListener('load', function (evt) { const _updateTimeoutMillis = 100 + const _reloadTimeoutMillis = 1000 let _timeout + let _timeoutReload const layout = { title: 'Untitled', uirevision: true, @@ -112,6 +128,22 @@ window.addEventListener('load', function (evt) { yaxis: { autorange: true }, } const name_input_div = document.getElementById('testname') + const button_input_div = document.getElementById('buttoncontainer') + const allButtons = [] + for (let i = 0; i < testNames.length; i++) { + const btn = document.createElement('input') + btn.type = 'button' + btn.value = testNames[i] + btn.onclick = function () { + name_input_div.value = 'gravimetric-' + btn.value + _setTestNameOfServer(null) + } + btn.style.backgroundColor = 'grey' + btn.style.marginRight = '5px' + button_input_div.appendChild(btn) + allButtons.push(btn) + } + const plotlyDivName = 'plotly' function _clearTimeout() { @@ -119,12 +151,22 @@ window.addEventListener('load', function (evt) { clearTimeout(_timeout) _timeout = undefined } + if (_timeoutReload) { + clearTimeout(_timeoutReload) + _timeoutReload = undefined + } } function _onScreenSizeUpdate(evt) { const div = document.getElementById(plotlyDivName) div.style.width = window.innerWidth - 50 + 'px' div.style.height = window.innerHeight - 100 + 'px' + button_input_div.style.width = window.innerWidth - 50 + 'px' + if (window.innerWidth - 160 > 400) { + name_input_div.style.width = window.innerWidth - 160 + 'px' + } else { + name_input_div.style.width = 400 + 'px' + } } function _initializePlot() { @@ -134,16 +176,31 @@ window.addEventListener('load', function (evt) { } function _onTestNameResponse() { + _clearTimeout() const responseData = JSON.parse(this.responseText) name_input_div.value = responseData.name - _clearTimeout() - _timeout = setTimeout(_getLatestDataFromServer, _updateTimeoutMillis) + let btn_val + for (let i = 0; i < allButtons.length; i++) { + btn_val = 'gravimetric-' + allButtons[i].value + if (btn_val === responseData.name) { + allButtons[i].style.backgroundColor = 'yellow' + } else { + allButtons[i].style.backgroundColor = '#bbb' + } + } _getLatestDataFromServer() } + function _onServerError(evt) { + document.body.style.backgroundColor = 'red' + document.body.innerHTML = '

Lost Connection (refresh)

' + location.reload() + } + function _getLatestDataFromServer(evt) { _clearTimeout() const oReq = new XMLHttpRequest() + oReq.addEventListener('error', _onServerError) oReq.addEventListener('load', function () { const responseData = JSON.parse(this.responseText) let newData = getEmptyPlotlyData() @@ -157,10 +214,12 @@ window.addEventListener('load', function (evt) { }) oReq.open('GET', 'http://' + window.location.host + '/data/latest') oReq.send() + _timeoutReload = setTimeout(_onServerError, _reloadTimeoutMillis) } function _getTestNameFromServer(evt) { const oReq = new XMLHttpRequest() + oReq.addEventListener('error', _onServerError) oReq.addEventListener('load', _onTestNameResponse) oReq.open('GET', 'http://' + window.location.host + '/name') oReq.send() @@ -170,6 +229,7 @@ window.addEventListener('load', function (evt) { _clearTimeout() _initializePlot() const oReq = new XMLHttpRequest() + oReq.addEventListener('error', _onServerError) oReq.addEventListener('load', _onTestNameResponse) oReq.open( 'GET',