diff --git a/.gitignore b/.gitignore
index c2cd1c95..0a17713c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,7 +56,6 @@ pytest_report.xml
.coverage.picamera
.coverage.main
picamera_test_hashes.json
-webapp/src/tests/fixtures/*
# Translations
*.mo
diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py
index bbece763..56b1ef67 100644
--- a/src/openflexure_microscope_server/logging.py
+++ b/src/openflexure_microscope_server/logging.py
@@ -17,7 +17,8 @@ from logging.handlers import RotatingFileHandler
from typing import Optional
from fastapi import HTTPException
-from fastapi.responses import PlainTextResponse
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import JSONResponse, PlainTextResponse
from labthings_fastapi.logs import inject_invocation_id
@@ -53,8 +54,9 @@ def configure_logging(log_folder: str, debug: bool = False) -> None:
# Add OFM_HANDLER first so it can capture the error log if the
# log file can't be accessed.
ofm_format_str = "[%(asctime)s] [%(levelname)s] %(message)s"
- OFM_HANDLER.setFormatter(logging.Formatter(ofm_format_str))
+ OFM_HANDLER.setFormatter(OFMLogFileFormatter(ofm_format_str))
OFM_HANDLER.addFilter(UvicornAccessFilter())
+ OFM_HANDLER.addFilter(inject_invocation_id)
OFM_HANDLER.level = root_logger.level
root_logger.addHandler(OFM_HANDLER)
@@ -84,7 +86,7 @@ def configure_logging(log_folder: str, debug: bool = False) -> None:
)
-def retrieve_log() -> PlainTextResponse:
+def retrieve_log() -> JSONResponse:
"""Return logs since the server started, up to a maximum of 250 messages.
This log is the one shown in the UI and on the logging page.
@@ -95,7 +97,7 @@ def retrieve_log() -> PlainTextResponse:
All logs, including ``uvicorn.access`` are logged to the OFM_LOG_FILE (see above)
this is the best place to get logs about crashes.
"""
- return PlainTextResponse(OFM_HANDLER.log_history)
+ return JSONResponse(content=jsonable_encoder(OFM_HANDLER.log_history))
def retrieve_log_from_file() -> PlainTextResponse:
@@ -155,23 +157,39 @@ class OFMHandler(logging.Handler):
how many can be returned over HTTP.
"""
super().__init__(level=level)
- self._log: list[str] = []
+ self._log: list[dict] = []
self._max_logs = max_logs
+ # start at -1 so first record is 0
+ self._running_counter = -1
+
+ def _as_record_dict(self, record: logging.LogRecord) -> dict:
+ """Format the record into the dictionary structure expected by the server."""
+ msg = record.message
+ self._running_counter += 1
+ return {
+ "timestamp": record.asctime,
+ "level": record.levelname,
+ "summary": msg.split("\n")[0],
+ "message": msg,
+ "invocation_id": getattr(record, "invocation_id", None),
+ "logger": record.name,
+ "sequence": self._running_counter,
+ }
def append_record(self, record: logging.LogRecord) -> None:
- """Format message and append it to a list of records.
-
- The built in formatter is used to format the record.
+ """Append the record as a dictionary to a log of records.
Any records in excess of the maximum number of logs are removed.
"""
- self._log.append(self.format(record))
+ self._log.append(self._as_record_dict(record))
while len(self._log) > self._max_logs:
self._log.pop(0)
def emit(self, record: logging.LogRecord) -> None:
"""Emit will save the logged record to the log."""
try:
+ # Always format before trying to read or the attributes are not set.
+ self.format(record)
if record.levelno >= self.level:
self.append_record(record)
except RecursionError as e:
@@ -182,9 +200,24 @@ class OFMHandler(logging.Handler):
self.handleError(record)
@property
- def log_history(self) -> str:
- """Return the log history up to the maximum number of logs."""
- return "\n".join(self._log)
+ def log_history(self) -> list[dict]:
+ """Return the log history up to the maximum number of logs.
+
+ Order is reversed so most recent logs are at the top.
+ """
+ return list(reversed(self._log))
+
+ @property
+ def get_formatted_log_history(self) -> str:
+ """Return the log history as a formatted string.
+
+ This is to be used for the fallback server, the formatting does not contain invocation
+ IDs as no actions should have started.
+ """
+ output = ""
+ for entry in self._log:
+ output += f"[{entry['timestamp']} [{entry['level']}] <{entry['logger']}> {entry['message']}\n"
+ return output
class UvicornAccessFilter(logging.Filter):
diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py
index 2e1849bf..55a06bb6 100644
--- a/src/openflexure_microscope_server/server/__init__.py
+++ b/src/openflexure_microscope_server/server/__init__.py
@@ -150,7 +150,7 @@ def serve_from_cli(argv: Optional[list[str]] = None) -> None:
print(f"Error: {e}") # noqa: T201
print("Starting fallback server.") # noqa: T201
try:
- log_history = OFM_HANDLER.log_history
+ log_history = OFM_HANDLER.get_formatted_log_history
except BaseException:
# If log history fails for any reason carry on.
log_history = None
diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py
index 76ae0f24..4479084f 100644
--- a/tests/unit_tests/test_logging.py
+++ b/tests/unit_tests/test_logging.py
@@ -1,5 +1,6 @@
"""Test the server's logging configuration and handling."""
+import json
import logging
import os
import tempfile
@@ -8,7 +9,7 @@ from logging.handlers import RotatingFileHandler
import pytest
from fastapi import HTTPException
-from fastapi.responses import PlainTextResponse
+from fastapi.responses import JSONResponse, PlainTextResponse
from openflexure_microscope_server import logging as ofm_logging
@@ -90,18 +91,18 @@ def test_max_logs():
# But I would log five hundred times.
for i in range(500):
logging.info("log %s", i)
- logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
+ logs = ofm_logging.OFM_HANDLER.log_history
assert len(logs) == 250
- assert logs[0].endswith("log 250")
- assert logs[-1].endswith("log 499")
+ assert logs[-1]["message"] == "log 250"
+ assert logs[0]["message"] == "log 499"
# And I would log five hundred more.
for i in range(500):
logging.info("log %s", 500 + i)
# Just to be the test who logged a thousand times to check your hand-el-or!
- logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
+ logs = ofm_logging.OFM_HANDLER.log_history
assert len(logs) == 250
- assert logs[0].endswith("log 750")
- assert logs[-1].endswith("log 999")
+ assert logs[-1]["message"] == "log 750"
+ assert logs[0]["message"] == "log 999"
def test_ofm_handler_ignores_uvicorn_access():
@@ -113,17 +114,17 @@ def test_ofm_handler_ignores_uvicorn_access():
ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler()
with tmp_logging_dir() as tmpdir:
ofm_logging.configure_logging(tmpdir)
- logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
+ logs = ofm_logging.OFM_HANDLER.log_history
starting_len = len(logs)
logging.info("Root 1")
uvicorn_logger = logging.getLogger("uvicorn.access")
for i in range(500):
uvicorn_logger.info("Uvicorn log %s", i)
logging.info("Root 2")
- logs = ofm_logging.OFM_HANDLER.log_history.split("\n")
+ logs = ofm_logging.OFM_HANDLER.log_history
assert len(logs) == starting_len + 2
- assert logs[starting_len].endswith("Root 1")
- assert logs[-1].endswith("Root 2")
+ assert logs[starting_len]["message"] == "Root 1"
+ assert logs[0]["message"] == "Root 2"
def test_server_responses():
@@ -140,8 +141,10 @@ def test_server_responses():
for i in range(500):
logging.info("log %s", i)
log_response = ofm_logging.retrieve_log()
- assert isinstance(log_response, PlainTextResponse)
- logs = log_response.body.decode().split("\n")
+ # Retrieve log should get a json that can be decoded into a list
+ assert isinstance(log_response, JSONResponse)
+ logs = json.loads(log_response.body.decode())
+ assert isinstance(logs, list)
assert len(logs) == 250
file_log_response = ofm_logging.retrieve_log_from_file()
assert isinstance(file_log_response, PlainTextResponse)
diff --git a/webapp/src/components/tabContentComponents/loggingContent.vue b/webapp/src/components/tabContentComponents/loggingContent.vue
index deaffe4d..87d0f19a 100644
--- a/webapp/src/components/tabContentComponents/loggingContent.vue
+++ b/webapp/src/components/tabContentComponents/loggingContent.vue
@@ -185,51 +185,8 @@ export default {
}
},
async updateLogs() {
- let response = await axios.get(this.logURI);
- let lines = response.data.split("\n");
- let logs = [];
- let regexp = /^\[(.+?)\] \[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\] (.*)$/;
- for (let line of lines) {
- if (line.length > 0) {
- let m = line.match(regexp);
- if (m) {
- logs.push({
- timestamp: m[1],
- level: m[2],
- summary: m[3],
- message: this.escapeText(m[3]),
- sequence: logs.length,
- expanded: false,
- });
- } else if (logs) {
- // If a line does not look like a log entry, append it to the last
- // log entry (i.e. allow multi-line messages)
- let entry = logs[logs.length - 1];
- m = line.match(/^( *)(\^+)/); // detect python stack trace "underlines"
- if (m) {
- let linestart = entry.message.lastIndexOf("\n") + 1;
- let ustart = linestart + m[1].length;
- let uend = ustart + m[2].length;
- entry.message =
- entry.message.substring(0, ustart) +
- "" +
- entry.message.substring(ustart, uend) +
- "" +
- entry.message.substring(uend);
- } else {
- entry.message += "\n" + this.escapeText(line);
- if (entry.message.startsWith("Traceback")) {
- entry.summary = line; // For tracebacks, the last line is the best summary
- }
- }
- } else {
- // if there's no existing log message to append to, discard lines
- // until we find one.
- continue;
- }
- }
- }
- this.logs = logs.reverse(); // Display in reverse chronological order
+ let logs = await axios.get(this.logURI);
+ this.logs = Object.values(logs.data);
},
formatDateTime: function (isoDateTimeString) {
isoDateTimeString = isoDateTimeString.replace(",", ".");
diff --git a/webapp/src/tests/fixtures/realLog.json b/webapp/src/tests/fixtures/realLog.json
new file mode 100644
index 00000000..7dbbcc85
--- /dev/null
+++ b/webapp/src/tests/fixtures/realLog.json
@@ -0,0 +1,1018 @@
+[
+ {
+ "timestamp": "2026-07-08 14:45:25,390",
+ "level": "INFO",
+ "summary": "Moving to (1521, -1063)",
+ "message": "Moving to (1521, -1063)",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 113
+ },
+ {
+ "timestamp": "2026-07-08 14:42:18,550",
+ "level": "WARNING",
+ "summary": "Global lock was busy: didn't run fast_autofocus.",
+ "message": "Global lock was busy: didn't run fast_autofocus.",
+ "invocation_id": "3dde1256-288b-45a6-8fb5-e4fc990b638f",
+ "logger": "labthings_fastapi.things.autofocus",
+ "sequence": 111
+ },
+ {
+ "timestamp": "2026-07-08 14:42:18,319",
+ "level": "WARNING",
+ "summary": "Global lock was busy: didn't run fast_autofocus.",
+ "message": "Global lock was busy: didn't run fast_autofocus.",
+ "invocation_id": "4bc36649-3a05-4449-9bc0-c34c6b16a16b",
+ "logger": "labthings_fastapi.things.autofocus",
+ "sequence": 110
+ },
+ {
+ "timestamp": "2026-07-08 14:42:16,928",
+ "level": "WARNING",
+ "summary": "Global lock was busy: didn't run fast_autofocus.",
+ "message": "Global lock was busy: didn't run fast_autofocus.",
+ "invocation_id": "f384f770-98f0-4b49-aa67-79ec546053c1",
+ "logger": "labthings_fastapi.things.autofocus",
+ "sequence": 109
+ },
+ {
+ "timestamp": "2026-07-08 14:42:12,003",
+ "level": "INFO",
+ "summary": "Stitching complete",
+ "message": "Stitching complete",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 108
+ },
+ {
+ "timestamp": "2026-07-08 14:42:02,144",
+ "level": "INFO",
+ "summary": "Exporting stitched image. Depending on the size of your scan, this may take a while.",
+ "message": "Exporting stitched image. Depending on the size of your scan, this may take a while.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 107
+ },
+ {
+ "timestamp": "2026-07-08 14:42:01,943",
+ "level": "INFO",
+ "summary": "Final image will be exported with size (3235, 4316) pixels",
+ "message": "Final image will be exported with size (3235, 4316) pixels",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 106
+ },
+ {
+ "timestamp": "2026-07-08 14:42:00,338",
+ "level": "INFO",
+ "summary": "A better starting estimate of the csm is [[ 0.0096, 0.946 ], [ 0.9427, -0.0089]].",
+ "message": "A better starting estimate of the csm is [[ 0.0096, 0.946 ], [ 0.9427, -0.0089]].",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 105
+ },
+ {
+ "timestamp": "2026-07-08 14:42:00,338",
+ "level": "INFO",
+ "summary": "Starting csm is [[ 0.0167, 0.9234], [ 0.918 , -0.0092]]",
+ "message": "Starting csm is [[ 0.0167, 0.9234], [ 0.918 , -0.0092]]",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 104
+ },
+ {
+ "timestamp": "2026-07-08 14:42:00,337",
+ "level": "INFO",
+ "summary": "Using 25 of the 29 pairs for final optimisation",
+ "message": "Using 25 of the 29 pairs for final optimisation",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 103
+ },
+ {
+ "timestamp": "2026-07-08 14:41:58,331",
+ "level": "INFO",
+ "summary": "After filtering and resampling 4 peak quality thresholds and 29 stage discrepancy thresholds will be trialed for fit quality.",
+ "message": "After filtering and resampling 4 peak quality thresholds and 29 stage discrepancy thresholds will be trialed for fit quality.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 102
+ },
+ {
+ "timestamp": "2026-07-08 14:41:58,331",
+ "level": "INFO",
+ "summary": "Starting with 4 possible peak quality thresholds and 29 possible stage discrepancy thresholds",
+ "message": "Starting with 4 possible peak quality thresholds and 29 possible stage discrepancy thresholds",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 101
+ },
+ {
+ "timestamp": "2026-07-08 14:41:58,330",
+ "level": "INFO",
+ "summary": "Finding the optimal threshold",
+ "message": "Finding the optimal threshold",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 100
+ },
+ {
+ "timestamp": "2026-07-08 14:41:57,126",
+ "level": "INFO",
+ "summary": "Plotting the inputs to stitching",
+ "message": "Plotting the inputs to stitching",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 99
+ },
+ {
+ "timestamp": "2026-07-08 14:41:57,126",
+ "level": "INFO",
+ "summary": "Getting all the image info ready to stitch",
+ "message": "Getting all the image info ready to stitch",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 98
+ },
+ {
+ "timestamp": "2026-07-08 14:41:57,125",
+ "level": "INFO",
+ "summary": "Correlation complete. Pairs loaded from cache: 29, pairs calculated: 0",
+ "message": "Correlation complete. Pairs loaded from cache: 29, pairs calculated: 0",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 97
+ },
+ {
+ "timestamp": "2026-07-08 14:41:56,925",
+ "level": "INFO",
+ "summary": "Starting correlation of overlapping image pairs.",
+ "message": "Starting correlation of overlapping image pairs.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 96
+ },
+ {
+ "timestamp": "2026-07-08 14:41:56,924",
+ "level": "INFO",
+ "summary": "Finding a list of all overlapping images",
+ "message": "Finding a list of all overlapping images",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 95
+ },
+ {
+ "timestamp": "2026-07-08 14:41:54,516",
+ "level": "INFO",
+ "summary": "Stitching final image (may take some time)...",
+ "message": "Stitching final image (may take some time)...",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 94
+ },
+ {
+ "timestamp": "2026-07-08 14:41:52,309",
+ "level": "INFO",
+ "summary": "Waiting for background processes to finish...",
+ "message": "Waiting for background processes to finish...",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 93
+ },
+ {
+ "timestamp": "2026-07-08 14:41:51,039",
+ "level": "INFO",
+ "summary": "Returning to starting position.",
+ "message": "Returning to starting position.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 92
+ },
+ {
+ "timestamp": "2026-07-08 14:41:51,010",
+ "level": "INFO",
+ "summary": "Camera started",
+ "message": "Camera started",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 91
+ },
+ {
+ "timestamp": "2026-07-08 14:41:50,967",
+ "level": "INFO",
+ "summary": "Starting picamera MJPEG stream...",
+ "message": "Starting picamera MJPEG stream...",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "openflexure_microscope_server.things.camera.picamera",
+ "sequence": 90
+ },
+ {
+ "timestamp": "2026-07-08 14:41:50,896",
+ "level": "INFO",
+ "summary": "Configuration successful!",
+ "message": "Configuration successful!",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 89
+ },
+ {
+ "timestamp": "2026-07-08 14:41:50,859",
+ "level": "INFO",
+ "summary": "Camera configuration has been adjusted!",
+ "message": "Camera configuration has been adjusted!",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 88
+ },
+ {
+ "timestamp": "2026-07-08 14:41:50,429",
+ "level": "INFO",
+ "summary": "Camera stopped",
+ "message": "Camera stopped",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 87
+ },
+ {
+ "timestamp": "2026-07-08 14:41:50,292",
+ "level": "INFO",
+ "summary": "Stopping scan because it was cancelled.",
+ "message": "Stopping scan because it was cancelled.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 86
+ },
+ {
+ "timestamp": "2026-07-08 14:41:45,858",
+ "level": "INFO",
+ "summary": "Moving to (1521, 189)",
+ "message": "Moving to (1521, 189)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 85
+ },
+ {
+ "timestamp": "2026-07-08 14:41:35,525",
+ "level": "INFO",
+ "summary": "Moving to (1521, -437)",
+ "message": "Moving to (1521, -437)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 84
+ },
+ {
+ "timestamp": "2026-07-08 14:41:25,390",
+ "level": "INFO",
+ "summary": "Moving to (1521, -1063)",
+ "message": "Moving to (1521, -1063)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 83
+ },
+ {
+ "timestamp": "2026-07-08 14:41:15,872",
+ "level": "INFO",
+ "summary": "Moving to (1521, -1689)",
+ "message": "Moving to (1521, -1689)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 82
+ },
+ {
+ "timestamp": "2026-07-08 14:41:06,012",
+ "level": "INFO",
+ "summary": "Moving to (895, -1689)",
+ "message": "Moving to (895, -1689)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 81
+ },
+ {
+ "timestamp": "2026-07-08 14:40:56,898",
+ "level": "INFO",
+ "summary": "Moving to (269, -1689)",
+ "message": "Moving to (269, -1689)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 80
+ },
+ {
+ "timestamp": "2026-07-08 14:40:47,820",
+ "level": "INFO",
+ "summary": "Moving to (-357, -1689)",
+ "message": "Moving to (-357, -1689)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 79
+ },
+ {
+ "timestamp": "2026-07-08 14:40:38,679",
+ "level": "INFO",
+ "summary": "Moving to (-983, -1689)",
+ "message": "Moving to (-983, -1689)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 78
+ },
+ {
+ "timestamp": "2026-07-08 14:40:29,111",
+ "level": "INFO",
+ "summary": "Moving to (-983, -1063)",
+ "message": "Moving to (-983, -1063)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 77
+ },
+ {
+ "timestamp": "2026-07-08 14:40:19,003",
+ "level": "INFO",
+ "summary": "Moving to (-983, -437)",
+ "message": "Moving to (-983, -437)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 76
+ },
+ {
+ "timestamp": "2026-07-08 14:40:09,008",
+ "level": "INFO",
+ "summary": "Moving to (-983, 189)",
+ "message": "Moving to (-983, 189)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 75
+ },
+ {
+ "timestamp": "2026-07-08 14:39:58,953",
+ "level": "INFO",
+ "summary": "Moving to (-357, 189)",
+ "message": "Moving to (-357, 189)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 74
+ },
+ {
+ "timestamp": "2026-07-08 14:39:47,619",
+ "level": "INFO",
+ "summary": "Moving to (269, 189)",
+ "message": "Moving to (269, 189)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 73
+ },
+ {
+ "timestamp": "2026-07-08 14:39:37,533",
+ "level": "INFO",
+ "summary": "Moving to (895, 189)",
+ "message": "Moving to (895, 189)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 72
+ },
+ {
+ "timestamp": "2026-07-08 14:39:27,245",
+ "level": "INFO",
+ "summary": "Moving to (895, -437)",
+ "message": "Moving to (895, -437)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 71
+ },
+ {
+ "timestamp": "2026-07-08 14:39:18,362",
+ "level": "INFO",
+ "summary": "Moving to (895, -1063)",
+ "message": "Moving to (895, -1063)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 70
+ },
+ {
+ "timestamp": "2026-07-08 14:39:09,468",
+ "level": "INFO",
+ "summary": "Moving to (269, -1063)",
+ "message": "Moving to (269, -1063)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 69
+ },
+ {
+ "timestamp": "2026-07-08 14:39:00,527",
+ "level": "INFO",
+ "summary": "Moving to (-357, -1063)",
+ "message": "Moving to (-357, -1063)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 68
+ },
+ {
+ "timestamp": "2026-07-08 14:38:51,230",
+ "level": "INFO",
+ "summary": "Moving to (-357, -437)",
+ "message": "Moving to (-357, -437)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 67
+ },
+ {
+ "timestamp": "2026-07-08 14:38:47,661",
+ "level": "WARNING",
+ "summary": "Global lock was busy: didn't run move_in_image_coordinates.",
+ "message": "Global lock was busy: didn't run move_in_image_coordinates.",
+ "invocation_id": "00db1d9b-5940-4ef6-a20f-ea6d534f8c42",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 66
+ },
+ {
+ "timestamp": "2026-07-08 14:38:42,729",
+ "level": "INFO",
+ "summary": "Moving to (269, -437)",
+ "message": "Moving to (269, -437)",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.smart_scan",
+ "sequence": 65
+ },
+ {
+ "timestamp": "2026-07-08 14:38:39,807",
+ "level": "INFO",
+ "summary": "Scanning with steps of dx=626 and dy=626.",
+ "message": "Scanning with steps of dx=626 and dy=626.",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "labthings_fastapi.things.histo_scan_workflow",
+ "sequence": 64
+ },
+ {
+ "timestamp": "2026-07-08 14:38:39,611",
+ "level": "INFO",
+ "summary": "Camera started",
+ "message": "Camera started",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 63
+ },
+ {
+ "timestamp": "2026-07-08 14:38:39,574",
+ "level": "INFO",
+ "summary": "Starting picamera MJPEG stream...",
+ "message": "Starting picamera MJPEG stream...",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "openflexure_microscope_server.things.camera.picamera",
+ "sequence": 62
+ },
+ {
+ "timestamp": "2026-07-08 14:38:39,385",
+ "level": "INFO",
+ "summary": "Configuration successful!",
+ "message": "Configuration successful!",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 61
+ },
+ {
+ "timestamp": "2026-07-08 14:38:39,363",
+ "level": "INFO",
+ "summary": "Camera configuration has been adjusted!",
+ "message": "Camera configuration has been adjusted!",
+ "invocation_id": "acfeefad-9923-44f1-ab02-238647ee4e89",
+ "logger": "picamera2.picamera2",
+ "sequence": 60
+ },
+ {
+ "timestamp": "2026-07-08 14:38:38,942",
+ "level": "INFO",
+ "summary": "Camera stopped",
+ "message": "Camera stopped",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 59
+ },
+ {
+ "timestamp": "2026-07-08 14:37:36,323",
+ "level": "INFO",
+ "summary": "CSM matrix is [0.03, 1.85,],[1.84, -0.02].",
+ "message": "CSM matrix is [0.03, 1.85,],[1.84, -0.02].",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 58
+ },
+ {
+ "timestamp": "2026-07-08 14:37:36,322",
+ "level": "INFO",
+ "summary": "Calibration complete, updating metadata.",
+ "message": "Calibration complete, updating metadata.",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 57
+ },
+ {
+ "timestamp": "2026-07-08 14:37:36,245",
+ "level": "INFO",
+ "summary": "Residuals were about 0.01 times the step size",
+ "message": "Residuals were about 0.01 times the step size",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 56
+ },
+ {
+ "timestamp": "2026-07-08 14:37:36,245",
+ "level": "INFO",
+ "summary": "Stage-to-image ratio 0.272 pixels/step",
+ "message": "Stage-to-image ratio 0.272 pixels/step",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 55
+ },
+ {
+ "timestamp": "2026-07-08 14:37:36,245",
+ "level": "INFO",
+ "summary": "Estimated backlash 126 steps",
+ "message": "Estimated backlash 126 steps",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 54
+ },
+ {
+ "timestamp": "2026-07-08 14:37:31,998",
+ "level": "INFO",
+ "summary": "Moving back to the start, correcting for backlash...",
+ "message": "Moving back to the start, correcting for backlash...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 53
+ },
+ {
+ "timestamp": "2026-07-08 14:37:25,754",
+ "level": "INFO",
+ "summary": "Moving the stage forwards to measure backlash (2/2)",
+ "message": "Moving the stage forwards to measure backlash (2/2)",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 52
+ },
+ {
+ "timestamp": "2026-07-08 14:37:20,146",
+ "level": "INFO",
+ "summary": "Moving the stage backwards to measure backlash (1/2)",
+ "message": "Moving the stage backwards to measure backlash (1/2)",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 51
+ },
+ {
+ "timestamp": "2026-07-08 14:37:17,876",
+ "level": "INFO",
+ "summary": "Moving the stage to the edge of the field of view...",
+ "message": "Moving the stage to the edge of the field of view...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 50
+ },
+ {
+ "timestamp": "2026-07-08 14:37:13,289",
+ "level": "INFO",
+ "summary": "Moving the stage until we see motion...",
+ "message": "Moving the stage until we see motion...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 49
+ },
+ {
+ "timestamp": "2026-07-08 14:37:12,711",
+ "level": "INFO",
+ "summary": "Calibrating X axis:",
+ "message": "Calibrating X axis:",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 48
+ },
+ {
+ "timestamp": "2026-07-08 14:37:12,634",
+ "level": "INFO",
+ "summary": "Residuals were about 0.01 times the step size",
+ "message": "Residuals were about 0.01 times the step size",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 47
+ },
+ {
+ "timestamp": "2026-07-08 14:37:12,634",
+ "level": "INFO",
+ "summary": "Stage-to-image ratio 0.271 pixels/step",
+ "message": "Stage-to-image ratio 0.271 pixels/step",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 46
+ },
+ {
+ "timestamp": "2026-07-08 14:37:12,633",
+ "level": "INFO",
+ "summary": "Estimated backlash 95 steps",
+ "message": "Estimated backlash 95 steps",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 45
+ },
+ {
+ "timestamp": "2026-07-08 14:37:07,612",
+ "level": "INFO",
+ "summary": "Moving back to the start, correcting for backlash...",
+ "message": "Moving back to the start, correcting for backlash...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 44
+ },
+ {
+ "timestamp": "2026-07-08 14:37:01,892",
+ "level": "INFO",
+ "summary": "Moving the stage forwards to measure backlash (2/2)",
+ "message": "Moving the stage forwards to measure backlash (2/2)",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 43
+ },
+ {
+ "timestamp": "2026-07-08 14:36:56,890",
+ "level": "INFO",
+ "summary": "Moving the stage backwards to measure backlash (1/2)",
+ "message": "Moving the stage backwards to measure backlash (1/2)",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 42
+ },
+ {
+ "timestamp": "2026-07-08 14:36:55,660",
+ "level": "INFO",
+ "summary": "Moving the stage to the edge of the field of view...",
+ "message": "Moving the stage to the edge of the field of view...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 41
+ },
+ {
+ "timestamp": "2026-07-08 14:36:50,353",
+ "level": "INFO",
+ "summary": "Moving the stage until we see motion...",
+ "message": "Moving the stage until we see motion...",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 40
+ },
+ {
+ "timestamp": "2026-07-08 14:36:49,769",
+ "level": "INFO",
+ "summary": "Calibrating Y axis:",
+ "message": "Calibrating Y axis:",
+ "invocation_id": "de2c30df-6a54-4ff6-8e9f-d63437c349bf",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 39
+ },
+ {
+ "timestamp": "2026-07-08 14:36:31,316",
+ "level": "ERROR",
+ "summary": "The fit didn't look successful! The residuals of fit were too large.",
+ "message": "The fit didn't look successful! The residuals of fit were too large.",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 38
+ },
+ {
+ "timestamp": "2026-07-08 14:36:31,299",
+ "level": "INFO",
+ "summary": "Returning to starting position due to failed calibration",
+ "message": "Returning to starting position due to failed calibration",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 37
+ },
+ {
+ "timestamp": "2026-07-08 14:36:18,710",
+ "level": "INFO",
+ "summary": "Moving the stage forwards to measure backlash (2/2)",
+ "message": "Moving the stage forwards to measure backlash (2/2)",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 36
+ },
+ {
+ "timestamp": "2026-07-08 14:36:13,764",
+ "level": "INFO",
+ "summary": "Moving the stage backwards to measure backlash (1/2)",
+ "message": "Moving the stage backwards to measure backlash (1/2)",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 35
+ },
+ {
+ "timestamp": "2026-07-08 14:36:11,493",
+ "level": "INFO",
+ "summary": "Moving the stage to the edge of the field of view...",
+ "message": "Moving the stage to the edge of the field of view...",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 34
+ },
+ {
+ "timestamp": "2026-07-08 14:36:06,506",
+ "level": "INFO",
+ "summary": "Moving the stage until we see motion...",
+ "message": "Moving the stage until we see motion...",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 33
+ },
+ {
+ "timestamp": "2026-07-08 14:36:05,900",
+ "level": "INFO",
+ "summary": "Calibrating Y axis:",
+ "message": "Calibrating Y axis:",
+ "invocation_id": "03875b8b-7769-4d35-bbf3-4e358f84e175",
+ "logger": "labthings_fastapi.things.camera_stage_mapping",
+ "sequence": 32
+ },
+ {
+ "timestamp": "2026-07-08 14:35:19,024",
+ "level": "INFO",
+ "summary": "Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)",
+ "message": "Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)",
+ "invocation_id": null,
+ "logger": "uvicorn",
+ "sequence": 31
+ },
+ {
+ "timestamp": "2026-07-08 14:35:19,004",
+ "level": "INFO",
+ "summary": "Application startup complete.",
+ "message": "Application startup complete.",
+ "invocation_id": null,
+ "logger": "uvicorn",
+ "sequence": 30
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,980",
+ "level": "INFO",
+ "summary": "Enabling backwards-compatible behaviour on new Sangaboard firmware",
+ "message": "Enabling backwards-compatible behaviour on new Sangaboard firmware",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 29
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,965",
+ "level": "INFO",
+ "summary": "Loading modules...",
+ "message": "Loading modules...",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 28
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,964",
+ "level": "INFO",
+ "summary": "Firmware response: Sangaboard Firmware v1.0.4",
+ "message": "Firmware response: Sangaboard Firmware v1.0.4",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 27
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,959",
+ "level": "INFO",
+ "summary": "Running firmware checks...",
+ "message": "Running firmware checks...",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 26
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,959",
+ "level": "INFO",
+ "summary": "Checking valid firmware...",
+ "message": "Checking valid firmware...",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 25
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,959",
+ "level": "INFO",
+ "summary": "Opened ESI connection to port None",
+ "message": "Opened ESI connection to port None",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 24
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,958",
+ "level": "INFO",
+ "summary": "Firmware response: Sangaboard Firmware v1.0.4",
+ "message": "Firmware response: Sangaboard Firmware v1.0.4",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 23
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,953",
+ "level": "INFO",
+ "summary": "Running firmware checks...",
+ "message": "Running firmware checks...",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 22
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,953",
+ "level": "INFO",
+ "summary": "Testing communication to SangaBoard",
+ "message": "Testing communication to SangaBoard",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 21
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,953",
+ "level": "INFO",
+ "summary": "Created Serial\u003Cid=0x7f48f6bf70, open=True\u003E(port='/dev/ttyAMA0', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)",
+ "message": "Created Serial\u003Cid=0x7f48f6bf70, open=True\u003E(port='/dev/ttyAMA0', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 20
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,952",
+ "level": "INFO",
+ "summary": "Creating serial.Serial instance...",
+ "message": "Creating serial.Serial instance...",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 19
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,952",
+ "level": "INFO",
+ "summary": "Success!",
+ "message": "Success!",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 18
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,951",
+ "level": "INFO",
+ "summary": "Firmware response: Sangaboard Firmware v1.0.4",
+ "message": "Firmware response: Sangaboard Firmware v1.0.4",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 17
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,946",
+ "level": "INFO",
+ "summary": "Running firmware checks...",
+ "message": "Running firmware checks...",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 16
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,946",
+ "level": "INFO",
+ "summary": "Testing communication to SangaBoard",
+ "message": "Testing communication to SangaBoard",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 15
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,945",
+ "level": "INFO",
+ "summary": "Created Serial\u003Cid=0x7f48fd56f0, open=True\u003E(port='/dev/ttyAMA0', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)",
+ "message": "Created Serial\u003Cid=0x7f48fd56f0, open=True\u003E(port='/dev/ttyAMA0', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 14
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,945",
+ "level": "INFO",
+ "summary": "Creating serial.Serial instance...",
+ "message": "Creating serial.Serial instance...",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 13
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,939",
+ "level": "INFO",
+ "summary": "Opening ESI connection to port None",
+ "message": "Opening ESI connection to port None",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 12
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,939",
+ "level": "INFO",
+ "summary": "Updating ESI port settings",
+ "message": "Updating ESI port settings",
+ "invocation_id": null,
+ "logger": "sangaboard.extensible_serial_instrument",
+ "sequence": 11
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,939",
+ "level": "INFO",
+ "summary": "Initialising ExtensibleSerialInstrument on port None",
+ "message": "Initialising ExtensibleSerialInstrument on port None",
+ "invocation_id": null,
+ "logger": "sangaboard.sangaboard",
+ "sequence": 10
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,920",
+ "level": "INFO",
+ "summary": "Camera started",
+ "message": "Camera started",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 9
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,884",
+ "level": "INFO",
+ "summary": "Starting picamera MJPEG stream...",
+ "message": "Starting picamera MJPEG stream...",
+ "invocation_id": null,
+ "logger": "openflexure_microscope_server.things.camera.picamera",
+ "sequence": 8
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,853",
+ "level": "INFO",
+ "summary": "Configuration successful!",
+ "message": "Configuration successful!",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 7
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,850",
+ "level": "INFO",
+ "summary": "Camera configuration has been adjusted!",
+ "message": "Camera configuration has been adjusted!",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 6
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,844",
+ "level": "INFO",
+ "summary": "Camera now open.",
+ "message": "Camera now open.",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 5
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,843",
+ "level": "INFO",
+ "summary": "Initialization successful.",
+ "message": "Initialization successful.",
+ "invocation_id": null,
+ "logger": "picamera2.picamera2",
+ "sequence": 4
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,804",
+ "level": "INFO",
+ "summary": "Creating new Picamera2 object",
+ "message": "Creating new Picamera2 object",
+ "invocation_id": null,
+ "logger": "openflexure_microscope_server.things.camera.picamera",
+ "sequence": 3
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,785",
+ "level": "INFO",
+ "summary": "Waiting for application startup.",
+ "message": "Waiting for application startup.",
+ "invocation_id": null,
+ "logger": "uvicorn",
+ "sequence": 2
+ },
+ {
+ "timestamp": "2026-07-08 14:35:18,784",
+ "level": "INFO",
+ "summary": "Started server process [5839]",
+ "message": "Started server process [5839]",
+ "invocation_id": null,
+ "logger": "uvicorn",
+ "sequence": 1
+ },
+ {
+ "timestamp": "2026-07-08 14:35:17,570",
+ "level": "INFO",
+ "summary": "OFM server root logger has been set up at INFO level",
+ "message": "OFM server root logger has been set up at INFO level",
+ "invocation_id": null,
+ "logger": "openflexure_microscope_server.logging",
+ "sequence": 0
+ }
+]
\ No newline at end of file
diff --git a/webapp/src/tests/fixtures/realLog.txt b/webapp/src/tests/fixtures/realLog.txt
deleted file mode 100644
index 4e6d447d..00000000
--- a/webapp/src/tests/fixtures/realLog.txt
+++ /dev/null
@@ -1,5618 +0,0 @@
-[2026-04-23 18:43:21,236] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 18:43:21,280] [INFO] Camera started
-[2026-04-23 18:46:40,604] [INFO] Camera stopped
-[2026-04-23 18:46:41,028] [INFO] Stopped MJPEG stream.
-[2026-04-23 18:46:41,233] [INFO] Camera configuration has been adjusted!
-[2026-04-23 18:46:41,244] [INFO] Configuration successful!
-[2026-04-23 18:46:41,295] [INFO] Camera started
-[2026-04-23 18:46:42,257] [INFO] Camera stopped
-[2026-04-23 18:46:42,263] [INFO] Camera configuration has been adjusted!
-[2026-04-23 18:46:42,274] [INFO] Configuration successful!
-[2026-04-23 18:46:42,312] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 18:46:42,355] [INFO] Camera started
-[2026-04-23 18:47:39,154] [INFO] Camera stopped
-[2026-04-23 18:47:39,579] [INFO] Stopped MJPEG stream.
-[2026-04-23 18:47:39,786] [INFO] Camera configuration has been adjusted!
-[2026-04-23 18:47:39,797] [INFO] Configuration successful!
-[2026-04-23 18:47:39,847] [INFO] Camera started
-[2026-04-23 18:47:40,808] [INFO] Camera stopped
-[2026-04-23 18:47:40,813] [INFO] Camera configuration has been adjusted!
-[2026-04-23 18:47:40,824] [INFO] Configuration successful!
-[2026-04-23 18:47:40,863] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 18:47:40,904] [INFO] Camera started
-[2026-04-23 19:41:03,249] [INFO] Camera stopped
-[2026-04-23 19:41:03,627] [INFO] Stopped MJPEG stream.
-[2026-04-23 19:41:03,832] [INFO] Camera configuration has been adjusted!
-[2026-04-23 19:41:03,842] [INFO] Configuration successful!
-[2026-04-23 19:41:03,891] [INFO] Camera started
-[2026-04-23 19:41:04,850] [INFO] Camera stopped
-[2026-04-23 19:41:04,855] [INFO] Camera configuration has been adjusted!
-[2026-04-23 19:41:04,865] [INFO] Configuration successful!
-[2026-04-23 19:41:04,900] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 19:41:04,939] [INFO] Camera started
-[2026-04-23 19:43:16,398] [INFO] Camera stopped
-[2026-04-23 19:43:16,834] [INFO] Stopped MJPEG stream.
-[2026-04-23 19:43:17,044] [INFO] Camera configuration has been adjusted!
-[2026-04-23 19:43:17,061] [INFO] Configuration successful!
-[2026-04-23 19:43:17,136] [INFO] Camera started
-[2026-04-23 19:43:18,178] [INFO] Camera stopped
-[2026-04-23 19:43:18,183] [INFO] Camera configuration has been adjusted!
-[2026-04-23 19:43:18,193] [INFO] Configuration successful!
-[2026-04-23 19:43:18,227] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 19:43:18,267] [INFO] Camera started
-[2026-04-23 20:03:03,144] [INFO] Camera stopped
-[2026-04-23 20:03:03,573] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:03:03,590] [INFO] Configuration successful!
-[2026-04-23 20:03:03,898] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:03:03,961] [INFO] Camera started
-[2026-04-23 20:03:15,581] [INFO] Moving to (12505, 4175)
-[2026-04-23 20:03:26,399] [INFO] Moving to (16959, 4175)
-[2026-04-23 20:03:56,129] [INFO] Moving to (21413, 4175)
-[2026-04-23 20:04:20,523] [INFO] Moving to (12505, 837)
-[2026-04-23 20:04:43,210] [INFO] Moving to (16959, 837)
-[2026-04-23 20:05:17,604] [INFO] Moving to (21413, 837)
-[2026-04-23 20:05:44,490] [INFO] Camera stopped
-[2026-04-23 20:05:45,496] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:05:45,570] [INFO] Configuration successful!
-[2026-04-23 20:05:45,687] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:05:45,824] [INFO] Camera started
-[2026-04-23 20:05:45,857] [INFO] Returning to starting position.
-[2026-04-23 20:05:54,796] [INFO] Waiting for background processes to finish...
-[2026-04-23 20:10:02,630] [INFO] Camera stopped
-[2026-04-23 20:10:03,105] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:03,789] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:03,790] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:04,275] [INFO] Camera closed successfully.
-[2026-04-23 20:10:04,277] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:10,801] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:12,464] [INFO] Initialization successful.
-[2026-04-23 20:10:12,471] [INFO] Camera now open.
-[2026-04-23 20:10:12,496] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:12,502] [INFO] Configuration successful!
-[2026-04-23 20:10:12,554] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:12,606] [INFO] Camera started
-[2026-04-23 20:10:12,676] [INFO] Camera stopped
-[2026-04-23 20:10:13,042] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:13,264] [INFO] Configuration successful!
-[2026-04-23 20:10:13,355] [INFO] Camera started
-[2026-04-23 20:10:15,436] [INFO] {'level': 132, 'exposure_time': 75, 'analog_gain': 1.0}
-[2026-04-23 20:10:16,446] [INFO] {'level': 254, 'exposure_time': 226, 'analog_gain': 1.0}
-[2026-04-23 20:10:17,435] [INFO] {'level': 347, 'exposure_time': 340, 'analog_gain': 1.0}
-[2026-04-23 20:10:18,339] [INFO] {'level': 377, 'exposure_time': 378, 'analog_gain': 1.0}
-[2026-04-23 20:10:19,234] [INFO] {'level': 393, 'exposure_time': 396, 'analog_gain': 1.0}
-[2026-04-23 20:10:19,598] [INFO] {'level': 392, 'exposure_time': 396, 'analog_gain': 1.0}
-[2026-04-23 20:10:19,599] [INFO] Brightness has converged to within 5%.
-[2026-04-23 20:10:20,125] [INFO] Camera stopped
-[2026-04-23 20:10:20,131] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:20,141] [INFO] Configuration successful!
-[2026-04-23 20:10:20,177] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:20,216] [INFO] Camera started
-[2026-04-23 20:10:20,272] [INFO] Camera stopped
-[2026-04-23 20:10:20,645] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:20,871] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:20,871] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:21,087] [INFO] Camera closed successfully.
-[2026-04-23 20:10:21,088] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:21,436] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:21,501] [INFO] Initialization successful.
-[2026-04-23 20:10:21,502] [INFO] Camera now open.
-[2026-04-23 20:10:21,514] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:21,519] [INFO] Configuration successful!
-[2026-04-23 20:10:21,557] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:21,598] [INFO] Camera started
-[2026-04-23 20:10:21,659] [INFO] Camera stopped
-[2026-04-23 20:10:22,387] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:22,647] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:22,648] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:22,883] [INFO] Camera closed successfully.
-[2026-04-23 20:10:22,885] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:23,380] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:23,518] [INFO] Initialization successful.
-[2026-04-23 20:10:23,520] [INFO] Camera now open.
-[2026-04-23 20:10:23,544] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:23,553] [INFO] Configuration successful!
-[2026-04-23 20:10:23,603] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:23,665] [INFO] Camera started
-[2026-04-23 20:10:23,743] [INFO] Camera stopped
-[2026-04-23 20:10:24,449] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:24,661] [INFO] Configuration successful!
-[2026-04-23 20:10:24,712] [INFO] Camera started
-[2026-04-23 20:10:24,938] [INFO] Camera stopped
-[2026-04-23 20:10:25,272] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:25,273] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:25,551] [INFO] Camera closed successfully.
-[2026-04-23 20:10:25,552] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:25,880] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:25,945] [INFO] Initialization successful.
-[2026-04-23 20:10:25,946] [INFO] Camera now open.
-[2026-04-23 20:10:25,958] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:25,965] [INFO] Configuration successful!
-[2026-04-23 20:10:25,995] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:26,035] [INFO] Camera started
-[2026-04-23 20:10:47,216] [INFO] Camera stopped
-[2026-04-23 20:10:47,648] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:47,875] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:47,875] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:48,223] [INFO] Camera closed successfully.
-[2026-04-23 20:10:48,223] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:48,555] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:48,623] [INFO] Initialization successful.
-[2026-04-23 20:10:48,624] [INFO] Camera now open.
-[2026-04-23 20:10:48,636] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:48,641] [INFO] Configuration successful!
-[2026-04-23 20:10:48,671] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:48,712] [INFO] Camera started
-[2026-04-23 20:10:48,769] [INFO] Camera stopped
-[2026-04-23 20:10:49,145] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:49,366] [INFO] Configuration successful!
-[2026-04-23 20:10:49,447] [INFO] Camera started
-[2026-04-23 20:10:50,876] [INFO] {'level': 131, 'exposure_time': 75, 'analog_gain': 1.0}
-[2026-04-23 20:10:51,856] [INFO] {'level': 254, 'exposure_time': 226, 'analog_gain': 1.0}
-[2026-04-23 20:10:53,007] [INFO] {'level': 346, 'exposure_time': 340, 'analog_gain': 1.0}
-[2026-04-23 20:10:54,225] [INFO] {'level': 375, 'exposure_time': 378, 'analog_gain': 1.0}
-[2026-04-23 20:10:55,179] [INFO] {'level': 393, 'exposure_time': 396, 'analog_gain': 1.0}
-[2026-04-23 20:10:55,529] [INFO] {'level': 389, 'exposure_time': 396, 'analog_gain': 1.0}
-[2026-04-23 20:10:55,530] [INFO] Brightness has converged to within 5%.
-[2026-04-23 20:10:56,061] [INFO] Camera stopped
-[2026-04-23 20:10:56,067] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:56,077] [INFO] Configuration successful!
-[2026-04-23 20:10:56,113] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:56,154] [INFO] Camera started
-[2026-04-23 20:10:56,211] [INFO] Camera stopped
-[2026-04-23 20:10:56,584] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:56,837] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:56,847] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:57,235] [INFO] Camera closed successfully.
-[2026-04-23 20:10:57,237] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:57,751] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:57,896] [INFO] Initialization successful.
-[2026-04-23 20:10:57,898] [INFO] Camera now open.
-[2026-04-23 20:10:57,923] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:57,930] [INFO] Configuration successful!
-[2026-04-23 20:10:57,981] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:58,046] [INFO] Camera started
-[2026-04-23 20:10:58,126] [INFO] Camera stopped
-[2026-04-23 20:10:58,493] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:10:58,741] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:10:58,743] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:10:59,156] [INFO] Camera closed successfully.
-[2026-04-23 20:10:59,157] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:10:59,653] [INFO] Creating new Picamera2 object
-[2026-04-23 20:10:59,791] [INFO] Initialization successful.
-[2026-04-23 20:10:59,793] [INFO] Camera now open.
-[2026-04-23 20:10:59,817] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:10:59,825] [INFO] Configuration successful!
-[2026-04-23 20:10:59,875] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:10:59,938] [INFO] Camera started
-[2026-04-23 20:11:00,017] [INFO] Camera stopped
-[2026-04-23 20:11:00,719] [INFO] Stopped MJPEG stream.
-[2026-04-23 20:11:00,931] [INFO] Configuration successful!
-[2026-04-23 20:11:00,982] [INFO] Camera started
-[2026-04-23 20:11:01,233] [INFO] Camera stopped
-[2026-04-23 20:11:01,552] [INFO] Closing picamera object for reinitialisation
-[2026-04-23 20:11:01,552] [INFO] Camera object already exists, closing for reinitialisation
-[2026-04-23 20:11:01,846] [INFO] Camera closed successfully.
-[2026-04-23 20:11:01,847] [INFO] Picamera closed, deleting picamera
-[2026-04-23 20:11:02,173] [INFO] Creating new Picamera2 object
-[2026-04-23 20:11:02,238] [INFO] Initialization successful.
-[2026-04-23 20:11:02,239] [INFO] Camera now open.
-[2026-04-23 20:11:02,252] [INFO] Camera configuration has been adjusted!
-[2026-04-23 20:11:02,257] [INFO] Configuration successful!
-[2026-04-23 20:11:02,287] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 20:11:02,332] [INFO] Camera started
-[2026-04-23 20:14:15,849] [INFO] Calibrating Y axis:
-[2026-04-23 20:14:17,729] [INFO] Moving the stage until we see motion...
-[2026-04-23 20:14:32,772] [INFO] Moving the stage to the edge of the field of view...
-[2026-04-23 20:14:35,598] [INFO] Moving the stage backwards to measure backlash (1/2)
-[2026-04-23 20:14:50,104] [INFO] Moving the stage forwards to measure backlash (2/2)
-[2026-04-23 20:15:08,453] [INFO] Moving back to the start, correcting for backlash...
-[2026-04-23 20:15:19,980] [INFO] Estimated backlash 95 steps
-[2026-04-23 20:15:19,983] [INFO] Stage-to-image ratio 0.050 pixels/step
-[2026-04-23 20:15:19,984] [INFO] Residuals were about 0.03 times the step size
-[2026-04-23 20:15:20,288] [INFO] Calibrating X axis:
-[2026-04-23 20:15:21,668] [INFO] Moving the stage until we see motion...
-[2026-04-23 20:15:35,860] [INFO] Moving the stage to the edge of the field of view...
-[2026-04-23 20:15:41,098] [INFO] Moving the stage backwards to measure backlash (1/2)
-[2026-04-23 20:15:58,893] [INFO] Moving the stage forwards to measure backlash (2/2)
-[2026-04-23 20:16:17,078] [INFO] Moving back to the start, correcting for backlash...
-[2026-04-23 20:16:25,507] [INFO] Estimated backlash 40 steps
-[2026-04-23 20:16:25,510] [INFO] Stage-to-image ratio 0.050 pixels/step
-[2026-04-23 20:16:25,511] [INFO] Residuals were about 0.01 times the step size
-[2026-04-23 20:16:25,814] [INFO] Calibration complete, updating metadata.
-[2026-04-23 20:16:25,816] [INFO] CSM matrix is [0.07, -9.93,],[10.05, 0.02].
-[2026-04-23 23:42:26,623] [INFO] OFM server root logger has been set up at INFO level
-[2026-04-23 23:42:27,492] [WARNING] An extra key stack_dz was found in the settings file. It will be deleted the next time settings are saved.
-[2026-04-23 23:42:27,494] [WARNING] An extra key stack_images_to_save was found in the settings file. It will be deleted the next time settings are saved.
-[2026-04-23 23:42:27,494] [WARNING] An extra key stack_min_images_to_test was found in the settings file. It will be deleted the next time settings are saved.
-[2026-04-23 23:42:28,987] [INFO] Started server process [514]
-[2026-04-23 23:42:28,988] [INFO] Waiting for application startup.
-[2026-04-23 23:42:29,031] [INFO] Creating new Picamera2 object
-[2026-04-23 23:42:29,172] [INFO] Initialization successful.
-[2026-04-23 23:42:29,173] [INFO] Camera now open.
-[2026-04-23 23:42:29,187] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,192] [INFO] Configuration successful!
-[2026-04-23 23:42:29,216] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,219] [INFO] Configuration successful!
-[2026-04-23 23:42:29,244] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,247] [INFO] Configuration successful!
-[2026-04-23 23:42:29,272] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,275] [INFO] Configuration successful!
-[2026-04-23 23:42:29,319] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,322] [INFO] Configuration successful!
-[2026-04-23 23:42:29,345] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,348] [INFO] Configuration successful!
-[2026-04-23 23:42:29,371] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,374] [INFO] Configuration successful!
-[2026-04-23 23:42:29,397] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,401] [INFO] Configuration successful!
-[2026-04-23 23:42:29,440] [INFO] Camera configuration has been adjusted!
-[2026-04-23 23:42:29,444] [INFO] Configuration successful!
-[2026-04-23 23:42:29,480] [INFO] Starting picamera MJPEG stream...
-[2026-04-23 23:42:29,531] [INFO] Camera started
-[2026-04-23 23:42:29,564] [INFO] Initialising ExtensibleSerialInstrument on port None
-[2026-04-23 23:42:29,565] [INFO] Updating ESI port settings
-[2026-04-23 23:42:29,566] [INFO] Opening ESI connection to port None
-[2026-04-23 23:42:29,582] [INFO]