From 469562df4b808c90b50bd2e4c77d8ada679c7ac0 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 13:11:41 +0100 Subject: [PATCH 1/9] Add failing unit test as default tuning is updated during calibration --- .../picamera2/test_acquisition.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_acquisition.py b/hardware-specific-tests/picamera2/test_acquisition.py index b8e2fe88..4876e701 100644 --- a/hardware-specific-tests/picamera2/test_acquisition.py +++ b/hardware-specific-tests/picamera2/test_acquisition.py @@ -1,18 +1,29 @@ """Test data collection from the Raspberry Picamera.""" +import tempfile + from fastapi.testclient import TestClient from PIL import Image import numpy as np from pytest import fixture -from labthings_fastapi.server import ThingServer -from labthings_fastapi.client import ThingClient +import labthings_fastapi as lt from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 -@fixture(scope="module") -def client(): +@fixture() +def picamera_thing() -> StreamingPiCamera2: + """Return a StreamingPiCamera2 Thing. + + This is the Thing that the fixture client uses. It can be used to probe the + Thing directly to check actions had the expected response. + """ + return StreamingPiCamera2() + + +@fixture() +def client(picamera_thing) -> lt.ThingClient: """Initialise a test client for the StreamingPiCamera2 Thing. This fixture: @@ -21,16 +32,23 @@ def client(): * Registers a StreamingPiCamera2 instance at the "/camera/" endpoint * Provides a ThingClient for interacting with it during tests. """ - server = ThingServer() - server.add_thing(StreamingPiCamera2(), "/camera/") + temp_folder = tempfile.TemporaryDirectory() + server = lt.ThingServer(settings_folder=temp_folder.name) + server.add_thing(picamera_thing, "/camera/") with TestClient(server.app) as test_client: - client = ThingClient.from_url("/camera/", client=test_client) + client = lt.ThingClient.from_url("/camera/", client=test_client) yield client -def test_calibration(client): +def test_calibration(picamera_thing, client): """Check that full auto calibrate completes without an exception.""" + tuning = picamera_thing.tuning + default_tuning = picamera_thing.default_tuning + # Tuning should start the same as the server is loading with no settings. + assert default_tuning == tuning + # After calibration they should be different client.full_auto_calibrate() + assert default_tuning != tuning def test_jpeg_and_array(client): From ab0bece70bcc682fd2c3716d1230935cd79e4d3c Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 13:14:16 +0100 Subject: [PATCH 2/9] Deepcopy default tuning file so it isn't affected by calibration. --- src/openflexure_microscope_server/things/camera/picamera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 130537de..b12b888a 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -20,6 +20,7 @@ from datetime import datetime import json import logging import os +import copy import tempfile import time from contextlib import contextmanager @@ -138,7 +139,7 @@ class StreamingPiCamera2(BaseCamera): # Set tuning to default tuning. This will be overwritten when the Thing is # connects to the server if tuning is saved to disk. try: - self.tuning = self.default_tuning + self.tuning = copy.deepcopy(self.default_tuning) except NotConnectedToServerError: # This will throw an error after setting as we are not connected to # a server. But we know this, so we ignore the error. From c209d524c5f89ada4b2d71e9f2dd462b1ce3767f Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 13:22:35 +0100 Subject: [PATCH 3/9] Split calibration and aquisition tests --- .../picamera2/test_acquisition.py | 32 ++---------- .../picamera2/test_calibration.py | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 hardware-specific-tests/picamera2/test_calibration.py diff --git a/hardware-specific-tests/picamera2/test_acquisition.py b/hardware-specific-tests/picamera2/test_acquisition.py index 4876e701..c787f227 100644 --- a/hardware-specific-tests/picamera2/test_acquisition.py +++ b/hardware-specific-tests/picamera2/test_acquisition.py @@ -1,7 +1,5 @@ """Test data collection from the Raspberry Picamera.""" -import tempfile - from fastapi.testclient import TestClient from PIL import Image import numpy as np @@ -12,18 +10,8 @@ import labthings_fastapi as lt from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 -@fixture() -def picamera_thing() -> StreamingPiCamera2: - """Return a StreamingPiCamera2 Thing. - - This is the Thing that the fixture client uses. It can be used to probe the - Thing directly to check actions had the expected response. - """ - return StreamingPiCamera2() - - -@fixture() -def client(picamera_thing) -> lt.ThingClient: +@fixture(scope="module") +def client() -> lt.ThingClient: """Initialise a test client for the StreamingPiCamera2 Thing. This fixture: @@ -32,25 +20,13 @@ def client(picamera_thing) -> lt.ThingClient: * Registers a StreamingPiCamera2 instance at the "/camera/" endpoint * Provides a ThingClient for interacting with it during tests. """ - temp_folder = tempfile.TemporaryDirectory() - server = lt.ThingServer(settings_folder=temp_folder.name) - server.add_thing(picamera_thing, "/camera/") + server = lt.ThingServer() + server.add_thing(StreamingPiCamera2(), "/camera/") with TestClient(server.app) as test_client: client = lt.ThingClient.from_url("/camera/", client=test_client) yield client -def test_calibration(picamera_thing, client): - """Check that full auto calibrate completes without an exception.""" - tuning = picamera_thing.tuning - default_tuning = picamera_thing.default_tuning - # Tuning should start the same as the server is loading with no settings. - assert default_tuning == tuning - # After calibration they should be different - client.full_auto_calibrate() - assert default_tuning != tuning - - def test_jpeg_and_array(client): """Check that a jpeg grabbed from the stream is the same size as other captures. diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py new file mode 100644 index 00000000..6dc8ffc4 --- /dev/null +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -0,0 +1,49 @@ +"""Test data collection from the Raspberry Picamera.""" + +import tempfile + +from fastapi.testclient import TestClient +from pytest import fixture + +import labthings_fastapi as lt + +from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 + + +@fixture() +def picamera_thing() -> StreamingPiCamera2: + """Return a StreamingPiCamera2 Thing. + + This is the Thing that the fixture client uses. It can be used to probe the + Thing directly to check actions had the expected response. + """ + return StreamingPiCamera2() + + +@fixture() +def client(picamera_thing) -> lt.ThingClient: + """Initialise a test client for the StreamingPiCamera2 Thing. + + This fixture: + + * Sets up a ThingServer, + * Registers a StreamingPiCamera2 instance at the "/camera/" endpoint + * Provides a ThingClient for interacting with it during tests. + """ + temp_folder = tempfile.TemporaryDirectory() + server = lt.ThingServer(settings_folder=temp_folder.name) + server.add_thing(picamera_thing, "/camera/") + with TestClient(server.app) as test_client: + client = lt.ThingClient.from_url("/camera/", client=test_client) + yield client + + +def test_calibration(picamera_thing, client): + """Check that full auto calibrate completes without an exception.""" + tuning = picamera_thing.tuning + default_tuning = picamera_thing.default_tuning + # Tuning should start the same as the server is loading with no settings. + assert default_tuning == tuning + # After calibration they should be different + client.full_auto_calibrate() + assert default_tuning != tuning From 46be94383a394de2f6bb0c69273e8c8ba061ddc9 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 14:19:58 +0100 Subject: [PATCH 4/9] Add more testing to aquisition test --- hardware-specific-tests/picamera2/test_acquisition.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_acquisition.py b/hardware-specific-tests/picamera2/test_acquisition.py index c787f227..62b41058 100644 --- a/hardware-specific-tests/picamera2/test_acquisition.py +++ b/hardware-specific-tests/picamera2/test_acquisition.py @@ -35,12 +35,15 @@ def test_jpeg_and_array(client): # Grab a jpeg from the stream blob = client.grab_jpeg() mjpeg_frame = Image.open(blob.open()) - assert mjpeg_frame + # Verify throws an error if there are issues with the image + mjpeg_frame.verify() + assert mjpeg_frame.format == "JPEG" # Capture a jpeg blob = client.capture_jpeg(resolution="main") jpeg_capture = Image.open(blob.open()) - assert jpeg_capture + jpeg_capture.verify() + assert jpeg_capture.format == "JPEG" # Capture an array arrlist = client.capture_array(stream_name="main") From fdae0b9226338aadcf822a48a080fdf7728a8ab1 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 14:21:34 +0100 Subject: [PATCH 5/9] Add get_tuning_algo method to StreamingPiCamera2 --- .../picamera2/test_calibration.py | 20 +++++++++++++++++++ .../things/camera/picamera.py | 20 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index 6dc8ffc4..bd1e0ef9 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -38,6 +38,26 @@ def client(picamera_thing) -> lt.ThingClient: yield client +def test_get_tuning_algo(picamera_thing): + """Test that get_tuning algorithm retrieves tuning algorithms.""" + # Missing algorithm returns None + assert picamera_thing.get_tuning_algo("foo") is None + # Real algorithm is a dict and contains expected keys + assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict) + assert "offset" in picamera_thing.get_tuning_algo("rpi.geq") + + # Set the tuning to None as it is technically optional. And check this + # is handled gracefully. + try: + picamera_thing.tuning = None + except lt.exceptions.NotConnectedToServerError: + # Labthings will complain that it is not connected to a server + pass + # Check it is set to None. + assert picamera_thing.tuning is None + assert picamera_thing.get_tuning_algo("rpi.geq") is None + + def test_calibration(picamera_thing, client): """Check that full auto calibrate completes without an exception.""" tuning = picamera_thing.tuning diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index b12b888a..734e2a88 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -308,6 +308,19 @@ class StreamingPiCamera2(BaseCamera): tuning = lt.ThingSetting(Optional[dict], None, readonly=True) """The Raspberry PiCamera Tuning File JSON.""" + def get_tuning_algo(self, algorithm_name: str) -> Optional[dict]: + """Return the active tuning algorithm settings for the given algorithm. + + :returns: The algorithm dictionary if found, returns None if no tuning data + is loaded or if the tuning algorithm is not found. + """ + if self.tuning is None: + return None + try: + return Picamera2.find_tuning_algo(self.tuning, algorithm_name) + except StopIteration: + return None + def _initialise_picamera(self): """Acquire the picamera device and store it as ``self._picamera``. @@ -754,6 +767,7 @@ class StreamingPiCamera2(BaseCamera): * ``auto_expose_from_minimum`` * ``set_static_green_equalisation`` to set geq offset to max * ``calibrate_lens_shading`` + * ``reset_ccm`` * ``calibrate_white_balance`` * ``set_background`` """ @@ -761,8 +775,8 @@ class StreamingPiCamera2(BaseCamera): self.auto_expose_from_minimum() self.set_static_green_equalisation() self.calibrate_lens_shading() - self.calibrate_white_balance() self.reset_ccm() + self.calibrate_white_balance() self.set_background(portal) @lt.thing_action @@ -881,7 +895,7 @@ class StreamingPiCamera2(BaseCamera): return None # Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction" - alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc") + alsc = self.get_tuning_algo("rpi.alsc") # Check there is exactly 1 correction table for red-difference chroma (Cr) # and blue-difference chroma (Cb) @@ -952,7 +966,7 @@ class StreamingPiCamera2(BaseCamera): colour across the image. """ with self._streaming_picamera(pause_stream=True): - alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc") + alsc = self.get_tuning_algo("rpi.alsc") luminance = alsc["luminance_lut"] flat = np.ones((12, 16)) recalibrate_utils.set_static_lst(self.tuning, luminance, flat, flat) From 677fb8d98892f70a5df1f054dd23c218105212a2 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 14:35:39 +0100 Subject: [PATCH 6/9] Add more checks to the calibration test --- .../picamera2/test_calibration.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index bd1e0ef9..a589ca7b 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -59,11 +59,25 @@ def test_get_tuning_algo(picamera_thing): def test_calibration(picamera_thing, client): - """Check that full auto calibrate completes without an exception.""" - tuning = picamera_thing.tuning - default_tuning = picamera_thing.default_tuning + """Check that full auto calibrate completes and set the expected values.""" # Tuning should start the same as the server is loading with no settings. - assert default_tuning == tuning - # After calibration they should be different + assert picamera_thing.default_tuning == picamera_thing.tuning + # lens_shading tables should be None when loaded on default tuning as it is + # adaptive + assert picamera_thing.lens_shading_tables is None + # The background detector isn't ready as there is no background image. + assert not picamera_thing.background_detector_status.ready + + # Run full auto calibrate client.full_auto_calibrate() - assert default_tuning != tuning + + # After calibration the tuning files should be different + assert picamera_thing.default_tuning != picamera_thing.tuning + # Lens shading tablesshould no longer be None + assert picamera_thing.lens_shading_tables is not None + # There should be exactly one colour correction matrix + # (no variations by colour temp) + assert len(picamera_thing.get_tuning_algo("rpi.ccm")["ccms"]) == 1 + # Green equalisation offset should be set to maximum. + assert picamera_thing.get_tuning_algo("rpi.geq")["offset"] == 65535 + assert picamera_thing.background_detector_status.ready From 0a9b6be9bd680b6d512d30fc815a48165799dd4e Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 21 Aug 2025 12:25:52 +0100 Subject: [PATCH 7/9] Raise error if tuning algorithm cannot be returned, unless explicityly asked not to --- .../picamera2/test_calibration.py | 32 +++++++++++++----- .../things/camera/picamera.py | 33 +++++++++++++++++-- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index a589ca7b..732c02a4 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -3,14 +3,17 @@ import tempfile from fastapi.testclient import TestClient -from pytest import fixture +import pytest import labthings_fastapi as lt -from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 +from openflexure_microscope_server.things.camera.picamera import ( + StreamingPiCamera2, + MissingCalibrationError, +) -@fixture() +@pytest.fixture() def picamera_thing() -> StreamingPiCamera2: """Return a StreamingPiCamera2 Thing. @@ -20,7 +23,7 @@ def picamera_thing() -> StreamingPiCamera2: return StreamingPiCamera2() -@fixture() +@pytest.fixture() def client(picamera_thing) -> lt.ThingClient: """Initialise a test client for the StreamingPiCamera2 Thing. @@ -40,22 +43,33 @@ def client(picamera_thing) -> lt.ThingClient: def test_get_tuning_algo(picamera_thing): """Test that get_tuning algorithm retrieves tuning algorithms.""" - # Missing algorithm returns None - assert picamera_thing.get_tuning_algo("foo") is None + # Missing algorithm raises an error. + with pytest.raises(MissingCalibrationError): + picamera_thing.get_tuning_algo("foo") + # Or returns None if explicitly told not to error. + assert picamera_thing.get_tuning_algo("foo", raise_if_missing=False) is None + # Real algorithm is a dict and contains expected keys assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict) assert "offset" in picamera_thing.get_tuning_algo("rpi.geq") # Set the tuning to None as it is technically optional. And check this - # is handled gracefully. + # is handled gracefully. This needs to be done in a try block as LabThings + # tries and fails to emit an event. try: picamera_thing.tuning = None except lt.exceptions.NotConnectedToServerError: # Labthings will complain that it is not connected to a server pass - # Check it is set to None. + + # Check it is set to None despite the above LabThings issue. assert picamera_thing.tuning is None - assert picamera_thing.get_tuning_algo("rpi.geq") is None + + # Raises an error as there is no tuning file set. + with pytest.raises(MissingCalibrationError): + picamera_thing.get_tuning_algo("rpi.geq") + # Or returns None if explicitly told not to error. + assert picamera_thing.get_tuning_algo("rpi.geq", raise_if_missing=False) is None def test_calibration(picamera_thing, client): diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 734e2a88..f3282c61 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -15,7 +15,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf """ from __future__ import annotations -from typing import Annotated, Iterator, Literal, Mapping, Optional +from typing import Annotated, Iterator, Literal, Mapping, Optional, overload from datetime import datetime import json import logging @@ -47,6 +47,10 @@ from . import picamera_recalibrate_utils as recalibrate_utils from . import BaseCamera, JPEGBlob, ArrayModel +class MissingCalibrationError(RuntimeError): + """Picamera tuning file is missing or doesn't contain the requested algorithm.""" + + class PicameraStreamOutput(Output): """An Output class that sends frames to a stream.""" @@ -308,17 +312,40 @@ class StreamingPiCamera2(BaseCamera): tuning = lt.ThingSetting(Optional[dict], None, readonly=True) """The Raspberry PiCamera Tuning File JSON.""" - def get_tuning_algo(self, algorithm_name: str) -> Optional[dict]: + # Use overload to clarify that only a dictionary is returned if `raise_if_missing` + # is True + @overload + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: Literal[True] + ) -> dict: ... + # Otherwise may also be None + @overload + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: bool + ) -> Optional[dict]: ... + + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: bool = True + ) -> Optional[dict]: """Return the active tuning algorithm settings for the given algorithm. :returns: The algorithm dictionary if found, returns None if no tuning data is loaded or if the tuning algorithm is not found. + + :raises MissingCalibrationError: If raise_if_missing is true and there is no + tuning file is available, or the requested algorithm is not present. """ if self.tuning is None: + if raise_if_missing: + raise MissingCalibrationError("No tuning data is set.") return None try: return Picamera2.find_tuning_algo(self.tuning, algorithm_name) - except StopIteration: + except StopIteration as e: + if raise_if_missing: + raise MissingCalibrationError( + f"No tuning algorithm with name {algorithm_name}." + ) from e return None def _initialise_picamera(self): From 4e5317291f60bcbff86ecf434445e27ab8406b93 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 21 Aug 2025 12:29:33 +0100 Subject: [PATCH 8/9] Improve robustness of the full_auto_calibrate test on PiCamera --- hardware-specific-tests/picamera2/test_calibration.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index 732c02a4..7c56a9c8 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -1,6 +1,7 @@ """Test data collection from the Raspberry Picamera.""" import tempfile +from copy import deepcopy from fastapi.testclient import TestClient import pytest @@ -74,6 +75,8 @@ def test_get_tuning_algo(picamera_thing): def test_calibration(picamera_thing, client): """Check that full auto calibrate completes and set the expected values.""" + # Save copy of default tuning file for end of test + original_default = deepcopy(picamera_thing.default_tuning) # Tuning should start the same as the server is loading with no settings. assert picamera_thing.default_tuning == picamera_thing.tuning # lens_shading tables should be None when loaded on default tuning as it is @@ -87,7 +90,9 @@ def test_calibration(picamera_thing, client): # After calibration the tuning files should be different assert picamera_thing.default_tuning != picamera_thing.tuning - # Lens shading tablesshould no longer be None + # The default should be unchanged + assert picamera_thing.default_tuning == original_default + # Lens shading tables should no longer be None assert picamera_thing.lens_shading_tables is not None # There should be exactly one colour correction matrix # (no variations by colour temp) From af53982b814b70298446e671f9ac74fbd49b1af4 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 21 Aug 2025 12:47:02 +0100 Subject: [PATCH 9/9] Update the picamera coverage zip --- picamera_coverage.zip | Bin 53913 -> 53913 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 784d83d9d4d54e0d7c7b13448037a76f9f66432f..2090ce5fb05f0e6b75d0c899bc474b25d5746c29 100644 GIT binary patch delta 363 zcmbQalzHY-W}yIYW)=|!5D-iijpkgu#(krZN`pWT1OHF{kNorad-zlMZu9kQb`&V% zn|!b@pq`D5m65ZFhmDEh-~0T7XPEMg{z%SXJg4xW-tYQX(FcocZExLQeDMDA|8u`d zt!d;lQ>aAIRU!%&?&0|KIE4b7bpoNj2m$D3~$KW@LD<^T&5-Glma0 z89vk}IP73-*xkrr(jd+7kL`jf$Y~M`5)2Fn7}OToGD`_CFfd3k@FX~>v9d67`mr+A zrx{4_yfMAOIeBS+A>*~p;uDk|I6$7+_T%N1$xat8YNQz(Sr{do8Kxy!m|3P8rC21J zo26PB8(A8pB^eo;m|LW#7^b9|B~QL^!De###b`SdBZFkaloZ2MQeLFt>dz delta 358 zcmbQalzHY-W}yIYW)=|!5O`L_6a6YR{MSYyl?H){4E#U&Kl0Dx@8M73d%!nwv!g&Y z-{d`g0riY*tc;vZOl(XH|GsZe5G$Dc@caRu0yf6~#k~@$7Di4#R;Kzi0|}lt zrV5&X4J5oEO5yJ;edcko&Ylrdkp{p