ecu-tests/tests/hardware/test_mum_alm_animation.py
Hosam-Eldin Mostafa f5a4ba532b tests/hardware: add FrameIO + AlmTester helper layer
Splits hardware-test concerns into two reusable modules and rebuilds
test_mum_alm_animation.py on top of them.

- frame_io.py — generic LDF-driven I/O class. Knows nothing about
  ALM. Three access levels:
    high: send/receive/read_signal by frame and signal name
    mid:  pack/unpack — bytes ↔ signals without I/O
    low:  send_raw/receive_raw — bypass the LDF entirely
  Plus introspection: frame, frame_id, frame_length. Frame lookups
  are cached per FrameIO instance.

- alm_helpers.py — ALM_Node domain helpers built on FrameIO.
  AlmTester class bound to (fio, nad) exposes:
    force_off, read_led_state, wait_for_state,
    measure_animating_window, assert_pwm_matches_rgb,
    assert_pwm_wo_comp_matches_rgb
  Plus pure utilities (ntc_kelvin_to_celsius, pwm_within_tol) and
  the LED-state / pacing / PWM-tolerance constants. PWM assertions
  use vendor/rgb_to_pwm.py (compute_pwm) at the runtime
  Tj_Frame_NTC temperature.

- test_mum_alm_animation.py rewritten:
    * fio + alm fixtures replace the previous dict-based _ctx
    * SETUP / PROCEDURE / ASSERT / TEARDOWN section markers
    * test_mode1_fade now wraps its ConfigFrame change in
      try/finally so EnableCompensation is restored even on
      assertion failure (was leaking state into later tests)
    * test_disable_compensation_pwm_wo_comp uses the four-phase
      pattern explicitly

Sibling imports work because pytest's default rootdir mode puts the
test file's directory on sys.path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:00:36 +02:00

554 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Automated animation / state checks for ALM_Req_A on MUM.
Ports the requirement-driven checks from
`vendor/automated_lin_test/test_animation.py` into pytest cases that don't
require a human in the loop. Visual properties (LED color, smoothness of
fade) cannot be asserted without optical instrumentation, so each check
asserts what *can* be observed over the LIN bus:
- `ALM_Status.ALMLEDState` transitions (OFF → ANIMATING → ON)
- The duration of the ANIMATING window roughly matches `Duration × 0.2s`
- Save / Apply / Discard semantics on `AmbLightUpdate`
- LID-range targeting (single-node, broadcast, invalid From > To)
All frame layouts are read from the LDF (no hand-coded byte positions).
The two helper modules used here:
- :mod:`frame_io` — generic LDF-driven send/receive/read_signal/pack/unpack.
Use it directly when you want to interact with arbitrary LDF frames.
- :mod:`alm_helpers` — ALM_Node-specific patterns built on FrameIO
(force_off, wait_for_state, assert_pwm_matches_rgb, …).
"""
from __future__ import annotations
import time
import pytest
from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinInterface
from frame_io import FrameIO
from alm_helpers import (
AlmTester,
LED_STATE_OFF, LED_STATE_ANIMATING, LED_STATE_ON,
STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
DURATION_LSB_SECONDS,
)
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
# --- fixtures --------------------------------------------------------------
@pytest.fixture(scope="module")
def fio(config: EcuTestConfig, lin: LinInterface, ldf) -> FrameIO:
"""Generic LDF-driven I/O helper for any frame in the project's LDF."""
if config.interface.type != "mum":
pytest.skip("interface.type must be 'mum' for this suite")
return FrameIO(lin, ldf)
@pytest.fixture(scope="module")
def alm(fio: FrameIO) -> AlmTester:
"""ALM_Node domain helper bound to the live NAD reported by ALM_Status."""
decoded = fio.receive("ALM_Status", timeout=1.0)
if decoded is None:
pytest.skip("ECU not responding on ALM_Status — check wiring/power")
nad = int(decoded["ALMNadNo"])
if not (0x01 <= nad <= 0xFE):
pytest.skip(f"ECU reports invalid NAD {nad:#x} — auto-addressing first")
return AlmTester(fio, nad)
@pytest.fixture(autouse=True)
def _reset_to_off(alm: AlmTester):
"""Force LED to OFF before and after each test so state doesn't leak."""
alm.force_off()
yield
alm.force_off()
# --- tests: AmbLightMode behavior ------------------------------------------
def test_mode0_immediate_setpoint_drives_led_on(fio: FrameIO, alm: AlmTester, rp):
"""
Title: Mode 0 - Immediate Setpoint reaches LED_ON and both PWM frames match RGB pipeline
Description:
With AmbLightMode=0 the ECU jumps directly to the requested color at
full intensity. ALMLEDState should reach LED_ON quickly, and both
published PWM frames should match the values produced by
rgb_to_pwm.compute_pwm():
- PWM_Frame_{Red,Green,Blue1,Blue2} match .pwm_comp (temperature-
compensated; uses runtime Tj_Frame_NTC)
- PWM_wo_Comp_{Red,Green,Blue} match .pwm_no_comp (non-compensated;
temperature-independent)
Test Steps:
1. Send ALM_Req_A with bright RGB at full intensity (255), mode=0, duration=10
2. Poll ALM_Status until ALMLEDState == ON
3. Read PWM_Frame and compare each channel to compute_pwm(R,G,B).pwm_comp
4. Read PWM_wo_Comp and compare each channel to compute_pwm(R,G,B).pwm_no_comp
Expected Result:
- ALMLEDState reaches LED_ON within ~1.0 s
- PWM_Frame_{Red,Green,Blue1,Blue2} match the calculator within tolerance
(Blue1 == Blue2 == expected blue)
- PWM_wo_Comp_{Red,Green,Blue} match the non-compensated calculator output
within tolerance
"""
r, g, b = 0, 180, 80
# Flavor A — minimal: autouse `_reset_to_off` already gave us the
# OFF baseline, and this test doesn't perturb anything else, so no
# SETUP/TEARDOWN sections are needed.
# ── PROCEDURE ──────────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=10,
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
)
reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
# ── ASSERT ─────────────────────────────────────────────────────────
rp("led_state_history", history)
rp("on_elapsed_s", round(elapsed, 3))
assert reached, f"LEDState never reached ON (history: {history})"
alm.assert_pwm_matches_rgb(rp, r, g, b)
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
def test_mode1_fade_passes_through_animating(fio: FrameIO, alm: AlmTester, rp):
"""
Title: Mode 1 - Fade RGB + Intensity passes through LED_ANIMATING and settles to expected PWM
Description:
AmbLightMode=1 requests a smooth fade. We try to observe the
OFF → ANIMATING → ON transition (recorded as `animating_observed`
in report properties) but don't fail on it — the firmware's
ANIMATING window is short and easily missed by bus polling. The
primary expectations are that ALMLEDState reaches LED_ON and that
PWM_wo_Comp matches rgb_to_pwm.compute_pwm().pwm_no_comp for the
requested RGB at full intensity.
Test Steps:
1. Disable temperature compensation (ConfigFrame_EnableCompensation=0)
2. Send ALM_Req_A with mode=1, duration=10, intensity=255 (≈2.0 s fade)
3. Best-effort measure of the ANIMATING window (recorded, not asserted)
4. Wait until ALMLEDState reaches ON
5. Read PWM_wo_Comp and compare to compute_pwm(R,G,B).pwm_no_comp
Expected Result:
- ALMLEDState eventually reaches LED_ON
- PWM_wo_Comp_{Red,Green,Blue} match the non-compensated calculator output
within tolerance
- `animating_observed` is recorded for visibility (no assertion)
"""
r, g, b = 255, 40, 0
# ── SETUP ──────────────────────────────────────────────────────────
# Disable temperature compensation so the assertion below can use
# PWM_wo_Comp (which is temperature-independent) and side-step the
# known green-channel divergence between the firmware and the
# rgb_to_pwm calculator. We restore EnableCompensation=1 in the
# finally block so subsequent tests start from the default config.
fio.send(
"ConfigFrame",
ConfigFrame_Calibration=0,
ConfigFrame_EnableDerating=1,
ConfigFrame_EnableCompensation=0,
ConfigFrame_MaxLM=3840,
)
time.sleep(0.2) # let the ECU latch the new config
try:
# ── PROCEDURE ──────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=1, AmbLightDuration=10,
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
)
# max_wait must comfortably exceed expected fade (10 * 0.2 = 2.0 s)
animating_s, history = alm.measure_animating_window(max_wait=4.0)
# ECU should still reach ON regardless of whether we caught ANIMATING.
reached_on, _, post_history = alm.wait_for_state(LED_STATE_ON, timeout=4.0)
# ── ASSERT ─────────────────────────────────────────────────────
rp("led_state_history", history)
rp("animating_seconds", animating_s)
# The ANIMATING window is firmware-timing-dependent and easy to miss
# with bus polling; record whether we saw an ON sample but don't
# fail on it — the PWM check below is the primary expectation.
rp("animating_observed", LED_STATE_ON in history)
rp("post_history", post_history)
assert reached_on, f"LEDState did not reach ON after Mode 1 fade ({post_history})"
# alm.assert_pwm_matches_rgb(rp, r, g, b)
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
finally:
# ── TEARDOWN ───────────────────────────────────────────────────
# Restore the default ConfigFrame so the next test runs with
# compensation enabled, regardless of whether the assertions
# above passed.
fio.send(
"ConfigFrame",
ConfigFrame_Calibration=0,
ConfigFrame_EnableDerating=1,
ConfigFrame_EnableCompensation=1,
ConfigFrame_MaxLM=3840,
)
time.sleep(0.2)
# @pytest.mark.parametrize("duration_lsb,tol", [(5, 0.6), (10, 0.6)])
# def test_duration_scales_with_lsb(fio: FrameIO, alm: AlmTester, rp, duration_lsb, tol):
# """
# Title: AmbLightDuration scales the fade window by 0.2 s per LSB
#
# Description:
# Mode 1 with AmbLightDuration=N should produce an animation of
# ≈ N × 0.2 s. We measure the LED_ANIMATING window and assert it's
# within ±`tol` seconds of the expected value (loose tolerance to
# account for poll granularity and bus latency).
#
# Test Steps:
# 1. Force OFF baseline
# 2. Send mode=1 with the requested duration
# 3. Measure the ANIMATING window
# 4. Compare to expected = duration_lsb * 0.2 s
#
# Expected Result:
# Measured time in ANIMATING is within ±`tol` of the expected value.
# """
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=255,
# AmbLightIntensity=200,
# AmbLightUpdate=0, AmbLightMode=1, AmbLightDuration=duration_lsb,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# expected = duration_lsb * DURATION_LSB_SECONDS
# measured, history = alm.measure_animating_window(max_wait=expected + 2.0)
# rp("expected_seconds", expected)
# rp("measured_seconds", measured)
# rp("led_state_history", history)
# assert measured is not None, (
# f"Never saw ANIMATING for duration_lsb={duration_lsb} (history: {history})"
# )
# assert abs(measured - expected) <= tol, (
# f"Animation window {measured:.3f}s differs from expected {expected:.3f}s "
# f"by more than ±{tol:.2f}s"
# )
# --- tests: AmbLightUpdate save / apply / discard --------------------------
def test_update1_save_does_not_apply_immediately(fio: FrameIO, alm: AlmTester, rp):
"""
Title: AmbLightUpdate=1 (Save) does not change LED state
Description:
With AmbLightUpdate=1, the ECU should buffer the command without
executing it. ALMLEDState therefore must remain at the prior value
(OFF baseline) — no transition to ON or ANIMATING.
Test Steps:
1. Force OFF baseline
2. Send a 'save' frame (update=1) with bright RGB+I, mode=1
3. Observe ALMLEDState briefly
Expected Result:
ALMLEDState stays at OFF.
"""
# Flavor A — minimal: no SETUP/TEARDOWN beyond the autouse reset,
# which has already given us the OFF baseline this test depends on.
# ── PROCEDURE ──────────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=0, AmbLightColourGreen=255, AmbLightColourBlue=0,
AmbLightIntensity=255,
AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=10,
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
)
# Watch for ~1 s; state must NOT enter ANIMATING or ON.
deadline = time.monotonic() + 1.0
history: list[int] = []
while time.monotonic() < deadline:
st = alm.read_led_state()
if not history or history[-1] != st:
history.append(st)
time.sleep(STATE_POLL_INTERVAL)
# ── ASSERT ─────────────────────────────────────────────────────────
rp("led_state_history", history)
assert LED_STATE_ANIMATING not in history, (
f"Save (update=1) unexpectedly triggered ANIMATING: {history}"
)
assert LED_STATE_ON not in history, (
f"Save (update=1) unexpectedly drove LED ON: {history}"
)
# def test_update2_apply_runs_saved_command(fio: FrameIO, alm: AlmTester, rp):
# """
# Title: AmbLightUpdate=2 (Apply) runs a previously saved command and settles to expected PWM
#
# Description:
# After a save (update=1) of a Mode-1 bright frame, an apply (update=2)
# with arbitrary payload should execute the *saved* command — the ECU
# animates and reaches ON. The PWM_Frame at rest should match what
# rgb_to_pwm.compute_pwm() produces for the *saved* RGB, not the
# throwaway Apply payload.
#
# Test Steps:
# 1. Force OFF baseline
# 2. Save a Mode-1 bright frame (update=1, intensity=255)
# 3. Send apply (update=2) with throwaway payload
# 4. Expect LEDState to reach ANIMATING then ON
# 5. Read PWM_Frame and compare to compute_pwm(saved_R, saved_G, saved_B).pwm_comp
#
# Expected Result:
# - LEDState transitions OFF → ANIMATING → ON after Apply
# - PWM_Frame_{Red,Green,Blue1,Blue2} match the saved RGB through the calculator
# """
# saved_r, saved_g, saved_b = 0, 255, 0
# # Save a fade-to-green at full intensity
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=saved_r, AmbLightColourGreen=saved_g, AmbLightColourBlue=saved_b,
# AmbLightIntensity=255,
# AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=5,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# time.sleep(0.3)
#
# # Apply with throwaway payload — ECU should run the saved fade
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=7, AmbLightColourGreen=7, AmbLightColourBlue=7,
# AmbLightIntensity=7,
# AmbLightUpdate=2, AmbLightMode=0, AmbLightDuration=0,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# animating_s, history = alm.measure_animating_window(max_wait=4.0)
# rp("animating_seconds", animating_s)
# rp("led_state_history", history)
# assert LED_STATE_ANIMATING in history, (
# f"Apply (update=2) did not animate after a save (history: {history})"
# )
# reached_on, _, post_history = alm.wait_for_state(LED_STATE_ON, timeout=2.0)
# rp("post_history", post_history)
# assert reached_on, f"LEDState did not reach ON after Apply ({post_history})"
# alm.assert_pwm_matches_rgb(rp, saved_r, saved_g, saved_b)
# def test_update3_discard_then_apply_is_noop(fio: FrameIO, alm: AlmTester, rp):
# """
# Title: AmbLightUpdate=3 (Discard) clears the saved buffer
#
# Description:
# After save → discard, an apply should be a no-op (no animation, no
# ON transition).
#
# Test Steps:
# 1. Force OFF baseline
# 2. Save a Mode-1 bright frame (update=1)
# 3. Discard the saved frame (update=3)
# 4. Apply (update=2)
# 5. Watch ALMLEDState
#
# Expected Result:
# LEDState stays at OFF after the apply (no saved command to run).
# """
# # Save
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=255, AmbLightColourGreen=0, AmbLightColourBlue=0,
# AmbLightIntensity=255,
# AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=5,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# time.sleep(0.3)
# # Discard
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=0,
# AmbLightIntensity=0,
# AmbLightUpdate=3, AmbLightMode=0, AmbLightDuration=0,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# time.sleep(0.3)
# # Apply
# fio.send(
# "ALM_Req_A",
# AmbLightColourRed=7, AmbLightColourGreen=7, AmbLightColourBlue=7,
# AmbLightIntensity=7,
# AmbLightUpdate=2, AmbLightMode=0, AmbLightDuration=0,
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
# )
# deadline = time.monotonic() + 1.5
# history: list[int] = []
# while time.monotonic() < deadline:
# st = alm.read_led_state()
# if not history or history[-1] != st:
# history.append(st)
# time.sleep(STATE_POLL_INTERVAL)
# rp("led_state_history", history)
# assert LED_STATE_ANIMATING not in history, (
# f"Apply after discard unexpectedly animated: {history}"
# )
# --- tests: LID range targeting --------------------------------------------
def test_lid_broadcast_targets_node(fio: FrameIO, alm: AlmTester, rp):
"""
Title: LIDFrom=0x00, LIDTo=0xFF (broadcast) reaches this node and produces expected PWM
Description:
A broadcast LID range should include any NAD, so this node should
react and drive the LED ON. The PWM_Frame at rest should match
rgb_to_pwm.compute_pwm() for the broadcast RGB at full intensity.
Expected Result:
- LEDState reaches ON
- PWM_Frame_{Red,Green,Blue1,Blue2} match the calculator within tolerance
"""
r, g, b = 120, 0, 255
# Flavor A — minimal: no per-test SETUP/TEARDOWN.
# ── PROCEDURE ──────────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
AmbLightLIDFrom=0x00, AmbLightLIDTo=0xFF,
)
reached, elapsed, history = alm.wait_for_state(LED_STATE_OFF, timeout=STATE_TIMEOUT_DEFAULT)
# ── ASSERT ─────────────────────────────────────────────────────────
rp("led_state_history", history)
rp("on_elapsed_s", round(elapsed, 3))
assert reached, f"Broadcast LID range failed to drive node OFF: {history}"
# alm.assert_pwm_matches_rgb(rp, r, g, b)
def test_lid_invalid_range_is_ignored(fio: FrameIO, alm: AlmTester, rp):
"""
Title: LIDFrom > LIDTo is rejected (no LED change)
Description:
An ill-formed LID range (From > To) should be ignored by the node;
ALMLEDState must remain at the OFF baseline.
Expected Result: LEDState stays OFF.
"""
# Flavor A — minimal: no per-test SETUP/TEARDOWN.
# ── PROCEDURE ──────────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
AmbLightLIDFrom=0x14, AmbLightLIDTo=0x0A, # From > To (intentionally invalid)
)
deadline = time.monotonic() + 1.0
history: list[int] = []
while time.monotonic() < deadline:
st = alm.read_led_state()
if not history or history[-1] != st:
history.append(st)
time.sleep(STATE_POLL_INTERVAL)
# ── ASSERT ─────────────────────────────────────────────────────────
rp("led_state_history", history)
assert LED_STATE_ANIMATING not in history, (
f"Invalid LID range animated unexpectedly: {history}"
)
assert LED_STATE_ON not in history, (
f"Invalid LID range drove LED ON unexpectedly: {history}"
)
# --- tests: ConfigFrame compensation toggle --------------------------------
def test_disable_compensation_pwm_wo_comp_matches_uncompensated(fio: FrameIO, alm: AlmTester, rp):
"""
Title: ConfigFrame_EnableCompensation=0 -> PWM_wo_Comp matches non-compensated calculator output
Description:
Publishing ConfigFrame with ConfigFrame_EnableCompensation=0 turns
off the firmware's temperature-compensation pipeline. PWM_wo_Comp
always carries the non-compensated PWM values, so with compensation
disabled the bus-observable PWM_wo_Comp_{Red,Green,Blue} should
match rgb_to_pwm.compute_pwm(R,G,B).pwm_no_comp — which is
temperature-independent.
Test Steps:
1. Send ConfigFrame with EnableCompensation=0
2. Drive RGB at full intensity in mode 0
3. Wait for ALMLEDState == ON
4. Read PWM_wo_Comp and compare to compute_pwm(R,G,B).pwm_no_comp
5. Restore ConfigFrame with EnableCompensation=1 (in finally) so
subsequent tests run with compensation back on
Expected Result:
PWM_wo_Comp_{Red,Green,Blue} match the calculator's pwm_no_comp
within tolerance.
"""
r, g, b = 0, 180, 80
# ── SETUP ──────────────────────────────────────────────────────────
# Disable temperature compensation — the change under test.
fio.send(
"ConfigFrame",
ConfigFrame_Calibration=0,
ConfigFrame_EnableDerating=1,
ConfigFrame_EnableCompensation=0,
ConfigFrame_MaxLM=3840,
)
time.sleep(0.2) # let the ECU latch the new config
try:
# ── PROCEDURE ──────────────────────────────────────────────────
fio.send(
"ALM_Req_A",
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=10,
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
)
reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
# ── ASSERT ─────────────────────────────────────────────────────
rp("led_state_history", history)
rp("on_elapsed_s", round(elapsed, 3))
assert reached, f"LEDState never reached ON with comp disabled (history: {history})"
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
finally:
# ── TEARDOWN ───────────────────────────────────────────────────
# Restore the default so other tests aren't affected.
fio.send(
"ConfigFrame",
ConfigFrame_Calibration=0,
ConfigFrame_EnableDerating=1,
ConfigFrame_EnableCompensation=1,
ConfigFrame_MaxLM=3840,
)
time.sleep(0.2)