Skip to content

Commit f433137

Browse files
authored
feat(hardware-testing): flex stacker EVT diagnostic script for connectivity (#16835)
Setting qc test suite for flex-stacker & adding connectivity script.
1 parent 1e6df83 commit f433137

File tree

6 files changed

+295
-0
lines changed

6 files changed

+295
-0
lines changed

hardware-testing/Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ test-liquid-sense:
166166
.PHONY: test-integration
167167
test-integration: test-production-qc test-examples test-scripts test-gravimetric
168168

169+
.PHONY: test-stacker
170+
test-stacker:
171+
$(python) -m hardware_testing.modules.flex_stacker_evt_qc --simulate
172+
169173
.PHONY: lint
170174
lint:
171175
$(python) -m mypy hardware_testing tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""FLEX Stacker QC scripts for EVT."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""FLEX Stacker EVT QC."""
2+
from os import environ
3+
4+
# NOTE: this is required to get WIFI test to work
5+
if "OT_SYSTEM_VERSION" not in environ:
6+
environ["OT_SYSTEM_VERSION"] = "0.0.0"
7+
8+
import argparse
9+
import asyncio
10+
from pathlib import Path
11+
from typing import Tuple
12+
13+
from hardware_testing.data import ui
14+
from hardware_testing.data.csv_report import CSVReport
15+
16+
from .config import TestSection, TestConfig, build_report, TESTS
17+
from .driver import FlexStacker
18+
19+
20+
def build_stacker_report(is_simulating: bool) -> Tuple[CSVReport, FlexStacker]:
21+
"""Report setup for FLEX Stacker qc script."""
22+
test_name = Path(__file__).parent.name.replace("_", "-")
23+
ui.print_title(test_name.upper())
24+
25+
stacker = FlexStacker.build_simulator() if is_simulating else FlexStacker.build()
26+
27+
report = build_report(test_name)
28+
report.set_operator(
29+
"simulating" if is_simulating else input("enter OPERATOR name: ")
30+
)
31+
info = stacker.get_device_info()
32+
if not is_simulating:
33+
barcode = input("SCAN device barcode: ").strip()
34+
else:
35+
barcode = "STACKER-SIMULATOR-SN"
36+
report.set_tag(info.sn)
37+
report.set_device_id(info.sn, barcode)
38+
return report, stacker
39+
40+
41+
async def _main(cfg: TestConfig) -> None:
42+
# BUILD REPORT
43+
report, stacker = build_stacker_report(cfg.simulate)
44+
45+
# RUN TESTS
46+
for section, test_run in cfg.tests.items():
47+
ui.print_title(section.value)
48+
test_run(stacker, report, section.value)
49+
50+
# SAVE REPORT
51+
ui.print_title("DONE")
52+
report.save_to_disk()
53+
report.print_results()
54+
55+
56+
if __name__ == "__main__":
57+
parser = argparse.ArgumentParser()
58+
parser.add_argument("--simulate", action="store_true")
59+
# add each test-section as a skippable argument (eg: --skip-connectivity)
60+
for s in TestSection:
61+
parser.add_argument(f"--skip-{s.value.lower()}", action="store_true")
62+
parser.add_argument(f"--only-{s.value.lower()}", action="store_true")
63+
args = parser.parse_args()
64+
_t_sections = {s: f for s, f in TESTS if getattr(args, f"only_{s.value.lower()}")}
65+
if _t_sections:
66+
assert (
67+
len(list(_t_sections.keys())) < 2
68+
), 'use "--only" for just one test, not multiple tests'
69+
else:
70+
_t_sections = {
71+
s: f for s, f in TESTS if not getattr(args, f"skip_{s.value.lower()}")
72+
}
73+
_config = TestConfig(simulate=args.simulate, tests=_t_sections)
74+
asyncio.run(_main(_config))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Config."""
2+
from dataclasses import dataclass
3+
import enum
4+
from typing import Dict, Callable
5+
6+
from hardware_testing.data.csv_report import CSVReport, CSVSection
7+
8+
from . import (
9+
test_connectivity,
10+
)
11+
12+
13+
class TestSection(enum.Enum):
14+
"""Test Section."""
15+
16+
CONNECTIVITY = "CONNECTIVITY"
17+
18+
19+
@dataclass
20+
class TestConfig:
21+
"""Test Config."""
22+
23+
simulate: bool
24+
tests: Dict[TestSection, Callable]
25+
26+
27+
TESTS = [
28+
(
29+
TestSection.CONNECTIVITY,
30+
test_connectivity.run,
31+
),
32+
]
33+
34+
35+
def build_report(test_name: str) -> CSVReport:
36+
"""Build report."""
37+
return CSVReport(
38+
test_name=test_name,
39+
sections=[
40+
CSVSection(
41+
title=TestSection.CONNECTIVITY.value,
42+
lines=test_connectivity.build_csv_lines(),
43+
)
44+
],
45+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""FLEX Stacker Driver."""
2+
from dataclasses import dataclass
3+
import serial # type: ignore[import]
4+
from serial.tools.list_ports import comports # type: ignore[import]
5+
import re
6+
from enum import Enum
7+
8+
STACKER_VID = 0x483
9+
STACKER_PID = 0xEF24
10+
STACKER_FREQ = 115200
11+
12+
13+
class HardwareRevision(Enum):
14+
"""Hardware Revision."""
15+
16+
NFF = "nff"
17+
EVT = "a1"
18+
19+
20+
@dataclass
21+
class StackerInfo:
22+
"""Stacker Info."""
23+
24+
fw: str
25+
hw: HardwareRevision
26+
sn: str
27+
28+
29+
class FlexStacker:
30+
"""FLEX Stacker Driver."""
31+
32+
@classmethod
33+
def build(cls, port: str = "") -> "FlexStacker":
34+
"""Build FLEX Stacker driver."""
35+
if not port:
36+
for i in comports():
37+
if i.vid == STACKER_VID and i.pid == STACKER_PID:
38+
port = i.device
39+
break
40+
assert port, "could not find connected FLEX Stacker"
41+
return cls(port)
42+
43+
@classmethod
44+
def build_simulator(cls, port: str = "") -> "FlexStacker":
45+
"""Build FLEX Stacker simulator."""
46+
return cls(port, simulating=True)
47+
48+
def __init__(self, port: str, simulating: bool = False) -> None:
49+
"""Constructor."""
50+
self._simulating = simulating
51+
if not self._simulating:
52+
self._serial = serial.Serial(port, baudrate=STACKER_FREQ)
53+
54+
def _send_and_recv(self, msg: str, guard_ret: str = "") -> str:
55+
"""Internal utility to send a command and receive the response."""
56+
assert self._simulating
57+
self._serial.write(msg.encode())
58+
ret = self._serial.readline()
59+
if guard_ret:
60+
if not ret.startswith(guard_ret.encode()):
61+
raise RuntimeError(f"Incorrect Response: {ret}")
62+
if ret.startswith("ERR".encode()):
63+
raise RuntimeError(ret)
64+
return ret.decode()
65+
66+
def get_device_info(self) -> StackerInfo:
67+
"""Get Device Info."""
68+
if self._simulating:
69+
return StackerInfo(
70+
"STACKER-FW", HardwareRevision.EVT, "STACKER-SIMULATOR-SN"
71+
)
72+
73+
_DEV_INFO_RE = re.compile(
74+
"^M115 FW:(?P<fw>.+) HW:Opentrons-flex-stacker-(?P<hw>.+) SerialNo:(?P<sn>.+) OK\n"
75+
)
76+
res = self._send_and_recv("M115\n", "M115 FW:")
77+
m = _DEV_INFO_RE.match(res)
78+
if not m:
79+
raise RuntimeError(f"Incorrect Response: {res}")
80+
return StackerInfo(
81+
m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
82+
)
83+
84+
def set_serial_number(self, sn: str) -> None:
85+
"""Set Serial Number."""
86+
if self._simulating:
87+
return
88+
self._send_and_recv(f"M996 {sn}\n", "M996 OK")
89+
90+
def __del__(self) -> None:
91+
"""Close serial port."""
92+
if not self._simulating:
93+
self._serial.close()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Test Connectivity."""
2+
from typing import List, Union
3+
4+
from hardware_testing.data import ui
5+
from hardware_testing.data.csv_report import (
6+
CSVReport,
7+
CSVLine,
8+
CSVLineRepeating,
9+
CSVResult,
10+
)
11+
12+
from .driver import FlexStacker, HardwareRevision
13+
14+
15+
def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]:
16+
"""Build CSV Lines."""
17+
return [
18+
CSVLine("usb-get-device-info", [str, str, str, CSVResult]),
19+
CSVLine("eeprom-set-serial-number", [str, str, CSVResult]),
20+
CSVLine("led-blinking", [bool, CSVResult]),
21+
]
22+
23+
24+
def test_gcode(driver: FlexStacker, report: CSVReport) -> None:
25+
"""Send and receive response for GCODE M115."""
26+
success = True
27+
info = driver.get_device_info()
28+
print("Hardware Revision: ", info.hw, "\n")
29+
if info is None or info.hw != HardwareRevision.EVT:
30+
print("Hardware Revision must be EVT")
31+
success = False
32+
report(
33+
"CONNECTIVITY",
34+
"usb-get-device-info",
35+
[info.fw, info.hw, info.sn, CSVResult.from_bool(success)],
36+
)
37+
38+
39+
def test_eeprom(driver: FlexStacker, report: CSVReport) -> None:
40+
"""Set serial number and make sure device info is updated accordingly."""
41+
success = True
42+
if not driver._simulating:
43+
serial = input("enter Serial Number: ")
44+
else:
45+
serial = "STACKER-SIMULATOR-SN"
46+
driver.set_serial_number(serial)
47+
info = driver.get_device_info()
48+
if info.sn != serial:
49+
print("Serial number is not set properly")
50+
success = False
51+
report(
52+
"CONNECTIVITY",
53+
"eeprom-set-serial-number",
54+
[serial, info.sn, CSVResult.from_bool(success)],
55+
)
56+
57+
58+
def test_leds(driver: FlexStacker, report: CSVReport) -> None:
59+
"""Prompt tester to verify the status led is blinking."""
60+
if not driver._simulating:
61+
is_blinking = ui.get_user_answer("Is the status LED blinking?")
62+
else:
63+
is_blinking = True
64+
report(
65+
"CONNECTIVITY", "led-blinking", [is_blinking, CSVResult.from_bool(is_blinking)]
66+
)
67+
68+
69+
def run(driver: FlexStacker, report: CSVReport, section: str) -> None:
70+
"""Run."""
71+
ui.print_header("USB Communication")
72+
test_gcode(driver, report)
73+
74+
ui.print_header("EEPROM Communication")
75+
test_eeprom(driver, report)
76+
77+
ui.print_header("LED Blinking")
78+
test_leds(driver, report)

0 commit comments

Comments
 (0)