diff --git a/.github/workflows/run-templates.yml b/.github/workflows/run-templates.yml deleted file mode 100644 index e750daa..0000000 --- a/.github/workflows/run-templates.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Run Python Mock-up Data Script - -on: - schedule: - # Run every weekday at 7:00 AM UTC - - cron: "0 7 * * 1-5" - # Run the script from GitHub interface - workflow_dispatch: - -jobs: - execute-python-scripts: - runs-on: ubuntu-latest - - steps: - # Checkout the repository - - name: Checkout code - uses: actions/checkout@v3 - - # Set up Python environment - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - # Install tofupilot package - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - # Set TOFUPILOT_API_KEY and run all Python files in corresponding folder - - name: Run Battery-Testing Script - env: - TOFUPILOT_API_KEY: ${{ secrets.GET_STARTED_BATTERY_API_KEY }} - run: | - cd welcome_aboard/battery_testing - for i in {1..10}; do - echo "Execution $i" - python main.py - done diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml deleted file mode 100644 index d712852..0000000 --- a/.github/workflows/test-scripts.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Run Python Scripts - -on: - push: - branches: - - main - pull_request: - branches: - - main - # Run the script from GitHub interface - workflow_dispatch: - -jobs: - execute-python-scripts: - # Running on both ubuntu and windows - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - - env: - TOFUPILOT_API_KEY: ${{ secrets.PRODUCTION_API_KEY}} - # Forcing Python to run in UTF-8 mode for all steps in this job - PYTHONUTF8: "1" - - steps: - # Checking out the repository - - name: Checking out the repository - uses: actions/checkout@v3 - - - name: Changing code page to UTF-8 (Windows only) - if: runner.os == 'Windows' - run: chcp 65001 - - # Setting up Python - - name: Setting up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - # Installing dependencies - - name: Installing dependencies - shell: bash - run: | - if [[ "$RUNNER_OS" == "Windows" ]]; then - python -m pip install --upgrade pip - else - pip install --upgrade pip - fi - - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - # Running all Python scripts - - name: Running all Python scripts - shell: bash - run: | - find . -type f -name 'main.py' \ - -not -path '*/venv/*' \ - -not -path '*/.*/*' \ - -print0 | while IFS= read -r -d '' file; do - - directory="$(dirname "$file")" - - if [ -f "$directory/requirements.txt" ]; then - pip install -r "$directory/requirements.txt" - fi - - echo "Running files in $directory" - - ( - cd "$directory" || exit - python main.py - ) - done diff --git a/api/index.py b/api/index.py deleted file mode 100644 index b5323fd..0000000 --- a/api/index.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import os -from http.server import BaseHTTPRequestHandler - -import welcome_aboard.openhtf.main as openhtf -import welcome_aboard.vanilla.main as vanilla - - -class Handler(BaseHTTPRequestHandler): - # Setting common CORS headers - def _set_cors_headers(self): - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS") - self.send_header( - "Access-Control-Allow-Headers", - "Authorization, Content-Type") - - # Sending error response with appropriate CORS headers - def _send_error_response(self, status_code, message): - self.send_response(status_code) - self._set_cors_headers() - self.end_headers() - self.wfile.write(message.encode("utf-8")) - - # Handling preflight requests for CORS - def do_OPTIONS(self): - self.send_response(200) - self._set_cors_headers() - self.end_headers() - - # Handling POST requests - def do_POST(self): - auth_header = self.headers.get("Authorization") - - # Checking if Authorization header is present - if not auth_header: - self._send_error_response(401, "Missing Authorization header") - return - - # Parsing the Authorization header - try: - _, api_key = auth_header.split(" ") - except ValueError: - self._send_error_response( - 400, "Invalid Authorization header format") - return - - # Reading and parsing request body - content_length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(content_length).decode("utf-8") - try: - body_data = json.loads(body) if body else {} - except json.JSONDecodeError: - body_data = {} - - url = body_data.get("url", None) - framework = body_data.get("framework", "openhtf") - serial_number = body_data.get("serial_number", None) - - os.environ["TOFUPILOT_URL"] = url - os.environ["TOFUPILOT_API_KEY"] = api_key - - # Calling the appropriate function based on the framework - if framework == "vanilla": - vanilla.main(serial_number) - elif framework == "openhtf": - openhtf.main(serial_number) - - # Sending success response - self.send_response(200) - self.send_header("Content-type", "application/json") - self._set_cors_headers() - self.end_headers() - - response_body = {"message": "Request processed successfully"} - self.wfile.write(json.dumps(response_body).encode("utf-8")) diff --git a/docs_openhtf/attachments/by_path/data/sample_file.txt b/docs_openhtf/attachments/by_path/data/sample_file.txt deleted file mode 100644 index 0089758..0000000 --- a/docs_openhtf/attachments/by_path/data/sample_file.txt +++ /dev/null @@ -1 +0,0 @@ -OpenHTF + TofuPilot = <3 \ No newline at end of file diff --git a/docs_openhtf/attachments/by_path/main.py b/docs_openhtf/attachments/by_path/main.py deleted file mode 100644 index df6d51f..0000000 --- a/docs_openhtf/attachments/by_path/main.py +++ /dev/null @@ -1,20 +0,0 @@ -import openhtf as htf -from tofupilot.openhtf import TofuPilot - - -def phase_file_attachment(test): - test.attach_from_file("data/sample_file.txt") - return htf.PhaseResult.CONTINUE - - -def main(): - test = htf.Test( - phase_file_attachment, - procedure_id="TEST0", - part_number="00389") - with TofuPilot(test): - test.execute(lambda: "PCB0001") - - -if __name__ == "__main__": - main() diff --git a/docs_tofupilot/attachments/client/data/temperature-map.png b/docs_tofupilot/api_v1/attachments/client/data/temperature-map.png similarity index 100% rename from docs_tofupilot/attachments/client/data/temperature-map.png rename to docs_tofupilot/api_v1/attachments/client/data/temperature-map.png diff --git a/docs_tofupilot/attachments/client/main.py b/docs_tofupilot/api_v1/attachments/client/main.py similarity index 85% rename from docs_tofupilot/attachments/client/main.py rename to docs_tofupilot/api_v1/attachments/client/main.py index 49daaae..dc49f63 100644 --- a/docs_tofupilot/attachments/client/main.py +++ b/docs_tofupilot/api_v1/attachments/client/main.py @@ -12,7 +12,7 @@ def main(): attachments = phase_file_attachment() client.create_run( - procedure_id="FVT1", + procedure_id="FVT1", # Create the procedure first in the Application unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, run_passed=True, attachments=attachments, diff --git a/docs_tofupilot/attachments/openhtf/data/temperature-map.png b/docs_tofupilot/api_v1/attachments/openhtf/data/temperature-map.png similarity index 100% rename from docs_tofupilot/attachments/openhtf/data/temperature-map.png rename to docs_tofupilot/api_v1/attachments/openhtf/data/temperature-map.png diff --git a/docs_tofupilot/attachments/openhtf/main.py b/docs_tofupilot/api_v1/attachments/openhtf/main.py similarity index 77% rename from docs_tofupilot/attachments/openhtf/main.py rename to docs_tofupilot/api_v1/attachments/openhtf/main.py index 6053e23..11c425d 100644 --- a/docs_tofupilot/attachments/openhtf/main.py +++ b/docs_tofupilot/api_v1/attachments/openhtf/main.py @@ -11,8 +11,9 @@ def phase_file_attachment(test): def main(): test = htf.Test( phase_file_attachment, - procedure_id="FVT1", - part_number="PCB1") + procedure_id="FVT1", # Create the procedure first in the Application + part_number="PCB1", + ) with TofuPilot(test): test.execute(lambda: "PCB1A001") diff --git a/docs_tofupilot/integrations/client/main.py b/docs_tofupilot/api_v1/integrations/client/main.py similarity index 80% rename from docs_tofupilot/integrations/client/main.py rename to docs_tofupilot/api_v1/integrations/client/main.py index 38e6028..c75b970 100644 --- a/docs_tofupilot/integrations/client/main.py +++ b/docs_tofupilot/api_v1/integrations/client/main.py @@ -5,7 +5,7 @@ def main(): client = TofuPilotClient() client.create_run( - procedure_id="FVT1", # Define the TofuPilot's procedure ID + procedure_id="FVT1", # Create the procedure first in the Application unit_under_test={ "serial_number": "PCB1A001", "part_number": "PCB1", diff --git a/docs_tofupilot/integrations/openhtf/main.py b/docs_tofupilot/api_v1/integrations/openhtf/main.py similarity index 66% rename from docs_tofupilot/integrations/openhtf/main.py rename to docs_tofupilot/api_v1/integrations/openhtf/main.py index a072fc2..7acc686 100644 --- a/docs_tofupilot/integrations/openhtf/main.py +++ b/docs_tofupilot/api_v1/integrations/openhtf/main.py @@ -2,12 +2,14 @@ from tofupilot.openhtf import TofuPilot # Import OpenHTF for TofuPilot -def phase_one(test): +def phase_one(): return htf.PhaseResult.CONTINUE def main(): - test = htf.Test(phase_one) + test = htf.Test( + phase_one, procedure_id="FVT1", part_number="PCB01" + ) # Specify procedure and part_number with TofuPilot(test): # One-line integration test.execute(lambda: "PCB1A001") diff --git a/docs_tofupilot/api_v1/logger/client/main.py b/docs_tofupilot/api_v1/logger/client/main.py new file mode 100644 index 0000000..0c3f33f --- /dev/null +++ b/docs_tofupilot/api_v1/logger/client/main.py @@ -0,0 +1,61 @@ +import logging +import sys +from datetime import datetime + +from tofupilot import TofuPilotClient + + +class TofuPilotLogHandler(logging.Handler): + """Handler that captures logs in a format compatible with TofuPilot API.""" + + def __init__(self): + super().__init__() + self.logs = [] + + def emit(self, record): + # Format log with ISO-8601 timestamp (UTC, ms) for TofuPilot API + log_entry = { + "level": record.levelname, + "timestamp": datetime.utcfromtimestamp(record.created).isoformat( + timespec="milliseconds" + ) + + "Z", + "message": record.getMessage(), + "source_file": record.filename, + "line_number": record.lineno, + } + self.logs.append(log_entry) + + +# Initialize the TofuPilot client to report test results +client = TofuPilotClient() + +# Set up local logger with custom name and prevent propagation to parent +# loggers +local_logger = logging.getLogger("test_logger") +local_logger.setLevel(logging.DEBUG) +local_logger.propagate = False + +# Add handlers: one for TofuPilot API capture and one for console output +capture_handler = TofuPilotLogHandler() +local_logger.addHandler(capture_handler) +local_logger.addHandler(logging.StreamHandler(sys.stdout)) + + +# Log examples at different severity levels +local_logger.debug("Debug message: Detailed information for troubleshooting") +local_logger.info("Info message: Normal operation information") +local_logger.warning("Warning: Something unexpected but not critical") +local_logger.error("Error: A significant problem that needs attention") +local_logger.critical("Critical: System unstable, immediate action required") + +# Create a run and send captured logs to TofuPilot +try: + client.create_run( + procedure_id="FVT1", + unit_under_test={"serial_number": "00007035", "part_number": "LOGS01"}, + run_passed=True, + logs=capture_handler.logs, + ) +finally: + local_logger.removeHandler(capture_handler) diff --git a/docs_tofupilot/api_v1/logger/openhtf/main.py b/docs_tofupilot/api_v1/logger/openhtf/main.py new file mode 100644 index 0000000..42d3eca --- /dev/null +++ b/docs_tofupilot/api_v1/logger/openhtf/main.py @@ -0,0 +1,24 @@ +import openhtf as htf +from tofupilot.openhtf import TofuPilot + + +@htf.measures(htf.Measurement("boolean_measure").equals(True)) +def phase_with_info_logger(test): + test.measurements.boolean_measure = True + # You can log info, warning, error, and critical. By default, debug. + test.logger.info("Logging an information") + + +def main(): + test = htf.Test( + phase_with_info_logger, + procedure_id="FVT1", + part_number="PCB01", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB1A001") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/boolean/client/main.py b/docs_tofupilot/api_v1/measurements/boolean/client/main.py new file mode 100644 index 0000000..343f7e5 --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/boolean/client/main.py @@ -0,0 +1,44 @@ +import random +from datetime import datetime + +from tofupilot import MeasurementOutcome, PhaseOutcome, TofuPilotClient + +client = TofuPilotClient() + + +def phase_led(): + start_time_millis = datetime.now().timestamp() * 1000 + + phase = [ + { + "name": "phase_led", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start_time_millis, + "end_time_millis": start_time_millis + 1000, + "measurements": [ + { + "name": "is_led_switch_on", + "measured_value": True, + "outcome": MeasurementOutcome.PASS, + } + ], + } + ] + + return phase + + +def main(): + phases = phase_led() + + client.create_run( + procedure_id="FVT1", # Create the procedure first in the Application + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, + phases=phases, + run_passed=all( + phase["outcome"] == PhaseOutcome.PASS for phase in phases), + ) + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/boolean/openhtf/main.py b/docs_tofupilot/api_v1/measurements/boolean/openhtf/main.py new file mode 100644 index 0000000..0cb8035 --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/boolean/openhtf/main.py @@ -0,0 +1,22 @@ +import openhtf as htf +from tofupilot.openhtf import TofuPilot + + +@htf.measures(htf.Measurement("is_led_switch_on").equals(True)) +def phase_led(test): + test.measurements.is_led_switch_on = True + + +def main(): + test = htf.Test( + phase_led, + procedure_id="FVT1", # Create the procedure first in the Application + part_number="PCB1", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB001") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/dimensional/client/main.py b/docs_tofupilot/api_v1/measurements/dimensional/client/main.py new file mode 100644 index 0000000..2953753 --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/dimensional/client/main.py @@ -0,0 +1,77 @@ +import random +from datetime import datetime + +import numpy as np +from tofupilot import MeasurementOutcome, PhaseOutcome, TofuPilotClient + +client = TofuPilotClient() + + +def standard(): + """Python lists approach - one point at a time""" + start = datetime.now().timestamp() * 1000 + measurements = [] + + for t in range(100): + timestamp = t / 100 # Time dimension + voltage = round(random.uniform(3.3, 3.5), 2) # Voltage dimension + current = round(random.uniform(0.3, 0.8), 3) # Current dimension + measurements.append((timestamp, voltage, current, voltage / current)) + + return { + "name": "loop_approach", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start, + "end_time_millis": start + 30000, + "measurements": [ + { + "name": "current_voltage_resistance_over_time", + "units": ["s", "V", "A", "Ohm"], + "measured_value": measurements, + "outcome": MeasurementOutcome.PASS, + } + ], + } + + +def numpy_way(): + """NumPy approach - all points at once""" + start = datetime.now().timestamp() * 1000 + + # Generate all dimensions simultaneously + timestamps = np.linspace(0, 0.99, 100) + voltages = np.round(np.random.uniform(3.3, 3.5, 100), 2) + currents = np.round(np.random.uniform(0.3, 0.8, 100), 3) + + measurements = [ + tuple(x) + for x in np.column_stack((timestamps, voltages, currents, voltages / currents)) + ] + + return { + "name": "vector_approach", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start, + "end_time_millis": start + 30000, + "measurements": [ + { + "name": "current_voltage_resistance_over_time", + "units": ["s", "V", "A", "Ohm"], + "measured_value": measurements, + "outcome": MeasurementOutcome.PASS, + } + ], + } + + +def main(): + client.create_run( + procedure_id="FVT1", + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, + phases=[standard(), numpy_way()], + run_passed=True, + ) + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/dimensional/openhtf/main.py b/docs_tofupilot/api_v1/measurements/dimensional/openhtf/main.py new file mode 100644 index 0000000..9d52d7a --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/dimensional/openhtf/main.py @@ -0,0 +1,38 @@ +import random + +import openhtf as htf +from openhtf.util import units +from tofupilot.openhtf import TofuPilot + + +@htf.measures( + htf.Measurement("current_voltage_resistance_over_time") + .with_dimensions( + units.SECOND, units.VOLT, units.AMPERE + ) # Input axes: time, voltage, current + .with_units(units.OHM) # Output unit: resistance in ohms +) +def power_phase(test): + for t in range(100): + timestamp = t / 100 + voltage = round(random.uniform(3.3, 3.5), 2) + current = round(random.uniform(0.3, 0.8), 3) + resistance = voltage / current + test.measurements.current_voltage_resistance_over_time[ + timestamp, voltage, current + ] = resistance + + +def main(): + test = htf.Test( + power_phase, + procedure_id="FVT1", + part_number="PCB01", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB1A003") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/multi-measurements/client/main.py b/docs_tofupilot/api_v1/measurements/multi-measurements/client/main.py new file mode 100644 index 0000000..3c02735 --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/multi-measurements/client/main.py @@ -0,0 +1,79 @@ +import random +from datetime import datetime + +from tofupilot import MeasurementOutcome, PhaseOutcome, TofuPilotClient + +client = TofuPilotClient() + + +def phase_multi_measurements(): + start_time_millis = datetime.now().timestamp() * 1000 + + is_connected = True + firmware_version = "1.2.7" + input_voltage = round(random.uniform(3.29, 3.42), 2) + input_current = round(random.uniform(1.0, 1.55), 3) + + phase = [ + { + "name": "phase_multi_measurements", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start_time_millis, + "end_time_millis": start_time_millis + 5000, + "measurements": [ + { + "name": "is_connected", + "measured_value": is_connected, + "outcome": MeasurementOutcome.PASS, + }, + { + "name": "firmware_version", + "measured_value": firmware_version, + "outcome": MeasurementOutcome.PASS, + }, + { + "name": "input_voltage", + "units": "V", + "lower_limit": 3.2, + "upper_limit": 3.4, + "measured_value": input_voltage, + "outcome": ( + MeasurementOutcome.PASS + if 3.2 <= input_voltage <= 3.4 + else MeasurementOutcome.FAIL + ), + }, + { + "name": "input_current", + "units": "A", + "upper_limit": 1.5, + "measured_value": input_current, + "outcome": ( + MeasurementOutcome.PASS + if input_current <= 1.5 + else MeasurementOutcome.FAIL + ), + }, + ], + } + ] + + return phase + + +def main(): + phases = phase_multi_measurements() + + client.create_run( + procedure_id="FVT1", + unit_under_test={ + "serial_number": "PCB1A004", + "part_number": "PCB01", + }, + phases=phases, + run_passed=all(p["outcome"] == PhaseOutcome.PASS for p in phases), + ) + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/multi-measurements/openhtf/main.py b/docs_tofupilot/api_v1/measurements/multi-measurements/openhtf/main.py new file mode 100644 index 0000000..f8f34ba --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/multi-measurements/openhtf/main.py @@ -0,0 +1,35 @@ +import random + +import openhtf as htf +from openhtf.util import units +from tofupilot.openhtf import TofuPilot + + +@htf.measures( + htf.Measurement("is_connected").equals(True), # Boolean measure + htf.Measurement("firmware_version").equals("1.2.7"), # String measure + htf.Measurement("input_voltage").in_range(3.2, 3.4).with_units(units.VOLT), + htf.Measurement("input_current").in_range( + maximum=1.5).with_units(units.AMPERE), +) +def phase_multi_measurements(test): + test.measurements.is_connected = True + test.measurements.firmware_version = "1.2.7" + + test.measurements.input_voltage = round(random.uniform(3.29, 3.42), 2) + test.measurements.input_current = round(random.uniform(1.0, 1.55), 3) + + +def main(): + test = htf.Test( + phase_multi_measurements, + procedure_id="FVT1", + part_number="PCB01", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB1A004") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/numerical/client/main.py b/docs_tofupilot/api_v1/measurements/numerical/client/main.py new file mode 100644 index 0000000..4d0813a --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/numerical/client/main.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from tofupilot import MeasurementOutcome, PhaseOutcome, TofuPilotClient + +client = TofuPilotClient() + + +def phase_temperature(): + start_time_millis = datetime.now().timestamp() * 1000 + + phase = [ + { + "name": "phase_temperature", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start_time_millis, + "end_time_millis": start_time_millis + 1000, + "measurements": [ + { + "name": "temperature", + "measured_value": 25, + "units": "C", + "outcome": MeasurementOutcome.PASS, + "lower_limit": 0, + "upper_limit": 100, + } + ], + } + ] + + return phase + + +def main(): + phases = phase_temperature() + + client.create_run( + procedure_id="FVT1", # Create the procedure first in the Application + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, + phases=phases, + run_passed=all( + phase["outcome"] == PhaseOutcome.PASS for phase in phases), + ) + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/numerical/openhtf/main.py b/docs_tofupilot/api_v1/measurements/numerical/openhtf/main.py new file mode 100644 index 0000000..44aa197 --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/numerical/openhtf/main.py @@ -0,0 +1,27 @@ +import openhtf as htf +from openhtf.util import units +from tofupilot.openhtf import TofuPilot + + +@htf.measures( + htf.Measurement("temperature") # Declares the measurement name + .in_range(0, 100) # Defines the lower and upper limits + .with_units(units.DEGREE_CELSIUS) # Specifies the unit +) +def phase_temperature(test): + test.measurements.temperature = 25 # Set the temperature measured value to 25°C + + +def main(): + test = htf.Test( + phase_temperature, + procedure_id="FVT1", # Create the procedure first in the Application + part_number="PCB1", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB001") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/string/client/main.py b/docs_tofupilot/api_v1/measurements/string/client/main.py new file mode 100644 index 0000000..2d83e8b --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/string/client/main.py @@ -0,0 +1,43 @@ +from datetime import datetime + +from tofupilot import MeasurementOutcome, PhaseOutcome, TofuPilotClient + +client = TofuPilotClient() + + +def phase_firmware(): + start_time_millis = datetime.now().timestamp() * 1000 + + phase = [ + { + "name": "phase_firmware", + "outcome": PhaseOutcome.PASS, + "start_time_millis": start_time_millis, + "end_time_millis": start_time_millis + 1000, + "measurements": [ + { + "name": "temperature", + "measured_value": "1.2.4", + "outcome": MeasurementOutcome.PASS, + } + ], + } + ] + + return phase + + +def main(): + phases = phase_firmware() + + client.create_run( + procedure_id="FVT1", # Create the procedure first in the Application + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, + phases=phases, + run_passed=all( + phase["outcome"] == PhaseOutcome.PASS for phase in phases), + ) + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/api_v1/measurements/string/openhtf/main.py b/docs_tofupilot/api_v1/measurements/string/openhtf/main.py new file mode 100644 index 0000000..ab5b1bf --- /dev/null +++ b/docs_tofupilot/api_v1/measurements/string/openhtf/main.py @@ -0,0 +1,22 @@ +import openhtf as htf +from tofupilot.openhtf import TofuPilot + + +@htf.measures(htf.Measurement("firmware_version").equals("1.2.4")) +def phase_firmware(test): + test.measurements.firmware_version = "1.2.4" + + +def main(): + test = htf.Test( + phase_firmware, + procedure_id="FVT1", # Create the procedure first in the Application + part_number="PCB1", + ) + + with TofuPilot(test): + test.execute(lambda: "PCB001") + + +if __name__ == "__main__": + main() diff --git a/docs_tofupilot/offline-upload/client/main.py b/docs_tofupilot/api_v1/offline-upload/client/main.py similarity index 84% rename from docs_tofupilot/offline-upload/client/main.py rename to docs_tofupilot/api_v1/offline-upload/client/main.py index 580da85..5674b06 100644 --- a/docs_tofupilot/offline-upload/client/main.py +++ b/docs_tofupilot/api_v1/offline-upload/client/main.py @@ -7,7 +7,7 @@ def main(): client = TofuPilotClient() client.create_run( - procedure_id="FVT1", + procedure_id="FVT1", # Create the procedure first in the Application started_at=datetime.now() - timedelta(days=1), # Run performed the day before unit_under_test={ "serial_number": "PCB1A003", diff --git a/docs_tofupilot/offline-upload/openhtf/data/PCB01A69658.openhtf_test.2025-01-20_15-32-06-058.json b/docs_tofupilot/api_v1/offline-upload/openhtf/data/PCB01A69658.openhtf_test.2025-01-20_15-32-06-058.json similarity index 100% rename from docs_tofupilot/offline-upload/openhtf/data/PCB01A69658.openhtf_test.2025-01-20_15-32-06-058.json rename to docs_tofupilot/api_v1/offline-upload/openhtf/data/PCB01A69658.openhtf_test.2025-01-20_15-32-06-058.json diff --git a/docs_tofupilot/offline-upload/openhtf/main.py b/docs_tofupilot/api_v1/offline-upload/openhtf/main.py similarity index 100% rename from docs_tofupilot/offline-upload/openhtf/main.py rename to docs_tofupilot/api_v1/offline-upload/openhtf/main.py diff --git a/docs_tofupilot/phases/advanced/client/main.py b/docs_tofupilot/api_v1/phases/advanced/client/main.py similarity index 96% rename from docs_tofupilot/phases/advanced/client/main.py rename to docs_tofupilot/api_v1/phases/advanced/client/main.py index 8dd7f29..a5a6c2d 100644 --- a/docs_tofupilot/phases/advanced/client/main.py +++ b/docs_tofupilot/api_v1/phases/advanced/client/main.py @@ -9,7 +9,7 @@ def main(): start_time_millis = datetime.now().timestamp() * 1000 client.create_run( - procedure_id="FVT1", # First create procedure in Application + procedure_id="FVT1", # Create the procedure first in the Application unit_under_test={ "serial_number": "PCB1A002", "part_number": "PCB1", diff --git a/docs_tofupilot/phases/advanced/openhtf/main.py b/docs_tofupilot/api_v1/phases/advanced/openhtf/main.py similarity index 80% rename from docs_tofupilot/phases/advanced/openhtf/main.py rename to docs_tofupilot/api_v1/phases/advanced/openhtf/main.py index 99dcfd5..f8ecced 100644 --- a/docs_tofupilot/phases/advanced/openhtf/main.py +++ b/docs_tofupilot/api_v1/phases/advanced/openhtf/main.py @@ -16,9 +16,11 @@ def phase_firmware_version_check(test): # Phase with multiple measurements @htf.measures( - htf.Measurement("input_voltage").in_range(3.1, 3.5).with_units(units.VOLT), - htf.Measurement("output_voltage").in_range(1.1, 1.3).with_units(units.VOLT), -) + htf.Measurement("input_voltage").in_range( + 3.1, 3.5).with_units( + units.VOLT), htf.Measurement("output_voltage").in_range( + 1.1, 1.3).with_units( + units.VOLT), ) def phase_voltage_measurements(test): test.measurements.input_voltage = 3.3 test.measurements.output_voltage = 1.2 diff --git a/docs_tofupilot/phases/optional/client/main.py b/docs_tofupilot/api_v1/phases/optional/client/main.py similarity index 87% rename from docs_tofupilot/phases/optional/client/main.py rename to docs_tofupilot/api_v1/phases/optional/client/main.py index fa9a47d..ed4bba3 100644 --- a/docs_tofupilot/phases/optional/client/main.py +++ b/docs_tofupilot/api_v1/phases/optional/client/main.py @@ -35,10 +35,8 @@ def main(): phases = [phase_voltage_measure()] client.create_run( - procedure_id="FVT1", - unit_under_test={ - "serial_number": "PCB1A001", - "part_number": "PCB1"}, + procedure_id="FVT1", # Create the procedure first in the Application + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, phases=phases, run_passed=all( phase["outcome"] == PhaseOutcome.PASS for phase in phases), diff --git a/docs_tofupilot/phases/optional/openhtf/main.py b/docs_tofupilot/api_v1/phases/optional/openhtf/main.py similarity index 100% rename from docs_tofupilot/phases/optional/openhtf/main.py rename to docs_tofupilot/api_v1/phases/optional/openhtf/main.py diff --git a/docs_tofupilot/phases/required/client/main.py b/docs_tofupilot/api_v1/phases/required/client/main.py similarity index 81% rename from docs_tofupilot/phases/required/client/main.py rename to docs_tofupilot/api_v1/phases/required/client/main.py index e43bd91..f467f33 100644 --- a/docs_tofupilot/phases/required/client/main.py +++ b/docs_tofupilot/api_v1/phases/required/client/main.py @@ -21,10 +21,8 @@ def main(): phases = [phase_one()] client.create_run( - procedure_id="FVT1", - unit_under_test={ - "serial_number": "PCB1A001", - "part_number": "PCB1"}, + procedure_id="FVT1", # Create the procedure first in the Application + unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, phases=phases, run_passed=all( phase["outcome"] == PhaseOutcome.PASS for phase in phases), diff --git a/docs_tofupilot/phases/required/openhtf/main.py b/docs_tofupilot/api_v1/phases/required/openhtf/main.py similarity index 100% rename from docs_tofupilot/phases/required/openhtf/main.py rename to docs_tofupilot/api_v1/phases/required/openhtf/main.py diff --git a/docs_tofupilot/procedures/client/main.py b/docs_tofupilot/api_v1/procedures/client/main.py similarity index 81% rename from docs_tofupilot/procedures/client/main.py rename to docs_tofupilot/api_v1/procedures/client/main.py index 01213c0..37ef2a5 100644 --- a/docs_tofupilot/procedures/client/main.py +++ b/docs_tofupilot/api_v1/procedures/client/main.py @@ -1,4 +1,5 @@ from datetime import timedelta + from tofupilot import TofuPilotClient @@ -6,7 +7,7 @@ def main(): client = TofuPilotClient() client.create_run( - procedure_id="FVT1", # First create procedure in Application + procedure_id="FVT1", # Create the procedure first in the Application run_passed=True, unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, duration=timedelta(minutes=1, seconds=45), diff --git a/docs_tofupilot/procedures/openhtf/main.py b/docs_tofupilot/api_v1/procedures/openhtf/main.py similarity index 78% rename from docs_tofupilot/procedures/openhtf/main.py rename to docs_tofupilot/api_v1/procedures/openhtf/main.py index fb373c4..1c2db5e 100644 --- a/docs_tofupilot/procedures/openhtf/main.py +++ b/docs_tofupilot/api_v1/procedures/openhtf/main.py @@ -4,7 +4,7 @@ def main(): test = htf.Test( - procedure_id="FVT1", # Procedure name set by default as "openhtf_test" + procedure_id="FVT1", # Create the procedure first in the Application part_number="PCB1", ) with TofuPilot(test): diff --git a/docs_tofupilot/stations/client/main.py b/docs_tofupilot/api_v1/stations/client/main.py similarity index 78% rename from docs_tofupilot/stations/client/main.py rename to docs_tofupilot/api_v1/stations/client/main.py index c4c6d9f..f22adcb 100644 --- a/docs_tofupilot/stations/client/main.py +++ b/docs_tofupilot/api_v1/stations/client/main.py @@ -1,10 +1,12 @@ # Before running the script, ensure you have created a station in the TofuPilot interface # and linked it to the specified procedure ID ("FVT1" in this example). # You also need to save your API key in an environment variable named "STATION_API_KEY" -# or pass it directly as an argument like this: TofuPilotClient(api_key="STATION_API_KEY") +# or pass it directly as an argument like this: +# TofuPilotClient(api_key="STATION_API_KEY") -from datetime import timedelta, datetime -from tofupilot import TofuPilotClient, PhaseOutcome +from datetime import datetime, timedelta + +from tofupilot import PhaseOutcome, TofuPilotClient def phase_one(): @@ -26,7 +28,8 @@ def main(): client.create_run( procedure_id="FVT1", # Create a station in TofuPilot linked to this procedure ID - run_passed=all(phase["outcome"] == PhaseOutcome.PASS for phase in phases), + run_passed=all( + phase["outcome"] == PhaseOutcome.PASS for phase in phases), unit_under_test={"serial_number": "PCB1A001", "part_number": "PCBA01"}, phases=phases, duration=timedelta(minutes=1, seconds=45), diff --git a/docs_tofupilot/stations/openhtf/main.py b/docs_tofupilot/api_v1/stations/openhtf/main.py similarity index 88% rename from docs_tofupilot/stations/openhtf/main.py rename to docs_tofupilot/api_v1/stations/openhtf/main.py index 530101d..010ebca 100644 --- a/docs_tofupilot/stations/openhtf/main.py +++ b/docs_tofupilot/api_v1/stations/openhtf/main.py @@ -1,7 +1,8 @@ # Before running the script, ensure you have created a station in the TofuPilot interface # and linked it to the specified procedure ID ("FVT1" in this example). # You also need to save your API key in an environment variable named "STATION_API_KEY" -# or pass it directly as an argument like this: TofuPilot(test, api_key="STATION_API_KEY") +# or pass it directly as an argument like this: TofuPilot(test, +# api_key="STATION_API_KEY") import openhtf as htf diff --git a/docs_tofupilot/sub-units/client/main.py b/docs_tofupilot/api_v1/sub-units/client/main.py similarity index 70% rename from docs_tofupilot/sub-units/client/main.py rename to docs_tofupilot/api_v1/sub-units/client/main.py index 117b214..a4ce1d9 100644 --- a/docs_tofupilot/sub-units/client/main.py +++ b/docs_tofupilot/api_v1/sub-units/client/main.py @@ -9,10 +9,11 @@ def main(): client = TofuPilotClient() client.create_run( - procedure_id="FVT2", # First create procedure in Application + procedure_id="FVT2", # Create the procedure first in the Application unit_under_test={"serial_number": "CAM1A001", "part_number": "CAM1"}, run_passed=True, - sub_units=[{"serial_number": "PCB1A001"}, {"serial_number": "LEN1A001"}], + sub_units=[{"serial_number": "PCB1A001"}, + {"serial_number": "LEN1A001"}], ) diff --git a/docs_tofupilot/sub-units/openhtf/main.py b/docs_tofupilot/api_v1/sub-units/openhtf/main.py similarity index 70% rename from docs_tofupilot/sub-units/openhtf/main.py rename to docs_tofupilot/api_v1/sub-units/openhtf/main.py index 6a866f2..ba37a40 100644 --- a/docs_tofupilot/sub-units/openhtf/main.py +++ b/docs_tofupilot/api_v1/sub-units/openhtf/main.py @@ -8,9 +8,10 @@ def main(): test = Test( - procedure_id="FVT2", # First create procedure in Application + procedure_id="FVT2", # Create the procedure first in the Application part_number="CAM1", - sub_units=[{"serial_number": "PCB1A001"}, {"serial_number": "LEN1A001"}], + sub_units=[{"serial_number": "PCB1A001"}, + {"serial_number": "LEN1A001"}], ) with TofuPilot(test): test.execute(lambda: "CAM1A001") diff --git a/docs_tofupilot/unit-under-test/client/main.py b/docs_tofupilot/api_v1/unit-under-test/client/main.py similarity index 84% rename from docs_tofupilot/unit-under-test/client/main.py rename to docs_tofupilot/api_v1/unit-under-test/client/main.py index d4a6ca5..ca05bfd 100644 --- a/docs_tofupilot/unit-under-test/client/main.py +++ b/docs_tofupilot/api_v1/unit-under-test/client/main.py @@ -7,7 +7,7 @@ def main(): phases = [] client.create_run( - procedure_id="FVT1", + procedure_id="FVT1", # Create the procedure first in the Application unit_under_test={ "serial_number": "PCB1A002", "part_number": "PCB1", diff --git a/docs_tofupilot/unit-under-test/openhtf/main.py b/docs_tofupilot/api_v1/unit-under-test/openhtf/main.py similarity index 83% rename from docs_tofupilot/unit-under-test/openhtf/main.py rename to docs_tofupilot/api_v1/unit-under-test/openhtf/main.py index 3659e70..6600dc8 100644 --- a/docs_tofupilot/unit-under-test/openhtf/main.py +++ b/docs_tofupilot/api_v1/unit-under-test/openhtf/main.py @@ -6,7 +6,7 @@ def main(): phases = [] # Your test phases here test = Test( phases, - procedure_id="FVT1", + procedure_id="FVT1", # Create the procedure first in the Application part_number="PCB1", revision="A", # optional batch_number="12-24", # optional diff --git a/dvt_excel_migration/README.md b/dvt_excel_migration/README.md new file mode 100644 index 0000000..f4a53cb --- /dev/null +++ b/dvt_excel_migration/README.md @@ -0,0 +1,75 @@ +# DVT Excel migration + +Moving a design-verification report out of a spreadsheet and onto the bench, in +two steps. + +## Step 1 — Import the reports you already have + +`xcu_import.py` reads a DVT workbook and creates one TofuPilot run from it. + +TofuPilot imports Excel natively, but two shapes common in hand-written reports +defeat the column mapper: + +- **Stacked cells.** A cell holding `22.33 - 10.11 / 22.34 - 10.10 / 22.35 - 10.11` + is three samples of a two-rail measurement, not one value. +- **Embedded plots.** Screenshots live in the drawing layer, so a mapper that + reads cells never sees them. Their anchor row is what ties a plot to the test + it proves. + +The script flattens both: it expands stacked cells to one measurement per +sample per temperature block, and pulls the images out of the `.xlsx` zip keyed +by anchor row. + +```bash +pip install tofupilot openpyxl +export TOFUPILOT_API_KEY=... + +python xcu_import.py "DVT report.xlsx" \ + --procedure-id \ + --serial-number SN-0001 \ + --part-number PCB-REV-A +``` + +Add `--dry-run` to see what it would create without uploading, and +`--waveforms ` to promote curves to multi-dimensional measurements when +scope CSVs are available (named `_.csv`). + +On a real 5-sheet report this produced one run with 13 phases, 111 +measurements and 49 attached plots in about 30 seconds. + +## Step 2 — Stop writing the report + +`smps-dvt/` is the same tests as a TofuPilot procedure, so values are captured +as they are measured rather than read off a screen and typed into a sheet. + +``` +smps-dvt/ +├── procedure.yaml phases, measurements and limits +├── phases/ the Python each phase runs +└── plugs/ one class per instrument +``` + +```bash +tofupilot run ./smps-dvt +``` + +The plugs return representative data so the procedure runs anywhere. Every +method keeps its real SCPI call directly above, commented out — swap the two +and the same procedure drives the bench: + +- `plugs/scope.py` — Tektronix MSO5-series, 12-bit High Res, AC-coupled ripple + with a 20 MHz bandwidth limit, `CURVe?` waveform readback with + `YMUlt`/`YOFf`/`YZEro`/`XINcr` scaling +- `plugs/ac_source.py` — programmable AC source for the mains sweep + +What the spreadsheet cannot do: + +- The eleven crossed-regulation rows become **one sweep**, recorded as a + regulation curve indexed by input voltage, with `min`/`max` aggregations + validated against the rail limits. +- Each ripple test keeps **the waveform itself**, not a picture of it, and the + 400 mV ceiling is checked against the trace rather than a transcribed number. + +Because the curve is data, the same measurement stays comparable across samples +and across temperature setpoints. Run one per chamber setpoint and the drift is +visible directly. diff --git a/dvt_excel_migration/smps_dvt/phases/crossed_regulation.py b/dvt_excel_migration/smps_dvt/phases/crossed_regulation.py new file mode 100644 index 0000000..75d7a5c --- /dev/null +++ b/dvt_excel_migration/smps_dvt/phases/crossed_regulation.py @@ -0,0 +1,12 @@ +"""Both rails at nominal input: one row of the report.""" + +CH_23V, CH_9V = 1, 2 + + +def crossed_regulation(measurements, scope, ac_source): + ac_source.set_voltage(230) + scope.configure_channel(CH_23V, 5.0) + scope.configure_channel(CH_9V, 2.0) + + measurements.rail_23v = scope.measure_dc(CH_23V) + measurements.rail_9v = scope.measure_dc(CH_9V) diff --git a/dvt_excel_migration/smps_dvt/phases/crossed_regulation_sweep.py b/dvt_excel_migration/smps_dvt/phases/crossed_regulation_sweep.py new file mode 100644 index 0000000..69158ca --- /dev/null +++ b/dvt_excel_migration/smps_dvt/phases/crossed_regulation_sweep.py @@ -0,0 +1,39 @@ +"""The rows a report fills in one at a time, as a single sweep. + +Each rail becomes a curve indexed by input voltage, so regulation is something +to compare across samples rather than a column of numbers to read. +""" + +# Nominal 230 Vac +/-10%. +MAINS_SWEEP_VAC = (207, 216, 230, 244, 253) + +CH_23V, CH_9V = 1, 2 + + +def crossed_regulation_sweep(measurements, scope, ac_source): + mains = [] + rail_23v = [] + rail_9v = [] + + for volts in MAINS_SWEEP_VAC: + ac_source.set_voltage(volts) + mains.append(volts) + rail_23v.append(scope.measure_dc(CH_23V)) + rail_9v.append(scope.measure_dc(CH_9V)) + + measurements.rail_23v_vs_mains.x_axis = mains + measurements.rail_23v_vs_mains.y_axis.rail_23v = rail_23v + # The sweep passes when the rail stays inside its limits at every point. + measurements.rail_23v_vs_mains.y_axis.rail_23v.aggregations.min = min( + rail_23v) + measurements.rail_23v_vs_mains.y_axis.rail_23v.aggregations.max = max( + rail_23v) + + measurements.rail_9v_vs_mains.x_axis = mains + measurements.rail_9v_vs_mains.y_axis.rail_9v = rail_9v + measurements.rail_9v_vs_mains.y_axis.rail_9v.aggregations.min = min( + rail_9v) + measurements.rail_9v_vs_mains.y_axis.rail_9v.aggregations.max = max( + rail_9v) + + ac_source.set_voltage(230) diff --git a/dvt_excel_migration/smps_dvt/phases/ripple_23v.py b/dvt_excel_migration/smps_dvt/phases/ripple_23v.py new file mode 100644 index 0000000..7ac0e85 --- /dev/null +++ b/dvt_excel_migration/smps_dvt/phases/ripple_23v.py @@ -0,0 +1,20 @@ +"""23V ripple: the two transcribed figures, plus the trace they came from.""" + +CH_23V = 1 + + +def ripple_23v(measurements, scope): + scope.configure_channel(CH_23V, 0.02) # 20 mV/div + scope.set_timebase(0.01) # 10 ms/div: ~20 cycles of 100 Hz ripple + scope.acquire() + + peak_mv, rms_mv = scope.measure_ripple(CH_23V) + measurements.ripple_23v_peak_to_peak = peak_mv + measurements.ripple_23v_rms = rms_mv + + times, values = scope.capture_waveform(CH_23V) + measurements.ripple_23v_waveform.x_axis = times + measurements.ripple_23v_waveform.y_axis.ripple = values + # The same ceiling as the transcribed figure, checked against the trace. + measurements.ripple_23v_waveform.y_axis.ripple.aggregations.peak_to_peak = max( + values) - min(values) diff --git a/dvt_excel_migration/smps_dvt/phases/ripple_9v.py b/dvt_excel_migration/smps_dvt/phases/ripple_9v.py new file mode 100644 index 0000000..8d4b477 --- /dev/null +++ b/dvt_excel_migration/smps_dvt/phases/ripple_9v.py @@ -0,0 +1,20 @@ +"""9V ripple: the two transcribed figures, plus the trace they came from.""" + +CH_9V = 2 + + +def ripple_9v(measurements, scope): + scope.configure_channel(CH_9V, 0.02) # 20 mV/div + scope.set_timebase(0.01) # 10 ms/div: ~20 cycles of 100 Hz ripple + scope.acquire() + + peak_mv, rms_mv = scope.measure_ripple(CH_9V) + measurements.ripple_9v_peak_to_peak = peak_mv + measurements.ripple_9v_rms = rms_mv + + times, values = scope.capture_waveform(CH_9V) + measurements.ripple_9v_waveform.x_axis = times + measurements.ripple_9v_waveform.y_axis.ripple = values + # The same ceiling as the transcribed figure, checked against the trace. + measurements.ripple_9v_waveform.y_axis.ripple.aggregations.peak_to_peak = max( + values) - min(values) diff --git a/dvt_excel_migration/smps_dvt/plugs/ac_source.py b/dvt_excel_migration/smps_dvt/plugs/ac_source.py new file mode 100644 index 0000000..6e4ee38 --- /dev/null +++ b/dvt_excel_migration/smps_dvt/plugs/ac_source.py @@ -0,0 +1,31 @@ +"""Programmable AC source driving the mains side of the regulation sweep.""" + +# VISA resource of the AC source on the bench LAN. +AC_SOURCE_RESOURCE = "TCPIP0::192.0.2.11::inst0::INSTR" + +LINE_FREQUENCY_HZ = 50.0 + + +class ACSource: + def __init__(self): + self._source = None + + # import pyvisa + # + # self._source = pyvisa.ResourceManager().open_resource(AC_SOURCE_RESOURCE) + # self._source.timeout = 10_000 + # self._source.write(f"SOURce:FREQuency {LINE_FREQUENCY_HZ}") + # self._source.write("OUTPut:STATe ON") + + def __del__(self): + # self._source.write("OUTPut:STATe OFF") + pass + + def identity(self) -> str: + # return self._source.query("*IDN?").strip() + return "GW-INSTEK,APS-7100,SIMULATED,1.0" + + def set_voltage(self, volts: float) -> None: + # self._source.write(f"SOURce:VOLTage {volts}") + # self._source.query("*OPC?") + pass diff --git a/dvt_excel_migration/smps_dvt/plugs/scope.py b/dvt_excel_migration/smps_dvt/plugs/scope.py new file mode 100644 index 0000000..75bdb82 --- /dev/null +++ b/dvt_excel_migration/smps_dvt/plugs/scope.py @@ -0,0 +1,142 @@ +"""Tektronix MSO5-series oscilloscope. + +Reads the same figures a hand-written report transcribes off the screen +(Peak-to-Peak and RMS) and, unlike a screenshot, also returns the waveform +behind them. + +The methods below generate representative data so the procedure runs anywhere. +Each keeps its real instrument call directly above, commented out: swap the two +and the same procedure drives the bench. +""" + +import math +import random + +# VISA resource of the scope on the bench LAN. +SCOPE_RESOURCE = "TCPIP0::192.0.2.10::inst0::INSTR" + +RAIL_23V_NOM = 23.0 +RAIL_9V_NOM = 9.0 +CH_23V = 1 + +# Rectified mains ripple sits at twice the line frequency. +LINE_FREQUENCY_HZ = 50.0 + + +class Oscilloscope: + def __init__(self): + self._scope = None + + # import pyvisa + # + # self._scope = pyvisa.ResourceManager().open_resource(SCOPE_RESOURCE) + # self._scope.timeout = 20_000 + # self._scope.write("*RST") + # # 12-bit High Res: at 400 mV of ripple on a 23 V rail the extra bits + # # are the difference between measuring ripple and measuring the + # # quantiser. + # self._scope.write("ACQuire:MODe HIRes") + # self._scope.write("HORizontal:MODe AUTO") + + def identity(self) -> str: + """Model and firmware, recorded with the run for traceability.""" + # return self._scope.query("*IDN?").strip() + return "TEKTRONIX,MSO54,SIMULATED,1.0" + + def configure_channel(self, channel: int, volts_per_div: float) -> None: + """AC-couple the rail so its DC level does not eat the vertical range.""" + # self._scope.write(f"DISplay:GLObal:CH{channel}:STATE ON") + # self._scope.write(f"CH{channel}:COUPling AC") + # # 20 MHz is the usual bandwidth limit for a ripple measurement. + # self._scope.write(f"CH{channel}:BANdwidth 20E6") + # self._scope.write(f"CH{channel}:SCAle {volts_per_div}") + # self._scope.write(f"CH{channel}:OFFSet 0") + pass + + def set_timebase(self, seconds_per_div: float) -> None: + # self._scope.write(f"HORizontal:SCAle {seconds_per_div}") + pass + + def acquire(self) -> None: + """One single-sequence acquisition, so every read is the same capture.""" + # self._scope.write("ACQuire:STOPAfter SEQuence") + # self._scope.write("ACQuire:STATE RUN") + # self._scope.query("*OPC?") + pass + + def measure_dc(self, channel: int) -> float: + """Rail voltage in volts, DC-coupled mean.""" + # self._scope.write(f"CH{channel}:COUPling DC") + # self.acquire() + # self._scope.write("MEASUrement:ADDMEAS MEAN") + # self._scope.write(f"MEASUrement:MEAS1:SOUrce CH{channel}") + # self._scope.write("MEASUrement:MEAS1:TYPe MEAN") + # self._scope.query("*OPC?") + # return float( + # self._scope.query("MEASUrement:MEAS1:RESUlt:CURRentacq:MEAN?") + # ) + nominal = RAIL_23V_NOM if channel == CH_23V else RAIL_9V_NOM + return round(random.gauss(nominal, 0.04), 3) + + def measure_ripple(self, channel: int): + """Peak-to-peak and RMS ripple in millivolts, AC-coupled.""" + # self._scope.write("MEASUrement:ADDMEAS PK2PK") + # self._scope.write(f"MEASUrement:MEAS1:SOUrce CH{channel}") + # self._scope.write("MEASUrement:MEAS1:TYPe PK2PK") + # self._scope.write("MEASUrement:ADDMEAS RMS") + # self._scope.write(f"MEASUrement:MEAS2:SOUrce CH{channel}") + # self._scope.write("MEASUrement:MEAS2:TYPe RMS") + # self._scope.query("*OPC?") + # peak_volts = float( + # self._scope.query("MEASUrement:MEAS1:RESUlt:CURRentacq:MEAN?") + # ) + # rms_volts = float( + # self._scope.query("MEASUrement:MEAS2:RESUlt:CURRentacq:MEAN?") + # ) + # return peak_volts * 1000.0, rms_volts * 1000.0 + peak_mv = random.uniform(120.0, 340.0) + # Rectified-mains ripple sits near a crest factor of 4. + return round(peak_mv, 1), round(peak_mv / 4.2, 2) + + def capture_waveform(self, channel: int): + """The trace behind those numbers, as (seconds, millivolts). + + The record is decimated to a couple of thousand points: enough to keep + the ripple envelope, small enough to store on every run. + """ + # self._scope.write(f"DATa:SOUrce CH{channel}") + # self._scope.write("DATa:ENCdg ASCII") + # self._scope.write("DATa:STARt 1") + # record = int(self._scope.query("HORizontal:RECOrdlength?")) + # self._scope.write(f"DATa:STOP {record}") + # + # counts = [float(v) for v in self._scope.query("CURVe?").split(",")] + # y_multiplier = float(self._scope.query("WFMOutpre:YMUlt?")) + # y_offset = float(self._scope.query("WFMOutpre:YOFf?")) + # y_zero = float(self._scope.query("WFMOutpre:YZEro?")) + # x_increment = float(self._scope.query("WFMOutpre:XINcr?")) + # + # stride = max(1, len(counts) // 2000) + # times, values = [], [] + # for index in range(0, len(counts), stride): + # volts = (counts[index] - y_offset) * y_multiplier + y_zero + # times.append(index * x_increment) + # values.append(volts * 1000.0) + # return times, values + + points = 500 + duration = 0.2 # 10 ms/div across 20 divisions + step = duration / points + amplitude = random.uniform(60.0, 170.0) + ripple_hz = 2 * LINE_FREQUENCY_HZ + + times, values = [], [] + for index in range(points): + moment = index * step + fundamental = amplitude * \ + math.sin(2 * math.pi * ripple_hz * moment) + # Switching noise rides on the rectified ripple. + noise = random.gauss(0, amplitude * 0.06) + times.append(round(moment, 6)) + values.append(round(fundamental + noise, 3)) + return times, values diff --git a/dvt_excel_migration/smps_dvt/procedure.yaml b/dvt_excel_migration/smps_dvt/procedure.yaml new file mode 100644 index 0000000..189fe0d --- /dev/null +++ b/dvt_excel_migration/smps_dvt/procedure.yaml @@ -0,0 +1,132 @@ +name: SMPS Regulation and Ripple +version: 1.0.0 + +unit: + auto_identify: true + serial_number: + default_value: "SN-0001" + part_number: + default_value: "PCB-REV-A" + +plugs: + - key: scope + name: Oscilloscope + python: plugs.scope:Oscilloscope + - key: ac_source + name: AC Source + python: plugs.ac_source:ACSource + +main: + - name: Crossed Regulation + python: phases.crossed_regulation + measurements: + # Rail limits come straight from the report's Min./Max. criteria columns. + - name: Rail 23V + unit: V + validators: + - operator: ">=" + expected_value: 21.9 + - operator: "<=" + expected_value: 23.1 + - name: Rail 9V + unit: V + validators: + - operator: ">=" + expected_value: 8.5 + - operator: "<=" + expected_value: 9.5 + + - name: Crossed Regulation Sweep + python: phases.crossed_regulation_sweep + measurements: + # The rows a report fills in one at a time become a regulation curve. + - name: Rail 23V vs Mains + title: 23V rail across the mains range + x_axis: + legend: Mains + unit: Vac + y_axis: + - legend: Rail 23V + unit: V + aggregations: + - type: min + validators: + - operator: ">=" + expected_value: 21.9 + - type: max + validators: + - operator: "<=" + expected_value: 23.1 + - name: Rail 9V vs Mains + title: 9V rail across the mains range + x_axis: + legend: Mains + unit: Vac + y_axis: + - legend: Rail 9V + unit: V + aggregations: + - type: min + validators: + - operator: ">=" + expected_value: 8.5 + - type: max + validators: + - operator: "<=" + expected_value: 9.5 + + - name: Ripple 23V + python: phases.ripple_23v + measurements: + # The two figures a hand-written report transcribes off the scope screen. + - name: Ripple 23V Peak-to-Peak + unit: mV + validators: + - operator: "<=" + expected_value: 400 + - name: Ripple 23V RMS + unit: mV + validators: + - operator: "<=" + expected_value: 100 + # ...and the trace they were measured on, kept as data. + - name: Ripple 23V Waveform + title: 23V ripple + x_axis: + legend: Time + unit: s + y_axis: + - legend: Ripple + unit: mV + aggregations: + - type: peak_to_peak + validators: + - operator: "<=" + expected_value: 400 + + - name: Ripple 9V + python: phases.ripple_9v + measurements: + - name: Ripple 9V Peak-to-Peak + unit: mV + validators: + - operator: "<=" + expected_value: 400 + - name: Ripple 9V RMS + unit: mV + validators: + - operator: "<=" + expected_value: 100 + - name: Ripple 9V Waveform + title: 9V ripple + x_axis: + legend: Time + unit: s + y_axis: + - legend: Ripple + unit: mV + aggregations: + - type: peak_to_peak + validators: + - operator: "<=" + expected_value: 400 diff --git a/dvt_excel_migration/xcu_import.py b/dvt_excel_migration/xcu_import.py new file mode 100644 index 0000000..4afbd14 --- /dev/null +++ b/dvt_excel_migration/xcu_import.py @@ -0,0 +1,393 @@ +"""Import a DVT report spreadsheet into TofuPilot. + +Written for reports that keep several sample readings inside a single cell and +store their plots as embedded images: neither shape survives the native Excel +import, because the column mapper reads one value per cell and never sees the +drawing layer. This script flattens both. + + pip install tofupilot openpyxl + export TOFUPILOT_API_KEY=... + python xcu_import.py "DVT report.xlsx" --procedure-id \\ + --serial-number SN-0001 --part-number PCB-REV-A + +Waveform CSVs exported from the scope are picked up automatically when a +--waveforms directory is passed: a curve then lands as a real multi-dimensional +measurement (per-axis units, validators, comparable across runs) instead of a +screenshot pinned to the run. +""" + +from __future__ import annotations + +import argparse +import os +import re +import struct +import zipfile +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from urllib.request import Request, urlopen + +import openpyxl +from tofupilot.v2 import TofuPilot + +# Row 50 names the temperature block, row 51 the columns; readings start at 52. +BLOCK_ROW = 50 +HEADER_ROW = 51 +SHEET = "SMPS reg+ripple" + +# Each temperature block repeats (value, units, result) at a fixed offset. The +# sample class sits in row 48 above the same columns. +BLOCK_COLUMNS = (18, 23, 28, 33, 38, 43, 48) # R, W, AB, AG, AL, AQ, AV +VALUE_OFFSET, UNITS_OFFSET, RESULT_OFFSET = 0, 1, 3 + +# "22.33 - 10.11" is one reading of a dual-rail test, not two measurements. +RAIL_SPLIT = re.compile(r"\s*-\s*") +NUMERIC = re.compile(r"-?\d+(?:[.,]\d+)?") + + +@dataclass +class Reading: + """One test row, expanded to one entry per sample.""" + + test_number: str + description: str + procedure: str + conditions: str + sample: str + values: list[float] + units: str + lower: float | None + upper: float | None + outcome: str + plots: list[str] = field(default_factory=list) + + +def parse_stacked(cell: object) -> list[list[float]]: + """Split a cell holding one reading per line into per-sample values. + + "22.33 - 10.11\\n22.34 - 10.10\\n22.35 - 10.11" is three samples of a + two-rail measurement, so it yields [[22.33, 10.11], [22.34, 10.10], ...]. + """ + if cell is None: + return [] + if isinstance(cell, (int, float)): + return [[float(cell)]] + + rows: list[list[float]] = [] + for line in str(cell).replace("\r", "\n").split("\n"): + if not line.strip(): + continue + rails = [NUMERIC.search(part) for part in RAIL_SPLIT.split(line)] + values = [float(m.group().replace(",", ".")) for m in rails if m] + if values: + rows.append(values) + return rows + + +# Letterheads and icons sit in the drawing layer next to the real captures. +# A scope screenshot is a full window grab, so it is wide, roughly landscape, +# and never a small banner. Filtering on pixels rather than file size keeps a +# lightly-compressed plot and drops a detailed logo. +MIN_PLOT_WIDTH = 600 +MIN_PLOT_HEIGHT = 300 + + +def png_size(payload: bytes) -> tuple[int, int] | None: + """Width and height from a PNG's IHDR, without a decoder dependency.""" + if len(payload) < 24 or payload[:8] != b"\x89PNG\r\n\x1a\n": + return None + width, height = struct.unpack(">II", payload[16:24]) + return width, height + + +def is_plot(payload: bytes) -> bool: + size = png_size(payload) + if size is None: + return True # not a PNG we can measure; let it through rather than lose data + width, height = size + return width >= MIN_PLOT_WIDTH and height >= MIN_PLOT_HEIGHT + + +def extract_images(workbook_path: Path, + out_dir: Path) -> dict[int, list[Path]]: + """Pull embedded PNGs out of the .xlsx and group them by anchor row. + + openpyxl drops images on load, so the drawing XML is read straight from the + zip. The anchor row is what ties a plot to the test it proves — the whole + reason these cannot go in as one undifferentiated pile. + + Logos are skipped, and an image repeated across rows is only kept once. + """ + out_dir.mkdir(parents=True, exist_ok=True) + by_row: dict[int, list[Path]] = {} + seen: set[str] = set() + + with zipfile.ZipFile(workbook_path) as z: + rels = {} + for name in z.namelist(): + if re.fullmatch(r"xl/drawings/_rels/drawing\d+\.xml\.rels", name): + body = z.read(name).decode("utf8", "ignore") + rels[name] = re.findall( + r'Id="([^"]+)"[^>]*?media/([^"]+)"', body) + + for name in z.namelist(): + if not re.fullmatch(r"xl/drawings/drawing\d+\.xml", name): + continue + rel_key = name.replace("drawings/", "drawings/_rels/") + ".rels" + media = dict(rels.get(rel_key, [])) + body = z.read(name).decode("utf8", "ignore") + + for anchor in re.finditer( + r".*?(\d+).*?embed=\"([^\"]+)\"", + body, + re.S, + ): + row = int(anchor.group(1)) + 1 # xdr rows are 0-based + target = media.get(anchor.group(2)) + if not target: + continue + + payload = z.read(f"xl/media/{target}") + if not is_plot(payload): + continue # letterhead or icon, not a capture + if target in seen: + continue # same image anchored on several rows + + seen.add(target) + dest = out_dir / target + if not dest.exists(): + dest.write_bytes(payload) + by_row.setdefault(row, []).append(dest) + + return by_row + + +def load_waveform(path: Path) -> tuple[list[float], list[float]]: + """Read a two-column scope CSV into (time, amplitude).""" + xs: list[float] = [] + ys: list[float] = [] + for line in path.read_text(errors="ignore").splitlines(): + parts = line.replace(";", ",").split(",") + if len(parts) < 2: + continue + try: + xs.append(float(parts[0])) + ys.append(float(parts[1])) + except ValueError: + continue # header or preamble line + return xs, ys + + +def read_report(path: Path, images: dict[int, list[Path]]) -> list[Reading]: + sheet = openpyxl.load_workbook(path, data_only=True)[SHEET] + readings: list[Reading] = [] + + def text(row: int, col: int) -> str: + value = sheet.cell(row=row, column=col).value + return "" if value is None else str(value).strip() + + for row in range(HEADER_ROW + 1, sheet.max_row + 1): + description = text(row, 3) # C: Testdescription + if not description: + continue + + limits = [ + sheet.cell( + row=row, + column=col).value for col in ( + 10, + 12)] # J, L + lower, upper = ( + float(v) if isinstance(v, (int, float)) else None for v in limits + ) + conditions = " ".join( + filter(None, (text(row, col) for col in range(5, 10))) # E..I + ) + + for block in BLOCK_COLUMNS: + # Row 50 gives the temperature, row 48 the sample class. + temperature = text(BLOCK_ROW, block) or "Room temp." + sample_class = text(48, block) + per_sample = parse_stacked( + sheet.cell(row=row, column=block + VALUE_OFFSET).value + ) + if not per_sample: + continue + + units = text(row, block + UNITS_OFFSET).split("\n")[0] + outcome = text(row, block + RESULT_OFFSET) + + for index, values in enumerate(per_sample, start=1): + label = f"{sample_class} #{index}" if sample_class else f"#{index}" + readings.append( + Reading( + test_number=text(row, 2) or str(row), # B: Testnr + description=description, + procedure=text(row, 4), # D: Measuring procedure + conditions=conditions, + sample=f"{label} @ {temperature}", + values=values, + units=units, + lower=lower, + upper=upper, + outcome=outcome or "UNSET", + plots=[str(p) for p in images.get(row, [])], + ) + ) + + return readings + + +def to_outcome(raw: str) -> str: + """Their result column is prose ("Same result, Pass", "ToDo").""" + lowered = raw.strip().lower() + if "fail" in lowered: + return "FAIL" + if "pass" in lowered: + return "PASS" + return "UNSET" + + +def build_phases(readings: list[Reading], + waveforms: Path | None) -> list[dict]: + phases: dict[str, dict] = {} + + for reading in readings: + phase = phases.setdefault( + reading.description, + { + "name": reading.description, + "outcome": "PASS", + "started_at": datetime.now(timezone.utc), + "ended_at": datetime.now(timezone.utc), + "docstring": reading.procedure or None, + "measurements": [], + }, + ) + + outcome = to_outcome(reading.outcome) + if outcome == "FAIL": + phase["outcome"] = "FAIL" + + validators = [] + if reading.lower is not None: + validators.append( + {"operator": ">=", "expected_value": reading.lower}) + if reading.upper is not None: + validators.append( + {"operator": "<=", "expected_value": reading.upper}) + + curve = None + if waveforms: + candidate = waveforms / \ + f"{reading.test_number}_{reading.sample}.csv" + if candidate.exists(): + curve = load_waveform(candidate) + + measurement: dict = { + "name": f"{reading.description} — {reading.sample}", + "outcome": outcome, + "docstring": reading.conditions or None, + } + + if curve: + # A real curve carries its own axes, so it stays comparable across + # samples and temperatures rather than being a picture of a result. + times, amplitudes = curve + measurement["x_axis"] = { + "name": "Time", "units": "s", "data": times} + measurement["y_axis"] = [ + { + "name": reading.description, + "units": reading.units or "V", + "data": amplitudes, + "validators": validators or None, + } + ] + else: + measurement["measured_value"] = reading.values[0] + measurement["units"] = reading.units or None + if validators: + measurement["validators"] = validators + + phase["measurements"].append(measurement) + + return list(phases.values()) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("workbook", type=Path) + parser.add_argument("--procedure-id", required=True) + parser.add_argument("--serial-number", required=True) + parser.add_argument("--part-number", required=True) + parser.add_argument( + "--waveforms", + type=Path, + help="Directory of scope CSVs named _.csv", + ) + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + plots_dir = args.workbook.parent / "plots" + images = extract_images(args.workbook, plots_dir) + readings = read_report(args.workbook, images) + phases = build_phases(readings, args.waveforms) + + curves = sum(1 for p in phases for m in p["measurements"] if "y_axis" in m) + print( + f"{len(readings)} readings across {len(phases)} phases " + f"({curves} as waveforms, {sum(len(v) for v in images.values())} plots)") + + if args.dry_run: + for phase in phases: + print( + f" {phase['name']}: {len(phase['measurements'])} measurements") + return + + now = datetime.now(timezone.utc) + client_options = {"api_key": os.environ["TOFUPILOT_API_KEY"]} + if os.environ.get("TOFUPILOT_URL"): + client_options["server_url"] = os.environ["TOFUPILOT_URL"] + + with TofuPilot(**client_options) as client: + run = client.runs.create( + outcome="FAIL" if any( + p["outcome"] == "FAIL" for p in phases) else "PASS", + procedure_id=args.procedure_id, + started_at=now, + ended_at=now, + serial_number=args.serial_number, + part_number=args.part_number, + phases=phases, + ) + print(f"run {run.id}") + + # Screenshots ride along on the run; a curve imported as a measurement + # above already sits on its own test. Each upload is initialize -> PUT + # to the pre-signed URL -> finalize. + uploads = [] + for paths in images.values(): + for path in paths: + blob = Path(path) + upload = client.attachments.initialize(name=blob.name) + response = urlopen( + Request( + upload.upload_url, + data=blob.read_bytes(), + method="PUT", + headers={"Content-Type": "image/png"}, + ) + ) + response.read() + client.attachments.finalize(id=upload.id) + uploads.append(upload.id) + + if uploads: + client.runs.update(id=run.id, attachments=uploads) + print(f"{len(uploads)} plots attached") + + +if __name__ == "__main__": + main() diff --git a/qa/client/create_run/basic/main.py b/qa/client/create_run/basic/main.py deleted file mode 100644 index 21c96b7..0000000 --- a/qa/client/create_run/basic/main.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Basic example showing how to create a test run using the TofuPilotClient. - -This script creates a test run for a unit with the specified serial number and part number, -indicating whether the test passed. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient -import random - - -def main(): - # Initialize the TofuPilot client. - client = TofuPilotClient() - # Create a test run for the unit with serial number "00102" and part - # number "PCB01" - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220D4K{random_digits}" - client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": serial_number, "part_number": "PCB01"}, - run_passed=True, - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/create_run/fpy/main.py b/qa/client/create_run/fpy/main.py deleted file mode 100644 index 2dd951f..0000000 --- a/qa/client/create_run/fpy/main.py +++ /dev/null @@ -1,25 +0,0 @@ -import random -from datetime import datetime, timedelta - -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - - -def handle_test(): - # Create 2 test for same SN - for testnumber in range(2): - client.create_run( - procedure_id="FVT112", - started_at=datetime.now(), - unit_under_test={ - "part_number": "FPY", - "serial_number": "FPY-0123410", - }, - # First Good, Second KO, to check FPY - run_passed=True if testnumber == 0 else False, - ) - - -if __name__ == "__main__": - handle_test() diff --git a/qa/client/create_run/multiple_sub_units/main.py b/qa/client/create_run/multiple_sub_units/main.py deleted file mode 100644 index 32315d0..0000000 --- a/qa/client/create_run/multiple_sub_units/main.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Example script demonstrating how to create a test run in TofuPilot with multiple sub-units. - -This script creates a test run for a unit with the specified serial number and part number, -and includes multiple sub-units in the run. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - -# Create a test run for the unit with serial number "00003" and part number "SI002", -# including sub-units with serial numbers "00002" and "00102" -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00003", "part_number": "SI002"}, - run_passed=True, - sub_units=[{"serial_number": "00002"}, {"serial_number": "00102"}], -) diff --git a/qa/client/create_run/phases_string_outcome/main.py b/qa/client/create_run/phases_string_outcome/main.py deleted file mode 100644 index d900ed7..0000000 --- a/qa/client/create_run/phases_string_outcome/main.py +++ /dev/null @@ -1,56 +0,0 @@ -import random -import time -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - - -def flash_firmware(): - passed = bool(random.randint(0, 1)) - measured_value = "1.2.4" if passed else "1.2.0" - return passed, measured_value, None, None, None - - -def handle_test(): - serial_number = f"SI0364A{random.randint(10000, 99999)}" - - start_time = int(time.time() * 1000) - passed, measured_value, unit, limit_low, limit_high = flash_firmware() - end_time = int(time.time() * 1000) - - outcome = "PASS" if passed else "FAIL" - - phase = { - "name": "flash_firmware", - "outcome": outcome, - "start_time_millis": start_time, - "end_time_millis": end_time, - "measurements": [ - { - "name": "flash_firmware", - "outcome": outcome, - "measured_value": measured_value, - "units": unit, - "lower_limit": limit_low, - "upper_limit": limit_high, - } - ], - } - - client.create_run( - procedure_id="FVT9", - procedure_name="Test_QA", - unit_under_test={ - "part_number": "SI03645A", - "part_name": "test-QA", - "revision": "3.1", - "batch_number": "11-24", - "serial_number": serial_number, - }, - run_passed=passed, - phases=[phase], - ) - - -if __name__ == "__main__": - handle_test() diff --git a/qa/client/create_run/procedure_version/main.py b/qa/client/create_run/procedure_version/main.py deleted file mode 100644 index d4e7bec..0000000 --- a/qa/client/create_run/procedure_version/main.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import time -from datetime import datetime, timedelta -from tofupilot import TofuPilotClient - - -def main(): - client = TofuPilotClient() - - # Generate SN - serial_number = f"SI0364A{random.randint(10000, 99999)}" - - # 1 Phase test - start_time_millis = int(time.time() * 1000) - voltage = round(random.uniform(3, 4), 1) - limits = {"limit_low": 3.1, "limit_high": 3.5} - passed = limits["limit_low"] <= voltage <= limits["limit_high"] - outcome = {True: "PASS", False: "FAIL"}[passed] - end_time_millis = int(time.time() * 1000) - - client.create_run( - unit_under_test={ - "part_number": "SI0364", - "serial_number": serial_number, - "revision": "A", - }, - procedure_id="FVT1", # First create procedure in Application - procedure_version="1.2.20", # Create procedure version - phases=[ - { - "name": "test_voltage", - "outcome": outcome, - "start_time_millis": start_time_millis, - "end_time_millis": end_time_millis, - "measurements": [ - { - "name": "voltage_input", - "outcome": outcome, - "measured_value": voltage, - "units": "V", - "lower_limit": limits["limit_low"], - "upper_limit": limits["limit_high"], - }, - ], - }, - ], - run_passed=passed, - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/create_run/single_sub_unit/main.py b/qa/client/create_run/single_sub_unit/main.py deleted file mode 100644 index 5645c7f..0000000 --- a/qa/client/create_run/single_sub_unit/main.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Example script demonstrating how to create a test run in TofuPilot with a single sub-unit. - -This script creates a test run for a unit with the specified serial number and part number, -and includes a single sub-unit in the run. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - -# Create a test run for the unit with serial number "00002" and part number "SI001", -# including a sub-unit with serial number "00102" -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00002", "part_number": "SI001"}, - run_passed=True, - sub_units=[{"serial_number": "00102"}], -) diff --git a/qa/client/create_run/started_at/main.py b/qa/client/create_run/started_at/main.py deleted file mode 100644 index 22f5fbf..0000000 --- a/qa/client/create_run/started_at/main.py +++ /dev/null @@ -1,23 +0,0 @@ -from tofupilot import TofuPilotClient -import random -from datetime import datetime, timedelta - - -def main(): - # Initialize the TofuPilot client. - client = TofuPilotClient() - # Create a test run for the unit with serial number "00102" and part - # number "PCB01" - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220D4K{random_digits}" - client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": serial_number, "part_number": "PCB01"}, - run_passed=True, - started_at=datetime.now() - timedelta(days=1), - duration=timedelta(seconds=23), - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/create_run/with_all_types_of_phases/main.py b/qa/client/create_run/with_all_types_of_phases/main.py deleted file mode 100644 index 7138be7..0000000 --- a/qa/client/create_run/with_all_types_of_phases/main.py +++ /dev/null @@ -1,89 +0,0 @@ -import random -from datetime import datetime, timedelta -from random import randint - -from tofupilot import TofuPilotClient - -# Reference time to calculate start_time_millis in milliseconds since epoch -epoch = datetime(1970, 1, 1) - - -# Function to calculate milliseconds since epoch -def to_millis(dt): - return int((dt - epoch).total_seconds() * 1000) - - -client = TofuPilotClient() - -client.create_run( - procedure_id="FVT1", - unit_under_test={ - "serial_number": "SN17", - "part_number": "PNrstsrtsr", - "batch_number": "B", - }, - run_passed=True, # Overall run status - phases=[ - { - "name": "phase_connect", # First phase - "outcome": "PASS", - "start_time_millis": to_millis( - datetime.now() - ), # Start time of the step in ms - "end_time_millis": to_millis( - datetime.now() + timedelta(seconds=5, milliseconds=12) - ), # End time in ms - "measurements": [ - { - "name": "numeric_measurement", - "outcome": "PASS", - "measured_value": 12, - "units": "Hertz", - "lower_limit": 1, - "upper_limit": 20, - }, - { - "name": "string_measurement", - "outcome": "PASS", - "measured_value": "Test value", - "units": "Unitless", - "docstring": "This is a string measurement example", - }, - { - "name": "boolean_measurement_true", - "outcome": "PASS", - "measured_value": True, - "units": "BooleanUnit", - "docstring": "This is a boolean measurement example", - }, - { - "name": "boolean_measurement_false", - "outcome": "PASS", - "measured_value": False, - "units": "BooleanUnit", - "docstring": "This is a boolean measurement example", - }, - { - "name": "json_measurement", - "outcome": "PASS", - "measured_value": {"key1": "value1", "key2": 42}, - "units": "JSONUnit", - "docstring": "This is a JSON measurement example", - }, - { - "name": "empty_measurement", - "outcome": "PASS", - "measured_value": None, - "units": "EmptyUnit", - "docstring": "This is a measurement with a null value", - }, - { - "name": "no_value_measurement", - "outcome": "PASS", - "units": "NoValueUnit", - "docstring": "This is a measurement with no value specified", - }, - ], - } - ], -) diff --git a/qa/client/create_run/with_attachments/data/performance-report.pdf b/qa/client/create_run/with_attachments/data/performance-report.pdf deleted file mode 100644 index ce6914b..0000000 Binary files a/qa/client/create_run/with_attachments/data/performance-report.pdf and /dev/null differ diff --git a/qa/client/create_run/with_attachments/data/temperature-map.png b/qa/client/create_run/with_attachments/data/temperature-map.png deleted file mode 100644 index c7eb24b..0000000 Binary files a/qa/client/create_run/with_attachments/data/temperature-map.png and /dev/null differ diff --git a/qa/client/create_run/with_attachments/main.py b/qa/client/create_run/with_attachments/main.py deleted file mode 100644 index 7f9fe21..0000000 --- a/qa/client/create_run/with_attachments/main.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Example script demonstrating how to create a test run in TofuPilot with attachments. - -This script creates a test run for a unit with the specified serial number and part number, -and includes attachments, such as images and reports, that are related to the test. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - -# Create a test run for the unit with serial number "00102" and part number "PCB01", -# including attachments such as images and PDF reports -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00102", "part_number": "PCB01"}, - run_passed=True, - attachments=[ - "data/temperature-map.png", # Path to your local files - "data/performance-report.pdf", - ], -) diff --git a/qa/client/create_run/with_duration/main.py b/qa/client/create_run/with_duration/main.py deleted file mode 100644 index d96aaac..0000000 --- a/qa/client/create_run/with_duration/main.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Example script demonstrating how to create a test run in TofuPilot with the duration of the test. - -This script measures the duration of a test function, then creates a test run for a unit -with the specified serial number and part number, including the duration as part of the run data. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -import time -from datetime import timedelta - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - - -def test_function(): - """ - Simulates a test execution. - """ - # Simulate test execution with a delay - time.sleep(1) # Placeholder for test execution time - return True - - -# Measure the duration of the test_function -start_time = time.time() -run_passed = test_function() -end_time = time.time() -duration = timedelta(seconds=end_time - start_time) # Calculate duration - -# Create a test run for the unit with serial number "00102" and part number "PCB01", -# including the duration of the test -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00102", "part_number": "PCB01"}, - run_passed=run_passed, - duration=duration, # Optional argument to include the duration -) diff --git a/qa/client/create_run/with_phases_and_steps/main.py b/qa/client/create_run/with_phases_and_steps/main.py deleted file mode 100644 index 9c729d6..0000000 --- a/qa/client/create_run/with_phases_and_steps/main.py +++ /dev/null @@ -1,183 +0,0 @@ -import random -from datetime import datetime, timedelta -from random import randint - -from tofupilot import TofuPilotClient - -# Reference time to calculate start_time_millis in milliseconds since epoch -epoch = datetime(1970, 1, 1) - - -# Function to calculate milliseconds since epoch -def to_millis(dt): - return int((dt - epoch).total_seconds() * 1000) - - -client = TofuPilotClient() - -client.create_run( - procedure_id="FVT1", - unit_under_test={ - "serial_number": "SN17", - "part_number": "PNrstsrtsr", - "batch_number": "B", - }, - run_passed=True, # Overall run status - steps=[ - { - "name": "step_connect", # First step - "step_passed": True, # Status of the step - # Duration of the step - "duration": timedelta(seconds=5, milliseconds=12), - "started_at": datetime.now(), # Start time of the step - }, - { - "name": "step_string2", # First step - "step_passed": True, # Status of the step - # Duration of the step - "duration": timedelta(seconds=5, milliseconds=12), - "started_at": datetime.now(), # Start time of the step - "measurement_value": "This is a string", - }, - { - "name": "step_initial_charge_check", # Second step - "step_passed": True, # Status of the step - "duration": timedelta( - seconds=3, milliseconds=100 - ), # Duration of the step (<1s) - "started_at": datetime.now() - + timedelta(seconds=3), # Start time of the second step - "measurement_value": randint(90, 100), # Measured value - }, - { - "name": "step_initial_temp_check", # Third step - "step_passed": True, # Status of the step - "duration": timedelta( - seconds=1, milliseconds=100 - ), # Duration of the step (<1s) - "started_at": datetime.now() - # Start time of the third step - + timedelta(seconds=2, milliseconds=500), - "measurement_value": randint(-5, 20), # Measured temperature value - "measurement_unit": "°C", # Unit of the measurement (temperature) - "limit_low": 0, # Lower limit of acceptable temperature - }, - { - "name": "step_temp_calibration", # Fourth step - "step_passed": False, # Status of the step - "duration": timedelta( - seconds=3, milliseconds=100 - ), # Duration of the step (<1s) - "started_at": datetime.now() - timedelta(days=1, minutes=20), - "measurement_value": randint( - 69, 81 - ), # Measured temperature value after calibration - "measurement_unit": "°C", # Unit of the measurement (temperature) - "limit_low": 70, # Lower limit of acceptable temperature - "limit_high": 80, # Upper limit of acceptable temperature - }, - ], - phases=[ - { - "name": "phase_connect", # First phase - "outcome": "PASS", - "start_time_millis": to_millis( - datetime.now() - ), # Start time of the step in ms - "end_time_millis": to_millis( - datetime.now() + timedelta(seconds=5, milliseconds=12) - ), # End time in ms - "measurements": [ - { - "name": "connectivity_check", - "outcome": "PASS", # Measurement outcome - "measured_value": None, - "units": None, - "lower_limit": None, - "upper_limit": None, - } - ], - }, - { - "name": "phase_initial_charge_check", # Second phase - "outcome": "PASS", - "start_time_millis": to_millis( - datetime.now() + timedelta(seconds=3) - ), # Start time in ms - "end_time_millis": to_millis( - datetime.now() + timedelta(seconds=6, milliseconds=100) - ), # End time in ms - "measurements": [ - { - "name": "initial_charge", - "outcome": "PASS", # Measurement outcome - "measured_value": randint(90, 100), # Measured value - "units": None, - "lower_limit": None, - "upper_limit": None, - }, - { - "name": "initial_temperature", - "outcome": "PASS", # Measurement outcome - # Measured temperature value - "measured_value": randint(-5, 20), - "units": "°C", # Unit of the measurement - "lower_limit": 0, # Lower limit - "upper_limit": None, - }, - { - "name": "initial_temperature_2", - "outcome": "FAIL", # Measurement outcome - # Measured temperature value - "measured_value": randint(-5, 20), - "units": "°C", # Unit of the measurement - "lower_limit": 0, # Lower limit - "upper_limit": 15, # Upper limit - }, - ], - }, - { - "name": "phase_initial_temp_check", # Third phase - "outcome": "PASS", - "start_time_millis": to_millis( - datetime.now() + timedelta(seconds=2, milliseconds=500) - ), # Start time in ms - "end_time_millis": to_millis( - datetime.now() + timedelta(seconds=3, milliseconds=600) - ), # End time in ms - "measurements": [ - { - "name": "initial_temperature", - "outcome": "PASS", # Measurement outcome - # Measured temperature value - "measured_value": randint(-5, 20), - "units": "°C", # Unit of the measurement - "lower_limit": 0, # Lower limit - "upper_limit": None, - } - ], - }, - { - "name": "phase_temp_calibration", # Fourth phase - "outcome": "FAIL", - "start_time_millis": to_millis( - datetime.now() - timedelta(days=1, minutes=20) - ), # Start time in ms - "end_time_millis": to_millis( - datetime.now() - - timedelta(days=1, minutes=20) - + timedelta(seconds=3, milliseconds=100) - ), # End time in ms - "measurements": [ - { - "name": "temperature_calibration", - "outcome": "FAIL", # Measurement outcome - "measured_value": randint(69, 81), # Measured value - "units": "°C", # Unit of the measurement - "lower_limit": 70, # Lower limit - "upper_limit": 80, - } - ], - }, - ], -) diff --git a/qa/client/create_run_from_openhtf_report/basic/main.py b/qa/client/create_run_from_openhtf_report/basic/main.py deleted file mode 100644 index 536f5e9..0000000 --- a/qa/client/create_run_from_openhtf_report/basic/main.py +++ /dev/null @@ -1,36 +0,0 @@ -from openhtf import PhaseResult, Test -from openhtf.output.callbacks import json_factory -from openhtf.plugs import user_input -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - - -# Define a test phase to simulate the power-on procedure -def power_on_test(test): - print("Power on.") - return PhaseResult.CONTINUE - - -# Function to execute the test and save results to a JSON file -def execute_test(file_path): - test = Test(power_on_test, serial_number="PCB01") - - # Set output callback to save the test results as a JSON file - test.add_output_callbacks(json_factory.OutputToJSON(file_path)) - - # Execute the test with a specific device identifier - test.execute(lambda: "0001") - - -def main(): - # Specify the file path for saving test results - file_path = "./test_result.json" - execute_test(file_path) - - # Upload the test results to TofuPilot, specifying the importer type - client.create_run_from_openhtf_report(file_path) - - -if __name__ == "__main__": - main() diff --git a/qa/client/create_run_from_openhtf_report/with_attachments/data/oscilloscope.jpeg b/qa/client/create_run_from_openhtf_report/with_attachments/data/oscilloscope.jpeg deleted file mode 100644 index d49014b..0000000 Binary files a/qa/client/create_run_from_openhtf_report/with_attachments/data/oscilloscope.jpeg and /dev/null differ diff --git a/qa/client/create_run_from_openhtf_report/with_attachments/data/sample_file.txt b/qa/client/create_run_from_openhtf_report/with_attachments/data/sample_file.txt deleted file mode 100644 index 0089758..0000000 --- a/qa/client/create_run_from_openhtf_report/with_attachments/data/sample_file.txt +++ /dev/null @@ -1 +0,0 @@ -OpenHTF + TofuPilot = <3 \ No newline at end of file diff --git a/qa/client/create_run_from_openhtf_report/with_attachments/main.py b/qa/client/create_run_from_openhtf_report/with_attachments/main.py deleted file mode 100644 index a0189e0..0000000 --- a/qa/client/create_run_from_openhtf_report/with_attachments/main.py +++ /dev/null @@ -1,49 +0,0 @@ -import openhtf as htf -from openhtf import PhaseResult, Test -from openhtf.output.callbacks import json_factory -from openhtf.plugs import user_input -from tofupilot import TofuPilotClient -from tofupilot.openhtf import TofuPilot - -client = TofuPilotClient() - - -# Define a test phase to simulate the power-on procedure -def power_on_test(test): - print("Power on.") - return PhaseResult.CONTINUE - - -# Define a phase that attaches a file -def phase_file_attachment(test): - test.attach_from_file("data/sample_file.txt") - test.attach_from_file("data/oscilloscope.jpeg") - return htf.PhaseResult.CONTINUE - - -# Function to execute the test and save results to a JSON file -def execute_test(file_path): - test = Test( - power_on_test, - phase_file_attachment, - procedure_id="FVT7", - serial_number="PCB01") - - # Set output callback to save the test results as a JSON file - test.add_output_callbacks(json_factory.OutputToJSON(file_path)) - - # Execute the test with a specific device identifier - test.execute(lambda: "0001") - - -def main(): - # Specify the file path for saving test results - file_path = "./test_result.json" - execute_test(file_path) - - # Upload the test results to TofuPilot, specifying the importer type - client.create_run_from_openhtf_report(file_path) - - -if __name__ == "__main__": - main() diff --git a/qa/client/delete_run/basic/main.py b/qa/client/delete_run/basic/main.py deleted file mode 100644 index e62f1eb..0000000 --- a/qa/client/delete_run/basic/main.py +++ /dev/null @@ -1,13 +0,0 @@ -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - -response = client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00102", "part_number": "PCB01"}, - run_passed=True, -) - -run_id = response.get("id") - -client.delete_run(run_id=run_id) diff --git a/qa/client/delete_unit/basic/main.py b/qa/client/delete_unit/basic/main.py deleted file mode 100644 index 11343f0..0000000 --- a/qa/client/delete_unit/basic/main.py +++ /dev/null @@ -1,15 +0,0 @@ -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - -response = client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "NEW_UNIT", "part_number": "PCB01"}, - run_passed=True, -) - -run_id = response.get("id") - -client.delete_run(run_id=run_id) - -client.delete_unit(serial_number="NEW_UNIT") diff --git a/qa/client/get_runs/basic/main.py b/qa/client/get_runs/basic/main.py deleted file mode 100644 index 6480b75..0000000 --- a/qa/client/get_runs/basic/main.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Example script demonstrating how to create and then fetch a test run in TofuPilot. - -This script first creates a test run for a unit with a specified serial number and part number, -then retrieves the run data using the serial number. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - -# Define the serial number of the unit under test -serial_number = "SN00102" - -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": serial_number, "part_number": "PCB01"}, - run_passed=True, -) - -# Fetch the created run using the serial number -client.get_runs(serial_number=serial_number) diff --git a/qa/client/legacy/steps/advanced/main.py b/qa/client/legacy/steps/advanced/main.py deleted file mode 100644 index 8ae17fe..0000000 --- a/qa/client/legacy/steps/advanced/main.py +++ /dev/null @@ -1,45 +0,0 @@ -from datetime import datetime, timedelta - -from tofupilot import TofuPilotClient - - -def main(): - client = TofuPilotClient() - - client.create_run( - procedure_id="FVT1", - unit_under_test={ - "serial_number": "PCB1A002", - "part_number": "PCB1", - }, - run_passed=True, - steps=[ - { - "name": "step_connect", - "step_passed": True, - "duration": timedelta(seconds=3), - "started_at": datetime.now(), - }, - { - "name": "test_firmware_version_check", - "step_passed": True, - "duration": timedelta(minutes=1, seconds=42), - "started_at": datetime.now() + timedelta(seconds=3), - "measurement_value": "v2.5.1", - }, - { - "name": "step_temp_calibration", - "step_passed": True, - "duration": timedelta(milliseconds=500), - "started_at": datetime.now() + timedelta(seconds=4), - "measurement_value": 75, - "measurement_unit": "°C", - "limit_low": 70, - "limit_high": 80, - }, - ], - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/legacy/steps/optional/main.py b/qa/client/legacy/steps/optional/main.py deleted file mode 100644 index 04b2cfa..0000000 --- a/qa/client/legacy/steps/optional/main.py +++ /dev/null @@ -1,37 +0,0 @@ -from datetime import datetime, timedelta - -from tofupilot import TofuPilotClient - - -def step_one(): - step = [ - { - "name": "step_one", - "step_passed": True, - "started_at": datetime.now(), - "duration": timedelta(seconds=30), - # Add a measurement with value, units and limits - "measurement_unit": "V", - "measurement_value": 3.3, - "limit_low": 3.1, - "limit_high": 3.5, - } - ] - return step - - -def main(): - steps = step_one() - - client = TofuPilotClient() - - client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, - steps=steps, - run_passed=all(step["step_passed"] for step in steps), - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/legacy/steps/required/main.py b/qa/client/legacy/steps/required/main.py deleted file mode 100644 index 54524d3..0000000 --- a/qa/client/legacy/steps/required/main.py +++ /dev/null @@ -1,32 +0,0 @@ -from datetime import datetime, timedelta - -from tofupilot import TofuPilotClient - - -def step_one(): - step = [ - { - "name": "step_one", - "step_passed": True, - "duration": timedelta(seconds=30), - "started_at": datetime.now(), - } - ] - return step - - -def main(): - steps = step_one() - - client = TofuPilotClient() - - client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "PCB1A001", "part_number": "PCB1"}, - steps=steps, - run_passed=all(step["step_passed"] for step in steps), - ) - - -if __name__ == "__main__": - main() diff --git a/qa/client/update_unit/basic/main.py b/qa/client/update_unit/basic/main.py deleted file mode 100644 index 9385262..0000000 --- a/qa/client/update_unit/basic/main.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Simple example showing how to add a sub-unit to an existing unit using the TofuPilotClient. - -This script assumes you already have two units with the respective serial numbers "00102" and "00103". -If the units do not exist, you can uncomment the lines to create them first. - -Ensure your API key is stored in the environment variables as per the documentation: -https://tofupilot.com/docs/user-management#api-key -""" - -from tofupilot import TofuPilotClient - -# Initialize the TofuPilot client -client = TofuPilotClient() - -# Creating units -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00102", "part_number": "PCB01"}, - run_passed=True, -) -client.create_run( - procedure_id="FVT1", - unit_under_test={"serial_number": "00103", "part_number": "PSU01"}, - run_passed=True, -) - -# Update unit "00102" by adding unit "00103" as a sub-unit -client.update_unit(serial_number="00102", - sub_units=[{"serial_number": "00103"}]) diff --git a/qa/openhtf/all_the_things/data/oscilloscope.jpeg b/qa/openhtf/all_the_things/data/oscilloscope.jpeg deleted file mode 100644 index d49014b..0000000 Binary files a/qa/openhtf/all_the_things/data/oscilloscope.jpeg and /dev/null differ diff --git a/qa/openhtf/all_the_things/main.py b/qa/openhtf/all_the_things/main.py deleted file mode 100644 index d79b503..0000000 --- a/qa/openhtf/all_the_things/main.py +++ /dev/null @@ -1,182 +0,0 @@ -import os.path -import time - -import openhtf as htf -from openhtf import util -from openhtf.output import callbacks -from openhtf.output.callbacks import console_summary, json_factory -from openhtf.util import units -from tofupilot.openhtf import TofuPilot - - -@htf.measures( - htf.Measurement("widget_type") - .matches_regex(r".*Widget$") - .doc("""This measurement tracks the type of widgets."""), - htf.Measurement("widget_color").doc("Color of the widget"), - htf.Measurement("widget_size").in_range(1, 4).doc("Size of widget"), -) -@htf.measures( - "specified_as_args", - docstring="Helpful docstring", - units=units.HERTZ, - validators=[util.validators.matches_regex("Measurement")], -) -def hello_world(test): - """A hello world test phase.""" - test.logger.info("Hello World!") - test.measurements.widget_type = "MyWidget" - test.measurements.widget_color = "Black" - test.measurements.widget_size = 3 - test.measurements.specified_as_args = "Measurement args specified directly" - - -# Timeout if this phase takes longer than 10 seconds. -@htf.PhaseOptions(timeout_s=10) -@htf.measures(*(htf.Measurement("level_%s" % i) - for i in ["none", "some", "all"])) -def set_measurements(test): - """Test phase that sets a measurement.""" - test.measurements.level_none = 0 - time.sleep(1) - test.measurements.level_some = 8 - time.sleep(1) - test.measurements.level_all = 9 - time.sleep(1) - level_all = test.get_measurement("level_all") - assert level_all.value == 9 - - -@htf.measures( - htf.Measurement("dimensions").with_dimensions(units.HERTZ), - htf.Measurement("lots_of_dims").with_dimensions( - units.HERTZ, - units.SECOND, - htf.Dimension(description="my_angle", unit=units.RADIAN), - ), -) -def dimensions(test): - """Phase with dimensioned measurements.""" - for dim in range(5): - test.measurements.dimensions[dim] = 1 << dim - for x, y, z in zip( - list( - range( - 1, 5)), list( - range( - 21, 25)), list( - range( - 101, 105))): - test.measurements.lots_of_dims[x, y, z] = x + y + z - - -@htf.measures( - htf.Measurement("replaced_min_only").in_range( - "{minimum}", 5, type=int), htf.Measurement("replaced_max_only").in_range( - 0, "{maximum}", type=int), htf.Measurement("replaced_min_max").in_range( - "{minimum}", "{maximum}", type=int), ) -def measures_with_args(test, minimum, maximum): - """Phase with measurement with arguments.""" - del minimum # Unused. - del maximum # Unused. - test.measurements.replaced_min_only = 1 - test.measurements.replaced_max_only = 1 - test.measurements.replaced_min_max = 1 - - -@htf.measures( - htf.Measurement("replaced_marginal_min_only").in_range( - 0, 10, "{marginal_minimum}", 8, type=int - ), - htf.Measurement("replaced_marginal_max_only").in_range( - 0, 10, 2, "{marginal_maximum}", type=int - ), - htf.Measurement("replaced_marginal_min_max").in_range( - 0, 10, "{marginal_minimum}", "{marginal_maximum}", type=int - ), -) -def measures_with_marginal_args(test, marginal_minimum, marginal_maximum): - """Phase with measurement with marginal arguments.""" - del marginal_minimum # Unused. - del marginal_maximum # Unused. - test.measurements.replaced_marginal_min_only = 3 - test.measurements.replaced_marginal_max_only = 3 - test.measurements.replaced_marginal_min_max = 3 - - -def attachments(test): - test.attach( - "test_attachment", - "This is test attachment data.".encode("utf-8")) - test.attach_from_file("data/oscilloscope.jpeg") - - test_attachment = test.get_attachment("test_attachment") - assert test_attachment.data == b"This is test attachment data." - - -@htf.PhaseOptions(run_if=lambda: False) -def skip_phase(): - """Don't run this phase.""" - - -def analysis(test): # pylint: disable=missing-function-docstring - level_all = test.get_measurement("level_all") - assert level_all.value == 9 - test_attachment = test.get_attachment("test_attachment") - assert test_attachment.data == b"This is test attachment data." - lots_of_dims = test.get_measurement("lots_of_dims") - assert lots_of_dims.value.value == [ - (1, 21, 101, 123), - (2, 22, 102, 126), - (3, 23, 103, 129), - (4, 24, 104, 132), - ] - test.logger.info( - "Pandas datafram of lots_of_dims \n:%s", - lots_of_dims.value.to_dataframe()) - - -def teardown(test): - test.logger.info("Running teardown") - - -def make_test(): - return htf.Test( - htf.PhaseGroup.with_teardown(teardown)( - hello_world, - set_measurements, - dimensions, - attachments, - skip_phase, - measures_with_args.with_args(minimum=1, maximum=4), - measures_with_marginal_args.with_args( - marginal_minimum=4, marginal_maximum=6 - ), - analysis, - ), - test_name="MyTest", - test_description="OpenHTF Example Test", - test_version="1.0.0", - part_number="PCB01", - ) - - -def main(): - test = make_test() - test.add_output_callbacks( - callbacks.OutputToFile( - "./{dut_id}.{metadata[test_name]}.{start_time_millis}.pickle" - ) - ) - test.add_output_callbacks( - json_factory.OutputToJSON( - "./{dut_id}.{metadata[test_name]}.{start_time_millis}.json", - indent=4)) - test.add_output_callbacks(console_summary.ConsoleSummary()) - - with TofuPilot(test): - test.execute(lambda: "00220D4K") - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/generic/data/oscilloscope.jpeg b/qa/openhtf/generic/data/oscilloscope.jpeg deleted file mode 100644 index d49014b..0000000 Binary files a/qa/openhtf/generic/data/oscilloscope.jpeg and /dev/null differ diff --git a/qa/openhtf/generic/main.py b/qa/openhtf/generic/main.py deleted file mode 100644 index a71a2de..0000000 --- a/qa/openhtf/generic/main.py +++ /dev/null @@ -1,95 +0,0 @@ -import random -import time -from datetime import datetime, timedelta -import openhtf as htf -from openhtf.util import units -from tofupilot.openhtf import TofuPilot - - -@htf.measures(htf.Measurement("firmware_version").equals("1.4.3")) -def pcba_firmware_version(test): - test.measurements.firmware_version = "1.4.3" if random.random() < 0.99 else "1.4.2" - - -@htf.measures(htf.Measurement("button_status").equals(True)) -def check_button(test): - test.measurements.button_status = random.choice([True, False]) - time.sleep(1) - - -@htf.measures(htf.Measurement("input_voltage").in_range(4.5, 5).with_units(units.VOLT)) -def test_voltage_input(test): - test.measurements.input_voltage = round(random.uniform(3.7, 4.9), 2) - - -@htf.measures( - htf.Measurement("output_voltage").in_range(3.2, 3.4).with_units(units.VOLT) -) -def test_voltage_output(test): - test.measurements.output_voltage = round(random.uniform(2.95, 3.35), 2) - - -@htf.measures( - htf.Measurement("current_protection_triggered") - .in_range(maximum=1.5) - .with_units(units.AMPERE) -) -def test_overcurrent_protection(test): - test.measurements.current_protection_triggered = round(random.uniform(1.0, 1.7), 3) - time.sleep(1) - - -def test_battery_switch(): - if random.random() < 0.9: - return htf.PhaseResult.CONTINUE - else: - return htf.PhaseResult.STOP - - -@htf.measures( - htf.Measurement("efficiency").in_range(85, 98).with_units(units.Unit("%")) -) -def test_converter_efficiency(test): - input_power = 500 - output_power = round(random.uniform(425, 480)) - test.measurements.efficiency = round(((output_power / input_power) * 100), 1) - time.sleep(1) - - -def visual_control_pcb_coating(test): - if random.random() < 0.98: - test.attach_from_file("qa/openhtf/generic/data/oscilloscope.jpeg") - return htf.PhaseResult.CONTINUE - else: - return htf.PhaseResult.STOP - - -def main(): - # Define the test plan with all steps - test = htf.Test( - pcba_firmware_version, - check_button, - test_voltage_input, - test_voltage_output, - test_overcurrent_protection, - test_battery_switch, - test_converter_efficiency, - visual_control_pcb_coating, - procedure_id="FVT1", - part_number="00220", - revision="A", - # batch_number="1024-0001", - # sub_units=[{"serial_number": "00102"}], - ) - - # Generate random Serial Number - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220B4K{random_digits}" - - # Execute the test - with TofuPilot(test): - test.execute(lambda: serial_number) - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/logger/main.py b/qa/openhtf/logger/main.py deleted file mode 100644 index aacdfd0..0000000 --- a/qa/openhtf/logger/main.py +++ /dev/null @@ -1,51 +0,0 @@ -import openhtf as htf -from tofupilot.openhtf import TofuPilot -from openhtf.output.callbacks import json_factory -import random - - -@htf.measures(htf.Measurement("button_status").equals(True)) -def phase_info_logger(test): - test.measurements.button_status = True - test.logger.info("Logging an info in the logger") - - -def phase_error_logger(test): - test.logger.error("Logging error in the logger") - return htf.PhaseResult.CONTINUE - - -def phase_warning_logger(test): - test.logger.warning("Logging a warning in the logger") - return htf.PhaseResult.CONTINUE - - -def phase_critical_logger(test): - test.logger.critical("Logging a critical in the logger") - return htf.PhaseResult.CONTINUE - - -def main(): - # Define the test plan with all steps - test = htf.Test( - phase_info_logger, - phase_error_logger, - phase_warning_logger, - phase_critical_logger, - procedure_id="FVT9", - part_number="00220D", - ) - - # Generate random Serial Number - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220D4K{random_digits}" - - test.add_output_callbacks(json_factory.OutputToJSON("test_result.json", indent=2)) - - # Execute the test - with TofuPilot(test): - test.execute(lambda: serial_number) - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/multi_dimensions/main.py b/qa/openhtf/multi_dimensions/main.py deleted file mode 100644 index caff6ce..0000000 --- a/qa/openhtf/multi_dimensions/main.py +++ /dev/null @@ -1,100 +0,0 @@ -import openhtf as htf -from openhtf.util import units -from tofupilot.openhtf import TofuPilot -from openhtf.output.callbacks import json_factory -import random -import time -import numpy as np - - -@htf.measures( - htf.Measurement("voltage").with_dimensions(units.SECOND).with_units(units.VOLT), - htf.Measurement("mean_voltage").with_units(units.VOLT).in_range(3.3, 3.5), - htf.Measurement("sinus").with_dimensions(units.SECOND).with_units(units.AMPERE), - htf.Measurement("neg_x_axis").with_dimensions(units.SECOND).with_units(units.VOLT), - htf.Measurement("neg_y_axis").with_dimensions(units.SECOND).with_units(units.VOLT), - htf.Measurement("parameters"), -) -def multi_dimension_phase(test): - len = 50 - sum_voltage = 0.0 - min_voltage = float("inf") - max_voltage = float("-inf") - for t in range(len): - voltage = round(random.uniform(3.3, 3.5), 2) - test.measurements.voltage[t] = voltage - sum_voltage += voltage - - if voltage < min_voltage: - min_voltage = voltage - if voltage > max_voltage: - max_voltage = voltage - - negative_timestamp = -t - test.measurements.neg_x_axis[negative_timestamp] = voltage - test.measurements.neg_y_axis[t] = -voltage - - test.measurements.mean_voltage = sum_voltage / len - - # metadata array - test.measurements.parameters = { - "mean_voltage": test.measurements.mean_voltage, - "min_voltage": min_voltage, - "max_voltage": max_voltage, - "duration": 345, - } - - # Sinus - x = np.linspace(0, 2 * np.pi, 100) - y = np.sin(x) - for i, amp in enumerate(y): - test.measurements.sinus[i] = amp - - -@htf.measures( - htf.Measurement("current_and_voltage_over_time") - .with_dimensions(units.SECOND, units.VOLT) - .with_units(units.AMPERE), - htf.Measurement("current_voltage_resistence_over_time") - .with_dimensions(units.SECOND, units.VOLT, units.AMPERE) - .with_units(units.OHM), -) -def power_phase(test): - for t in range(100): - timestamp = t / 100 - voltage = round(random.uniform(3.3, 3.5), 2) - current = round(random.uniform(0.3, 0.8), 3) - resistance = voltage / current - test.measurements.current_and_voltage_over_time[timestamp, voltage] = current - test.measurements.current_voltage_resistence_over_time[ - timestamp, voltage, current - ] = resistance - - -def main(): - test = htf.Test( - multi_dimension_phase, - power_phase, - procedure_id="FVT2", - part_number="00220S", - revision="B", - batch_number="1124-0001", - ) - - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220D4K{random_digits}" - - test.add_output_callbacks(json_factory.OutputToJSON("test_result.json", indent=2)) - - start = time.time() - # Execute the test - with TofuPilot(test): - test.execute(lambda: serial_number) - - end = time.time() - duration = end - start - print(f"Durée : {duration:.2f} secondes") - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/multi_measurements/main.py b/qa/openhtf/multi_measurements/main.py deleted file mode 100644 index 0e9bd41..0000000 --- a/qa/openhtf/multi_measurements/main.py +++ /dev/null @@ -1,104 +0,0 @@ -import openhtf as htf -from openhtf.util import units -from tofupilot.openhtf import TofuPilot -import random - - -@htf.measures( - htf.Measurement("firmware_version").equals("1.4.3"), - htf.Measurement("power_mode_functional").equals("on"), -) -def string_test(test): - test.measurements.firmware_version = "1.4.3" if random.random() < 0.99 else "1.4.2" - test.measurements.power_mode_functional = "on" - - -@htf.measures(htf.Measurement("button_status").equals(True)) -def boolean_test(test): - test.measurements.button_status = True - - -def phaseresult_test(): - return htf.PhaseResult.CONTINUE - - -@htf.measures( - htf.Measurement("two_limits").in_range(4.5, 5).with_units(units.VOLT), - htf.Measurement("one_limit").in_range(maximum=1.5).with_units(units.AMPERE), - htf.Measurement("percentage").in_range(85, 98).with_units(units.Unit("%")), -) -def measure_test_with_limits(test): - test.measurements.two_limits = round(random.uniform(3.8, 4.9), 2) - test.measurements.one_limit = round(random.uniform(1.0, 1.6), 3) - input_power = 500 - output_power = round(random.uniform(425, 480)) - test.measurements.percentage = round(((output_power / input_power) * 100), 1) - - -@htf.measures( - htf.Measurement("is_connected").equals(True), - htf.Measurement("firmware_version").equals("1.2.7"), - htf.Measurement("temperature").in_range(20, 25).with_units(units.DEGREE_CELSIUS), -) -def phase_multi_measurements(test): - test.measurements.is_connected = True - test.measurements.firmware_version = ( - "1.2.7" if test.measurements.is_connected else "N/A" - ) - test.measurements.temperature = round(random.uniform(22.5, 23), 2) - - -@htf.measures( - htf.Measurement("dimensions").with_dimensions(units.HERTZ), - htf.Measurement("lots_of_dims").with_dimensions( - units.HERTZ, - units.SECOND, - htf.Dimension(description="my_angle", unit=units.RADIAN), - ), -) -def dimensions(test): - """Phase with dimensioned measurements.""" - for dim in range(5): - test.measurements.dimensions[dim] = 1 << dim - for x, y, z in zip(list(range(1, 5)), list(range(21, 25)), list(range(101, 105))): - test.measurements.lots_of_dims[x, y, z] = x + y + z - - -@htf.measures( - htf.Measurement("binary_measure").equals(True), - htf.Measurement("string_measure").equals("1.2.7"), - htf.Measurement("numerical_measure") - .in_range(20, 25) - .with_units(units.DEGREE_CELSIUS), -) -def not_working_multi_measurements(test): - test.measurements.binary_measure = 42 - test.measurements.string_measure = 123 - test.measurements.numerical_measure = 35 - - -def main(): - # Define the test plan with all steps - test = htf.Test( - phase_multi_measurements, - dimensions, - string_test, - boolean_test, - phaseresult_test, - measure_test_with_limits, - not_working_multi_measurements, - procedure_id="FVT9", - part_number="00220D", - ) - - # Generate random Serial Number - random_digits = "".join([str(random.randint(0, 9)) for _ in range(5)]) - serial_number = f"00220D4K{random_digits}" - - # Execute the test - with TofuPilot(test): - test.execute(lambda: serial_number) - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/procedure_version/main.py b/qa/openhtf/procedure_version/main.py deleted file mode 100644 index e215db2..0000000 --- a/qa/openhtf/procedure_version/main.py +++ /dev/null @@ -1,29 +0,0 @@ -import random -import openhtf as htf -from tofupilot.openhtf import TofuPilot - - -@htf.measures(htf.Measurement("button_status").equals(True)) -def check_button(test): - test.measurements.button_status = bool(random.randint(0, 1)) - - -def main(): - test = htf.Test( - check_button, - procedure_id="FVT1", # No need to specify the ID - procedure_version="1.2.20", # Create procedure version - part_number="00220", - revision="B", - ) - - # Generate random Serial Number - serial_number = f"00220B4K{random.randint(10000, 99999)}" - - # Execute the test - with TofuPilot(test, stream=False): - test.execute(lambda: serial_number) - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/regex_serial_number/main.py b/qa/openhtf/regex_serial_number/main.py deleted file mode 100644 index 971a9ba..0000000 --- a/qa/openhtf/regex_serial_number/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import random -import openhtf as htf -from tofupilot.openhtf import TofuPilot - - -@htf.measures(htf.Measurement("button_status").equals(True)) -def check_button(test): - test.measurements.button_status = bool(random.randint(0, 1)) - - -def main(): - test = htf.Test( - check_button, - procedure_id="FVT1", - # REGEX is defined in the Settings from Serial Number for: - # part_number="PCB01", - ) - - with TofuPilot(test): - test.execute(lambda: "PCB01A123") - - -if __name__ == "__main__": - main() diff --git a/qa/openhtf/without_streaming/data/oscilloscope.jpeg b/qa/openhtf/without_streaming/data/oscilloscope.jpeg deleted file mode 100644 index d49014b..0000000 Binary files a/qa/openhtf/without_streaming/data/oscilloscope.jpeg and /dev/null differ diff --git a/qa/openhtf/without_streaming/main.py b/qa/openhtf/without_streaming/main.py deleted file mode 100644 index 1ee0488..0000000 --- a/qa/openhtf/without_streaming/main.py +++ /dev/null @@ -1,182 +0,0 @@ -import os.path -import time - -import openhtf as htf -from openhtf import util -from openhtf.output import callbacks -from openhtf.output.callbacks import console_summary, json_factory -from openhtf.util import units -from tofupilot.openhtf import TofuPilot - - -@htf.measures( - htf.Measurement("widget_type") - .matches_regex(r".*Widget$") - .doc("""This measurement tracks the type of widgets."""), - htf.Measurement("widget_color").doc("Color of the widget"), - htf.Measurement("widget_size").in_range(1, 4).doc("Size of widget"), -) -@htf.measures( - "specified_as_args", - docstring="Helpful docstring", - units=units.HERTZ, - validators=[util.validators.matches_regex("Measurement")], -) -def hello_world(test): - """A hello world test phase.""" - test.logger.info("Hello World!") - test.measurements.widget_type = "MyWidget" - test.measurements.widget_color = "Black" - test.measurements.widget_size = 3 - test.measurements.specified_as_args = "Measurement args specified directly" - - -# Timeout if this phase takes longer than 10 seconds. -@htf.PhaseOptions(timeout_s=10) -@htf.measures(*(htf.Measurement("level_%s" % i) - for i in ["none", "some", "all"])) -def set_measurements(test): - """Test phase that sets a measurement.""" - test.measurements.level_none = 0 - time.sleep(1) - test.measurements.level_some = 8 - time.sleep(1) - test.measurements.level_all = 9 - time.sleep(1) - level_all = test.get_measurement("level_all") - assert level_all.value == 9 - - -@htf.measures( - htf.Measurement("dimensions").with_dimensions(units.HERTZ), - htf.Measurement("lots_of_dims").with_dimensions( - units.HERTZ, - units.SECOND, - htf.Dimension(description="my_angle", unit=units.RADIAN), - ), -) -def dimensions(test): - """Phase with dimensioned measurements.""" - for dim in range(5): - test.measurements.dimensions[dim] = 1 << dim - for x, y, z in zip( - list( - range( - 1, 5)), list( - range( - 21, 25)), list( - range( - 101, 105))): - test.measurements.lots_of_dims[x, y, z] = x + y + z - - -@htf.measures( - htf.Measurement("replaced_min_only").in_range( - "{minimum}", 5, type=int), htf.Measurement("replaced_max_only").in_range( - 0, "{maximum}", type=int), htf.Measurement("replaced_min_max").in_range( - "{minimum}", "{maximum}", type=int), ) -def measures_with_args(test, minimum, maximum): - """Phase with measurement with arguments.""" - del minimum # Unused. - del maximum # Unused. - test.measurements.replaced_min_only = 1 - test.measurements.replaced_max_only = 1 - test.measurements.replaced_min_max = 1 - - -@htf.measures( - htf.Measurement("replaced_marginal_min_only").in_range( - 0, 10, "{marginal_minimum}", 8, type=int - ), - htf.Measurement("replaced_marginal_max_only").in_range( - 0, 10, 2, "{marginal_maximum}", type=int - ), - htf.Measurement("replaced_marginal_min_max").in_range( - 0, 10, "{marginal_minimum}", "{marginal_maximum}", type=int - ), -) -def measures_with_marginal_args(test, marginal_minimum, marginal_maximum): - """Phase with measurement with marginal arguments.""" - del marginal_minimum # Unused. - del marginal_maximum # Unused. - test.measurements.replaced_marginal_min_only = 3 - test.measurements.replaced_marginal_max_only = 3 - test.measurements.replaced_marginal_min_max = 3 - - -def attachments(test): - test.attach( - "test_attachment", - "This is test attachment data.".encode("utf-8")) - test.attach_from_file("data/oscilloscope.jpeg") - - test_attachment = test.get_attachment("test_attachment") - assert test_attachment.data == b"This is test attachment data." - - -@htf.PhaseOptions(run_if=lambda: False) -def skip_phase(): - """Don't run this phase.""" - - -def analysis(test): # pylint: disable=missing-function-docstring - level_all = test.get_measurement("level_all") - assert level_all.value == 9 - test_attachment = test.get_attachment("test_attachment") - assert test_attachment.data == b"This is test attachment data." - lots_of_dims = test.get_measurement("lots_of_dims") - assert lots_of_dims.value.value == [ - (1, 21, 101, 123), - (2, 22, 102, 126), - (3, 23, 103, 129), - (4, 24, 104, 132), - ] - test.logger.info( - "Pandas datafram of lots_of_dims \n:%s", - lots_of_dims.value.to_dataframe()) - - -def teardown(test): - test.logger.info("Running teardown") - - -def make_test(): - return htf.Test( - htf.PhaseGroup.with_teardown(teardown)( - hello_world, - set_measurements, - dimensions, - attachments, - skip_phase, - measures_with_args.with_args(minimum=1, maximum=4), - measures_with_marginal_args.with_args( - marginal_minimum=4, marginal_maximum=6 - ), - analysis, - ), - test_name="MyTest", - test_description="OpenHTF Example Test", - test_version="1.0.0", - part_number="PCB01", - ) - - -def main(): - test = make_test() - test.add_output_callbacks( - callbacks.OutputToFile( - "./{dut_id}.{metadata[test_name]}.{start_time_millis}.pickle" - ) - ) - test.add_output_callbacks( - json_factory.OutputToJSON( - "./{dut_id}.{metadata[test_name]}.{start_time_millis}.json", - indent=4)) - test.add_output_callbacks(console_summary.ConsoleSummary()) - - with TofuPilot(test, stream=False): - test.execute(lambda: "00220D4K") - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt index a3b2c32..50b3d42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ openhtf six tofupilot -numpy \ No newline at end of file +numpy +paho-mqtt \ No newline at end of file diff --git a/touchpad_accuracy/README.md b/touchpad_accuracy/README.md new file mode 100644 index 0000000..65e9451 --- /dev/null +++ b/touchpad_accuracy/README.md @@ -0,0 +1,71 @@ +# Touchpad positional accuracy + +A touchpad is tested by pressing it. A robot puts a known force on a known +coordinate, and the pad reports where it thinks it was touched; the number that +matters is the distance between the two. + +`touchpad_ptp/` records that distance against the Precision Touchpad linearity +limits — **0.5 mm** across the pad, relaxed to **1.5 mm** within 3.5 mm of an +edge, where the sensor is least linear. Windows reports these distances in +himetric units (0.01 mm), which is what the HID read in `plugs/touch_robot.py` +converts from. + +``` +touchpad_ptp/ +├── procedure.yaml phases, measurements and limits +├── phases/ the Python each phase runs +└── plugs/ one class per instrument +``` + +```bash +tofupilot run ./touchpad_ptp +``` + +The plugs return representative data so the procedure runs anywhere. Every +method keeps its real call directly above, commented out — swap the two and the +same procedure drives the bench: + +- `plugs/touch_robot.py` — motion controller over VISA for the press, HID + digitizer report for the contact the pad reported +- `plugs/force_gauge.py` — load cell ramped until the dome switch closes, + triggered on the closure rather than sampled and compared afterwards + +## The grid is one measurement, not nine + +A manual test taps each target and writes down whether it looked right. Here +each grid becomes a curve indexed by target, with `max` validated against the +spec limit and `mean` against a tighter working limit: + +```yaml +- name: Edge Positional Error + x_axis: + legend: Target + y_axis: + - legend: Error + unit: mm + aggregations: + - type: max + validators: + - operator: "<=" + expected_value: 1.5 +``` + +`max` is what fails the unit — one target outside the limit is a failure no +average should absorb. Keeping the whole series alongside it is what separates +a pad that is off in one corner from one that is off everywhere, which is the +difference between a fixture problem and a sensor problem. + +The worst edge target is recorded a second time as a scalar, so the failing +number is filterable on its own and trendable across a population. + +## Two limits, one press + +The central grid and the edge band run the same code against the same fixture. +They are separate phases only because the spec allows three times the error +inside the border strip, and a single limit would either pass a bad centre or +fail a good edge. + +Click actuation force comes off the same fixture in the same cycle. It has +nothing to do with position, but a dome that stiffens or softens is an early +sign of the same mechanical drift that moves the positional error, and it costs +one extra phase to catch. diff --git a/touchpad_accuracy/touchpad_ptp/phases/center_linearity.py b/touchpad_accuracy/touchpad_ptp/phases/center_linearity.py new file mode 100644 index 0000000..cb0727f --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/phases/center_linearity.py @@ -0,0 +1,47 @@ +"""The nine targets a manual test taps by hand, as one curve. + +Each target is a commanded coordinate; the measurement is the distance between +it and the contact the pad reported. Keeping the whole grid as a series means a +unit that is off in one corner is distinguishable from one that is off +everywhere, which a single worst-case number hides. +""" + +import math + +# 2 mm grid across the central region, clear of the edge band. +TARGETS = ( + (26, 16), + (52, 16), + (79, 16), + (26, 32), + (52, 32), + (79, 32), + (26, 49), + (52, 49), + (79, 49), +) + + +def center_linearity(measurements, robot, log): + index = [] + errors = [] + + for n, (target_x, target_y) in enumerate(TARGETS, start=1): + reported_x, reported_y = robot.press(target_x, target_y) + error = round( + math.dist( + (target_x, target_y), (reported_x, reported_y)), 3) + + log.info( + f"({target_x},{target_y}) -> ({reported_x:.2f},{reported_y:.2f}) = {error} mm" + ) + index.append(n) + errors.append(error) + + measurements.center_positional_error.x_axis = index + measurements.center_positional_error.y_axis.error = errors + # The unit passes when every target is inside the limit, not on average. + measurements.center_positional_error.y_axis.error.aggregations.max = max( + errors) + measurements.center_positional_error.y_axis.error.aggregations.mean = round( + sum(errors) / len(errors), 3) diff --git a/touchpad_accuracy/touchpad_ptp/phases/click_actuation_force.py b/touchpad_accuracy/touchpad_ptp/phases/click_actuation_force.py new file mode 100644 index 0000000..796768f --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/phases/click_actuation_force.py @@ -0,0 +1,13 @@ +"""How hard the pad has to be pressed before it clicks. + +Unrelated to position, but it comes off the same fixture in the same cycle, and +a dome that stiffens or softens is an early sign of the same mechanical problem +that moves the positional error. +""" + + +def click_actuation_force(measurements, gauge, log): + grams = gauge.ramp_until_actuation() + log.info(f"Switch actuated at {grams} g") + + measurements.actuation_force = grams diff --git a/touchpad_accuracy/touchpad_ptp/phases/edge_band_linearity.py b/touchpad_accuracy/touchpad_ptp/phases/edge_band_linearity.py new file mode 100644 index 0000000..c016726 --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/phases/edge_band_linearity.py @@ -0,0 +1,49 @@ +"""The border, where touchpads actually fail. + +Same press, same measurement as the central grid, but within 3.5 mm of an edge +the spec allows three times the error. Sampling the corners and the middle of +each side is what catches a sensor whose linearity falls apart only at one end. +""" + +import math + +# Targets inside the 3.5 mm border strip: four corners, four side midpoints. +TARGETS = ( + (2, 2), + (52, 2), + (103, 2), + (2, 32), + (103, 32), + (2, 63), + (52, 63), + (103, 63), +) + + +def edge_band_linearity(measurements, robot, log): + index = [] + errors = [] + + for n, (target_x, target_y) in enumerate(TARGETS, start=1): + reported_x, reported_y = robot.press(target_x, target_y) + error = round( + math.dist( + (target_x, target_y), (reported_x, reported_y)), 3) + + log.info( + f"({target_x},{target_y}) -> ({reported_x:.2f},{reported_y:.2f}) = {error} mm" + ) + index.append(n) + errors.append(error) + + measurements.edge_positional_error.x_axis = index + measurements.edge_positional_error.y_axis.error = errors + measurements.edge_positional_error.y_axis.error.aggregations.max = max( + errors) + measurements.edge_positional_error.y_axis.error.aggregations.mean = round( + sum(errors) / len(errors), 3 + ) + + # Repeated as a scalar so the failing number is filterable on its own, and + # so drift on the worst target is trendable across a population. + measurements.worst_edge_error = max(errors) diff --git a/touchpad_accuracy/touchpad_ptp/phases/home_fixture.py b/touchpad_accuracy/touchpad_ptp/phases/home_fixture.py new file mode 100644 index 0000000..c04556f --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/phases/home_fixture.py @@ -0,0 +1,12 @@ +"""Home the probe and zero the gauge before anything is measured. + +Positional error is only comparable across units if every press starts from the +same reference, so this runs first and the rest depend on it implicitly. +""" + + +def home_fixture(log, robot, gauge): + log.info(f"Robot: {robot.identity()}") + log.info(f"Gauge: {gauge.identity()}") + gauge.zero() + log.info("Fixture homed and zeroed") diff --git a/touchpad_accuracy/touchpad_ptp/plugs/force_gauge.py b/touchpad_accuracy/touchpad_ptp/plugs/force_gauge.py new file mode 100644 index 0000000..875dac7 --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/plugs/force_gauge.py @@ -0,0 +1,42 @@ +"""Inline load cell reading the force at which the dome switch actuates.""" + +# Load cell indicator on the bench LAN. +import random + +GAUGE_RESOURCE = "TCPIP0::192.0.2.21::inst0::INSTR" + +# Ramp until the switch reports, or give up. +RAMP_LIMIT_G = 120.0 + + +class ForceGauge: + def __init__(self): + self._gauge = None + + # import pyvisa + # + # self._gauge = pyvisa.ResourceManager().open_resource(GAUGE_RESOURCE) + # self._gauge.timeout = 10_000 + # self._gauge.write("UNIT:FORCe GRAM") + + def __del__(self): + pass + + def identity(self) -> str: + # return self._gauge.query("*IDN?").strip() + return "SIMULATED-LOAD-CELL,1.0" + + def zero(self) -> None: + # self._gauge.write("SENSe:CORRection:COLLect:ZERO") + # self._gauge.query("*OPC?") + pass + + def ramp_until_actuation(self) -> float: + """Ramp force at the pad centre; return the force at the click, in grams.""" + # The switch closure is what stops the ramp, so the gauge is read on + # the edge rather than sampled and compared afterwards. + # self._gauge.write(f"SOURce:FORCe:RAMP {RAMP_LIMIT_G}") + # self._gauge.write("TRIGger:SOURce EXTernal") + # return float(self._gauge.query("FETCh:FORCe?")) + + return round(random.gauss(60.5, 3.4), 1) diff --git a/touchpad_accuracy/touchpad_ptp/plugs/touch_robot.py b/touchpad_accuracy/touchpad_ptp/plugs/touch_robot.py new file mode 100644 index 0000000..9f86a3d --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/plugs/touch_robot.py @@ -0,0 +1,71 @@ +"""Cartesian probe pressing a calibrated force at a commanded x/y target. + +Two halves on a real bench: the motion controller that puts the tip somewhere, +and the HID read that says where the pad thought it was touched. The positional +error is the distance between the two, so both have to come from the same press. +""" + +# Motion controller on the bench LAN. +import random + +ROBOT_RESOURCE = "TCPIP0::192.0.2.20::inst0::INSTR" + +# Pad geometry, mm. The border strip where the spec relaxes its limit. +PAD_W, PAD_H = 105.0, 65.0 +EDGE_BAND = 3.5 + +# Probe force held constant across the grid so error stays comparable. +PROBE_FORCE_G = 60.0 + + +class TouchRobot: + def __init__(self): + self._axes = None + self._hid = None + + # import pyvisa + # import hid + # + # self._axes = pyvisa.ResourceManager().open_resource(ROBOT_RESOURCE) + # self._axes.timeout = 10_000 + # self._axes.write("HOME") + # self._axes.query("*OPC?") + # self._axes.write(f"FORCe {PROBE_FORCE_G}") + # + # # The pad under test, as the OS sees it. + # self._hid = hid.Device(vid=0x0000, pid=0x0000) + + def __del__(self): + # self._axes.write("PARK") + pass + + def identity(self) -> str: + # return self._axes.query("*IDN?").strip() + return "SIMULATED-XY-STAGE,1.0" + + def in_edge_band(self, x: float, y: float) -> bool: + return ( + x < EDGE_BAND + or y < EDGE_BAND + or x > PAD_W - EDGE_BAND + or y > PAD_H - EDGE_BAND + ) + + def press(self, x: float, y: float) -> tuple[float, float]: + """Press at a commanded target; return the contact the pad reported.""" + # self._axes.write(f"MOVE {x:.3f} {y:.3f}") + # self._axes.query("*OPC?") + # self._axes.write("PRESS") + # self._axes.query("*OPC?") + # + # # Digitizer report: absolute X/Y in himetric (0.01 mm), the unit the + # # Precision Touchpad tests report distances in. + # report = self._hid.read(64, timeout=1000) + # reported_x = int.from_bytes(report[2:4], "little") / 100.0 + # reported_y = int.from_bytes(report[4:6], "little") / 100.0 + # return reported_x, reported_y + + # Simulated: the sensor is markedly less linear near the border, which + # is the whole reason the spec carries two limits. + sigma = 0.42 if self.in_edge_band(x, y) else 0.14 + return x + random.gauss(0.0, sigma), y + random.gauss(0.0, sigma) diff --git a/touchpad_accuracy/touchpad_ptp/procedure.yaml b/touchpad_accuracy/touchpad_ptp/procedure.yaml new file mode 100644 index 0000000..cf43eab --- /dev/null +++ b/touchpad_accuracy/touchpad_ptp/procedure.yaml @@ -0,0 +1,89 @@ +name: Touchpad Positional Accuracy +version: 1.0.0 + +unit: + auto_identify: true + serial_number: + default_value: "SN-0001" + part_number: + default_value: "TPAD-REV-A" + +plugs: + - key: robot + name: Touch Robot + python: plugs.touch_robot:TouchRobot + - key: gauge + name: Force Gauge + python: plugs.force_gauge:ForceGauge + +main: + - name: Home Fixture + key: home_fixture + python: phases.home_fixture + + # Every press is referenced to the homed position, so the grid phases wait + # for it rather than racing it. + - name: Center Linearity + python: phases.center_linearity + depends_on: + - home_fixture + measurements: + # Precision Touchpad linearity: 0.5 mm edge to edge, away from the border. + - name: Center Positional Error + title: Positional error across the central region + x_axis: + legend: Target + y_axis: + - legend: Error + unit: mm + aggregations: + - type: max + validators: + - operator: "<=" + expected_value: 0.5 + - type: mean + validators: + - operator: "<=" + expected_value: 0.35 + + - name: Edge Band Linearity + python: phases.edge_band_linearity + depends_on: + - home_fixture + measurements: + # Within 3.5 mm of an edge the same spec relaxes to 1.5 mm. + - name: Edge Positional Error + title: Positional error along the 3.5 mm edge band + x_axis: + legend: Target + y_axis: + - legend: Error + unit: mm + aggregations: + - type: max + validators: + - operator: "<=" + expected_value: 1.5 + - type: mean + validators: + - operator: "<=" + expected_value: 1.1 + # The worst single target is what fails a unit, so it is kept on its own. + - name: Worst Edge Error + unit: mm + validators: + - operator: "<=" + expected_value: 1.5 + + - name: Click Actuation Force + python: phases.click_actuation_force + depends_on: + - home_fixture + measurements: + - name: Actuation Force + unit: g + validators: + - operator: ">=" + expected_value: 50 + - operator: "<=" + expected_value: 70 diff --git a/welcome_aboard/battery_testing/data/pcb_coating.jpeg b/welcome_aboard/battery_testing/data/pcb_coating.jpeg deleted file mode 100644 index 5837497..0000000 Binary files a/welcome_aboard/battery_testing/data/pcb_coating.jpeg and /dev/null differ diff --git a/welcome_aboard/battery_testing/main.py b/welcome_aboard/battery_testing/main.py deleted file mode 100644 index 2e848ae..0000000 --- a/welcome_aboard/battery_testing/main.py +++ /dev/null @@ -1,377 +0,0 @@ -import random -from datetime import datetime, timedelta - -from tofupilot import TofuPilotClient - -client = TofuPilotClient() - - -# Simulate passing probability for a test result -def simulate_test_result(passed_prob): - return random.random() < passed_prob - - -# Cell Test Functions -def esr_test(): - passed = simulate_test_result(0.98) - value_measured = ( - round( - random.uniform( - 5, - 10), - 2) if passed else round( - random.uniform( - 15, - 20), - 2)) - return passed, value_measured, "mΩ", 5, 15 - - -def cell_voltage_test(): - passed = simulate_test_result(0.98) - value_measured = ( - round(random.uniform(3.0, 3.5), 2) - if passed - else round(random.uniform(2.5, 2.9), 2) - ) - return passed, value_measured, "V", 3.0, 3.5 - - -def ir_test(): - passed = simulate_test_result(0.98) - value_measured = ( - round( - random.uniform( - 5, - 10), - 2) if passed else round( - random.uniform( - 15, - 20), - 2)) - return passed, value_measured, "mΩ", 5, 15 - - -def charge_discharge_cycle_test(): - passed = simulate_test_result(0.95) - value_measured = ( - round(random.uniform(95, 100), 1) - if passed - else round(random.uniform(80, 94), 1) - ) - return passed, value_measured, "% Capacity", 95, 100 - - -# PCBA Test function -def flash_firmware_and_version(): - passed = simulate_test_result(0.99) - value_measured = "1.2.8" if passed else None - return passed, value_measured, None, None, None - - -def configuration_battery_gauge(): - passed = simulate_test_result(0.98) - return passed, None, None, None, None - - -def get_calibration_values_and_internal_statuses(): - passed = simulate_test_result(0.98) - return passed, None, None, None, None - - -def overvoltage_protection_test(): - passed = simulate_test_result(0.98) - value_measured = ( - round(random.uniform(4.20, 4.25), 3) - if passed - else round(random.uniform(4.30, 4.35), 3) - ) - return passed, value_measured, "V", 4.20, 4.25 - - -def undervoltage_protection_test(): - passed = simulate_test_result(0.98) - value_measured = ( - round(random.uniform(2.5, 2.6), 2) - if passed - else round(random.uniform(2.3, 2.4), 2) - ) - return passed, value_measured, "V", 2.5, 2.6 - - -def test_LED_and_button(): - passed = simulate_test_result(0.95) - return passed, None, None, None, None - - -def save_information_in_memory(): - passed = simulate_test_result(0.99) - return passed, None, None, None, None - - -def config_battery_gauge(): - passed = simulate_test_result(0.95) - return passed, None, None, None, None - - -def calibrate_temperature(): - passed = simulate_test_result(0.98) - value_measured = round(random.uniform(20.0, 25.0), 1) - return passed, value_measured, "°C", 20, 25 - - -# Assembly Test Functions -def battery_connection(): - passed = simulate_test_result(1.0) - return passed, None, None, None, None - - -def voltage_value(): - passed = simulate_test_result(0.98) - value_measured = ( - round(random.uniform(10.0, 12.0), 2) - if passed - else round(random.uniform(8.0, 9.5), 2) - ) - return passed, value_measured, "V", 10.0, 12.0 - - -def internal_resistance(): - passed = simulate_test_result(0.98) - value_measured = ( - round( - random.uniform( - 5, - 10), - 2) if passed else round( - random.uniform( - 15, - 20), - 2)) - return passed, value_measured, "mΩ", 5, 15 - - -def thermal_runaway_detection(): - passed = simulate_test_result(0.98) - value_measured = ( - round( - random.uniform( - 55, - 65), - 2) if passed else round( - random.uniform( - 66, - 70), - 2)) - return passed, value_measured, "°C", 55, 65 - - -def state_of_health(): - passed = simulate_test_result(0.98) - value_measured = ( - round(random.uniform(95, 100), 1) - if passed - else round(random.uniform(85, 94), 1) - ) - return passed, value_measured, "%", 95, None - - -def state_of_charge(): - passed = simulate_test_result(0.98) - value_measured = ( - round( - random.uniform( - 40, - 60), - 1) if passed else round( - random.uniform( - 25, - 35), - 1)) - return passed, value_measured, "%", 40, 60 - - -def visual_inspection(): - passed = simulate_test_result(1) - return passed, None, None, None, None - - -# Run a single test -def run_test(test, duration): - start_time = datetime.now() - passed, value_measured, unit, limit_low, limit_high = test() - - step = { - "name": test.__name__, - "started_at": start_time, - "duration": duration, - "step_passed": passed, - "measurement_unit": unit, - "measurement_value": value_measured, - "limit_low": limit_low, - "limit_high": limit_high, - } - return passed, step - - -def run_all_tests(tests, previous_failed_step=None): - steps = [] - all_tests_passed = True - failed_at_step = None - - for index, (test, duration) in enumerate(tests): - if previous_failed_step is not None and index == previous_failed_step["index"]: - passed = previous_failed_step["step_passed"] - step = previous_failed_step - else: - passed, step = run_test(test, duration) - - steps.append(step) - - if not passed: - failed_at_step = { - "index": index, - "step_passed": passed, - "name": step["name"], - "started_at": step["started_at"], - "duration": step["duration"], - "measurement_unit": step["measurement_unit"], - "measurement_value": step["measurement_value"], - "limit_low": step["limit_low"], - "limit_high": step["limit_high"], - } - all_tests_passed = False - break - - return all_tests_passed, steps, failed_at_step - - -# Run a list of tests sequentially -def handle_procedure( - procedure_id, - tests, - serial_number, - part_number, - revision, - batch_number, - sub_units, - attachments, -): - run_passed, steps, failed_step = run_all_tests(tests) - - if procedure_id == "FVT3" and run_passed: # Assembly Procedure - internal_resistance = steps[2]["measurement_value"] - voltage_value = steps[1]["measurement_value"] - - client.create_run( - procedure_id=procedure_id, - unit_under_test={ - "part_number": part_number, - "revision": revision, - "serial_number": serial_number, - "batch_number": batch_number, - }, - run_passed=run_passed, - steps=steps, - sub_units=sub_units, - attachments=attachments, - ) - return run_passed, failed_step - - -# Main Function for Executing Procedures -def execute_procedures(end): - for _ in range(end): - # Generate unique serial numbers - part_number_cell = "00143" - part_number_pcb = "00786" - part_number_assembly = "SI02430" - revision_cell = "A" - revision_pcb = "B" - revision_assembly = "B" - static_segment = "4J" - random_digits_cell = "".join( - [str(random.randint(0, 9)) for _ in range(5)]) - random_digits_pcb = "".join( - [str(random.randint(0, 9)) for _ in range(5)]) - random_digits_assembly = "".join( - [str(random.randint(0, 9)) for _ in range(5)]) - serial_number_cell = ( - f"{part_number_cell}{revision_cell}{static_segment}{random_digits_cell}" - ) - serial_number_pcb = ( - f"{part_number_pcb}{revision_pcb}{static_segment}{random_digits_pcb}" - ) - serial_number_assembly = f"{part_number_assembly}{revision_assembly}{static_segment}{random_digits_assembly}" - batch_number = "1024" - - # Execute PCBA Tests - tests_pcb = [ - (flash_firmware_and_version, timedelta(seconds=90)), - (configuration_battery_gauge, timedelta(seconds=1)), - (get_calibration_values_and_internal_statuses, timedelta(seconds=1)), - (overvoltage_protection_test, timedelta(seconds=5)), - (undervoltage_protection_test, timedelta(seconds=5)), - (test_LED_and_button, timedelta(seconds=6)), - (save_information_in_memory, timedelta(seconds=0.1)), - (visual_inspection, timedelta(seconds=10)), - ] - passed_pcb, failed_step_pcb = handle_procedure( - "FVT1", - tests_pcb, - serial_number_pcb, - part_number_pcb, - revision_pcb, - batch_number, - None, - ["data/pcb_coating.jpeg"], - ) - if not passed_pcb: - continue - - # Execute Cell Tests - tests_cell = [ - (esr_test, timedelta(seconds=2)), - (cell_voltage_test, timedelta(seconds=0.1)), - (ir_test, timedelta(seconds=5)), - (charge_discharge_cycle_test, timedelta(seconds=2)), - ] - passed_cell, failed_step_cell = handle_procedure( - "FVT2", - tests_cell, - serial_number_cell, - part_number_cell, - revision_cell, - batch_number, - None, - None, - ) - if not passed_cell: - continue - - # Execute Assembly Tests - tests_assembly = [ - (battery_connection, timedelta(seconds=0.1)), - (voltage_value, timedelta(seconds=1)), - (internal_resistance, timedelta(seconds=1)), - (thermal_runaway_detection, timedelta(seconds=2)), - (state_of_health, timedelta(seconds=2)), - (state_of_charge, timedelta(seconds=2)), - ] - handle_procedure( - "FVT4", - tests_assembly, - serial_number_assembly, - part_number_assembly, - revision_assembly, - batch_number, - [ - {"serial_number": serial_number_pcb}, - {"serial_number": serial_number_cell}, - ], - None, - ) - - -if __name__ == "__main__": - execute_procedures(1)