[
  {
    "path": ".dockerignore",
    "content": "**/__pycache__\n**/.env\n**/.git\n**/.venv\n**/.vscode\nconfig\n**/docs\n**/instance\n**/logs\n**/backups\nraspiCamSrv/static/config\nraspiCamSrv/static/events\nraspiCamSrv/static/photos\nraspiCamSrv/static/photoseries\nraspiCamSrv/static/tuning\n**/tests\n**/.dockerignore\n**/.gitignore\n**/docker-compose*\n**/Dockerfile*\nLICENSE\nREADME.md\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\ntests/\nuser_code/\nlogs/\noutput/\nbackups\nraspiCamSrv/static/calib_data/\nraspiCamSrv/static/calib_photos/\nraspiCamSrv/static/photos/\nraspiCamSrv/static/timelapse/\nraspiCamSrv/static/photoseries/\nraspiCamSrv/static/config/\nraspiCamSrv/static/events/\nraspiCamSrv/static/tuning/\n*.mp4\n*.h264\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Verwendet IntelliSense zum Ermitteln möglicher Attribute.\n    // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.\n    // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Python-Debugger: Aktuelle Datei\",\n            \"type\": \"debugpy\",\n            \"request\": \"launch\",\n            \"program\": \"${file}\",\n            \"console\": \"integratedTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1\nFROM dtcooper/raspberrypi-os:bookworm\n\nLABEL maintainer=\"signag\"\n\nRUN apt update && apt -y upgrade\n\nRUN apt update && apt install -y \\\n    gcc-aarch64-linux-gnu \\\n    systemd \\\n    systemd-timesyncd \\\n    python3 \\\n    python3-dev \\\n    python3-pip \\\n    python3-venv \\\n    python3-opencv \\\n    python3-gpiozero \\\n    python3-lgpio \\\n    ffmpeg \\\n    python3-picamera2 --no-install-recommends \\\n    imx500-all \\\n    dpkg-dev \\\n    v4l-utils\n\n\nRUN ln -s /usr/bin/python3 /usr/bin/python\n\n# Prevents Python from writing pyc files.\nENV PYTHONDONTWRITEBYTECODE=1\n\n# Keeps Python from buffering stdout and stderr to avoid situations where\n# the application crashes without emitting any logs due to buffering.\nENV PYTHONUNBUFFERED=1\n\n# Set environment variables\nENV DEBIAN_FRONTEND=noninteractive\n\nWORKDIR /app\n\n# Copy the source code into the container.\nCOPY . .\n\n# Install Python dependencies in virtual environment\nRUN python -m venv --system-site-packages .venv\nENV PATH=\".venv/bin:$PATH\"\nRUN pip install --no-cache-dir -r requirements.txt\n\n# Expose the port that the application listens on.\nEXPOSE 5000\n\n# Initialize database for Flask\nRUN flask --app raspiCamSrv init-db\n\n# Run the application.\nCMD gunicorn -b 0.0.0.0:5000 -w 1 -k gthread --threads 6 --timeout 0 --log-level info 'raspiCamSrv:create_app()'"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 signag\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# raspiCamSrv V4.10.0\n\n**raspiCamSrv** is a Web server for Raspberry Pi systems providing an App for control and streaming of CSI and USB cameras as well as for controlling a large variety of connected GPIO devices.\n\nWhile all currently connected cameras are accessible by the system, up to two cameras can be operated simultaneously at a time, supporting multi-camera features like Stereo Vision.\n\nInteroperability between Cameras and GPIO devices is achieved through the freely configurable event handling infrastructure.\n\n**raspiCamSrv** supports all Raspberry Pi platforms from Pi Zero to Pi 5, running Bullseye, Bookworm or Trixie OS.\n\nBesides the currently available Raspberry Pi cameras, also compatible CSI cameras from other providers can be used.   \nUSB web cams are seamlessly integrated.\n\n**raspiCamSrv** is built with Flask 3.x and uses the Picamera2 library.\n\nDue to responsive layout from W3.CSS, all modern browsers on PC, Mac or mobile devices can be used as clients.\n\nFor more details, refer to the [raspiCamSrv Documentation](https://signag.github.io/raspi-cam-srv/latest/), especially the [Features List](https://signag.github.io/raspi-cam-srv/latest/features/)    \nor check the [Release Notes](https://signag.github.io/raspi-cam-srv/latest/ReleaseNotes/) for current version and latest updates.\n\n\n![Live Overview](docs/img/Live.jpg)\n\nTo [get started with raspiCamSrv](https://signag.github.io/raspi-cam-srv/latest/getting_started_overview/),\n\n1. [Check necessary requirements](https://signag.github.io/raspi-cam-srv/latest/requirements/)\n2. [Set up your Raspberry Pi](https://signag.github.io/raspi-cam-srv/latest/system_setup/)\n3. [Install raspiCamSrv](https://signag.github.io/raspi-cam-srv/latest/installation/)\n4. Refer to the raspiCamSrv [User Guide](https://signag.github.io/raspi-cam-srv/latest/UserGuide/)"
  },
  {
    "path": "config/raspiCamSrv.service",
    "content": "[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=/home/<user>/prg/raspi-cam-srv/.venv/bin/python -m flask --app raspiCamSrv run --port 5000 --host=0.0.0.0\nEnvironment=\"PATH=/home/<user>/prg/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nWorkingDirectory=/home/<user>/prg/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\nUser=<user>\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "config/raspiCamSrv_gunicorn.service",
    "content": "[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=/home/<user>/prg/raspi-cam-srv/.venv/bin/gunicorn -b 0.0.0.0:5000 -w 1 -k gthread --threads 6 --timeout 0 --log-level info 'raspiCamSrv:create_app()'\nEnvironment=\"PATH=/home/<user>/prg/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nEnvironment=\"GUNICORN_THREADS=6\"\nWorkingDirectory=/home/<user>/prg/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\nUser=<user>\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "docker-compose.yml",
    "content": "name: raspi-cam-srv\nservices:\n  raspi-cam-srv:\n    container_name: raspi-cam-srv\n    build: .\n    image: signag/raspi-cam-srv\n    network_mode: \"host\"\n    ports:\n      - \"5000:5000\"\n    devices:\n      - /dev/video0:/dev/video0\n      - /dev/gpiochip0:/dev/gpiochip0\n    volumes:\n      - /dev:/dev\n      - /sys:/sys    \n      - /run/udev/:/run/udev:ro\n      - /run/systemd:/run/systemd:ro\n      - /etc/timezone:/etc/timezone:ro\n      - /etc/localtime:/etc/localtime:ro\n    environment:\n      - GPIOZERO_PIN_FACTORY=lgpio\n      - SYSTEMD_BUS_ADDRESS=unix:path=/run/systemd/private\n    restart: unless-stopped\n    privileged: true\n"
  },
  {
    "path": "docs/API.md",
    "content": "# raspiCamSrv API\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe **raspiCamSrv** API allows access to several RaspberryPi camera functions through WebService endpoints.\n\nConfiguration of the API is done on the [Settings/API](./SettingsAPI.md) screen.\n\n## Postman Test Collection\n\nFor testing the API, a [Postman](https://www.postman.com/) collection is available at [docs/api/postman](https://github.com/signag/raspi-cam-srv/tree/main/docs/api/postman) which can be downloaded and imported into a Postman instance.\n\n![PostmanColl](./img/API_Postman_collection.jpg)\n\n## API Documentation\n\nThe [docs/api/postman](https://github.com/signag/raspi-cam-srv/tree/main/docs/api/postman) folder contains also the [API Documentation](./api/postman/raspiCamSrv.postman_collection.pdf) generated from Postman.\n\n## Variables\n\nThe collection uses a set of variables:\n\n![PostmanVars](./img/API_Postman_variables.jpg)\n\n```base_url```, ```user``` and ```pwd``` need to be adjusted to the current environment for a user which has been previously created in raspiCamSrv.\n\n```access_token``` and ```refresh_token``` will be automatically filled from responses of the /api/login and /api/refresh endpoints.\n\n## Usage\n\n### 1. Login\n\nUse the ```api login``` request to log in to **raspiCamSrv** and receive an Access Token and a Refresh Token\n\n### 2. Interact with **raspiCamSrv**\n\nUse any of the GET requests to interact with **raspiCamSrv**.\n\nThese requests use the Access Token for authentication.\n\n### 3. Refresh the Access Token\n\nIf a request returns a token expiration error, refresh the Access Token using the ```api refresh``` request.\n\nThis will use the Refresh Token for authentication and return a fresh Access Token.\n\n\n"
  },
  {
    "path": "docs/AiCameraSupport.md",
    "content": "# raspiCamSrv AI Camera Support\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) has an integrated AI accelerator which can process Input Tensor data delivered by the camera ISP directly within the camera system.   \nOutput Tensor data with inference information are delivered to the Raspberry Pi host system in the context of image meta data.\n\nTherefore, the main computational effort for the neural network is on the camera system while the host system is left with analysis and visualization of inference from the meta data.\n\nThis makes it possible to see Neural Networks at work even on low cost system such as Raspberry Pi Zero 2 (See [Raspberry Models lower 5 and Raspberry Pi Zero](#raspberry-models-4-and-lower-and-raspberry-pi-zero)).\n\n**raspiCamSrv** from V4.6 supports AI features for the imx500 on the basis of published [neural network models](https://github.com/raspberrypi/imx500-models) which are deployed together with the imx500-all package and for which Picamera2 provides [demo implemetations](https://github.com/raspberrypi/picamera2/tree/main/examples/imx500).\n\nThese implementations have been integrated in **raspiCamSrv** with minor modifications for visualization on different camera streams.\n\n**NOTE**: Currently, Visualization of inference data always scales to the [Stream Size](./Configuration.md#stream-size-width-height) and does not consider cropping. Therefore you should avoid [zoom and pan](./ZoomPan.md).\n\n## Installation\n\nAll required packages are installed with the [automatic installer](./installation.md#installer) if you confirm to use the AI Camera.\n\n## Update from previous raspiCamSrv Versions\n\nIf you update from a previous **raspiCamSrv** version (V4.5 and earlier), you can just run the [automatic installer](./installation.md) again.   \nIt will recognize components which are already installed and only install new components or update existing ones to the latest version.\n\n## Activation\n\nIf an imx500 camera is connected to one of the CSI ports, this will be recognized by the raspiCamSrv Flask server at startup and is shown in the [Info/Cameras](./Information_Cam.md#ai-features) dialog.\n\nBy default, the imx500 camera is handled like a normal CSI camera.\n\nTo activate Camera AI support, you need to check *Use Camera AI* in the [Settings](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features) dialog.\n\n## Neural Network Configuration\n\nIf *Use Camera AI* is activated, the [Config](./Configuration.md) menu will show an additional submenu [AI Configuration](./Configuration_AI.md)\n\nHere you can choose a Task to be executed by the model and, subsequently, one of the models which implements the chosen task.\n\nIn addition, you can choose a set of parameters which allow restricting detections by specific threshold values or by number.\n\nBy default, inference results are visualized on the *lores* camera stream which is usually used for the Live View.\n\nIf you need this visualization also on photos and/or videos, you need to activate visualization on the *main* stream.\n\n**NOTE** For Fotos and Videos, the [Stream Size](./Configuration.md#stream-size-width-height) for the *main* stream should be set to values similar to those of the *lores* stream.    \nThe reson is that currently text sizes and line width of the visualization do not scale with the stream size.\n\n## Enabling a Neural Network\n\nOnce the configuration is done, you can enable the selected network model in the [AI Configuration](./Configuration_AI.md#enable-ai) dialog.\n\nNow, when the camera is started, the network model will be loaded onto the camera system.\n\nThis may take a while, depending on the platform used.\n\nIn order to avoid irritations about long camera startup times, **raspiCamSrv** shows an [animation](./UserGuide.md#live-stream-at-camera-start) when the camera is started until the first frame is delevered by the camera.\n\n## Recommendations\n\nThe current implementation of imx500 AI support is initial and not all combinations of network models and other configuration settings have been tested.\n\nThe following recommendations may serve as a starting point for gaining experience with this subject.\n\n### Configuration Settings\n\nIt is recommended setting the [Stream Size](./Configuration.md#stream-size-width-height) for *Photo* and *Video* to the same low value as for the *Live View* (e.g. **640 x 480**).    \nThis assures better representation of inference results on photos and videos, if these are activated.\n\n[Buffer Count](./Configuration.md#buffer-count) should be set to **12**, which is also used by the Picamera2 [demo implemetations](https://github.com/raspberrypi/picamera2/tree/main/examples/imx500)\n\n### Neural Network Models\n\nThe following models have been successfully tested within **raspiCamSrv**\n\n| Task             | Model                                                 |\n|------------------|-------------------------------------------------------|\n| Classification   | imx500_network_mobilenet_v2.rpk                       |\n| Object Detection | imx500_network_ssd_mobilenetv2_fpnlite_320x320_pp.rpk |\n| Pose Estimation  | imx500_network_higherhrnet_coco.rpk                   |\n| Segmentation     | imx500_network_deeplabv3plus.rpk\n\n### Parameter Settings\n\nFor performance reasons, you should enable *Draw Results on Stream main* only if needed for photos and videos.\n\nIf you do not see any results, you may need to:\n\n- Decrease *Detection Threshold*\n\nIf performance is low, for example on a Raspberry Pi Zero, you may need to:\n\n- Increase *Detection Threshold* in order to reduce the number of detections to be handled.\n\n\n\n### Raspberry Models 4 and lower and Raspberry Pi Zero\n\nFor these models, the *lores* stream needs to be in YUV format according to te [Picamera2 Manual](https://pip-assets.raspberrypi.com/categories/652-raspberry-pi-camera-module-2/documents/RP-008156-DS-2-picamera2-manual.pdf) ch. 4.2.\n\nThe YUV format is not supported by all AI postprocessing pipelines.\n\nIt is, therefore, recommended using the following [Configuration](./Configuration.md) settingss for **all** use cases except *Raw Photo*, :\n\n- Buffer Count: 12\n- Sensor Mode: Custom\n- Stream: main\n- Stream Size:  640 x 480\n- Stream Format: XBGR8888\n\nFor [AI Configuration](./Configuration_AI.md):\n\n- Disable: *Draw Results on Stream lores*\n- Enable: *Draw Results on Stream main*\n\n### Case for Raspberry Pi Zero 2\n\nThe official case for the Raspberry Pi Zero models is not suitable for the AI Camera.\n\nAs alternatives, there are 3D-printed cases, e.g. [Case for Raspberry Pi Zero Models with Camera](https://makerworld.com/en/models/1804527-case-for-raspberry-pi-zero-models-with-camera):\n\n![Pi Zero Cover](./img/PI_zero_cover_3dp_ai.jpg)\n\n## System Journal at Camera Startup\n\nBefore the camera is actually started, Picamera2 issues a message to stdout with a hint about long starting times:\n\n![imx500 Start](./img/Config_AI_log.jpg)\n\nDepending on whether a model is initially loaded or whether a previously loaded model is replaced, there will be kernel messages accompanying the loading process:\n\n![imx500 Log](./img/Config_AI_log2.jpg)\n\n\n## Live Stream\n\nDepending on the task of the neural network model, different types of visualizations will be used.\n\nResulting information is drawn on the individual frames in a pre_callback which is executed by Picamera2 before the frames are supplied to applications.\n\n### Classification\n\n![Classification](./img/imx500_object_classification.jpg)\n\nWith the *Classification* task, the neural network will identify individual classes out of a set of about 1000 classes (see [below](#classes))\n\nWhen photos are taken, the Metadata include on the input and output tensors used by the neural network.\n\n\n### Object Detection\n\n![Object Detection](./img/imx500_object_detection.jpg)\n\nWith the *Object Detection* task, the neural network will track and frame individual objects out of a set of about 1000 classes (see [below](#classes))\n\n\n### Segmentation\n\n![Segmentation](./img/imx500_segmentation.jpg)\n\nWith the *Segmentation* task, the neural network will track and mask individual objects.\n\nThe implementation in **raspiCamSrv** differs from the [demo implementation](https://github.com/raspberrypi/picamera2/blob/main/examples/imx500/imx500_segmentation_demo.py), in which masks are burned on the GPU preview only.    \nThese overlays are, therefore, not available in the *lores* and *main* streams used by **raspiCamSrv** for Live view and Photo/Video.\n\nSince the inference rate of 19 of the neural network is significantly lower than the framerate (30), a larger part of frames is not processed by the neural network.\nTo avoid flickering, overlays are cached and the last overlay is used for frames which have bypassed AI processing.\n\nAs a consequence, when photos are taken, there will be photos for which the *Metadata* do not show tensor information, although a mask is visible on the photo.\n\n## AI Camera as Second Camera\n\nYou can also use an AI camera as Second Camera or, on a Pi 5, you can work with two AI cameras.\n\n[AI configuration](./Configuration_AI.md) is treated like any other [Camera Configuration](./Configuration.md).\n\nThis means that, in order to preserve any configuration changes for a camera switch, you need to apply the [Save Active Camera Settings for Camera Switch](./CamMulticam.md#save-active-camera-settings-for-camera-switch) button before you replace the active camera with another camera or before you switch cameras.\n\n\n## Classes\n\nThe following classes are used in tasks *Classification* and *Object Detection*:\n\n```\n\n        \"classes\": {\n            \"labels\": [\n                \"0:background\",\n                \"1:tench, Tinca tinca\",\n                \"2:goldfish, Carassius auratus\",\n                \"3:great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias\",\n                \"4:tiger shark, Galeocerdo cuvieri\",\n                \"5:hammerhead, hammerhead shark\",\n                \"6:electric ray, crampfish, numbfish, torpedo\",\n                \"7:stingray\",\n                \"8:cock\",\n                \"9:hen\",\n                \"10:ostrich, Struthio camelus\",\n                \"11:brambling, Fringilla montifringilla\",\n                \"12:goldfinch, Carduelis carduelis\",\n                \"13:house finch, linnet, Carpodacus mexicanus\",\n                \"14:junco, snowbird\",\n                \"15:indigo bunting, indigo finch, indigo bird, Passerina cyanea\",\n                \"16:robin, American robin, Turdus migratorius\",\n                \"17:bulbul\",\n                \"18:jay\",\n                \"19:magpie\",\n                \"20:chickadee\",\n                \"21:water ouzel, dipper\",\n                \"22:kite\",\n                \"23:bald eagle, American eagle, Haliaeetus leucocephalus\",\n                \"24:vulture\",\n                \"25:great grey owl, great gray owl, Strix nebulosa\",\n                \"26:European fire salamander, Salamandra salamandra\",\n                \"27:common newt, Triturus vulgaris\",\n                \"28:eft\",\n                \"29:spotted salamander, Ambystoma maculatum\",\n                \"30:axolotl, mud puppy, Ambystoma mexicanum\",\n                \"31:bullfrog, Rana catesbeiana\",\n                \"32:tree frog, tree-frog\",\n                \"33:tailed frog, bell toad, ribbed toad, tailed toad, Ascaphus trui\",\n                \"34:loggerhead, loggerhead turtle, Caretta caretta\",\n                \"35:leatherback turtle, leatherback, leathery turtle, Dermochelys coriacea\",\n                \"36:mud turtle\",\n                \"37:terrapin\",\n                \"38:box turtle, box tortoise\",\n                \"39:banded gecko\",\n                \"40:common iguana, iguana, Iguana iguana\",\n                \"41:American chameleon, anole, Anolis carolinensis\",\n                \"42:whiptail, whiptail lizard\",\n                \"43:agama\",\n                \"44:frilled lizard, Chlamydosaurus kingi\",\n                \"45:alligator lizard\",\n                \"46:Gila monster, Heloderma suspectum\",\n                \"47:green lizard, Lacerta viridis\",\n                \"48:African chameleon, Chamaeleo chamaeleon\",\n                \"49:Komodo dragon, Komodo lizard, dragon lizard, giant lizard, Varanus komodoensis\",\n                \"50:African crocodile, Nile crocodile, Crocodylus niloticus\",\n                \"51:American alligator, Alligator mississipiensis\",\n                \"52:triceratops\",\n                \"53:thunder snake, worm snake, Carphophis amoenus\",\n                \"54:ringneck snake, ring-necked snake, ring snake\",\n                \"55:hognose snake, puff adder, sand viper\",\n                \"56:green snake, grass snake\",\n                \"57:king snake, kingsnake\",\n                \"58:garter snake, grass snake\",\n                \"59:water snake\",\n                \"60:vine snake\",\n                \"61:night snake, Hypsiglena torquata\",\n                \"62:boa constrictor, Constrictor constrictor\",\n                \"63:rock python, rock snake, Python sebae\",\n                \"64:Indian cobra, Naja naja\",\n                \"65:green mamba\",\n                \"66:sea snake\",\n                \"67:horned viper, cerastes, sand viper, horned asp, Cerastes cornutus\",\n                \"68:diamondback, diamondback rattlesnake, Crotalus adamanteus\",\n                \"69:sidewinder, horned rattlesnake, Crotalus cerastes\",\n                \"70:trilobite\",\n                \"71:harvestman, daddy longlegs, Phalangium opilio\",\n                \"72:scorpion\",\n                \"73:black and gold garden spider, Argiope aurantia\",\n                \"74:barn spider, Araneus cavaticus\",\n                \"75:garden spider, Aranea diademata\",\n                \"76:black widow, Latrodectus mactans\",\n                \"77:tarantula\",\n                \"78:wolf spider, hunting spider\",\n                \"79:tick\",\n                \"80:centipede\",\n                \"81:black grouse\",\n                \"82:ptarmigan\",\n                \"83:ruffed grouse, partridge, Bonasa umbellus\",\n                \"84:prairie chicken, prairie grouse, prairie fowl\",\n                \"85:peacock\",\n                \"86:quail\",\n                \"87:partridge\",\n                \"88:African grey, African gray, Psittacus erithacus\",\n                \"89:macaw\",\n                \"90:sulphur-crested cockatoo, Kakatoe galerita, Cacatua galerita\",\n                \"91:lorikeet\",\n                \"92:coucal\",\n                \"93:bee eater\",\n                \"94:hornbill\",\n                \"95:hummingbird\",\n                \"96:jacamar\",\n                \"97:toucan\",\n                \"98:drake\",\n                \"99:red-breasted merganser, Mergus serrator\",\n                \"100:goose\",\n                \"101:black swan, Cygnus atratus\",\n                \"102:tusker\",\n                \"103:echidna, spiny anteater, anteater\",\n                \"104:platypus, duckbill, duckbilled platypus, duck-billed platypus, Ornithorhynchus anatinus\",\n                \"105:wallaby, brush kangaroo\",\n                \"106:koala, koala bear, kangaroo bear, native bear, Phascolarctos cinereus\",\n                \"107:wombat\",\n                \"108:jellyfish\",\n                \"109:sea anemone, anemone\",\n                \"110:brain coral\",\n                \"111:flatworm, platyhelminth\",\n                \"112:nematode, nematode worm, roundworm\",\n                \"113:conch\",\n                \"114:snail\",\n                \"115:slug\",\n                \"116:sea slug, nudibranch\",\n                \"117:chiton, coat-of-mail shell, sea cradle, polyplacophore\",\n                \"118:chambered nautilus, pearly nautilus, nautilus\",\n                \"119:Dungeness crab, Cancer magister\",\n                \"120:rock crab, Cancer irroratus\",\n                \"121:fiddler crab\",\n                \"122:king crab, Alaska crab, Alaskan king crab, Alaska king crab, Paralithodes camtschatica\",\n                \"123:American lobster, Northern lobster, Maine lobster, Homarus americanus\",\n                \"124:spiny lobster, langouste, rock lobster, crawfish, crayfish, sea crawfish\",\n                \"125:crayfish, crawfish, crawdad, crawdaddy\",\n                \"126:hermit crab\",\n                \"127:isopod\",\n                \"128:white stork, Ciconia ciconia\",\n                \"129:black stork, Ciconia nigra\",\n                \"130:spoonbill\",\n                \"131:flamingo\",\n                \"132:little blue heron, Egretta caerulea\",\n                \"133:American egret, great white heron, Egretta albus\",\n                \"134:bittern\",\n                \"135:crane\",\n                \"136:limpkin, Aramus pictus\",\n                \"137:European gallinule, Porphyrio porphyrio\",\n                \"138:American coot, marsh hen, mud hen, water hen, Fulica americana\",\n                \"139:bustard\",\n                \"140:ruddy turnstone, Arenaria interpres\",\n                \"141:red-backed sandpiper, dunlin, Erolia alpina\",\n                \"142:redshank, Tringa totanus\",\n                \"143:dowitcher\",\n                \"144:oystercatcher, oyster catcher\",\n                \"145:pelican\",\n                \"146:king penguin, Aptenodytes patagonica\",\n                \"147:albatross, mollymawk\",\n                \"148:grey whale, gray whale, devilfish, Eschrichtius gibbosus, Eschrichtius robustus\",\n                \"149:killer whale, killer, orca, grampus, sea wolf, Orcinus orca\",\n                \"150:dugong, Dugong dugon\",\n                \"151:sea lion\",\n                \"152:Chihuahua\",\n                \"153:Japanese spaniel\",\n                \"154:Maltese dog, Maltese terrier, Maltese\",\n                \"155:Pekinese, Pekingese, Peke\",\n                \"156:Shih-Tzu\",\n                \"157:Blenheim spaniel\",\n                \"158:papillon\",\n                \"159:toy terrier\",\n                \"160:Rhodesian ridgeback\",\n                \"161:Afghan hound, Afghan\",\n                \"162:basset, basset hound\",\n                \"163:beagle\",\n                \"164:bloodhound, sleuthhound\",\n                \"165:bluetick\",\n                \"166:black-and-tan coonhound\",\n                \"167:Walker hound, Walker foxhound\",\n                \"168:English foxhound\",\n                \"169:redbone\",\n                \"170:borzoi, Russian wolfhound\",\n                \"171:Irish wolfhound\",\n                \"172:Italian greyhound\",\n                \"173:whippet\",\n                \"174:Ibizan hound, Ibizan Podenco\",\n                \"175:Norwegian elkhound, elkhound\",\n                \"176:otterhound, otter hound\",\n                \"177:Saluki, gazelle hound\",\n                \"178:Scottish deerhound, deerhound\",\n                \"179:Weimaraner\",\n                \"180:Staffordshire bullterrier, Staffordshire bull terrier\",\n                \"181:American Staffordshire terrier, Staffordshire terrier, American pit bull terrier, pit bull terrier\",\n                \"182:Bedlington terrier\",\n                \"183:Border terrier\",\n                \"184:Kerry blue terrier\",\n                \"185:Irish terrier\",\n                \"186:Norfolk terrier\",\n                \"187:Norwich terrier\",\n                \"188:Yorkshire terrier\",\n                \"189:wire-haired fox terrier\",\n                \"190:Lakeland terrier\",\n                \"191:Sealyham terrier, Sealyham\",\n                \"192:Airedale, Airedale terrier\",\n                \"193:cairn, cairn terrier\",\n                \"194:Australian terrier\",\n                \"195:Dandie Dinmont, Dandie Dinmont terrier\",\n                \"196:Boston bull, Boston terrier\",\n                \"197:miniature schnauzer\",\n                \"198:giant schnauzer\",\n                \"199:standard schnauzer\",\n                \"200:Scotch terrier, Scottish terrier, Scottie\",\n                \"201:Tibetan terrier, chrysanthemum dog\",\n                \"202:silky terrier, Sydney silky\",\n                \"203:soft-coated wheaten terrier\",\n                \"204:West Highland white terrier\",\n                \"205:Lhasa, Lhasa apso\",\n                \"206:flat-coated retriever\",\n                \"207:curly-coated retriever\",\n                \"208:golden retriever\",\n                \"209:Labrador retriever\",\n                \"210:Chesapeake Bay retriever\",\n                \"211:German short-haired pointer\",\n                \"212:vizsla, Hungarian pointer\",\n                \"213:English setter\",\n                \"214:Irish setter, red setter\",\n                \"215:Gordon setter\",\n                \"216:Brittany spaniel\",\n                \"217:clumber, clumber spaniel\",\n                \"218:English springer, English springer spaniel\",\n                \"219:Welsh springer spaniel\",\n                \"220:cocker spaniel, English cocker spaniel, cocker\",\n                \"221:Sussex spaniel\",\n                \"222:Irish water spaniel\",\n                \"223:kuvasz\",\n                \"224:schipperke\",\n                \"225:groenendael\",\n                \"226:malinois\",\n                \"227:briard\",\n                \"228:kelpie\",\n                \"229:komondor\",\n                \"230:Old English sheepdog, bobtail\",\n                \"231:Shetland sheepdog, Shetland sheep dog, Shetland\",\n                \"232:collie\",\n                \"233:Border collie\",\n                \"234:Bouvier des Flandres, Bouviers des Flandres\",\n                \"235:Rottweiler\",\n                \"236:German shepherd, German shepherd dog, German police dog, alsatian\",\n                \"237:Doberman, Doberman pinscher\",\n                \"238:miniature pinscher\",\n                \"239:Greater Swiss Mountain dog\",\n                \"240:Bernese mountain dog\",\n                \"241:Appenzeller\",\n                \"242:EntleBucher\",\n                \"243:boxer\",\n                \"244:bull mastiff\",\n                \"245:Tibetan mastiff\",\n                \"246:French bulldog\",\n                \"247:Great Dane\",\n                \"248:Saint Bernard, St Bernard\",\n                \"249:Eskimo dog, husky\",\n                \"250:malamute, malemute, Alaskan malamute\",\n                \"251:Siberian husky\",\n                \"252:dalmatian, coach dog, carriage dog\",\n                \"253:affenpinscher, monkey pinscher, monkey dog\",\n                \"254:basenji\",\n                \"255:pug, pug-dog\",\n                \"256:Leonberg\",\n                \"257:Newfoundland, Newfoundland dog\",\n                \"258:Great Pyrenees\",\n                \"259:Samoyed, Samoyede\",\n                \"260:Pomeranian\",\n                \"261:chow, chow chow\",\n                \"262:keeshond\",\n                \"263:Brabancon griffon\",\n                \"264:Pembroke, Pembroke Welsh corgi\",\n                \"265:Cardigan, Cardigan Welsh corgi\",\n                \"266:toy poodle\",\n                \"267:miniature poodle\",\n                \"268:standard poodle\",\n                \"269:Mexican hairless\",\n                \"270:timber wolf, grey wolf, gray wolf, Canis lupus\",\n                \"271:white wolf, Arctic wolf, Canis lupus tundrarum\",\n                \"272:red wolf, maned wolf, Canis rufus, Canis niger\",\n                \"273:coyote, prairie wolf, brush wolf, Canis latrans\",\n                \"274:dingo, warrigal, warragal, Canis dingo\",\n                \"275:dhole, Cuon alpinus\",\n                \"276:African hunting dog, hyena dog, Cape hunting dog, Lycaon pictus\",\n                \"277:hyena, hyaena\",\n                \"278:red fox, Vulpes vulpes\",\n                \"279:kit fox, Vulpes macrotis\",\n                \"280:Arctic fox, white fox, Alopex lagopus\",\n                \"281:grey fox, gray fox, Urocyon cinereoargenteus\",\n                \"282:tabby, tabby cat\",\n                \"283:tiger cat\",\n                \"284:Persian cat\",\n                \"285:Siamese cat, Siamese\",\n                \"286:Egyptian cat\",\n                \"287:cougar, puma, catamount, mountain lion, painter, panther, Felis concolor\",\n                \"288:lynx, catamount\",\n                \"289:leopard, Panthera pardus\",\n                \"290:snow leopard, ounce, Panthera uncia\",\n                \"291:jaguar, panther, Panthera onca, Felis onca\",\n                \"292:lion, king of beasts, Panthera leo\",\n                \"293:tiger, Panthera tigris\",\n                \"294:cheetah, chetah, Acinonyx jubatus\",\n                \"295:brown bear, bruin, Ursus arctos\",\n                \"296:American black bear, black bear, Ursus americanus, Euarctos americanus\",\n                \"297:ice bear, polar bear, Ursus Maritimus, Thalarctos maritimus\",\n                \"298:sloth bear, Melursus ursinus, Ursus ursinus\",\n                \"299:mongoose\",\n                \"300:meerkat, mierkat\",\n                \"301:tiger beetle\",\n                \"302:ladybug, ladybeetle, lady beetle, ladybird, ladybird beetle\",\n                \"303:ground beetle, carabid beetle\",\n                \"304:long-horned beetle, longicorn, longicorn beetle\",\n                \"305:leaf beetle, chrysomelid\",\n                \"306:dung beetle\",\n                \"307:rhinoceros beetle\",\n                \"308:weevil\",\n                \"309:fly\",\n                \"310:bee\",\n                \"311:ant, emmet, pismire\",\n                \"312:grasshopper, hopper\",\n                \"313:cricket\",\n                \"314:walking stick, walkingstick, stick insect\",\n                \"315:cockroach, roach\",\n                \"316:mantis, mantid\",\n                \"317:cicada, cicala\",\n                \"318:leafhopper\",\n                \"319:lacewing, lacewing fly\",\n                \"320:dragonfly, darning needle, devil's darning needle, sewing needle, snake feeder, snake doctor, mosquito hawk, skeeter hawk\",\n                \"321:damselfly\",\n                \"322:admiral\",\n                \"323:ringlet, ringlet butterfly\",\n                \"324:monarch, monarch butterfly, milkweed butterfly, Danaus plexippus\",\n                \"325:cabbage butterfly\",\n                \"326:sulphur butterfly, sulfur butterfly\",\n                \"327:lycaenid, lycaenid butterfly\",\n                \"328:starfish, sea star\",\n                \"329:sea urchin\",\n                \"330:sea cucumber, holothurian\",\n                \"331:wood rabbit, cottontail, cottontail rabbit\",\n                \"332:hare\",\n                \"333:Angora, Angora rabbit\",\n                \"334:hamster\",\n                \"335:porcupine, hedgehog\",\n                \"336:fox squirrel, eastern fox squirrel, Sciurus niger\",\n                \"337:marmot\",\n                \"338:beaver\",\n                \"339:guinea pig, Cavia cobaya\",\n                \"340:sorrel\",\n                \"341:zebra\",\n                \"342:hog, pig, grunter, squealer, Sus scrofa\",\n                \"343:wild boar, boar, Sus scrofa\",\n                \"344:warthog\",\n                \"345:hippopotamus, hippo, river horse, Hippopotamus amphibius\",\n                \"346:ox\",\n                \"347:water buffalo, water ox, Asiatic buffalo, Bubalus bubalis\",\n                \"348:bison\",\n                \"349:ram, tup\",\n                \"350:bighorn, bighorn sheep, cimarron, Rocky Mountain bighorn, Rocky Mountain sheep, Ovis canadensis\",\n                \"351:ibex, Capra ibex\",\n                \"352:hartebeest\",\n                \"353:impala, Aepyceros melampus\",\n                \"354:gazelle\",\n                \"355:Arabian camel, dromedary, Camelus dromedarius\",\n                \"356:llama\",\n                \"357:weasel\",\n                \"358:mink\",\n                \"359:polecat, fitch, foulmart, foumart, Mustela putorius\",\n                \"360:black-footed ferret, ferret, Mustela nigripes\",\n                \"361:otter\",\n                \"362:skunk, polecat, wood pussy\",\n                \"363:badger\",\n                \"364:armadillo\",\n                \"365:three-toed sloth, ai, Bradypus tridactylus\",\n                \"366:orangutan, orang, orangutang, Pongo pygmaeus\",\n                \"367:gorilla, Gorilla gorilla\",\n                \"368:chimpanzee, chimp, Pan troglodytes\",\n                \"369:gibbon, Hylobates lar\",\n                \"370:siamang, Hylobates syndactylus, Symphalangus syndactylus\",\n                \"371:guenon, guenon monkey\",\n                \"372:patas, hussar monkey, Erythrocebus patas\",\n                \"373:baboon\",\n                \"374:macaque\",\n                \"375:langur\",\n                \"376:colobus, colobus monkey\",\n                \"377:proboscis monkey, Nasalis larvatus\",\n                \"378:marmoset\",\n                \"379:capuchin, ringtail, Cebus capucinus\",\n                \"380:howler monkey, howler\",\n                \"381:titi, titi monkey\",\n                \"382:spider monkey, Ateles geoffroyi\",\n                \"383:squirrel monkey, Saimiri sciureus\",\n                \"384:Madagascar cat, ring-tailed lemur, Lemur catta\",\n                \"385:indri, indris, Indri indri, Indri brevicaudatus\",\n                \"386:Indian elephant, Elephas maximus\",\n                \"387:African elephant, Loxodonta africana\",\n                \"388:lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens\",\n                \"389:giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca\",\n                \"390:barracouta, snoek\",\n                \"391:eel\",\n                \"392:coho, cohoe, coho salmon, blue jack, silver salmon, Oncorhynchus kisutch\",\n                \"393:rock beauty, Holocanthus tricolor\",\n                \"394:anemone fish\",\n                \"395:sturgeon\",\n                \"396:gar, garfish, garpike, billfish, Lepisosteus osseus\",\n                \"397:lionfish\",\n                \"398:puffer, pufferfish, blowfish, globefish\",\n                \"399:abacus\",\n                \"400:abaya\",\n                \"401:academic gown, academic robe, judge's robe\",\n                \"402:accordion, piano accordion, squeeze box\",\n                \"403:acoustic guitar\",\n                \"404:aircraft carrier, carrier, flattop, attack aircraft carrier\",\n                \"405:airliner\",\n                \"406:airship, dirigible\",\n                \"407:altar\",\n                \"408:ambulance\",\n                \"409:amphibian, amphibious vehicle\",\n                \"410:analog clock\",\n                \"411:apiary, bee house\",\n                \"412:apron\",\n                \"413:ashcan, trash can, garbage can, wastebin, ash bin, ash-bin, ashbin, dustbin, trash barrel, trash bin\",\n                \"414:assault rifle, assault gun\",\n                \"415:backpack, back pack, knapsack, packsack, rucksack, haversack\",\n                \"416:bakery, bakeshop, bakehouse\",\n                \"417:balance beam, beam\",\n                \"418:balloon\",\n                \"419:ballpoint, ballpoint pen, ballpen, Biro\",\n                \"420:Band Aid\",\n                \"421:banjo\",\n                \"422:bannister, banister, balustrade, balusters, handrail\",\n                \"423:barbell\",\n                \"424:barber chair\",\n                \"425:barbershop\",\n                \"426:barn\",\n                \"427:barometer\",\n                \"428:barrel, cask\",\n                \"429:barrow, garden cart, lawn cart, wheelbarrow\",\n                \"430:baseball\",\n                \"431:basketball\",\n                \"432:bassinet\",\n                \"433:bassoon\",\n                \"434:bathing cap, swimming cap\",\n                \"435:bath towel\",\n                \"436:bathtub, bathing tub, bath, tub\",\n                \"437:beach wagon, station wagon, wagon, estate car, beach waggon, station waggon, waggon\",\n                \"438:beacon, lighthouse, beacon light, pharos\",\n                \"439:beaker\",\n                \"440:bearskin, busby, shako\",\n                \"441:beer bottle\",\n                \"442:beer glass\",\n                \"443:bell cote, bell cot\",\n                \"444:bib\",\n                \"445:bicycle-built-for-two, tandem bicycle, tandem\",\n                \"446:bikini, two-piece\",\n                \"447:binder, ring-binder\",\n                \"448:binoculars, field glasses, opera glasses\",\n                \"449:birdhouse\",\n                \"450:boathouse\",\n                \"451:bobsled, bobsleigh, bob\",\n                \"452:bolo tie, bolo, bola tie, bola\",\n                \"453:bonnet, poke bonnet\",\n                \"454:bookcase\",\n                \"455:bookshop, bookstore, bookstall\",\n                \"456:bottlecap\",\n                \"457:bow\",\n                \"458:bow tie, bow-tie, bowtie\",\n                \"459:brass, memorial tablet, plaque\",\n                \"460:brassiere, bra, bandeau\",\n                \"461:breakwater, groin, groyne, mole, bulwark, seawall, jetty\",\n                \"462:breastplate, aegis, egis\",\n                \"463:broom\",\n                \"464:bucket, pail\",\n                \"465:buckle\",\n                \"466:bulletproof vest\",\n                \"467:bullet train, bullet\",\n                \"468:butcher shop, meat market\",\n                \"469:cab, hack, taxi, taxicab\",\n                \"470:caldron, cauldron\",\n                \"471:candle, taper, wax light\",\n                \"472:cannon\",\n                \"473:canoe\",\n                \"474:can opener, tin opener\",\n                \"475:cardigan\",\n                \"476:car mirror\",\n                \"477:carousel, carrousel, merry-go-round, roundabout, whirligig\",\n                \"478:carpenter's kit, tool kit\",\n                \"479:carton\",\n                \"480:car wheel\",\n                \"481:cash machine, cash dispenser, automated teller machine, automatic teller machine, automated teller, automatic teller, ATM\",\n                \"482:cassette\",\n                \"483:cassette player\",\n                \"484:castle\",\n                \"485:catamaran\",\n                \"486:CD player\",\n                \"487:cello, violoncello\",\n                \"488:cellular telephone, cellular phone, cellphone, cell, mobile phone\",\n                \"489:chain\",\n                \"490:chainlink fence\",\n                \"491:chain mail, ring mail, mail, chain armor, chain armour, ring armor, ring armour\",\n                \"492:chain saw, chainsaw\",\n                \"493:chest\",\n                \"494:chiffonier, commode\",\n                \"495:chime, bell, gong\",\n                \"496:china cabinet, china closet\",\n                \"497:Christmas stocking\",\n                \"498:church, church building\",\n                \"499:cinema, movie theater, movie theatre, movie house, picture palace\",\n                \"500:cleaver, meat cleaver, chopper\",\n                \"501:cliff dwelling\",\n                \"502:cloak\",\n                \"503:clog, geta, patten, sabot\",\n                \"504:cocktail shaker\",\n                \"505:coffee mug\",\n                \"506:coffeepot\",\n                \"507:coil, spiral, volute, whorl, helix\",\n                \"508:combination lock\",\n                \"509:computer keyboard, keypad\",\n                \"510:confectionery, confectionary, candy store\",\n                \"511:container ship, containership, container vessel\",\n                \"512:convertible\",\n                \"513:corkscrew, bottle screw\",\n                \"514:cornet, horn, trumpet, trump\",\n                \"515:cowboy boot\",\n                \"516:cowboy hat, ten-gallon hat\",\n                \"517:cradle\",\n                \"518:crane\",\n                \"519:crash helmet\",\n                \"520:crate\",\n                \"521:crib, cot\",\n                \"522:Crock Pot\",\n                \"523:croquet ball\",\n                \"524:crutch\",\n                \"525:cuirass\",\n                \"526:dam, dike, dyke\",\n                \"527:desk\",\n                \"528:desktop computer\",\n                \"529:dial telephone, dial phone\",\n                \"530:diaper, nappy, napkin\",\n                \"531:digital clock\",\n                \"532:digital watch\",\n                \"533:dining table, board\",\n                \"534:dishrag, dishcloth\",\n                \"535:dishwasher, dish washer, dishwashing machine\",\n                \"536:disk brake, disc brake\",\n                \"537:dock, dockage, docking facility\",\n                \"538:dogsled, dog sled, dog sleigh\",\n                \"539:dome\",\n                \"540:doormat, welcome mat\",\n                \"541:drilling platform, offshore rig\",\n                \"542:drum, membranophone, tympan\",\n                \"543:drumstick\",\n                \"544:dumbbell\",\n                \"545:Dutch oven\",\n                \"546:electric fan, blower\",\n                \"547:electric guitar\",\n                \"548:electric locomotive\",\n                \"549:entertainment center\",\n                \"550:envelope\",\n                \"551:espresso maker\",\n                \"552:face powder\",\n                \"553:feather boa, boa\",\n                \"554:file, file cabinet, filing cabinet\",\n                \"555:fireboat\",\n                \"556:fire engine, fire truck\",\n                \"557:fire screen, fireguard\",\n                \"558:flagpole, flagstaff\",\n                \"559:flute, transverse flute\",\n                \"560:folding chair\",\n                \"561:football helmet\",\n                \"562:forklift\",\n                \"563:fountain\",\n                \"564:fountain pen\",\n                \"565:four-poster\",\n                \"566:freight car\",\n                \"567:French horn, horn\",\n                \"568:frying pan, frypan, skillet\",\n                \"569:fur coat\",\n                \"570:garbage truck, dustcart\",\n                \"571:gasmask, respirator, gas helmet\",\n                \"572:gas pump, gasoline pump, petrol pump, island dispenser\",\n                \"573:goblet\",\n                \"574:go-kart\",\n                \"575:golf ball\",\n                \"576:golfcart, golf cart\",\n                \"577:gondola\",\n                \"578:gong, tam-tam\",\n                \"579:gown\",\n                \"580:grand piano, grand\",\n                \"581:greenhouse, nursery, glasshouse\",\n                \"582:grille, radiator grille\",\n                \"583:grocery store, grocery, food market, market\",\n                \"584:guillotine\",\n                \"585:hair slide\",\n                \"586:hair spray\",\n                \"587:half track\",\n                \"588:hammer\",\n                \"589:hamper\",\n                \"590:hand blower, blow dryer, blow drier, hair dryer, hair drier\",\n                \"591:hand-held computer, hand-held microcomputer\",\n                \"592:handkerchief, hankie, hanky, hankey\",\n                \"593:hard disc, hard disk, fixed disk\",\n                \"594:harmonica, mouth organ, harp, mouth harp\",\n                \"595:harp\",\n                \"596:harvester, reaper\",\n                \"597:hatchet\",\n                \"598:holster\",\n                \"599:home theater, home theatre\",\n                \"600:honeycomb\",\n                \"601:hook, claw\",\n                \"602:hoopskirt, crinoline\",\n                \"603:horizontal bar, high bar\",\n                \"604:horse cart, horse-cart\",\n                \"605:hourglass\",\n                \"606:iPod\",\n                \"607:iron, smoothing iron\",\n                \"608:jack-o'-lantern\",\n                \"609:jean, blue jean, denim\",\n                \"610:jeep, landrover\",\n                \"611:jersey, T-shirt, tee shirt\",\n                \"612:jigsaw puzzle\",\n                \"613:jinrikisha, ricksha, rickshaw\",\n                \"614:joystick\",\n                \"615:kimono\",\n                \"616:knee pad\",\n                \"617:knot\",\n                \"618:lab coat, laboratory coat\",\n                \"619:ladle\",\n                \"620:lampshade, lamp shade\",\n                \"621:laptop, laptop computer\",\n                \"622:lawn mower, mower\",\n                \"623:lens cap, lens cover\",\n                \"624:letter opener, paper knife, paperknife\",\n                \"625:library\",\n                \"626:lifeboat\",\n                \"627:lighter, light, igniter, ignitor\",\n                \"628:limousine, limo\",\n                \"629:liner, ocean liner\",\n                \"630:lipstick, lip rouge\",\n                \"631:Loafer\",\n                \"632:lotion\",\n                \"633:loudspeaker, speaker, speaker unit, loudspeaker system, speaker system\",\n                \"634:loupe, jeweler's loupe\",\n                \"635:lumbermill, sawmill\",\n                \"636:magnetic compass\",\n                \"637:mailbag, postbag\",\n                \"638:mailbox, letter box\",\n                \"639:maillot\",\n                \"640:maillot, tank suit\",\n                \"641:manhole cover\",\n                \"642:maraca\",\n                \"643:marimba, xylophone\",\n                \"644:mask\",\n                \"645:matchstick\",\n                \"646:maypole\",\n                \"647:maze, labyrinth\",\n                \"648:measuring cup\",\n                \"649:medicine chest, medicine cabinet\",\n                \"650:megalith, megalithic structure\",\n                \"651:microphone, mike\",\n                \"652:microwave, microwave oven\",\n                \"653:military uniform\",\n                \"654:milk can\",\n                \"655:minibus\",\n                \"656:miniskirt, mini\",\n                \"657:minivan\",\n                \"658:missile\",\n                \"659:mitten\",\n                \"660:mixing bowl\",\n                \"661:mobile home, manufactured home\",\n                \"662:Model T\",\n                \"663:modem\",\n                \"664:monastery\",\n                \"665:monitor\",\n                \"666:moped\",\n                \"667:mortar\",\n                \"668:mortarboard\",\n                \"669:mosque\",\n                \"670:mosquito net\",\n                \"671:motor scooter, scooter\",\n                \"672:mountain bike, all-terrain bike, off-roader\",\n                \"673:mountain tent\",\n                \"674:mouse, computer mouse\",\n                \"675:mousetrap\",\n                \"676:moving van\",\n                \"677:muzzle\",\n                \"678:nail\",\n                \"679:neck brace\",\n                \"680:necklace\",\n                \"681:nipple\",\n                \"682:notebook, notebook computer\",\n                \"683:obelisk\",\n                \"684:oboe, hautboy, hautbois\",\n                \"685:ocarina, sweet potato\",\n                \"686:odometer, hodometer, mileometer, milometer\",\n                \"687:oil filter\",\n                \"688:organ, pipe organ\",\n                \"689:oscilloscope, scope, cathode-ray oscilloscope, CRO\",\n                \"690:overskirt\",\n                \"691:oxcart\",\n                \"692:oxygen mask\",\n                \"693:packet\",\n                \"694:paddle, boat paddle\",\n                \"695:paddlewheel, paddle wheel\",\n                \"696:padlock\",\n                \"697:paintbrush\",\n                \"698:pajama, pyjama, pj's, jammies\",\n                \"699:palace\",\n                \"700:panpipe, pandean pipe, syrinx\",\n                \"701:paper towel\",\n                \"702:parachute, chute\",\n                \"703:parallel bars, bars\",\n                \"704:park bench\",\n                \"705:parking meter\",\n                \"706:passenger car, coach, carriage\",\n                \"707:patio, terrace\",\n                \"708:pay-phone, pay-station\",\n                \"709:pedestal, plinth, footstall\",\n                \"710:pencil box, pencil case\",\n                \"711:pencil sharpener\",\n                \"712:perfume, essence\",\n                \"713:Petri dish\",\n                \"714:photocopier\",\n                \"715:pick, plectrum, plectron\",\n                \"716:pickelhaube\",\n                \"717:picket fence, paling\",\n                \"718:pickup, pickup truck\",\n                \"719:pier\",\n                \"720:piggy bank, penny bank\",\n                \"721:pill bottle\",\n                \"722:pillow\",\n                \"723:ping-pong ball\",\n                \"724:pinwheel\",\n                \"725:pirate, pirate ship\",\n                \"726:pitcher, ewer\",\n                \"727:plane, carpenter's plane, woodworking plane\",\n                \"728:planetarium\",\n                \"729:plastic bag\",\n                \"730:plate rack\",\n                \"731:plow, plough\",\n                \"732:plunger, plumber's helper\",\n                \"733:Polaroid camera, Polaroid Land camera\",\n                \"734:pole\",\n                \"735:police van, police wagon, paddy wagon, patrol wagon, wagon, black Maria\",\n                \"736:poncho\",\n                \"737:pool table, billiard table, snooker table\",\n                \"738:pop bottle, soda bottle\",\n                \"739:pot, flowerpot\",\n                \"740:potter's wheel\",\n                \"741:power drill\",\n                \"742:prayer rug, prayer mat\",\n                \"743:printer\",\n                \"744:prison, prison house\",\n                \"745:projectile, missile\",\n                \"746:projector\",\n                \"747:puck, hockey puck\",\n                \"748:punching bag, punch bag, punching ball, punchball\",\n                \"749:purse\",\n                \"750:quill, quill pen\",\n                \"751:quilt, comforter, comfort, puff\",\n                \"752:racer, race car, racing car\",\n                \"753:racket, racquet\",\n                \"754:radiator\",\n                \"755:radio, wireless\",\n                \"756:radio telescope, radio reflector\",\n                \"757:rain barrel\",\n                \"758:recreational vehicle, RV, R.V.\",\n                \"759:reel\",\n                \"760:reflex camera\",\n                \"761:refrigerator, icebox\",\n                \"762:remote control, remote\",\n                \"763:restaurant, eating house, eating place, eatery\",\n                \"764:revolver, six-gun, six-shooter\",\n                \"765:rifle\",\n                \"766:rocking chair, rocker\",\n                \"767:rotisserie\",\n                \"768:rubber eraser, rubber, pencil eraser\",\n                \"769:rugby ball\",\n                \"770:rule, ruler\",\n                \"771:running shoe\",\n                \"772:safe\",\n                \"773:safety pin\",\n                \"774:saltshaker, salt shaker\",\n                \"775:sandal\",\n                \"776:sarong\",\n                \"777:sax, saxophone\",\n                \"778:scabbard\",\n                \"779:scale, weighing machine\",\n                \"780:school bus\",\n                \"781:schooner\",\n                \"782:scoreboard\",\n                \"783:screen, CRT screen\",\n                \"784:screw\",\n                \"785:screwdriver\",\n                \"786:seat belt, seatbelt\",\n                \"787:sewing machine\",\n                \"788:shield, buckler\",\n                \"789:shoe shop, shoe-shop, shoe store\",\n                \"790:shoji\",\n                \"791:shopping basket\",\n                \"792:shopping cart\",\n                \"793:shovel\",\n                \"794:shower cap\",\n                \"795:shower curtain\",\n                \"796:ski\",\n                \"797:ski mask\",\n                \"798:sleeping bag\",\n                \"799:slide rule, slipstick\",\n                \"800:sliding door\",\n                \"801:slot, one-armed bandit\",\n                \"802:snorkel\",\n                \"803:snowmobile\",\n                \"804:snowplow, snowplough\",\n                \"805:soap dispenser\",\n                \"806:soccer ball\",\n                \"807:sock\",\n                \"808:solar dish, solar collector, solar furnace\",\n                \"809:sombrero\",\n                \"810:soup bowl\",\n                \"811:space bar\",\n                \"812:space heater\",\n                \"813:space shuttle\",\n                \"814:spatula\",\n                \"815:speedboat\",\n                \"816:spider web, spider's web\",\n                \"817:spindle\",\n                \"818:sports car, sport car\",\n                \"819:spotlight, spot\",\n                \"820:stage\",\n                \"821:steam locomotive\",\n                \"822:steel arch bridge\",\n                \"823:steel drum\",\n                \"824:stethoscope\",\n                \"825:stole\",\n                \"826:stone wall\",\n                \"827:stopwatch, stop watch\",\n                \"828:stove\",\n                \"829:strainer\",\n                \"830:streetcar, tram, tramcar, trolley, trolley car\",\n                \"831:stretcher\",\n                \"832:studio couch, day bed\",\n                \"833:stupa, tope\",\n                \"834:submarine, pigboat, sub, U-boat\",\n                \"835:suit, suit of clothes\",\n                \"836:sundial\",\n                \"837:sunglass\",\n                \"838:sunglasses, dark glasses, shades\",\n                \"839:sunscreen, sunblock, sun blocker\",\n                \"840:suspension bridge\",\n                \"841:swab, swob, mop\",\n                \"842:sweatshirt\",\n                \"843:swimming trunks, bathing trunks\",\n                \"844:swing\",\n                \"845:switch, electric switch, electrical switch\",\n                \"846:syringe\",\n                \"847:table lamp\",\n                \"848:tank, army tank, armored combat vehicle, armoured combat vehicle\",\n                \"849:tape player\",\n                \"850:teapot\",\n                \"851:teddy, teddy bear\",\n                \"852:television, television system\",\n                \"853:tennis ball\",\n                \"854:thatch, thatched roof\",\n                \"855:theater curtain, theatre curtain\",\n                \"856:thimble\",\n                \"857:thresher, thrasher, threshing machine\",\n                \"858:throne\",\n                \"859:tile roof\",\n                \"860:toaster\",\n                \"861:tobacco shop, tobacconist shop, tobacconist\",\n                \"862:toilet seat\",\n                \"863:torch\",\n                \"864:totem pole\",\n                \"865:tow truck, tow car, wrecker\",\n                \"866:toyshop\",\n                \"867:tractor\",\n                \"868:trailer truck, tractor trailer, trucking rig, rig, articulated lorry, semi\",\n                \"869:tray\",\n                \"870:trench coat\",\n                \"871:tricycle, trike, velocipede\",\n                \"872:trimaran\",\n                \"873:tripod\",\n                \"874:triumphal arch\",\n                \"875:trolleybus, trolley coach, trackless trolley\",\n                \"876:trombone\",\n                \"877:tub, vat\",\n                \"878:turnstile\",\n                \"879:typewriter keyboard\",\n                \"880:umbrella\",\n                \"881:unicycle, monocycle\",\n                \"882:upright, upright piano\",\n                \"883:vacuum, vacuum cleaner\",\n                \"884:vase\",\n                \"885:vault\",\n                \"886:velvet\",\n                \"887:vending machine\",\n                \"888:vestment\",\n                \"889:viaduct\",\n                \"890:violin, fiddle\",\n                \"891:volleyball\",\n                \"892:waffle iron\",\n                \"893:wall clock\",\n                \"894:wallet, billfold, notecase, pocketbook\",\n                \"895:wardrobe, closet, press\",\n                \"896:warplane, military plane\",\n                \"897:washbasin, handbasin, washbowl, lavabo, wash-hand basin\",\n                \"898:washer, automatic washer, washing machine\",\n                \"899:water bottle\",\n                \"900:water jug\",\n                \"901:water tower\",\n                \"902:whiskey jug\",\n                \"903:whistle\",\n                \"904:wig\",\n                \"905:window screen\",\n                \"906:window shade\",\n                \"907:Windsor tie\",\n                \"908:wine bottle\",\n                \"909:wing\",\n                \"910:wok\",\n                \"911:wooden spoon\",\n                \"912:wool, woolen, woollen\",\n                \"913:worm fence, snake fence, snake-rail fence, Virginia fence\",\n                \"914:wreck\",\n                \"915:yawl\",\n                \"916:yurt\",\n                \"917:web site, website, internet site, site\",\n                \"918:comic book\",\n                \"919:crossword puzzle, crossword\",\n                \"920:street sign\",\n                \"921:traffic light, traffic signal, stoplight\",\n                \"922:book jacket, dust cover, dust jacket, dust wrapper\",\n                \"923:menu\",\n                \"924:plate\",\n                \"925:guacamole\",\n                \"926:consomme\",\n                \"927:hot pot, hotpot\",\n                \"928:trifle\",\n                \"929:ice cream, icecream\",\n                \"930:ice lolly, lolly, lollipop, popsicle\",\n                \"931:French loaf\",\n                \"932:bagel, beigel\",\n                \"933:pretzel\",\n                \"934:cheeseburger\",\n                \"935:hotdog, hot dog, red hot\",\n                \"936:mashed potato\",\n                \"937:head cabbage\",\n                \"938:broccoli\",\n                \"939:cauliflower\",\n                \"940:zucchini, courgette\",\n                \"941:spaghetti squash\",\n                \"942:acorn squash\",\n                \"943:butternut squash\",\n                \"944:cucumber, cuke\",\n                \"945:artichoke, globe artichoke\",\n                \"946:bell pepper\",\n                \"947:cardoon\",\n                \"948:mushroom\",\n                \"949:Granny Smith\",\n                \"950:strawberry\",\n                \"951:orange\",\n                \"952:lemon\",\n                \"953:fig\",\n                \"954:pineapple, ananas\",\n                \"955:banana\",\n                \"956:jackfruit, jak, jack\",\n                \"957:custard apple\",\n                \"958:pomegranate\",\n                \"959:hay\",\n                \"960:carbonara\",\n                \"961:chocolate sauce, chocolate syrup\",\n                \"962:dough\",\n                \"963:meat loaf, meatloaf\",\n                \"964:pizza, pizza pie\",\n                \"965:potpie\",\n                \"966:burrito\",\n                \"967:red wine\",\n                \"968:espresso\",\n                \"969:cup\",\n                \"970:eggnog\",\n                \"971:alp\",\n                \"972:bubble\",\n                \"973:cliff, drop, drop-off\",\n                \"974:coral reef\",\n                \"975:geyser\",\n                \"976:lakeside, lakeshore\",\n                \"977:promontory, headland, head, foreland\",\n                \"978:sandbar, sand bar\",\n                \"979:seashore, coast, seacoast, sea-coast\",\n                \"980:valley, vale\",\n                \"981:volcano\",\n                \"982:ballplayer, baseball player\",\n                \"983:groom, bridegroom\",\n                \"984:scuba diver\",\n                \"985:rapeseed\",\n                \"986:daisy\",\n                \"987:yellow lady's slipper, yellow lady-slipper, Cypripedium calceolus, Cypripedium parviflorum\",\n                \"988:corn\",\n                \"989:acorn\",\n                \"990:hip, rose hip, rosehip\",\n                \"991:buckeye, horse chestnut, conker\",\n                \"992:coral fungus\",\n                \"993:agaric\",\n                \"994:gyromitra\",\n                \"995:stinkhorn, carrion fungus\",\n                \"996:earthstar\",\n                \"997:hen-of-the-woods, hen of the woods, Polyporus frondosus, Grifola frondosa\",\n                \"998:bolete\",\n                \"999:ear, spike, capitulum\",\n                \"1000:toilet tissue, toilet paper, bathroom tissue\"\n            ]\n        }\n\n```"
  },
  {
    "path": "docs/Authentication.md",
    "content": "# raspiCamSrv Authorization\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nAccess to the raspiCamSrv server requires login with Username and Password.   \nA user session will live as long as the browser remains open, even if the tab with a **raspiCamSrv** dialog has been closed.\n\nThe basic principle is that the first user in the system will be automatically registered as SuperUser.\nOnly the SuperUser will be able to register new users or remove users from the system.\n\nAfter the database has been initialized with ```flask --app raspiCamSrv init-db``` (see [RaspiCamSrv Installation](./installation.md)) Step 11), there is no user in the database.\n\nIn this situation, any connect to the server will open the *Register* screen:   \n![Register Initial](./img/Auth_RegisterInitial.jpg)\n\nNow, the SuperUser can complete his registration and will then be redirected to the *Log In* screen:   \n![Log In](./img/Auth_Login.jpg)   \nFrom now on, the *Register* screen will no longer be available through the menu.   \nAlso, direct access through the *Register* screen URL will only be allowed for the SuperUser. Other users will be redirected to the *Live* screen.\n\n## User Management\n\nFor management of users, the *Settings* screen has an additional section *Users* which is visible only for the SuperUser:   \n![User Management](./img/Auth_UserManagement.jpg)   \n\nThe list shows all registered users with\n\n- unique user *ID*\n- user *Name*\n- *Initial*, indicating whether the user has been initially created by the SuperUser and needs to change password on first log-in.\n- *SuperUser*, indicating the user registered as SuperUser\n\nThe SuperUser can\n\n- register new users using the *Register New User* button\n- remove users which have been selected in the list\n\n## Password\n\nUsers with flag \"Initial\" will automatically be requested to change their password when they log in for the first time.   \nAll users can change their password before they are logged in.\n\n![Password](./img/Auth_Password.jpg)    \nAfter the password has been successfully changed, the *Log In* screen will be opened.\n\n## Old User Schema\n\nThe functionality described above is available for systems installed after Feb. 15, 2024.   \nSystems installed before but updated (git pull) later, still work with the old user schema.\n\nIn this case, all users are considered SuperUsers.\n\nThe [User Management](#user-management) functionality in the *Settings* screen will be available for all users.   \nHowever, a hint is shown to update the database schema:\n\n![User Management Old](./img/Auth_UserManagement_old.jpg)"
  },
  {
    "path": "docs/Background Processes.md",
    "content": "# raspiCamSrv Tasks and Background Processes\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe figure below gives an overview of the different tasks available in **raspiCamSrv** and their relation to **raspiCamSrv** [Configurations](./Configuration.md) and camera strams.   \nFor more information on the components, see the [Picamera2 manual](./picamera2-manual.pdf), chapter 4.2.\n\n![stream usage](./img/CameraStreamUsage.jpg)\n\nThe tasks marked in green are executed in background processes (Threads) and may run simultaneously.\n\nThe status of each of these processes is indicated with [status indicators](./UserGuide.md#process-status-indicators):\n\n![Status Indicator](./img/ProcessIndicator4.jpg)\n\n\n## Default Configuration\n\nThe association between **raspiCamSrv** [Configurations](./Configuration.md) and camera streams shown in the figure above, is the default configuration.\n\nIn addition, default values for other configuration parameters are harmonized in such a way that all background processes can run simultaneously.   \nThis is especially important for the live stream which will remain active while a video is recorded, while photos are taken or while a Photo Series is executed.\n\n**raspiCamSrv** merges the different configurations to a single one which is applied when the camera is started.\n\nThis requires that the following configuration parameters must have the same values for the different configurations:\n\n- *Transform*\n- *Colour Space*\n- *Queue*\n\nThe values for *Buffer Count* can be different. In the merge process, the largest number of buffers will be selected.\n\n## Configuration Changes\n\nAll configuration scan be changed, including the association between configuration and camera stream (except raw).\n\nIf a configuration change, for example Transform, is made for a single configuration, for example *Video*, it is no longer possible to use a common configuration for all tasks.   \n\nIf then, for example, a video is recorded, the video thread needs to run in exclusive mode because it cannot share configuration with the Live Stream. For this purpose:\n\n1. The Live Stream must be stopped and paused during video recording\n2. The Encoder for the Live Stream must be stopped\n3. The camera must be stopped\n4. The camera must be configured with the Video configuration\n5. The camera must be started\n6. The encoder for video must be started while the video is being recorded\n7. The encoder must be stopped when video recording is finished\n8. The camera must be stopped\n9. The camera must be configured for the LiveStream, including eventally compatible configurations\n10. The camera must be started\n11. The MJPEG encoder for Live Stream must be started\n12. The Live Stream Thread must be started\n\nIn case of harmonized configurations, only steps 7 and 8 would have been required.\n"
  },
  {
    "path": "docs/Cam.md",
    "content": "# Cam - Camera Usage\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\n![Cam Menu](img/CamMenu.jpg)\n\nThis menu gives access to the available high-level camera usage scenarios, in particular in the case of multiple cameras connected to a Raspberry Pi (currently only Pi 5 can connect two CSI cameras).\n\nWhen connecting USB cameras, in addition to CSI cameras, you can in principle, get access to as many cameras as you can connect. However, raspiCamSrv can only operate up to 2 cameras simultaneusly. \n\nSo, you will need to select which of the connected cameras (see [Info/Cameras](./Information_Cam.md)) shall be used as *Active Camera* and as *Second Camera*.\n\n- The [Web Cam](./CamWebcam.md) dialog demonstrates how to stream video or image from your cameras.\n- The [Multi-Cam](./CamMulticam.md) dialog allows controlling both cameras individually or synchronously.     \n(This dialog is only available for multi-camera systems)\n- The [Camera Calibration](./CamCalibration.md) dialog can be used for calibration and rectification of stereo cameras.    \n(This dialog is only available if *Stereo Vision* is activated in the [Settings](./Settings.md#activating-and-deactivating-stereo-vision) dialog)\n- The [Stereo-Cam](./CamStereo.md) dialog can be used for visualization of depth maps as well as for viewing and recording of 3D videos.      \n(This dialog is only available if *Stereo Vision* is activated in the [Settings](./Settings.md#activating-and-deactivating-stereo-vision)) "
  },
  {
    "path": "docs/CamCalibration.md",
    "content": "# Camera Calibration\n\n[![Up](img/goup.gif)](./Cam.md)\n\nThis dialog allows calibration of the [stereo camera system](./CamStereo.md#stereo-camera) in order to improve the quality of the stereo result.\n\n**NOTE**:The dialog is only accessible if [Stereo Vision](./Settings.md#activating-and-deactivating-stereo-vision) has been activated.\n\n**REMINDER**: When finished with calibration you will need to [Store Configuration](./Settings.md#configuration) in order to preserve the results over a server restart.\n\n![Camera Calibration](img/CamCalibration1.jpg)\n\nWhen opened without existing calibration data, the dialog allows configuring the calibration process:\n\n- *Calibration Pattern* allows selecting the pattern to be used for calibration.    \nCurrently, only the [Chessboard pattern from OpenCV](https://github.com/opencv/opencv/blob/4.x/doc/pattern.png) is supported.    \nThis pattern needs to be printed or displayed on a tablet for being used during the calibration process.   \n- *Pattern Size* specifies the number of columns and rows of identifiable vertices of the pattern. 9x6 is appropriate for the standard chessboard pattern. (Only the inner vertices count)\n- In *Number of Photos required* you can specify how many photos you want to use for calibration.    \nThe dialog allows selecting as few as 5 photos but this is only to see how quality decreases with the number of photos. 20-30 will be a better choice.\n- *Number of Photos taken* will show the current number of photos.\n- *Rectify Scale* is a parameter used during rectification.     \n\"Valid Pixels Only\" will include only valid pixels in the final result and will remove black areas.    \n\"All Pixels\" will include all pixels in the final result of transformed images.\n\n## Picture Taking\n\nThe process of photo taking is automatic:\n\nWhen the pattern is presented to the cameras so that it is fully visible in both cameras, the system will try to find the specified number of corners.   \nOnly if this is successful, the image will be stored.   \nAfter 2 seconds, the next pair of images will be analyzed until sufficient photos have been taken.\n\nThe process is started with button **Start taking Pattern Photos** which will request a confirmation:\n\n![Confirmation](img/CamCalibration_conf.jpg)\n\nand signal readiness to take a photo:\n\n![Start](img/CamCalibration_start.jpg)\n\nNow you need to bring the pattern in the visible area and slowly change its orientation and position within the image areas.\n\n![Photo](img/CamCalibration_photo.jpg)\n\nIf not all required corners could be found at the current position, this will be indicated:\n\n![No Photo](img/CamCalibration_no_photo.jpg)\n\n## Picture Review\n\nAfter all required pictures have been taken, camera streaming stops and the stored pictures are presented for being reviewed:\n\n![Review](img/CamCalibration_rev.jpg)\n\nA navigation bar is shown which allows scrolling through the taken images.    \nImages with bad quality can be removed.\n\n*Show Corners* will present images where the found corners are shown:\n\n![Review](img/CamCalibration_rev_corn.jpg)\n\nIf photos have been removed, the missing ones need to be filled up using button \"Continue taking Pattern Photos\" \n\n![Review](img/CamCalibration_continue.jpg)\n\nButton **Reset Calibration Photos** will remove all photos and reset the calibration process after a confirmation.\n\n## Calibration\n\nWhen the required number of photos have been taken, you can continue with calibration.\n\n![Result](img/CamCalibration_result.jpg)\n\nDisplay of the time of calibration as well as the *RMS Re-Projection Error* indicate that valid calibration data are available.\n\nThe color with which the error is shown, (green, yellow, red) indicates the quality of the calibration:\n\n- < 0.5 px : excellent\n- 0.5 - 1 px : acceptable\n- &gt; 1 px : poor\n\n## Calibration Data Storage\n\nCalibration photos (with and without corners) are stored underneath the ```raspiCamSrv/static``` folder:\n\n\n![Storage](img/CamCalibration_storage.jpg)\n"
  },
  {
    "path": "docs/CamMulticam.md",
    "content": "# Multi-Camera Control\n\n[![Up](img/goup.gif)](./Cam.md)\n\n**NOTE**: This dialog is only available for systems with two or more CSI or USB cameras connected (see [Info](./Information.md)).\n\n**raspiCamSrv** can simultaneously operate two cameras: the *Active Camera* and the *Second Camera*.   \nIf more cameras are connected and available (usually USB cameras), *Active* and *Second* Camera must be selected out of these.\n\nWhile live streams for both cameras are shown, this dialog allows photo taking and video recording either individually with each of the two cameras or simultaneously with both.\n\n\n![Webcam](./img/CamMulticamUsb.jpg)\n\nThe left side of the page always shows the active camera.\n\nThe screenshot, above, shows a configuration with four available cameras.   \nIf more than two cameras are available, the stream titles are dropdown lists from which you can select *Active* or *Second Camera*.    \nIn every case, the other camera is not selectable because one camera cannot have two roles.\n\nIf there are just two cameras available, the drop-down lists are replaced by normal text.\n\nWhen switching the cameras, either with the **Switch Cameras** button on this side or by changing the camera in the [Settings](./Settings.md#switching-the-active-camera), the streams will be exchanged.\n\n### Buttons\n\n#### Photo / Raw / Video\n\nEvery camera has an own set of action buttons which apply only to this camera.   \nTheir function is identical to that of the corresponding [buttons on the Live screen](./Phototaking.md)\n\nThe resulting photos or place holders will not be shown on this page.    \nThey are stored in camera-specific subfolders and are accessible through the [Photos](./PhotoViewer.md) dialog\n\nFor the active camera, the last photo or placeholder is shown in the [Photo Display of the Live screen](./Phototaking.md#photo-display) and can be added to the display buffer for inspection of meta data or histogram..\n\n#### Photo - Both / Raw - Both / Video - Both\n\nWhen these buttons are pressed, the respective function is applied for both cameras.\n\nThe media files will have the same name for both cameras, which is generated from the timestamp of command execution, but are stored in their camera-specific subfolder.    \nThis allows identifying photos which have been synchronously taken and videos which have been synchronously started.\n\n**NOTE**: Currently, the actions on both cameras are executed sequentially, so that there may be a small subsecond delay.\n\n\n#### Save Active Camera Settings for Camera Switch\n\nThis button stores the current [Camera Configuration](./Configuration.md) for all configurations as well as the current [Controls](./CameraControls.md) settings for the active camera in a specific structure (streamingCfg) so that it can be reused for streaming in a case that the other camera has been activated.       \nIn addition, also the camera-related settings of [Trigger](./Trigger.md) ([Trigger/Motion](./TriggerMotion.md) and [Trigger/Camera](./TriggerCameraActions.md)) will be stored in the streamingCfg.    \n(See also [Configuring MJPEG Stream and jpeg Photo](#configuring-mjpeg-stream-and-jpeg-photo))\n\n#### Synchronize Configurations\n\nThis button is disabled if the two cameras are of different model.\n\nIf they are the same model, the button can be used to transfer the configuration, controls and camera-related Trigger settings from the active camera to the second camera. This is especially useful in the case of [Stereo Vision](./CamStereo.md) where both cameras should be operated with identical configuration.\n\n**NOTE**: the button does **not** save the settings for the active camera in the *streamingCfg* structure. This must be done explicitly and is recommended to be done before synchronizing.\n\n#### Switch Cameras\n\nWith this button, you can switch the cameras so that the one sown on the right side will become the active camera.\n\nIn case you have made changes of [Configuration](./Configuration.md), [Controls](./CameraControls.md) or camera-related [Trigger](./Trigger.md) settings ([Trigger/Motion](./TriggerMotion.md) and [Trigger/Camera](./TriggerCameraActions.md)) which have not yet been saved for Camera Switch, there will be a warning:    \n![CamSwitchWarning](./img/CamMulticamSwitchConfirm.jpg)   \nTo make sure your configuration changes survive the camera switxh, push [Save Active Camera Settings for Camera Switch](#save-active-camera-settings-for-camera-switch)\n\n## Process Status Indicators\n\n[Process Status Indicators](./UserGuide.md#process-status-indicators) show whether a camera is currently recording video or not.   \nThis is done independently for the active camera  ![StatusActiveCam](./img/ProcessIndicatorRecordingActive.jpg) and for the other camera ![StatusActiveCam](./img/ProcessIndicatorRecording2Active.jpg).\n\n**NOTE** that the recording status indicators are also activated when recording is started through the [API](./API.md)\n\n## Configuring MJPEG Stream and jpeg Photo\n\nWith **raspiCamSrv**, [Camera Configuration](./Configuration.md) and [Controls](./CameraControls.md) apply always to the active camera (which camera is the active one, can be selected in the [Settings](./Settings.md)).\n\nWhen the Flask server starts up without preloading stored configurations, the active camera and, if available, the second camera are preconfigured with parameter defaults.\n\nThe entire [Camera Configuration](./Configuration.md) as well as the [Controls](./CameraControls.md) for both cameras are stored in a specific streaming datastructure.\n\nWhen [Camera Configuration](./Configuration.md) and/or [Controls](./CameraControls.md) for the active camera are modified, these settings will **not** be automatically stored in the streaming configuration.    \nThis must be actively done with the **Save Active Camera Settings for Camera Switch**.\n\nWhen cameras are switched, configuration and controls for the active camera will be replaced by those from the second camera stored in the streaming datastructure.\n\nIn order to configure your camera setup, you can proceed as follows:\n\n1. Select one of the cameras as active camera\n2. Adjust the [Camera Configuration](./Configuration.md),    \nfor example *Transform* and/or *Sensor Mode* with *Stream Size*\n3. Adjust the [Controls](./CameraControls.md),    \nfor example *focus*/*lensposition*, *zoom*, *AutoExposure* or others\n4. When the setup is satisfactory, go to the *Multi-Cam* dialog and press the **Save Active Camera Settings for Camera Switch** button.\n5. Then switch cameras with the **<<< Switch Cameras >>>** button.\n6. Repeat steps 2. to 4. for the other camera\n7. If you now switch cameras, each stream, photo, raw photo and video should show in the way specifically configured for the camera.\n8. Now you can go to the [Settings](./Settings.md) screen and push the **Store Configuration** button in order to persist the streaming data along with the other configuration settings.\n9. If you want the entire configuration, including the streaming configuration, to be loaded when the server starts up, check the related checkbox in the [Settings](./Settings.md) screen.\n\n\n"
  },
  {
    "path": "docs/CamStereo.md",
    "content": "# Stereo-Cam\n\n[![Up](img/goup.gif)](./Cam.md)\n\nThis dialog features [stereo camera](#stereo-camera) capabilities and can be used for visualizing [Depth Maps](#depth-maps) and [3D Video](#3d-video). Both can also be [streamed independently](#streaming).\n\n**NOTE**:The dialog is only accessible if [Stereo Vision](./Settings.md#activating-and-deactivating-stereo-vision) has been activated.\n\n![Stereo-Cam](img/CamStereoCam1.jpg)\n\n## Stereo Camera\n\nA precondition for Stereo Vision with raspiCamSrv is a system which allows connecting a pair of non-USB cameras of the same model.     \nThese cameras need to be arranged as a stereo system with a typical human eye distance:\n\n![Stereo-System](img/Pi_Camera_3_Case_Stereo_front.JPG)\n\nFor a 3D-printable model, see [Raspberry Pi Camera 3 Stereo Case](https://makerworld.com/en/models/1742837-raspberry-pi-camera-3-stereo-case)\n\n**NOTE**: *Stereo Vision* needs to be activated in the [Settings](./Settings.md#activating-and-deactivating-stereo-vision) dialog.\n\n## Implementation\n\nStereo Vision in raspiCamSrv is based on [OpenCV](https://opencv.org/) and is inspired by a variety of examples on [LearnOpenCV](https://learnopencv.com/) such as [Making A Low-Cost Stereo Camera Using OpenCV](https://learnopencv.com/making-a-low-cost-stereo-camera-using-opencv/), from which also part of the code has been adapted and integrated with raspiCamSrv.\n\n## Depth Maps\n\n(See [Wikipedia article on Depth Maps](https://en.wikipedia.org/wiki/Depth_map))\n\nThe Stereo-Cam dialog usually opens with the following layout:\n\n![Stereo-Cam](img/CamStereoCam2.jpg)\n\nWhen opening, the system starts the Live view for both cameras as indicated by the [Process Status indicators](./UserGuide.md#process-status-indicators) and shows the streams of left and right camera in the upper part of the dialog.\n\nThe lower left part allows configuration of the intended Stereo Vision:\n\n- *Intent* distinguishes the basic intent and allows selection between **Depth Map** and **3D Video**.\n- *Rectified Images*, when activated uses the rectified images (see [Camera Calibration](./CamCalibration.md)) instead of the original ones to construct the stereo image.\n- *Algorithm* allows selecting the Open CV algorithm to be used when constructing the depth map.\n- *Algorithm Reference* links to the respective references in Open CV ([Stereo Block Matching](https://docs.opencv.org/4.6.0/d9/dba/classcv_1_1StereoBM.html) or [Semi-Global Matching](https://docs.opencv.org/4.6.0/d2/d85/classcv_1_1StereoSGBM.html)) where the parameters for the different algorithms are explained.\n\nPushing **Start** with *Depth Map* selected as Intent, will start a background process which applies the algorithm to the images of left and right video stream to produce a stream of depth map images:\n\n![Stereo-Cam](img/CamStereoCam3.jpg)\n\nThe activity of the Stereo thread, in addition to the two camera live view threads, is indicated by the different color of the [Process Status indicators](./UserGuide.md#process-status-indicators) for the two cameras.\n\n## Streaming\n\nThe Stereo stream can also be accessed through the URL which is shown underneath the stream in the dialog.\n\nThis URL, when called independently from the raspiCamSrv UI, will automatically start all necessary streaming processes.\n\nThe necessity of authentication can be configured in the [Settings](./Settings.md#configuring-authentication-for-streaming).\n\n## 3D Video\n\n (See [Wikipedia article on 3D Video](https://en.wikipedia.org/wiki/3D_film))\n\nStarting Stereo processing with *Intent* \"3D Video\" will show the scene as 3D video which needs to be viewed with Red-Cyan color 3D glasses to see the 3D effect:\n\n![Stereo-Cam](img/CamStereoCam1.jpg)\n\nHere, you also have the possibility to record the stream as video.\n\nVideos, recorded in this way, are stored under ```\\static\\photos\\camera_S``` and can be accessed with the [Photos](./PhotoViewer.md) dialog by selecting the \"Stereo\" camera:\n\n![Stereo-Cam](img/Photos_Stereo.jpg)\n"
  },
  {
    "path": "docs/CamWebcam.md",
    "content": "# Web Cam Access\n\n[![Up](img/goup.gif)](./Cam.md)\n\n**raspiCamSrv** enables webcam functionalities with Raspberry Pi cameras as well as with USB cameras.\n\nFor Pi 5 with two camera ports, both cameras can be streamed simultaneously.    \nAlternatively, you can choose one of the connected USB cameras as *Active* or *Second* camera.\n\nThis page shows the URLS for MJPEG streaming as well as for photo snapshots:\n\n![Webcam](./img/CamWebcam2.jpg)\n\nThe left side of the page always shows the active camera.   \nIf an additional camera is available, video stream and photo are shown on the right side.\n\nWhen switching the cameras, either with the **Switch Cameras** button in [Multi-Cam](./CamMulticam.md) or by changing the camera in the [Settings](./Settings.md#switching-the-active-camera), the streams will be exchanged.   \nThe *video_feed* endpoint will always refer to the active camera, which is also shown in the title bar.    \nThe *video_feed2* endpoint will always refer to the other camera, if available.\n\nThe configuration and camera stream used for video and photo capture are indicated.\n\nThe links shown on the page open a new browser window.\n\n## Video Stream\n\nThe video stream will always use the LIVE configuration.   \nBy default, this configuration uses the *lores* camera stream.   \nThe camera stream as well as its *stream size* can be configured in the [Configuration](./Configuration.md) screen.\n\n## Photo Snapshot\n\nFor the photo snapshot, you can choose between using the [LIVE configuration](./Configuration.md), usually configured with the *lores* stream with low resolution and a snapshot using the [FOTO configuration](./Configuration.md), usually configured with the *main* stream with high resolution.\n\n**NOTE**: Photo snapshots with high resolution assume an active live stream where the *lores* and the *main* streams of the camera are **simultaneously** configured (see [raspiCamSrv Tasks](./Background%20Processes.md)). \n\nIf your [configurations](./Configuration.md) for LIVE and FOTO are incompatible (for example using a different *Colour Space* or when both use the *main* stream with different resolutions), the live stream will request exclusive camera access with only the LIVE configuration activated. In this case, the URL for the high resolution photo snapshot will return an image with the resolution configured for LIVE.\n\nIf the live stream is active at the time when the photo snapshot is triggered, the snapshot will be taken immediately.    \nOtherwise, the live stream will be activated before the snapshot is taken. This requres starting the camera and giving it time to gather sufficient information for the auto-algorithms, which results in larger latency (1 to 1.5 sec).\n\n"
  },
  {
    "path": "docs/CameraControls.md",
    "content": "# raspiCamSrv Camera Controls\n\n[![Up](img/goup.gif)](./LiveScreen.md)\n\n**NOTE**: The subsequent description is essentially related to CSI cameras. For USB cameras, see [Camera Controls for USB Cameras](./CameraControls_UsbCams.md). \n\n\nPicamera2 allows for a set of 36 camera control parameters which can be adjusted while the camera is active.   \nFrom these, 8 parameters are just part of the image metadata and cannot be applied to the camera.\n\nIn principle, the remaining 28 parameters can be applied to the camera at different times\n\n1. As part of the [Camera Configuration](./Configuration.md).    \nHere **raspiCamSrv** supports adding any of these parameters to the configuration.   \nControl parameters included in the configuration have precedence over parameters not in the configuration.\n2. After camera configuration before camera start.   \nIn **raspiCamSrv**, this applies for all photos and videos taken in a raspiCamSrv session.\n3. After the camera has been started.   \nIn **raspiCamSrv**, this is only used for the live stream shown in the upper left quarter.   \nIf controls have been modified and submitted, they will be directly applied to the live stream.\n\nModification of camera controls does not affect raw photos.\n\n**NOTE**: For USB Cameras, [Handling of Controls](./CameraControls_UsbCams.md) is slightly different.\n\nIn **raspiCamSrv** all controls are explained through tooltips on the parameter name:\n![Tooltip](img/Tooltip.jpg)   \nThe texts for the tooltips have been mainly taken from the [Picamera2 Manual](./picamera2-manual.pdf) or the \nunderlying [libcamera documentation](https://libcamera.org/api-html/index.html).\n\nThe controls are grouped into\n\n- [Focus Handling](./FocusHandling.md)\n- [Zoom & Pan](./ZoomPan.md)\n- [Auto-Exposure](./CameraControls_AutoExposure.md)\n- [Exposure](./CameraControls_Exposure.md)\n- [Image](./CameraControls_Image.md)\n- [Ctrl](./CameraControls_Ctrl.md)\n\n## Basics\nAll Control Parameter tabs (except Zoom and Ctrl) are structured similarly:\n\n- Every tab is a form. This means that all parameters shown can be modified without any effect.   \nOnly when the form is submitted through the **Submit** button, the settings are saved in the server configuration and directly applied to the live stream.\n- Every parameter has a preceeding checkbox, which allows activation/deactivation of the control parameter within the configuration.   \nOnly if the checkbox is checked, the parameter can be modified.   \nIf the checkbox is unchecked, the control is not effective independently from its value.\n- Individual parameters may have restictions either as distinct values or ranges of allowed values.   \nIt should normally not be possible to enter a value which will not be accepted by the camera.\n- Some camera systems support only a subset of the available control parameters.   \nFor example, Raspberry Pi camera models 1 and 2 have no focus management.   \nThis is recognized by **rapiCamSrv** and these parameters will not be presented to the user.\n- All forms for the different parameter groups on different tabs are part of the same web page.   \nIf values are modified without submitting, the modification will be visible even if another tab has been selected in the meantime.   \n**If modifications are not submitted on their own tab, they will be lost in the next request/response cycle which can be triggered by a submit on another tab**.\n\n"
  },
  {
    "path": "docs/CameraControls_AutoExposure.md",
    "content": "# Camera Controls / Auto-Exposure\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n![Auto-Exposure](img/AutoExposure.jpg)\n\nThis tab includes parameters which control the Auto Exposure (AE) algorithm of the camera.\n"
  },
  {
    "path": "docs/CameraControls_Ctrl.md",
    "content": "# Camera Controls / Ctrl\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n![Image](img/Live_Ctrl.jpg)\n\nThis tab shows functional buttons which have been configured in [Settings/Live Buttons](./SettingsLButtons.md).\n\nTypically, these buttons will be used for control of devices which affect the camera position, such as servos controlling a Pan/Tilt device or a stepper motor for camera rotation.\n\nYou could also switch LEDs for illumination or control a slider motor.\n\nWhen controlled through buttons on this page, the effect on the camera image can imediately be seen.\n"
  },
  {
    "path": "docs/CameraControls_Exposure.md",
    "content": "# Camera Controls / Exposure\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n![Exposure](img/Exposure.jpg)\n\nThis tab includes parameters related to exposure control.\n"
  },
  {
    "path": "docs/CameraControls_Image.md",
    "content": "# Camera Controls / Image\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n![Image](img/Image.jpg)\n\nThis tab includes parameters controlling the image appearance\n"
  },
  {
    "path": "docs/CameraControls_UsbCams.md",
    "content": "# Camera Controls for USB Cameras\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n\n**raspiCamSrv** supports a limited set of controls for USB Cameras:\n\n- Switch between auto focus and manual focus\n- Adjustment of focal distance\n- Zoom, pan, tilt\n- Enabling/disabling automatic white balance\n- Adjusting the color temperature in case of manual white balance\n- Adjusting the sharpness\n- Adjusting the contrast\n- Adjusting color saturation\n- adjusting brightness\n\nWhereas zoom/pan/tilt, as well as horizontal and vertical flipping, is controlled through OpenCV by modifying each individual frame delivered by the camera, the other controls are affected through the V4l2 (Video for Linux) interface to the camera.\n\nEvery camera advertises the supported controls along with the related range of valid values.   \nThis information is [queried from the USB camera](./Information_Cam.md#determining-supported-controls) while **raspiCamSrv** initializes the camera information.\n\nThe list of supported controls as well as their minimum, maximum, step and default values are used tho customize the individual controls screens to the currently active camera.\n\n### Focus Handling USB Cameras\n\n![Focus USB](./img/Focus_USB.jpg)\n\n### Image Control USB Cameras\n\n![Image](./img/Image_USB.jpg)\n\nIn these dialogs, the input fields have value ranges and defaults appropriate for the active camera type.   \nValue ranges and default values are also visible in the tooltips.\n\n### Applying Controls to USB Cameras\n\nThe supported controls ara applied to USB cameras through v4l2 commands, such as:\n\n```v4l2-ctl --set-ctrl=contrast=50```\n"
  },
  {
    "path": "docs/Configuration.md",
    "content": "# raspiCamSrv Camera Configuration\n\nRelated Topics:\n\n- [AI Camera Configuration](./Configuration_AI.md)\n- [Camera Tuning](./Tuning.md)\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nConfiguration parameters are so basic that they need to be applied before the camera is started.\n\nPicamera2 provides three configuration bases which can be taken as is for a specific use case or they can be adjusted in one or several aspects.\n\nThese are:\n\n- Preview configuration   \nfor previews on a screen connected to the Raspberry Pi\n- Still configuration   \nfor photos\n- Video configuration   \nfor videos\n\n**raspiCamSrv** does not make direct use of these configurations.\n\nInstead, the following configurations can be fully configured:\n\n- Live View configuration   \nwhich will be applied to the live stream\n- Photo configuration   \nwhich will be applied when normal photos are taken\n- Raw Photo configuration   \nwhich will be applied when raw photos are taken\n- Video configuration   \nwhich will be applied when videos are recorded\n\nConfiguration changes may have an impact on the way how tasks and background processes are executed. If specific parameters, such as [Transform](#transform) are changed for a specific use case only, for example for *Video*, video recording will require that the Live Stream is stopped and paused while the video is being recorded.   \nFor more details, see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md).\n\n#### USB Cameras\n\nThe schema of configuration use cases, conceptually originating from Picamera2, is also retained for USB cameras. However, there are specific differences related to *Colour Space*, *Buffer Count*, *Queue* parameters. Also Controls cannot be included in the configuration as with Picamera2.\n\nA major difference is that USB cameras accessed through OpenCV, do not allow using different streams.   \nAs a consequence, live stream in parallel to photo taking or video recording would only be possible with identical configurations.    \nSince this does not seam to make sense because of performance issues, **raspiCamSrv** always requests exclusive use of the camera when taking photos or recording videos.   \n[Motion Capturing](./TriggerMotion.md) works in the same way for USB and CSI cameras.\n\n## Configuration Tab\n\nThe *Config* submenu includes a tab *Tuning* which is described in [raspiCamSrv Camera Tuning](./Tuning.md) and not here.\n\nAn individual configuration tab is available for each use case. All tabs have essentially the same structure:   \n\nAs a general aspect, the green [Submenu](./UserGuide.md#submenu) bar includes an option to synchronize the aspect ratio of [stream sizes](#stream-size-width-height) across all configurations if this has been changed for the current configuration.   \nIf this option is activated after it was previously deactivated, all aspect ratios will be set to the one of the current configuration. \n\n**NOTE**: For USB cameras this option is unchecked and the checkbox is disabled becaus USB cameras scale and crop with the correct aspect ratio intrinsically.\n\n![Configuration](img/Config.jpg)\n\n\n**As always: any modifications need to be submitted before they can be effective**\n\n### Transform\n\nWith *Transform*, you can specify whether the image needs to be flipped horizontally, vertically or both. The latter case is identical to rotation of 180°.\n\n**NOTE:** When this is modified for one configuration, the settings are automatically transferred to all other configurations.<br>\nThis is necessary because **raspiCamSrv** simultaneously configures all streams (lores, main and raw) in order to allow using these in parallel for different purposes, such as live stream and video recording. The transform settings cannot be different for different streams at the same time.\n\n### Colour Space\n\nThis allows selecting one of the supported colour spaces\n\n### Buffer Count\n\nSpecifies the number of buffers used by the camera for the specific use case.\n\nValues are preset in accordance with corresponding settings of the Picamera2 standard use cases\n\n### Queue\n\nSpecifies whether the camera is allowed to queue up a frame ready for a capture request.\n\n### Sensor Mode\n\nWhen **raspiCamSrv** starts up, one of the first things is to query the camera system for the available Sensor Modes.  \nThese can be inspected on the [Info](./Information.md) screen.\n\n\nThese modes are offered for selection here.   \nWhen a Sensor Mode is selected, its main characteristics are shown to the right.\n\nIn addition to the available Sensor Modes, a \"Custom\" mode can be selected which allows especially to set the intended stream size (width and height if the image in pixels)\n\nFor the *Raw Photo* use case, \"Custom\" cannot be selected. Raw photos will allways use the stream size of the selected Sensor Mode.\n\n### Stream\n\nSpecifies the stream to be used for the respective use case.\n\nThe camera system supports three streams (see [Picamera2 Manual](./picamera2-manual.pdf)):\n\n- the **main** stream\n- the **lowres** stream\n- and the **raw** stream\n\nThe latter is for raw data output which bypasses the image signal processor.\n\nFor the *Raw Photo* use case, this is the only stream which can be selected.\n\n### Stream Size (width, height)\n\nIf a standard Sensor Mode has been selected, the size related to the mode is shown.\n\nIf \"Custom\" has been selected as sensor mode, you may enter any size here (except for *Raw Photos*).   \nProduced photos or videos will then be in the specified format.\n\nIf the option to synchronize aspect ratios (right side of green Submenu bar) is selected, the *Stream Size*s for all other configurations will be adjusted to reproduce the aspect ratio of the current configuration.\n\n**NOTE:** If, after submitting a *Live View* configuration, you get an error message ```\nlores Stream Size must not exceed main Stream Size (Photo)```, you need to go to the *Photo* configuration and adjust its *Stream Size* to the desired value.\n\n### Stream size aligned with Sensor Modes\n\nIf this option is activated, and if a stream size has been specified which is different from stream sizes of the available Sensor Modes, the Picamera2 Configuration will be asked to align the stream size. This may result in slightly different sizes which are, however, in better accordance with available Sensor Modes.\n\n### Stream Format\n\nIt can be selected from a number of pixel and image formats supported by Picamera2.   \nFor details, see [Picamera2 Manual](./picamera2-manual.pdf), Appendix A.\n\nWhereas for *Live Stream*, *Photo* and *Video* a format can be selected from the same list, the formats available for *Raw Photo* are different. These are specified in the Sensor Modes (see [Info](./Information.md)).   \n**raspiCamSrv** queries these from Picamera2 at server startup and offers the found formats in the configuration screen.\n\n### Display\n\nThis parameter is just shown for completeness.   \nIt specifies the stream which shall be used for display on a monitor connected to the system.   \nThis is not relevant in the scenario addressed by **raspiCamSrv**, which is usually headless.\n\n### Encode\n\nThis specifies the stream which needs to be sent to the encoder.\n\nSettings are preconfigured and cannot be modified.\n\nEncoding is only necessary for the *Live View* (MJPEG encoding) and the *Video* use cases.\n\nFor *Video*, the encoder depends on the video format chosen.\n\n\n## Controls included in Configuration\n\nA configuration for a specific use case may also include Camera Controls.\n\nActually, Picamera2 requires that at least one control is included in the configuration.\n\n**raspiCamSrv** preconfigurs a control with specific settings in accordance with the Picamera2 standard use case configurations.\n\nIn addition, the button **Add Active Ctrls** will include all controls which are currently active in the [Camera Controls](./CameraControls.md) configuration.\n\nValues are taken as configured and cannot be modified here.\n\nIn case a control parameter has a wrong value, it can be selected and then removed from the configuration with the **Remove Selected Ctrls** button.\n"
  },
  {
    "path": "docs/Configuration_AI.md",
    "content": "# raspiCamSrv Camera AI Configuration\n\n[![Up](img/goup.gif)](./Configuration.md)\n\n**NOTE**: This dialog is only available if the Active Camera is a [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) and if AI features have been activated in the [Settings Diaolog](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features)\n\nThe IMX500 imaging sensor of the AI Camera can load a neural network model from a location on the Raspberry Pi and apply it to the individual frames delivered by the sensor. Inference data, generated by the model, are supplied to application2 through image meta data. This allows applications to evaluate these data and visualize them in the application context or through overlays on the individual frames.\n\n![Configuration](img/Config_AI.jpg)\n\nThe dialog has three distinct sections:\n\n## Configuration for AI\n\nHere you specify the neural network file which will be loaded by the AI Camera.\n\n**NOTE**: The model can only be changed if the imx500 camera is currently not open. So, you need to wait until the Live View background process has automatically stopped (within 10 seconds).\n\nYou can check the status Live stream of the Active Camera by clicking on the *Config* menu until you see the [Process Status Indicator](./UserGuide.md#process-status-indicators) for the Active Camera change to grey.\n\nThen, the fields for selection of the *AI Model File* will be enabled:\n\n- *Full Path to Folder with Model Files*<br>This is the folder where model files are stored.\n<br>With installation of the ```imx500-all``` package, a set of model files will already be available at\n<br>```/usr/share/imx500-models```.\n<br>Leaving the field empty will automatically set this as the default folder.\n- *Task*\n<br>Every model file has a specific *Task*.\n<br>You can currently choose between \n<br>- Classification\n<br>- Object Detection\n<br>- Pose Estimation\n<br>- Segmentation\n<br>When choosing one of these tasks, the entry for the *AI Model File* will be cleared to enforce selection.\n- *AI Model File*\n<br>After a *Task* has been chosen, raspiCamSrv will iterate the available model files in the specified folder and determine the *Task* for which these have been prepared.\n<br>Only files having the specified task will be offered for selection.\n- *Intrinsics*\n<br>Every model fileexposes a set of intrinsics characterizing its capabilities and operational details.\n<br>These will be shown here.\n\nSources and details for various Reference Neural Network Models can be found on [https://github.com/raspberrypi/imx500-models](https://github.com/raspberrypi/imx500-models)\n\nImplementations within **raspiCamSrv** are based on the [Demo Code Examples](https://github.com/raspberrypi/picamera2/tree/main/examples/imx500) coming with [Picamera2](https://github.com/raspberrypi/picamera2).\n\n## Settings\n\nThis section includes several parameters by which the visualization of inference data can be adjusted.   \nOnly a subset of these parameters is applicable for a specific model.    \nParameters which do not apply, are disabled when a model has been selected.\n\n- *Top K Indices* (Classification)\n<br>Only the given number of indices with the highest rating will be visualized.\n- *Detection Threshold*\n<br>Only detections having a score larger than the given threshold will be visualized.\n- *IOU Threshold*\n<br>Specifies the IoU (Intersection over Union) threshold for object detection.\n- *Max Detections*\n<br>specifies the maximum number of detections to be visualized\n- *Draw Resulte on Stream lores*\n<br>If activated, inference results will be visualized on the *lores* stream which is usually used for the Live Stream  [Configuration](./Configuration.md)\n- *Draw Resulte on Stream main*\n<br>If activated, inference results will be visualized on the *main* stream which is usually used for the Photo and Video  [Configuration](./Configuration.md)\n\n**NOTE**: Text sizes and line thickness used for visualization, as used in the Picamera2 demos are optimized for lower resolution previews (e.g. 640x480). If you intend to visualize on photos and/or videos, it is recommended to set the [Stream Size](./Configuration.md#stream-size-width-height) to a 'Custom' size of about the same size as the *lores* stream for *Live View*.\n\n## Enable AI\n\nHere you can enable the selected model together with the visualization parameters. Or you can disable a currently active model.\n\nEither way will require a confirmation.\n\n\n\n\n"
  },
  {
    "path": "docs/Console.md",
    "content": "# Console\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe Console group of dialogs provides functions for user interactions with the Raspberry Pi.\n\n![Console](./img/Console.jpg)\n\n- [Versatile Buttons](./ConsoleVButtons.md) allow interaction with the Operating System by running OS commands or scripts.\n- [Action Buttons](./ConsoleActionButtons.md) allow execution of various types [Actions] for interaction with GPIO-connected output devices or the camera system."
  },
  {
    "path": "docs/ConsoleActionButtons.md",
    "content": "# Console - Action Buttons\n\n[![Up](img/goup.gif)](./Console.md)\n\nThis page shows buttons which have been configured in the [Settings / Action Buttons](./SettingsAButtons.md):\n\n![aButtons](./img/Console_AButtons.jpg)\nThe example layout of this screenshot is based on the example configuration shown for the [Settings / Action Buttons](./SettingsAButtons.md) screen.\n\n## Button Execution\n\nWhen the [Action](./TriggerActions.md), configured for a button is executed, the behavior for the action is slightly different compared to invocation of an action on behalf of a [Trigger](./TriggerTriggers.md). \n\n- If a device is busy at the time when a button is pressed, the action is not executed. Instead a \"Device busy\" information is shown in the status line:    \n![busy](./img/Console_AButtonsDeviceBusy.jpg)\n\n\n- Also, the action is not executed in an own thread but synchronously, so that the user needs to wait for action completion which is confirmed in the message line.\n"
  },
  {
    "path": "docs/ConsoleVButtons.md",
    "content": "# Console - Versatile Buttons\n\n[![Up](img/goup.gif)](./Console.md)\n\nThis page shows buttons which have been configured in the [Settings / Versatile Buttons](./SettingsVButtons.md):\n\n![vButtons](./img/Console_VButtons.jpg)\nThe example layout of this screenshot is based on the example configuration shown for the [Settings / Versatile Buttons](./SettingsVButtons.md) screen.\n\n## Button Execution\n\nWhen a button is clicked which is configured to require confirmation, a confirmation dialog is shown where execution can be refused:\n\n![vButtonConfirm](./img/Console_VButtons_conf.jpg)\n\n## Execution Result\n\nIn the bottom part of the dialog the result of the command execution is shown:\n\n- *Command*<br>The command configured for the button.\n- *Run Arguments*<br>Command execution is done using the Python [subprocess.run](https://docs.python.org/3/library/subprocess.html) method which receives arguments as a list. This list is obtained by parsing the command string with spaces as separators.\n- *Return Code*<br>The return code returned from command execution.\n- *Stdout*<br>Output which command execution has sent to Stdout.<br>Multiline output can be scrolled.\n- *Stderr*<br>Error information which command execution has sent to Stderr.<br>Multiline output can be scrolled.\n- Status Line<br>The status line at the bottom shows whether the command could be executed or not, irrespective of errors which might have occurred during command execution.\n\nThe last *Execution Result* remains visible within the live time of the Flask server.\n\nOf course, no result will be visible if the Flask server has been restarted or of the Raspberry Pi has been rebooted.\n\n## Interactive Commandline\n\nIf the [Settings / Versatile Buttons](./SettingsVButtons.md) have declared the commandline to be interactive, commands can be directly entered on the commandline:\n\n![Commandline](./img/Console_VButtons_commandline.jpg)"
  },
  {
    "path": "docs/FocusHandling.md",
    "content": "# raspiCamSrv Focus Handling\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\nFocus handling is not supported by camera versions 1 and 2.   \n\n![Focus handling](img/Focus.jpg)\n\nThis tab includes various controls which affect the Auto Focus (AF) algorithm of the camera.\n\nThe **Autofocus Mode** can be set to \"Manual\", \"Auto\" or \"Continuous\"\n\n## Manual Focus\n\nWhen *Autofocus Mode* \"Manual\" is chosen, also the *Focal Distance* field must be activated and the distance must be set manually.\n\nPressing **Submit** will apply the setting to the live stream and the changed focus will be immediately visible.\n\n## Continuous Focus\n\nWhen *Autofocus Mode* is set to \"Continuous\", the camera will, after submitting, continuously try to focus under consideration of settings for other focus handling parameters.\n\n## Automatic Focus\n\nWhen *Autofocus Mode* is set to \"Auto\", the camera will automatically focus after an autofocus cycle has been triggered.\n\nBefore the cycle can be triggered through the **Trigger Autofocus** button, the settings must be applied with the **Submit** button.   \n\nSubmitting the \"Auto\" *Autofocus Mode* will have no effect on the live stream.\n\n## Trigger Autofocus\n\nThe effect of the autofocus cycle will essentially depend on the settings for the other AF control parameters.\n\nWhether or not the autofocus cycle was successful, will be shown in the message area at the bottom of the application window:\n![AFMessage](img/AFMessage.jpg)\n\nIf the autofocus cycle was successful, **raspiCamSrv** will request metadata from the camera and determine the focal distance from the LensPosition.\n\nThe Value will be entered in the *Focal Distance* field, which will also be activated automatically.\n\nAlso, the *Autofocus Mode* will be automatically set to \"Manual\" so that the measured *Focal Distance* can be used for future photos.\n\n![AFTrigger](img/AFTrigger2.jpg)\n\n## Autofocus Windows\n\nThese are rectangle areas within the image which will be used by the AF algorithm to focus.\n\nMultiple rectangle areas can be specified.\n\n**raspiCamSrv** supports graphical specification of these areas in the following way.\n\n1. Activate the checkbox for *Autofocus Windows*.  \nAs result, a canvas will be drawn over the live stream area which is visible as thin red border:   \n![AfWindows1](img/AFWindows1.jpg)   \n2. Now you can use the mouse to draw rectangles on this canvas:    \nPosition the cursor at one corner of the intended rectangle,   \npress the left mouse button,    \nand drag with mouse button down to the opposite corner.\n3. When the mouse button is released, the rectangle coordinates will be scaled to the current scaler crop settings and entered in the *Autofocus Windows* field.  \n![AfWindows2](img/AFWindows2.jpg)\n4. If required, you can draw additional rectangles in the same way.   \nWhile drawing rectangles, previously drawn rectangles will vanish without getting lost.\n5. Finally, when the mouse pointer leaves the canvas area, all rectangles will be shown over the live stream area.   \n![AfWindows3](img/AFWindows3.jpg)   \n**Don't forget to push Submit because otherwise, these settings will get lost!**\n6. In order to remove all areas, just deactivate the *Autofocus Windows* checkbox and activate it again.\n\nThe canvas and the rectangles representing the AF Windows will remain visible as long as the *Autofocus Windows* checkbox is activated and whenever the *Focus* tab is visible.\n\n\n"
  },
  {
    "path": "docs/Information.md",
    "content": "# raspiCamSrv Information\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\n![Info Menu](img/Info-Menu.jpg)\n\nThis menu gives access to detailed information on the raspiCamSrv system:   \n\n- [System](./Information_Sys.md)    \nshows detailed information about the Raspberry Pi system as well as on the software stack used by raspiCamSrv.\n\nThe following sub-menus are only visible if at least one camera is connected to the system:\n\n- [Cameras](./Information_Cam.md)   \nshows information on the installed cameras.    \n- [Camera Properties](./Information_CamPrp.md)    \nshows detailed information for the **active** camera.\n- [Sensor Mode n](./Information_Sensor.md)    \nThis set of tabs shows characteristics for the different sensor modes advertised by the **active** camera."
  },
  {
    "path": "docs/Information_Cam.md",
    "content": "# raspiCamSrv Info/Camera Information\n\n[![Up](img/goup.gif)](./Information.md)\n\nThis screen shows information on the installed cameras.\n\n![Cameras](img/Info-Cameras.jpg)\n\n*Software Stack* shows information on installed packages with Version (*Ver*) and the path from which the packages were loaded (*Loc*).\n\n## Camera x\n\nThe tab lists all cameras currently connected to the system.\n\nEach camera has an identifying number (0, 1, ...) shown in the title above each parameter list.   \nThe assignment of camera number to physical camera may change when CSI cameras are plugged into a different CSI port (Pi 5) or when USB cameras are plugged into a different USB port.\n\nWhen the server starts up, the first camera in the [list of cameras](#detection-of-cameras) is selected as active camera, unless a specific camera is activated when a [stored configuration](./SettingsConfiguration.md) is loaded at startup..\n\nYou may later switch to another camera on the [Settings](./Settings.md) screen or the [Multi Cam](./CamMulticam.md) screen\n\nThe active camera is indicated in the list.\n\nThe active camera will also be shown in the title bar of the application after log-in.\n\nFor USB cameras, the device through which the camera is accessible is also shown (See [Detection of Cameras](#detection-of-cameras)).\n\nThe information \"(Not in use)\" for a USB camera indicates that the camera has been detected by **raspiCamSrv** but it is not in use because USB cameras have been deactivated in the [Settings](./Settings_NoCam.md). In this case, the USB camera cannot be selected as active or second camera in the [Settings](./Settings.md) screen or the [Multi Cam](./CamMulticam.md) screen.\n\n### Status\n\n*Current Status* shows the status of the camera:\n\n- open / closed\n- started / stopped\n- current [Sensor Mode](./Information_Sensor.md)   \nThis is only shown for the currently active camera if it is started.    \nIf the Sensor Mode cannot currently be determined, 'unknown' is shown.    \nThe Sensor Mode is usually automatically selected by the camera and normally corresponds to the largest [Stream Size](./Configuration.md#stream-size-width-height), requested by one of the [Camera Configurations](./Configuration.md).\n- inactive<br>is shown for USB cameras which are currently not in use as active or second camera.\n- excluded<br>is shown for a USB camera which is, in principle, available for being used with **raspiCamSrv**, but currently excluded in the [Settings](./Settings.md)\n- not supported (OpenCV missing)<br>Is shown if a detected USB camera cannot be used within **raspiCamSrv** because OpenCV is not installed.\n\nSee [Camera Status and Number of Threads](#camera-status-and-number-of-threads)\n\nUnder *Tuning File*, you can see whether the Default or a custom tuning file are currently in use.    \nSee [raspiCamSrv Camera Tuning](./Tuning.md).\n\n### AI Features\n\nThis shows whether AI Features of a camera are available and active.    \nCurrently, this applies only to the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) with Sony IMX500 sensor.\n\n- Not Available\n<br>Indicates that the camera has no AI capabilities\n- Available\n<br>Indicates that the camera has AI capabilities and that AI features are enabled in the [Settings](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features).\n<br>Whether or not the camera is currently running a neural network model can be controlled in the [Camera AI Configuration](./Configuration_AI.md)\n- Disabled in Settings\n<br>Indicates that the camera has AI capabilities. However AI features are disabled in the [Settings](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features)\n\n### Camera connected but not in the list?\n\nIf you have a USB camera connected which does not show up in the list, you may have plugged in the camera while **raspiCamSrv** was running.   \nIn this case, you can use function [Reload Cameras](./SettingsConfiguration.md) to identify hot-plugged cameras.\n\n\n## Camera Status and Number of Threads\n\nThe number of threads used by the server process depends on the status of the camera(s).\n\n- When all cameras are closed, there is just the server process and, in case of Bookworm systems, 2 threads which are started with the import of Picamera2.    \nThus, there is a minimum of three threads (1 for Bullseye).\n- Opening a camera starts additional threads which remain active while the camera is open.   \nThe number of threads may depend on the camera infrastructure specific for the operating system.\n- Starting a camera and/or starting an encoder starts additional threads depending on the chosen camera function and encoder.\n- **raspiCamSrv** also uses threads for background processes, such as live stream, video recording, photo series and motion detection. These ramain active while these processes are running.\n- Stopping and closing a camera will also stop the dependent threads and thus reduce the number of active threads.\n- If .mp4 video is currently recorded ([started manually](./Phototaking.md) or as an action within [motion capturing](./Trigger.md)), there will be an additional ffmpeg process with additional threads.\n- In case of .mp4 video recording with H264Encoder and FfmpegOutput there seems to be an issue with threads:    \nIn this case, there may be threads surviving when the encoder is stopped (see [picamera2 Issue #1023](https://github.com/raspberrypi/picamera2/issues/1023)).   \nSo, when .mp4 videos have been recorded, the number of threads may not go down to 3 (1 for Bullseye) after all camaras have been closed.   \nExperience shows that such threads may survive for a longer time but typically, they show only minor or no CPU utilization.   \nOften, they vanish after the camera has been closed after live stream has stopped.\n\n**raspiCamSrv** closes the camera in case it is not used:\n\n- When the [live stream](./LiveScreen.md) stops after 10 seconds of inactivity, the camera used for the live stream will be stopped and closed.\n- After [photos have been taken or videos have been recorded](Phototaking.md), the camera will be stopped and closed.\n- For [Photo Series](./PhotoSeries.md), the camera will be stopped and closed after a shot if the interval to the next shot is >60 sec.   \nThis does not apply to [Exposure Series](./PhotoSeriesExp.md) and [Focus Stacks](./PhotoSeriesFocus.md).\n- If [motion detection](./Trigger.md) is active, the live stream is kept activated which keeps the camera open and started.\n- In case of [Stereo Vision](./CamStereo.md), the live streams for both cameras are kept active,\n\n## Detection of Cameras\n\n**raspiCamSrv** uses the ```Picamera2.global_camera_info()``` list (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), ch. 8.7) to identify the currently connected cameras.\n\nFor each camera, the information provided by Picamera2 includes\n\n- ```Num```: The camera number by which a camera is identified within Picamera2 as well as in raspiCamSrv.\n- ```Model```: The model name of the camera, as advertised by the camera driver\n- ```Location```: A number reporting how the camera is mounted, as reported by libcamera.\n- ```Rotation```: How the camera is rotated for normal operation, as reported by libcamera\n- ```ID```: An identifier string for the camera, indicating how the camera is connected. <br>You can tell from this value whether the camera is accessed using I2C or USB.\n\n\n\n## Identification of USB Cameras\n\nA camera is identified as USB camera, if ```usb``` is found in the ```ID```.\n\nIn **raspiCamSrv**, USB cameras are accessed through [OpenCV](https://opencv.org/) rather than through Picamera2, which provides only very limited support for USB cameras.\n\nHowever, with OpenCV, a camera cannot be accessed through the Picamera2 camera number (```Num```).    \nInstead, the ```/dev/videoX``` of the Linux kernel must be used.\n\nFor mapping of the Picamera2 camera number (```Num```) to the device number, **raspiCamSrv** uses the following algorithm:\n\nAssuming that the ```ID``` is structured in the following way:\n\ne.g.:   \n```/base/axi/pcie@1000120000/rp1/usb@200000-2:1.0-046d:085c```\n\n| Component                       | Meaning \n|---------------------------------|------------\n| ```/base/axi/pcie@1000120000``` | Root of the system-on-chip’s PCIe controller\n| ```/rp1/usb@200000```           | The RP1 I/O controller’s USB host controller (i.e. USB root hub)\n| ```-2:1.0```                    | USB device address and interface: port 2, interface 1.0\n| ```-046d:085c```                | Vendor ID : Product ID (046d = Logitech, 085c = C922 Pro Stream Webcam)\n\nNow, with Video for Linux (V4L2), we can list all video devices:\n\n```v4l2-ctl --list-devices``` reveals, for example:\n\n```\n...\nrpi-hevc-dec (platform:rpi-hevc-dec):\n        /dev/video19\n        /dev/media1\n\nLogi 4K Stream Edition (usb-xhci-hcd.0-1):\n        /dev/video2\n        /dev/video3\n        /dev/video4\n        /dev/video5\n        /dev/media4\n\nC922 Pro Stream Webcam (usb-xhci-hcd.0-2):\n        /dev/video0\n        /dev/video1\n        /dev/media3\n \n```\n\nEach header within the list shows the camera's model and port (```(usb-xhci-hcd.0-2)``` indicates port 2)\n\nNow, by mapping model and port from the Picamera2 ```ID``` with corresponding information from ```v4l2-ctl```, we can identify the group for each USB camera.\n\nThe first entry in the list of devices for this group is assigned to the camera number, assuming that this represents the main camera stream, whereas subsequent entries are for alternative functions.\n\n## Determining Camera Properties for USB Cameras\n\nWhereas camera properties of CSI cameras are directly provided by Picamera2 (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), Appendix D), this is not the case for USB cameras.\n\n**raspiCamSrv** maps properties of USB cameras as far as possible to the Picamera2 datastructure for seamless integration of USB cameras.\n\nThe USB camera properties are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --all```\n\ngiving:\n\n```\nDriver Info:\n        Driver name      : uvcvideo\n        Card type        : C922 Pro Stream Webcam\n        Bus info         : usb-xhci-hcd.0-2\n        Driver version   : 6.12.47\n        Capabilities     : 0x84a00001\n                Video Capture\n                Metadata Capture\n                Streaming\n                Extended Pix Format\n                Device Capabilities\n        Device Caps      : 0x04200001\n                Video Capture\n                Streaming\n                Extended Pix Format\nMedia Driver Info:\n        Driver name      : uvcvideo\n        Model            : C922 Pro Stream Webcam\n        Serial           : A9382BFF\n        Bus info         : usb-xhci-hcd.0-2\n        Media version    : 6.12.47\n        Hardware revision: 0x00000016 (22)\n        Driver version   : 6.12.47\nInterface Info:\n        ID               : 0x03000002\n        Type             : V4L Video\nEntity Info:\n        ID               : 0x00000001 (1)\n        Name             : C922 Pro Stream Webcam\n        Function         : V4L2 I/O\n        Flags            : default\n        Pad 0x01000007   : 0: Sink\n          Link 0x0200001f: from remote pad 0x100000a of entity 'Processing 3' (Video Pixel Formatter): Data, Enabled, Immutable\nPriority: 2\nVideo input : 0 (Camera 1: ok)\nFormat Video Capture:\n        Width/Height      : 640/480\n        Pixel Format      : 'YUYV' (YUYV 4:2:2)\n        Field             : None\n        Bytes per Line    : 1280\n        Size Image        : 614400\n        Colorspace        : sRGB\n        Transfer Function : Rec. 709\n        YCbCr/HSV Encoding: ITU-R 601\n        Quantization      : Default (maps to Limited Range)\n        Flags             :\nCrop Capability Video Capture:\n        Bounds      : Left 0, Top 0, Width 640, Height 480\n        Default     : Left 0, Top 0, Width 640, Height 480\n        Pixel Aspect: 1/1\nSelection Video Capture: crop_default, Left 0, Top 0, Width 640, Height 480, Flags:\nSelection Video Capture: crop_bounds, Left 0, Top 0, Width 640, Height 480, Flags:\nStreaming Parameters Video Capture:\n        Capabilities     : timeperframe\n        Frames per second: 30.000 (30/1)\n        Read buffers     : 0\n\nUser Controls\n\n                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128\n                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128\n                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128\n        white_balance_automatic 0x0098090c (bool)   : default=1 value=1\n                           gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=0\n           power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2 (60 Hz)\n                                0: Disabled\n                                1: 50 Hz\n                                2: 60 Hz\n      white_balance_temperature 0x0098091a (int)    : min=2000 max=6500 step=1 default=4000 value=4000 flags=inactive\n                      sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=128 value=128\n         backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=0 value=0\n\nCamera Controls\n\n                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 (Aperture Priority Mode)\n                                1: Manual Mode\n                                3: Aperture Priority Mode\n         exposure_time_absolute 0x009a0902 (int)    : min=3 max=2047 step=1 default=250 value=250 flags=inactive\n     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=1\n                   pan_absolute 0x009a0908 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                  tilt_absolute 0x009a0909 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                 focus_absolute 0x009a090a (int)    : min=0 max=250 step=5 default=0 value=0 flags=inactive\n     focus_automatic_continuous 0x009a090c (bool)   : default=1 value=1\n                  zoom_absolute 0x009a090d (int)    : min=100 max=500 step=1 default=100 value=100\n\n```\n\nBy parsing this information, relevant data for camera properties can be retrieved and mapped to camera property elements.\n\nThe ```PixelArraySize``` is determined as the maximum size of the Sensor Modes found (see [below](#determining-sensor-modes-for-usb-cameras))\n\n\n## Determining Sensor Modes for USB Cameras\n\nWhereas sensor modes of CSI cameras are directly provided by Picamera2 (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), ch. 4.2.2.3), this is not the case for USB cameras.\n\n**raspiCamSrv** maps video formats of USB cameras as far as possible to the Picamera2 sensor mode datastructure for seamless integration of USB cameras.\n\nThe USB camera video formats are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --list-formats-ext```\n\ngiving:\n\n```\nioctl: VIDIOC_ENUM_FMT\n        Type: Video Capture\n\n        [0]: 'YUYV' (YUYV 4:2:2)\n                Size: Discrete 640x480\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n                Size: Discrete 160x90\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n...\n\n                Size: Discrete 2304x1536\n                        Interval: Discrete 0.500s (2.000 fps)\n        [1]: 'MJPG' (Motion-JPEG, compressed)\n                Size: Discrete 640x480\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n...\n\n                Size: Discrete 1920x1080\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n\n```\n\nFrom this output the list of sensor modes is generated with information on Size and Format.\n\nThe FPS, stored for each sensor mode is the maximum value of fps found for each format.\n\n## Determining supported Controls\n\nEvery USB Camera advertises a list of controls which can be used to adjust focus or image appearance.\n\nSupported controls are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --list-ctrls```\n\ngiving:\n\n```\nUser Controls\n\n                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128\n                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128\n                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128\n        white_balance_automatic 0x0098090c (bool)   : default=1 value=0\n                           gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=0\n           power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2 (60 Hz)\n      white_balance_temperature 0x0098091a (int)    : min=2000 max=7500 step=10 default=4000 value=3000\n                      sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=128 value=128\n         backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=1 value=1\n\nCamera Controls\n\n                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 (Aperture Priority Mode)\n         exposure_time_absolute 0x009a0902 (int)    : min=3 max=2047 step=1 default=250 value=312 flags=inactive\n     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=0\n                   pan_absolute 0x009a0908 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                  tilt_absolute 0x009a0909 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                 focus_absolute 0x009a090a (int)    : min=0 max=255 step=5 default=0 value=10 flags=inactive\n     focus_automatic_continuous 0x009a090c (bool)   : default=1 value=1\n                  zoom_absolute 0x009a090d (int)    : min=100 max=500 step=1 default=100 value=100\n```\n\n**raspiCamSrv** analyzes this list with respect to a limited set of controls and registers\n\n- control name\n- value data type\n- minimum value\n- maximum value\n- default value\n\nFor the active camera, this information is appended to the Camera Controls data structure (see [Server Configuration Storage](./SettingsConfiguration.md#server-configuration-storage)), which will also be transferred to the Streaming Configuration in case of multiple cameras.\n\nIn order to establish this data structure, USB cameras need to be activated once as *Active Camera*.\n\nThe controls information is cused to customize the [Camera Controls](./CameraControls_UsbCams.md) screens when a USB camera is the *Active Camera*."
  },
  {
    "path": "docs/Information_CamPrp.md",
    "content": "# raspiCamSrv Info/Camera Properties\n\n[![Up](img/goup.gif)](./Information.md)\n\nThis screen shows properties of the active camera.\n\n![Camera Properties](img/Info-CamProps.jpg)\n\n\nSee also [Determining Camera Properties for USB Cameras](./Information_Cam.md#determining-camera-properties-for-usb-cameras)\n"
  },
  {
    "path": "docs/Information_Sensor.md",
    "content": "# raspiCamSrv Info/Sensor Mode\n\n[![Up](img/goup.gif)](./Information.md)\n\n\nThe camera system advertises the supported Sensor Modes with their characteristics.\n\nThese are referred to within the [Camera Configuration](./Configuration.md).\n\nThe characteristics for every Sensor Mode are shown on an individual tab:\n\n![Sensor Mode](img/Info_SensorMode.jpg)\n\nUSB cameras may advertise a large number of sensor modes. In this case, the remaining buttons for sensor mode selection can be found in a drop-down list:\n![Sensor Mode](img/Info_SensorModeUsb.jpg)\n\n"
  },
  {
    "path": "docs/Information_Sys.md",
    "content": "# raspiCamSrv Info/System Information\n\n[![Up](img/goup.gif)](./Information.md)\n\nThis screen shows information on the Raspberry Pi system as well as on the software stack required by raspiCamSrv.\n\n![System](img/Info-System.jpg)\n\n### Hardware and OS\n\nThis section shows information on the server hardware and operating system\n\n- *Model*   \nRaspberry Pi model frpm ```/proc/device-tree/model```\n- *Board Revision*    \nBoard revision from ```/proc/cpuinfo```\n- *Kernel Version*    \nKernel Version as reported by ```uname -r```\n- *Debian Version*    \nInformation on the operating system:    \n.. *Description* from ```lsb_release -a```   \n.. *Version* from ```/etc/debian_version```   \n.. 32-bit/64-bit from ```dpkg-architecture --query DEB_HOST_ARCH```\n\n### Processes\n\nThis section informs about active processes related to raspiCamSrv.\n\n\n#### Environment\n\nshows whether the raspiCamSrv server process is running\n\n- directly on the \"Host System\"\n- or in a \"Docker Container\"\n\n#### Server Process\n\nshows how the server process has been started:\n\n- \"Server started via systemd system service\"     \nin this case, audio cannot be recorded along with video.\n- \"Server started via systemd user service\"    \nin this case, audio can be recorded along with video.\n- \"Server started via command line\"\n\n#### WSGI Server\n\nraspiCamSrv is based on [Flask](https://flask.palletsprojects.com/en/stable/), which is a WSGI (Web Server Gateway Interface) application.    \nA WSGI server is required to run the application. \n\nThe server which is currently active is shown here.\n\nStandard [raspiCamSrv installations](./installation.md) support the following alternatives:   \n\n- *gunicorn*    \n[Gunicorn](https://gunicorn.org/) is a mature, stable and widely used WSGI server for production use.     \nFor Gunicorn, also the number of threads, configured for the worker process, are shown.     \nThe number of threads limit the number of simultaneous MJPEG streams (See [Gunicorn Settings](./installation_man.md#gunicorn-settings)).\n- *werkzeug*    \n[Werkzeug](https://werkzeug.palletsprojects.com/en/stable/) is the WSGI server integrated in Flask for development and testing purposes.    \nOn start, a warning is shown:   \n```Do not use it in a production deployment. Use a production WSGI server instead.```\n\n#### Process Info\n\nshows current process information for the raspiCamSrv server process (result of Linux ```ps -eLf``` command)\n\n- *PID*   \nProcess ID of WSGI server running Flask.    \nIn case of *werkzeug*, there is just one process.    \nFor *gunicorn*, there are two processes, where the first is the Master process with typically 2 threads which control a single worker process (restricted through ```-w 1``` option at startup) which is running the Flask application.\n- *Start*    \nProcess start time (STIME): either start time (HH:MM) at current day or day (MonDD) when process was started.\n- *#Threads*    \nNumber of threads (NLWP)\n- *CPU Process*    \nCPU time of process (TIME for LWP == PID) in HH:MM:SS\n- *CPU Threads*    \nSum of CPU time for threads ((TIME for LWP != PID)) in %H:MM:SS\n\n#### FFmpeg Info\n\nshows information on an ffmpeg process if encoding of .mp4 videos is currently active.\n\nRecording of .mp4 videos may have been [started manually](./Phototaking.md) or as an action within [motion capturing](./Trigger.md)\n\n#### raspiCamSrv Start\n\nshows the time when the raspiCamSrv server has been started.\n\nAt server start, raspiCamSrv checks whether or not the Raspberry Pi system time is synchronized with the time server.   \nWhen the device is booted and raspiCamSrv is automatically started, the time synchronization will occasionally be done after the Flask server has already been started.    \nIn this case, in order to avoid timing issues, raspiCamSrv will wait at startup until time synchronization is completed.   \nThe time shown here is the system time at the moment when the check for time synchronization was successful.\n\nraspiCamSrv analyzes the output of command ```timedatectl``` to check the system clock synchronization status.    \nIf this check fails or times out (60 sec), raspiCamSrv will start nevertheless.    \nIn this case, the information \"System time not synced at raspiCamSrv start\" will be shown here.\n\nIf the server is running in a Docker container (see [Running raspiCamSrv as Docker Container](./SetupDocker.md)), the time is assumed to be synchronized and the check will be skipped.     \nThis is indicated through     \n![Container](img/Info-Container.jpg)\n\n\n### Software Stack\n\nIn this section, information on installed packages is shown. \n\n- *Ver*    \nis the package version\n- *Loc*    \nis the path from which the packages were loaded.\n\n### Streaming Clients\n\n![Streaming Clients](./img/Info-StreamingClients.jpg)\n\nThe tab lists the clients which are currently using one of the camera streams.   \nAlong with the IP address of the client, a list of streams is shown which the client is using:\n\n- *live_view*<br> [The Live View](./LiveScreen.md) stream<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLiveActive.jpg)\n- *video_feed*<br>The [video Stream](./CamWebcam.md#video-stream) for the active camera<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLiveActive.jpg)\n- *video_feed2*<br>The [video Stream](./CamWebcam.md#video-stream) for the second camera, if available<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLive2Active.jpg)\n\n"
  },
  {
    "path": "docs/LiveDirectControl.md",
    "content": "# raspiCamSrv Live Direct Control\n\n[![Up](img/goup.gif)](./LiveScreen.md)\n\nThis screen is opened by clicking on the [Live Stream area](./LiveScreen.md#accessing-the-direct-control-panel). It allows direct control of selected control parameters.   \nControl parameters with numeric values are accessible if they have been activated on the [Camera Controls](./CameraControls.md) screens.    \nWhen finished with parameter tuning, return to the [Live](./LiveScreen.md) screen or select any other menu option.\n\n![DirctControl](./img/LiveDirectControl.jpg)\n\n## Focal Distance\n\nThe slider for *Focal Distance* is mapped to a range from 0 to 1.\n\nIf you drag the slider to a value below the minimum focal distance, it will snap back to the minimum.\n\nScaling uses an x**3 behavior so that lower values can be selected with higher precision.\n\n## Left and Right Sliders\n\nThe sliders for all parameters are mapped to a range from -1 to 1 with the default at 0, following an x**3 function:\n\n![Scaling](./img/LiveDirectControlSlider.jpg)\n\nTherefore, values closer to the default value can be selected with higher precision.\n\nBecause of the mapping, you will always see the same sliders, independently from the real value ranges which are significantly different for CSI and USB cameras and which can also vary between different USB camera models.\n\nFor some parameters, e.g. *Analogue Gain*, the default value is at the minimum of the parameter range.   \nIn this case, negative slider positions can not be set; the slider will snap back to 0.\n\n## Zoom Factor\n\nThe slider for the *Zoom Factor* directly shows the *Zoom Factor* with linear scaling.\n\nIf you previously have selected a specific image region (Scaler Crop) with the [Zoom and Pan](./ZoomPan.md) dialog, the resulting *Zoom Factor* will be the maximum which ca be set with the slider. Selecting a larger value will always snap back to this maximum.\n\nHowever, you can zoom into this area until the lowest zoom factor is reached.\n"
  },
  {
    "path": "docs/LiveScreen.md",
    "content": "# raspiCamSrv Live Screen\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe **Live** screen is the central part of the application.   \nAfter photos or videos have been taken in the current session, its layout is as shown below:\n\n![Live Screen](img/Live0.jpg)\n\n## Layout\n\n### Top Left Quarter\nThis is the area where a Live stream is shown, except for phases when videos are recorded and when [Configuration](./Configuration.md) for Live View and Video are not compatible..\n\n### Top Right Quarter\nThis area allows selecting and configuration of all [Camera Controls](./CameraControls.md) supported by Picamera2. These are parameters which affect the characteristics of images and outputs of the camera and which can be modified while the camera is running.   \nThe menu row of this section groups the controls into several categories.\n\n### Bottom Left Quarter\nThe bottom part of the screen is only shown if a Photo or video has been taken within the server's life time and if the user did not decide to hide this area.\n\nThe bottom left quarter presents function buttons for [Photo/Video taking](./Phototaking.md)   \nIn addition, there are also buttons controlling the photo buffer to which users can add or remove individual photos and navigate between them.\n\nRaw photos or videos are not shown directly. Instead a placeholder in the configured photo format is shown.\n\n### Bottom Right Quarter\nHere, the metadata of the currently visible photo/video are shown.\nThe metadata are captured within the same **Capturing Request** together with the photo itself.   \nIn the case of videos, the metadata are captured immediately before recording starts.\n\nAlternatively to metadata, the histogram of the photo can be shown.\n\n## Accessing the Direct Control Panel\n\nFor fine tuning all numeric control parameters (e.g. *Focal Distance*, *Zoom Factor*, *Contrast*, etc.), you can use the [Direct Control Panel](./LiveDirectControl.md).\n\nWhen hovering with the mouse over the Live Stream area, you will get a hint:\n\n![Direct Control hint](./img/LiveDirectControlOpen.jpg)\n\nBefore clicking on the Live Stream, you will need to activate those control parameters which you want to adjust:\n\n- [Focal Distance](./FocusHandling.md)\n- [Zoom Factor](./ZoomPan.md) (does not require activation)\n- [Exposure Time](./CameraControls_Exposure.md)\n- [Exposure Value](./CameraControls_Exposure.md)\n- [Analogue Gain](./CameraControls_Exposure.md)\n- [Colour Gain](./CameraControls_Exposure.md)\n- [Sharpness](./CameraControls_Image.md)\n- [Contrast](./CameraControls_Image.md)\n- [Saturation](./CameraControls_Image.md)\n- [Brightness](./CameraControls_Image.md)\n\nSome of these parameters might not be available if the Active Camera is a USB camera.\n\nFurthermore, if you want to restrict to a specific image section, you need specify this on the [Zoom](./ZoomPan.md) window first.   \nThe [Direct Control Panel](./LiveDirectControl.md) allows only zooming into that window but not changing the window itself.\n"
  },
  {
    "path": "docs/PhotoSeries.md",
    "content": "# raspiCamSrv Photo Series\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\n\nThe *Photo Series* screen allows the management of different kinds of Photo Series and includes means for series configuration, lifecycle management, photo shooting and supervision.   \nA series is a sequence of photos taken with specific time intervals. Special kinds of series are [Timelapse Series](./PhotoSeriesTimelapse.md), [Exposure Series](./PhotoSeriesExp.md) and [Focus Stacks](./PhotoSeriesFocus.md).\n\n![Photoseries Screen](img/Photoseries2.jpg)\n\n## Creation of a new Series\n\nWhen the *Photo Series* screen is opened for the first time, it offers the option to create a new series:\n\n![New Series](img/Photoseries0.jpg)\n\nYou need to enter a unique name for the series. Since the name will be used as folder name and as part of the filename for photos, you need to consider any restrictions for systems where you want to store and process these files.\n\nLinux is quite tolerant in this aspect but it is recommended using only letters, numbers and underscore characters.\n\n## Series Configuration\n\nWhen a series is initially created, some parameters are predefined which later need to be configured:\n\n![Series Config](img/Photoseries1.jpg)\n\n- The lifecycle of a Photo Series is represented as its **Status**.   \nTransitions between different states can be initiated by one or two buttons at the right of the series selection combo box.   \nFor details see the [Series State Chart](#photo-series-state-chart).\n- If multiple series have been created, the **active series** can be selected with a combo box showing the series names.\n- The *Series Type* distinguishes \"Normal\" series without special characteristics from specialized series, such as \"Exposure Series\", \"Focus Stacks\" or \"Timelapse Series\".\n- The *Path* is the path where all resources for the series are located.   \nFor details see [Photo Series in the File System](#photo-series-in-the-file-system)\n- The system has also initialized a [Series Configuration File](#series-configuration-file) a [Series Log File](#series-log-file) and a [Camera Settings File](#series-camera-file).\n- In addition, a \"hist\" subdirectory has been created where histogram images will be stored (currently only for *Exposure Series*).\n- Under *Photo Type*, it can be selected whether only ```jpg``` or ```raw+jpg``` photos shall be taken.\n- The *Start* time is initiated with the current time.   \nThis needs to be set to the time when the series shall start.\n- The *End* time can be set explicitly if a specific end time is required.   \nIf this is done, the number of shots will be calculated based on the specified *Interval*\n- As *Interval* , the time difference (in seconds) between successive shots can be specified.   \nFrom experience, the system will observe the given value within a tolerance of about 30 ms.\n- *On Dial Marks* specifies whether the shots shall be taken on whole hours, quarters, minutes, ..., depending on the intarval.<br>For example, if the interval is 900 sec, photos will be taken exactly (within tolerances) at :00, :15, :30, :45, or if the interval is 3600 sec, photos will be taken every full hour.\n- The *Number of Shosts* specifies the numper of photos intended for the series.   \nIf the *End* time has not been explicitly specified, it will be calculated from *Interval* and *Number of Shots* considering the specified *Start* time.\n- The checkbox *Cont. on Server Start* allows to automatically continue an active series in case of a server restart.    \nSuch a situation may happen if the server is stopped (explicitly or implicitly with a device shutdown) while a series is active.    \nFor example, if you have a long running timelapse series, there might be power outages forcing a system reboot. If you have set the series to automatic continuation it will be continued as soon as the server is restarted. Otherwise, it will be in status PAUSED.   \nAutomatic continuation is not used for [Exposure Series](./PhotoSeriesExp.md) or [Focus Stack](./PhotoSeriesFocus.md) series because these series are typically not running for a longer time.\n\nAfter the values have been entered, pressing the *Submit* button will calculate dependent parameters an change the [status](#photo-series-state-chart) of the series to \"READY\".\n\n**For Photo Series of type [Exposure Series](./PhotoSeriesExp.md), [Focus Stack](./PhotoSeriesFocus.md) and [Timelapse Series](./PhotoSeriesTimelapse.md), additional configurations are required.**\n\n## Series Start\n\nA series in state \"READY\" can be started with the *Start* button.   \nThis will execute the following steps:\n\n1. Set the status to \"ACTIVE\"\n2. Configure the camera with the active [Configuratien](./Configuration.md) for \"Photo\" or \"Raw Photo\", depending on the selected *Photo Type*\n3. Apply the active [Camera Controls](./CameraControls.md)<br>For [Exposure Series](./PhotoSeriesExp.md) and [Focus Stack](./PhotoSeriesFocus.md), specific controls will be adjusted or varied for each photo.\n4. Start the camera\n5. Wait until the start time\n6. Execute the necessary capture request (jpg, dng, metadata)\n7. Store the metadata in the [Series Log File](#series-log-file)\n8. Store the camera configuration as well as the controls parameters, which have been applied before request execution, in the [Camera Settings File](#series-camera-file)\n9. Wait until the next interval and repeat steps from 6. until either the configured *Number of Shots* or the configured *End* time has been reached.\n10. Finally, the series status will be \"FINISHED\".\n\nWhile the series is \"ACTIVE\", this is shown by the Series status indicator and the screen will show the progress:\n\n![Series Progress](img/Photoseries2.jpg)\n\nIn the *Preview* area, the time for the next photo to be taken is shown and the progress bar shows the time remaining.\n\nWhen the photo time is reached, the page will be reloaded and the latest photo will be shown on top. The last 20 photos are available in the scroll area.\n\n## Downloading a Series\n\nA series can be downloaded at any time after it has been created.\n\nWhether or not a series has been downloaded is shown under *Downloaded* which is either \"Never\" or the time of the last download.   \n\nPushing the *Download* button will require a confirmation before download will be executed.\n\nThe download will be named ```raspiCamSrvSeries_<name>_<YYYYMMDDHHMMSS>``` with the timestamp of the download.\n\nThe download is a zip archive including the entire folder structure of the series (see [Photo Series in the File System](#photo-series-in-the-file-system)):\n\n- All photos taken until the time of download\n- The [Series Cofiguration file](#series-configuration-file)\n- The [Series Camera File](#series-camera-file) (which will be empty if no camera configuration has been attached to the series)\n- The [Series Log File](#series-log-file) (which will be empty if the series has not yet been started)\n- A subfolder ```hist``` containing histograms, in case the series has been an [Exposure Series](./PhotoSeriesExp.md)\n\n\n## Finished Series\n\nWhen a series has ended or after it has been actively finished with the *Finish* button, its status is shown as \"FINISHED\":\n\n![Series End](img/Photoseries2b.jpg)\n\nIf the series has been downloaded after it had ended, can be seen by comparing the respective timestamps.\n\n## Live Stream\n\n### Active Live Stream\n\nIf the [Configuration](./Configuration.md) for the *Photo* and *Raw Photo* use cases are compliant with the configuration for *Live View*, the Live Stream will not be interrupted while the series is ACTIVE.    \nFor more details, see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md)\n\nSimultaneous activity of Live Stream and Photo Series is indicated by the process status indicators:\n\n![Process Status Indicators](./img/ProcessIndicator4.jpg)\n\nIf the series is an [Exposure Series](./PhotoSeriesExp.md) or a [Focus Stack](./PhotoSeriesFocus.md) Series, [Camera Controls](./CameraControls.md) will be modified while the series is active.    \n*Auto Exposure* and *Auto White Balance* will be deactivated and other parameters, such as *Exposure Time*, *Analogue Gain* or *Focal Distance*/*Lens Position* will vary from photo to photo.   \nThese variations will be visible in the Live Stream.\n\nAfter the series is FINISHED, the original control parameters will be restored.\n\n### Paused Live Stream\n\nIf the Live Stream is paused because photo taking requires exclusive camera access because of specific [Configuration](./Configuration.md) (see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md)), this is indicated by the process status indicators:\n\n![Process Status Indicators](./img/ProcessIndicator6.jpg)    \nand a placeholder image will be schown instead of the Live Stream:\n\n![Photoseries Active](img/Photoseries3.jpg)\n\n\n## Interrupting an ACTIVE Series\n\nWhile a Photo Series is ACTIVE, photo- and video taking is disabled:\n\n![Photoseries Active](img/Photoseries3b.jpg)\n\nThe thread in which the Photo Series is executed, checks every 2 seconds whether a request to pause or stop has been issued.\n\nIf the series shall be paused and (possibly) continued later, the *Pause* button in the *Series* screen can be used.\n\nIf the series shall be interrupted and terminated, the *Finish* button can be used.\n\n## Attaching Camara Configuration to a Photo Series\n\nBefore a Photo Series is started, normally the camera [configuration](./Configuration.md) and [controls](./CameraControls.md) will manually be adjusted for optimal photo quality.\n\nThese settings (configuration **and** controls) can be attached to a Photo Series with the *Attach Camera Config* button.\n\nThese will be persisted in the [series configuration file](#series-configuration-file)\n\nIf this has been done, the system offers to activate these settings at a later time:\n\n![Activate Config](img/PhotoSeries4.jpg)\n\nSo, if another series shall be run with the same or a similar setup, the camera configuration and control settings can be reused after they have been activated.\n\nThis feature can also be used to persist specific settings under an indicative name (the series name), also if it is not intended to really run such a series.\n\n\n## Photo Series State Chart\n\n![StateChart](img/PhotoSeriesStateChart.jpg)\n\n## Photo Series in the File System\n\nAll resources related to a Photo Series are stored in the file system under    \n```/home/<user>/prg/raspi-cam-srv/RaspiCamSrv/static/photoseries/<name>```   \nwhere ```<user>``` is the user ID specified during [system setup](./system_setup.md) and ```<name>``` is the name of the series.\n\nAfter [Creation of a new Series](#creation-of-a-new-series), the folder has been created and a [Series Configuration File](#series-configuration-file), a [Series Log File](#series-log-file) and a [Series Camera File](#series-camera-file) have been initiated:\n\n![Series in FS](img/PhotoSeries5.jpg).\n\nAfter the series has been [started](#series-start), also all photos (.jpg) and, if selected, also the raw photos (.dng) can be found in this folder:\n\n![Series in FS](img/PhotoSeries6.jpg).\n\nTypically, photo series will be processed on another system, especially if they have been taken with a Raspberry Pi Zero system.   \n**RaspiCamSrv** does not provide any means to download or transfer these data. There are numerous tools to achieve this (e.g. scp, Samba)\n\n### Series Configuration File\n\nThe file ```<name>_cfg.json``` contains the entire configuration of a series, including, if attached, the camera configuration and camera controls.\n\n![Series Configuration File](img/PhotoSeries7.jpg)\n\nWhen the server starts up, all folders under ```.../photoseries``` are searched for a configuration file and series configurations are created from their contents. These will then be available in the **raspiCamSrv** *Photo Series* dialog.\n\n### Series Log File\n\nThe file ```<name>_log.csv``` contains log entries for each photo of the series:\n\n![Series Log File](img/PhotoSeries8.jpg)\n\nBisides the name of the photo and the time of creation, the most important metadata are included which have been captured in the same request as the Raw Photo and / or jpg Photo.\n\n### Series Camera File\n\nThe file ```<name>_cam.json``` contains a JSON structure with the [camara configuration](./Configuration.md) and the [camera controls](./CameraControls.md) applied for each photo of the series. This is available only for [Exposure Series](./PhotoSeriesExp.md) and [Focus Stack Series](./PhotoSeriesFocus.md).\n\n![Camera](img/PhotoSeriesExp3.jpg)"
  },
  {
    "path": "docs/PhotoSeriesExp.md",
    "content": "# Photo Series of type \"Exposure Series\"\n\n[![Up](img/goup.gif)](./PhotoSeries.md)\n\n\nExposure series iterate through a specified range of an exposure parameter, keeping all other exposure parameters constant.   \nIn general, exposure is controlled by three parameters: aperture, exposure time and ISO value.   \n\nRaspberry Pi cameras have a fixed or manually controlled aperture and ISO values are not standardized.   \nInstead of ISO values, the Analogue gaing can be set. Roughly, the relation is *ISO* = 100 * *AnalogueGain*.\n\nThe *Exposure Series* subdialog allows specifying necessary parameters for series where either *Exposure Time* or *Analogue Gain* is varied.\n\n**NOTE**: This function is not available for USB cameras.\n\n![Exposure Series](img/PhotoSeriesExp1.jpg)\n\nThe dialog references the active Photo Series which is managed in the [Series](./PhotoSeries.md) subdialog of the *Photo Series* dialog.\n\nThe *Number of Shots* is shown here, because it will be affected by the chosen *Start*, *Stop* and *Interval* values.\n\nTo configure the active Photo Series as *Exposure Series*, proceed as follows\n\n1. Activate the *Exposure Series* checkbox\n2. Select the exposure parameter which shall be kept at a fixed value by checking one of the check boxes under *Exposure Time* or *Analogue Gain*\n3. For the selected parameter, enter the intended value in the *Start* field.\n4. Now, specify *Start* and *Stop* values for the variable parameter\n5. The interval is specified in terms of photographic [Exposure Values](https://en.wikipedia.org/wiki/Exposure_value) (EV)<br>With a value of 1/3 EV the series is obtained by multiplying the last value of *Exposure Time* or *Analogue Gain* by 2**(1/3) to get the next value.<br>With a value of 1 EV, the factor is 2**(1)=2<br>and with 2 EV, the factor is 2**(2)=4.<br>1/3 EV is the typical raster value for commercial cameras when modifying either aperture, exposure time or ISO.\n\nFinally push the **Submit** button to store the specified value.   \nThis will recalculate the *Number of Shots* required for the series.\n\nTo start photoshooting, go to the [Series](./PhotoSeries.md) subdialog \n\n## Result\n\nAfter the series has finished, the results can be inspected on the *Exposure Series* subscreen:\n\n![Exposure](img/PhotoSeriesExp2.jpg)\n\nTogether with each photo, the screen shows a histogram and characteristic metadata:\n- Exp: Exposure Time in seconds\n- Gain: Analogue Gain\n- Lux: An estimation of the brightness\n\n\nMore information can be gained from \n- the [Series Camera File](./PhotoSeries.md#series-camera-file) which lists the configuration and control parameters applied before shooting a photo\n- the [Series Log File](./PhotoSeries.md#series-log-file) which lists the metadata captured together with each photo.\n\n\n## Parameter Table (1/3 EV)\n\nThe following table contains  systematic values for Exposure Time and Analogue Gain with 1/3 EV, corresponding roughly to commercial camera settings\n\n![1/3EV](img/PhotoSeriesExpTab1_3.jpg)\n\n## Parameter Table (1 EV)\n\nThe following table contains  systematic values for Exposure Time and Analogue Gain with 1 EV, corresponding roughly to commercial camera settings\n\n![1/3EV](img/PhotoSeriesExpTab1.jpg)\n\n\n"
  },
  {
    "path": "docs/PhotoSeriesFocus.md",
    "content": "# Photo Series of Type \"Focus Stack\"\n\n[![Up](img/goup.gif)](./PhotoSeries.md)\n\n\nA Focus Stack series iterates the Lens Position (or Focal Distance).   \nWith suitable software, such a stack can be combined to achieve a large Depth of Field (DoF).    \n**NOTE**: This function is not available for USB cameras.\n\n![Focus Stack](img/PhotoSeriesFoc1.jpg)\n\nTo create a focus stack, you can proceed as follows:\n\n1. In the [Live Screen](./LiveScreen.md) use [Focus Handling](./FocusHandling.md) to determine the Focal Distance for the nearest and the furthest point of the scene.\n2. After initializing a Photo Series in the [Series](./PhotoSeries.md) subscreen, open the *Photo Stack* subscreen and check *Focus Stacking Series*\n3. Then enter the nearest and furthest Focal Distance as *Start* and *Stop* values and choose a suitable *Interval*\n4. Push **Submit** to configure the series\n5. In the [Series](./PhotoSeries.md) subscreen, start the Photo Series\n\n\nThe result will be shown in the *Focus Stack* subdialog:\n\n![Fochs Stack Final](img/PhotoSeriesFoc2.jpg)\n\nTogether with each photo, characteristic metadata are shown:\n\n- The *Lens Position* with which the photo was taken<br>(reciprocal of Focal Distance)\n- The *Focal Distance* which was varied within the series\n- The *Focus FoM*, a Figure of Merit (FoM) to indicate how in-focus the frame is. A larger FocusFoM value indicates a more in-focus frame.\n\nMore information can be gained from \n\n- the [Series Camera File](./PhotoSeries.md#series-camera-file) which lists the configuration and control parameters applied before shooting a photo\n- the [Series Log File](./PhotoSeries.md#series-log-file) which lists the metadata captured together with each photo.\n"
  },
  {
    "path": "docs/PhotoSeriesTimelapse.md",
    "content": "# Photo Series of Type \"Timelapse\"\n\n[![Up](img/goup.gif)](./PhotoSeries.md)\n\nThis screen allows special configurations for Photo Series in the Timelapse domain.    \nOf course, every normal Photo Series can be used for Timelapse purposes. However, users often require specific features like specific time slots for multi-day series or automatic exposure adjustment during sunset/sunrise phases (Autoramping / \"Timelapse Holy Grail\").\n\nThis page is dedicated to this kind of configuration settings.   \nCurrently, raspiCamSrv supports the following *Sun-control Mode*s:\n\n- *Sunrise/Sunset*\n<br>limiting photo shooting to configurable periods depending on sunrise and sunset.\n- *Azimuth*\n<br>taking photos at specific azimuth values over a period of days so that photos are taken with the same horizontal direction of sun.\n\nUsage of this feature requires calculation of sunrise and sunset, depending on date.    \nThe algorithm (see [Sunrise Equation](#sunrise-equation)) requires information about the geografic coordinates of the camera position.   \nThese need to be specified on the [Settings](./Settings.md) screen before a Series can be classified as \"Sun-controlled\".   \nIt is recommended to [store the configuration](./SettingsConfiguration.md#server-configuration-storage) in order to have these settings available after a server restart.\n\n## Sunrise/Sunset Mode\n\nWhen this screen is activated after a [new Series](./PhotoSeries.md#creation-of-a-new-series) has been created, it will be initialized with *Sunrise/Sunset* mode:\n\n![Timelapse1](./img/PhotoSeriesTL1.jpg)\n\n- The fields **Series**Name, **Interval** and **Number of Shots** refer to the same parameters as screen [Series](./PhotoSeries.md).\n- Activating the checkbox **Sun-controlled Series** will activate selective photo shooting in periods depending on sunrise and/or sunset.\n- You can specify the **Number of Days** for which the series shall be active\n- The fields **Sunrise** and **Sunset** will show values for the current day.<br>If the series will be running for several days, sunrise and sunset will be calculated individually for every day.\n- The system allows the definition of two periods per day during which photos will be shot with the given **Interval**:<br>These are named **Period 1** and **Period 2**\n- At least for **Period 1**, you need to specify **Start** and **End**<br>If only one is specified, the system reports an error and does not persist the specified data.<br>**Start** and **End** are specified if the **Reference** is not \"Unused\".\n- The **Reference** specifies whether \"Sunrise\" or \"Sunset\" will be used to limit the intended period.\n- For each **Period**s **Start** and **End**, you can specify a time **Shift** in Minutes by which the start or the end of the period will be shifted with respect to the selected **Reference**.<br>The **Shift** can be positive or negative.\n- After **Submit**, the system will calculate **Todays Values** for **Start** and **End**.\n- Also the time for the **Next Shot** will be shown.\n\nWhen submitting entries with a reasonable interval and Number of Days, the system will recalculate the required **Number of Shots** as well as the expected **End** of the Series, shown on the [Series](./PhotoSeries.md) screen.\n\n![Timelapse2](./img/PhotoSeriesTL2.jpg)\n\n### Example: Single Period for Daylight Photos\n\n![Timelapse3](./img/PhotoSeriesTL3.jpg)\n\n### Example: Two Periods around Sunrise and Sunset\n\n![Timelapse4](./img/PhotoSeriesTL4.jpg)\n\n## Azimuth Mode\n\nWhen choosing *Sun-Control Mode* \"Azimuth\", the screen layout will change to\n\n![Timelapse5](./img/PhotoSeriesTLAzimuth1.jpg)\n\n- *Time for Azimuth*\n<br>can be used to calculate the azimuth at a specific time.\n<br>When its value is not explicitely set, it will be updated with the current date/time. \n- *Azimuth [°]*\n<br>is the azimuth at the given time\n- *Elevation [°]*\n<br>is the elevation of the sun above horizon at the given time\n- *Azimuth 1*, ... *Azimuth 4*\n<br>Here, you can specify up to 4 azimuth values for which photos shall be taken at every day within the specified number of days from series start.\n<br>For each value the *Todays Time* at the current day will be calculated when the sun position will have this azimuth.\n<br>The system will also verify that azimuth values are valid for the entire period of the series. If this is not the case, an error message will be shown.\n<br>When entering more than one azimuth value, these will be sorted with increasing times.\n\n\n![Timelapse6](./img/PhotoSeriesTLAzimuth.jpg)\n\n### Series Log File\n\nFor photo series using *Azimuth* Mode, the [Series Log File](./PhotoSeries.md#series-log-file) will include the Azimuth value for each photo:\n\n![Timelapse7](./img/PhotoSeriesTLAzimuthLog.jpg)\n\n\n\n## Sunrise Equation\n\nThe algorithm for the sunrise/sunset equation has been taken from Wikipedia:   \n[https://en.wikipedia.org/wiki/Sunrise_equation](https://en.wikipedia.org/wiki/Sunrise_equation)\n\nThis article also publishes Python code which has been taken as is (version from August 11, 2024, 14:18) and integrated with minor technical adjustments into the RaspiCamSrv Flask server code.\n\nComparison of the results from this algorithm with those from the \"NOAA Solar Calculator\" ([https://gml.noaa.gov/grad/solcalc/](https://gml.noaa.gov/grad/solcalc/)) for the time of writing at the author's location showed a deviation of -2 Minutes for sunrise and +3 Minutes for sunset.   \nHowever, the NOAA Calculator does not seem to take elevation into account."
  },
  {
    "path": "docs/PhotoViewer.md",
    "content": "# raspiCamSrv Photo Viewer\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nAll photos, raw photos or videos taken wit **raspiCamSrv** are stored in a camera-specific folder on the server.\n\nCurrently, the folder is located within the folder where Flask expects static content    \n(```~/home/prgraspi-cam-srv/raspiCamSrv/static/photos/camera_n``` (n=0, 1)).   \nThe full path of the folder for the active camera is shown in the [Settings](./Settings.md) screen.\n\nThe current implementation of **raspiCamSrv** includes a very simple viewer which allows inspecting the available photos and videos as well as downloading and deleting selected files:\n\n![Photos](img/Photos.jpg)\n\nOn the left side, a selection of photos (.jpg placeholders for raw and video) are shown in a scroll area in reverse order with the newest one on top.\n\nThe file name in the photo (or placeholder) shows the correct filename of the resource represented by the picture.\n\nA large view of the photo or a video player is presented when a specific picture has been clicked on.\n\n- You need to select the **Camera** for which photos shall be shown.<br>In systems with multiple cameras, photos taken with a camera are stored in a camera-specific folder.\n- **From** and **To** date selectors allow restricting photos to a specific range of dates<br>When initially starting the dialog, the current day is selected.<br>Internally, **From** has time 00:00:00 and **To** 23:59:59.\n- Button **Today** restricts the time range to the current date\n- Button **All** sets **From** to January 1st, 1970 and **To** to today.\n- On the left of each thumbnail picture, there is a **checkbox** where you can select photos or videos for download or for deletion.\n- Buttons **Select all** and **Deselect all** apply to all photos currently shown in the scrolling area.\n- With button **Delete** you can delete all selected photos<br>Before deletion is executed, a confirmation is required.<br>If a specific media (e.g. video or raw photo) incudes the media file itself, a jpg placeholder and an optional histogram file, all are deleted.<br>Deletion of photos also clears the [Photo Display Buffer](./Phototaking.md#photo-display).\n- With the **Download** button, you can download the selected files.<br>Also here, a confirmation is required.<br>If more than one file has been selected, the selected files will be zipped into a file named *raspiCamSrvMedia_YYYYMMDD_HHMMSS.zip*<br>If a single file is selected, it will be downloades as is.<br>Placeholders for raw and videos as well as histogram are not included in the download."
  },
  {
    "path": "docs/Phototaking.md",
    "content": "# raspiCamSrv Photo Taking and Video Recording\n\n[![Up](img/goup.gif)](./LiveScreen.md)\n\nThe *Live* tab of **raspiCamSrv** provides functionality to take photos, raw photos and videos.\n\nIn all cases, the predefined [Camara Configuration](./Configuration.md) for the specific use case is applied together with the currently activated [Camera Controls](./CameraControls.md).\n\nAs long as no photo has been taken, the *Live* screen will show like below:\n\n![Foto0](img/Foto0.jpg)\n\nNow, you can use\n\n- the **Photo** button to take a photo,   \nwhere the file type can be selected in the [Settings](./Settings.md) screen. (\"jpg\" is recommended)\n- the **Raw** button to take a raw photo.   \nCurrently only the *.dng* format is supported.\n- the **Video** button to record a video,   \nwhere for the video format you may choose between *.mp4* and *.h264* in the [Settings](./Settings.md). Recommended is *.mp4*\n\n## Photo / Raw Photo\n\nIf the *Photo*/*Raw Photo* [Configuration](./Configuration.md) is not compliant with the *Live View* configuration, the live stream will be shortly interrupted in order to allow the system to apply the [Camera Configuration](./Configuration.md) for these use cases.   \nFor more details, see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md)\n\nThe photo (in the case of raw photos a placeholder photo) will be shown in the bottom area of the screen together with the Metadata.\n\n## Video\n\n### With active Live Stream\n\nWhen video recording is started, the system will first check whether the required [Camera Configuration](./Configuration.md) is compliant with the configuration of the active Live Stream.\n\nIf this is the case, first a normal photo of the scene will be taken, which will be used as placeholder. This will be shown in the bottom area.\n\n![Video1](img/Video1.jpg)\n\nThe **Video** button has changed to **Stop** which must be used to stop video recording.\n\nIf audio is recorded along with video (see [Settings](./Settings.md#recording-audio-along-with-video)), this will be indicated by the process indicator:\n\n![Processindicator](./img/ProcessIndicator22.jpg)\n\n### With paused Live Stream\n\nIf video recording requires exclusive camera access because of specific configuration (see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md)), the live stream will be paused and a placeholder is shown instead:\n\n\n![Video1](img/Video2.jpg)\n\n## Photo Display\n\nWhen a photo, raw photo or video has been taken, the bottom area will show the photo together with its Metadata or its histogram:\n\n![Foto1](img/Foto1.jpg)\n\nThe file name for the photo, raw photo or video has been generated automatically based on system date/time when the request is issued to the camera.\n\nAbove the photo, some buttons are shown which allow manipulation of a display buffer:\n\n![PhotoBuffer0](img/FotoBuffer0.jpg)\n\n- Pressing the **-** button will remove the photo from display.   \nIt will not be removed from the file system.\n\n- The active **+** button shows that the photo is not yet a member of the display buffer.   \nPressing this button will add it to the buffer.\n\nThis is shown as:\n\n![PhotoBuffer1](img/FotoBuffer1.jpg)\n\nThe indicator (x/y) above the **+** button shows the position of the Photo within the buffer as the left number and the total number of photos in the buffer as the right number.\n\nWith the larger number of photos within the buffer, navigation buttons **<** and **>** allow navigation within the buffer:\n\n![PhotoBuffer3](img/FotoBuffer3.jpg)\n\nAdditional buttons are available:\n\n- **Hide** / **Show** can be used to hide or show the bottom area with the display buffer\n- **Clr(x)** will clear the entire buffer.   \nThe number in brackets is the total number of elements in the buffer.\n- **<** will display the previous photo in the buffer\n- **>** will display the next photo within the buffer\n- **-** when applied to a member of the buffer, will remove it from the buffer\n\n## Metadata\n\nAlong with a photo, also its metadata will be shown.\n\nMetadata and photo have been captured within the same capturing request.\n\nFor the metadata, tooltips on the metadata properties explain the respective parameter.\n\n**Previous** and **Next** buttons allow scrolling the list of metadata if it does not fit on the screen.\n\n![Metadata1](img/Metadata1.jpg)\n\n![Metadata2](img/Metadata2.jpg)\n\n## Histogram\n\nAlternatively to the metadata, you may also switch to Histogram if usage of histograms has been activated in the [Settings](./Settings.md).\n\n![Histogram](img/MetaHistogram.jpg)\n\nThe setting whether metadata or histogram are shown, is kept and remains active also when the next photo is taken or when scrolling through the Photo Buffer.\n\nHistogram graphics are calculated on the fly when they are requested for the first time.    \nThey are stored in a \"hist\" subfolder under the *Path for Photos/Videos* (see [Settings](./Settings.md))"
  },
  {
    "path": "docs/ReleaseNotes.md",
    "content": "# Release Notes\n\n[![Up](img/goup.gif)](./index.md)\n\n## V4.10.0\n\n### New Features\n\n- [Photo Series of Type \"Timelapse\"](./PhotoSeriesTimelapse.md) were extended with a new [Azimuth Mode](./PhotoSeriesTimelapse.md#azimuth-mode). This allows taking multi-day photo series where photos are taken at times when the sun position has specific azimuth values.\n- Extended capabilities of ```GET api probe``` [API](./API.md) WebService endpoint:    \nNow, the ```PhotoSeriesCfg``` object and its properties are accessible which represents the settings for photo series.\n- [Information / Software Stack](./Information_Sys.md) now also shows version of libcamera.\n- A tutorial has been added which shows how to enable [Neural Network-based Automatic White Balance](./tutorials/AWB_with_neural_networks.md)\n\n### Bugfixes\n\n- Empty *ID* is no longer possible when creating a new [Device](./SettingsDevices.md)\n- Fixed error ```AttributeError: 'Picamera2' object has no attribute 'allocator'``` which could occur when the Flask server was started or restarted with an active [Photo Series](./PhotoSeries.md) for which *Cont. on Server Start* was active.\n- Fixed errors which could occur when creating Histograms ([Photo Taking on Live screen](./Phototaking.md#histogram) or [Exposure Series](./PhotoSeriesExp.md))\n<br>This resolves [raspi.cam-srv Issue #89](https://github.com/signag/raspi-cam-srv/discussions/89).\n\n## V4.9.0\n\n### New Features\n\n- New [Actions](./TriggerActions.md) can now be immediately [tested](./TriggerActions.md#testing-an-action) before they are used in a [Trigger assignment](./TriggerTriggerActions.md) or in an assignment to an [Action Button](./SettingsAButtons.md) or a [Live Button](./SettingsLButtons.md).\n- [Device Type Configuration](./SettingsDevices.md#device-type-configuration) for [gpiozero Output Devices](https://gpiozero.readthedocs.io/en/stable/api_output.html#regular-classes) has been extended to allow [Action Configuration](./TriggerActions.md) for more action methods:\n<br>LED   : toggle, blink, value\n<br>PWMLED: toggle, blink, pulse, value\n<br>RGBLED: on, off, toggle, blink, pulse\n<br>Buzzer: toggle, beep, value\n<br>Motor : reverse\n\n## V4.8.0\n\n### New Features\n\n- Added [Ctrl Pane](./CameraControls_Ctrl.md) to [Live Screen](./LiveScreen.md) for which you can [configure buttons](./SettingsLButtons.md) for execution of OS commands or [Actions](./TriggerActions.md) for control of various devices such as servos for Pan/Tilt control.\n- Added [ServoPWM](./gpioDevices/ServoPWM.md) as new [Device Type](./SettingsDevices.md).\n<br>This can be used as alternative to [gpiozero Servo](https://gpiozero.readthedocs.io/en/stable/api_output.html#servo), which uses software PWM (pigpio is currently not available under Trixie) resulting in significant jitter and is, therefore, not suitable to be used as device for camera positioning.\n<br>ServoPWM is based on the [rpi_hardware_pwm](https://github.com/Pioreactor/rpi_hardware_pwm) library and assures jitter-free servo control.\n- The [installation](./installation.md) has been extended with installation of [rpi_hardware_pwm](https://github.com/Pioreactor/rpi_hardware_pwm) required for [ServoPWM](./gpioDevices/ServoPWM.md)\n- Git now ignores a folder ```prg/raspi-cam-srv/user_code``` and any sub-folders.\n<br>This allows putting any bash scripts or python programs in this location for use with [Versatile Buttons](./SettingsVButtons.md) or [Live Buttons](./SettingsLButtons.md).\n\n### Bugfixes\n\n- Fixed ```TypeError: object of type 'CompletedProcess' has no len()``` which could occur when the server was restarted with button *Restart Server* in [Settings / Configuration](./SettingsConfiguration.md)\n- Fixed command execution from [Interactive Commandline](./ConsoleVButtons.md#interactive-commandline): In some situations it could happen that after command execution a different dialog was shown.\n- Created clearer error message in [Settings/Devices](./SettingsDevices.md) when test or calibration was started while [Event Handling](./TriggerControl.md) is active. Active event handling may have exclusive access on devices.\n- If a [Device Configuration](./SettingsDevices.md) has been modified while [Event Handling](./TriggerControl.md) is active, a hint to restart Event Handling is now displayed.\n- Fixed missing values for *Number of Rows* and *Number of Columns* in dialog [Settings/Action Buttons](./SettingsAButtons.md)\n- Fixed error ```Error in None: Error <class 'TypeError'> while executing action STP1-0: StepperMotor.rotate_to() got an unexpected keyword argument 'angle'```\n\n## V4.7.1\n\n### Bugfix\n\n- Fixed issue with checkboxes in dialog [Photos](./PhotoViewer.md).\n<br>For images where the descriptive text (filename) was larger than the image width, the checkbox was covered and could not be individually selected.\n<br>Resolves [raspi-cam-srv Issue #86](https://github.com/signag/raspi-cam-srv/issues/86) \n\n## V4.7.0\n\n### New Features\n\n- As an alternative to the Flask buil-in WSGI server (werkzeug), for publicly accessible systems, now [Gunicorn](https://gunicorn.org/) ('Green Unicorn') is supported.     \nGunicorn is now default for the [Automatic Installer](./installation.md) as well as for the [Docker Image](./SetupDocker.md).    \nIf you want to switch your existing installation to run with Gunicorn, just run the [Installer](./installation.md#installer) over your existing installation and confirm to use Gunicorn.\n- [Info/System](./Information_Sys.md) now includes information on the [WSGI server running](./Information_Sys.md#wsgi-server)\n\n### Changes\n\n- The [Info](./Information.md) screens were restructured. [System](./Information_Sys.md) and [Cameras](./Information_Cam.md) are now separated.\n\n### Bugfixes\n\n- Fixed error ```TypeError: CameraController.requestStop() got an unexpected keyword argument 'forCam2'``` which could occur in specific cases when a video was recorded.\n- Fixed error ```Camera.framesUsb - Exception: cannot access local variable 'cfg' where it is not associated with a value``` which could occur in specific situations if a USB camera could not be opened.\n- Fixed issue in [Settings (No Camera)](./Settings_NoCam.md) where an error occurred when *Use USB Camera* is activated.\n\n## V4.6.1\n\n### Bugfixes\n\n- Fixed deactivation of [Settings / *Use Camera AI*](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features):    \nIf a live stream is active with activated AI, the live stream is now restarted without AI.\n- Fixed [Config](./Configuration.md) for the case when the last active tab was *AI Configuration* and *Use Camera AI* was deactivated in [Settings](./Settings.md). In this case, the [Configuration for AI](./Configuration_AI.md) section was erronously visible. \n- Fixed errors which occurred during Docker container startup in case of [Running raspiCamSrv as Docker Container](./SetupDocker.md) (resolves [raspi-cam-srv Issue #83](https://github.com/signag/raspi-cam-srv/issues/83)):    \n.. Added ```dpkg-dev``` to the ```Dockerfile```    \n.. Skipping check of time sync in case raspiCamSrv is running in a container (see hint in [Info dialog](./Z_Legacy_Information.md#raspberry-pi))\n- Fixed support of USB WebCams when [Running raspiCamSrv as Docker Container](./SetupDocker.md).     \n.. Added ```v4l-utils``` to the ```Dockerfile```\n- Fixed error ```FileNotFoundError: [Errno 2] No such file or directory: '/sys/kernel/debug/imx500-fw:11-001a/fw_progress'``` for [AI Camera Support](./AiCameraSupport.md) in the case when [Running raspiCamSrv as Docker Container](./SetupDocker.md).    \n.. Extended ```docker-compose.yml``` to expose ```/sys/kernel/debug/imx500-fw``` to the container.\n\n## V4.6.0\n\n### New Features\n\n- Support of AI features for the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html):\n<br>You can specify the neural network model to be used by the camera (see [Camera AI Configuration](./Configuration_AI.md)) and see inference information visualized in the Live stream or on photos or videos.    \nSee [AI Camera Support](./AiCameraSupport.md)\n\n### Changes\n\n- The behavior of the Live view at camera start has been changed. Especially, in order to avoid irritation about long startup times for the imx500 [AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html), **raspiCamSrv** shows an [animation while the camera is starting](./UserGuide.md#live-stream-at-camera-start).\n- Maximum for [Buffer Count](./Configuration.md#buffer-count) in [Camera Configurations](./Configuration.md#configuration-tab) is increased from 6 to 12. The larger value has been used in the [imx500 examples of Picamera2](https://github.com/raspberrypi/picamera2/tree/main/examples/imx500)\n\n## V4.5.0\n\n### New Features\n\n- [Direct Control Panel](./LiveDirectControl.md) added to [Live View](./LiveScreen.md#accessing-the-direct-control-panel) for fine tuning of numeric [Camera Control](./CameraControls.md) parameters.\n- Added additional endpoints for photo snapshots with high resolution. Previews are available in the [Cam/WebCam](./CamWebcam.md) dialog. Please note the [special restrictions](./CamWebcam.md#photo-snapshot).    \nCovers [raspi-cam-srv Issue #79](https://github.com/signag/raspi-cam-srv/issues/79)\n\n### Changes\n\n- Style sheet switched to [W3.CSS](https://www.w3schools.com/w3css) 5.02\n\n### Bugfixes\n\n- Fixed ```TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType``` which could occur after a [Reset Server](./Settings.md#configuration)\n\n## V4.4.1\n\n### Bugfix\n\n- Fixed the update procedure with dialog [Settings/Update](./SettingsUpdate.md).    \nThe procedure implemented in V4.3.2 did not work correctly.    \nAlthough it confirmed that the update was successful, the version remained at the old version after restart.   \nIf you were running into that issue, you should  \n-- ```ssh``` to the server    \n-- ```cd prg/raspi-cam-srv```    \n-- ```git reset --hard origin/main```      \nThe [Update Procedure](./updating_raspiCamSrv.md) has been modified, accordingly.   \nFor versions 4.4.1 and later, the [Settings/Update](./SettingsUpdate.md) procedure should work correctly.\n\n## V4.4.0\n\n### New Feature\n\n- [Media Viewer](./UserGuide.md#media-viewer) added to all images and videos in the UI.\n\n## V4.3.2\n\n### New Feature\n\n- [raspiCamSrv Version Updates](./SettingsUpdate.md) supports periodical search for updates on GitHub with indication of new versions and version update as well as server restart from the UI.\n\n## V4.3.1\n\n### Bugfix\n\n- Fixed the issue with bad Live Stream quality for Raspberry Pi models ```<``` 5.    \nWhen starting the live stream, raspiCamSrv usually configurs the camera with all 3 streams (lowres, main, raw), so that the live stream (lowres) can remain active while photos (main), raw photos (raw) or videos (main) are taken.    \nFor Raspberry Pi Zero and other models ```<``` 5, it turned out that the stream quality is bad when the raw stream is included in the configuration.    \nNow, for models ```<``` 5, the raw stream is excluded from the configuration for the live stream, resulting in better quality.   \nFor these models, when a raw photo is taken, the live stream will be temporarily stopped and reactivated without raw stream configuration after the raw photo has been stored.\n\n## V4.3.0\n\n### Changes\n\n- Documentation has been moved to [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).     \nThe entire documentation will be versioned with the software and Online Help links will point to documentation with the version of the software, rather than to the latest version.\n\n## V4.2.0\n\n### New Features\n- For [Motion Detection](./TriggerMotion.md), *Regions of Interest* as well as *Regions of NO Interest* can be specified.    \nThese are respected during Motion Detection for CSI as well as USB cameras and for all *Motion Detection Algorithms*\n- In case of multiple cameras, the camera-related [Ttigger](./Trigger.md) settings ([Trigger/Motion](./TriggerMotion.md) and [Trigger/Camera](./TriggerCameraActions.md)) are now camera-specific.     \nWhen the active camera is switched, these settings are replaced by those which had been defined and [saved for camera switch](./CamMulticam.md#save-active-camera-settings-for-camera-switch) for the new camera, before.    \nThis is especially important when regions of interest are used for motion detection. These settings will not be lost when cameras are switched.\n- [Backup and Restorage](./SettingsConfiguration.md) of configuration and other stored data.\n\n### Bugfixes\n\n- For USB cameras an error occurred when *Focus* was set to 0.0 with *Autofocus Mode* \"Manual\". This is now fixed.\n- For [Motion Capturing](./TriggerMotion.md) with USB cameras, the frame rate for video recording was increased from 14.5 to 30. With this frame rate, the video length is in better accord with the configured duration.\n\n## V4.1.0\n\n### New Features\n\n- [Zoom and Pan](./ZoomPan.md) are now also available for USB Cameras.\n- [Focus handling and Image Controls](./CameraControls_UsbCams.md) are now available for USB Cameras.    \n**NOTE** These features have currently been tested with different Logitech cameras. Please notify if other cameras show unexpected behavior.\n\n### Changes\n\n- For USB cameras, it is no longer possible to choose *Sensor Mode* \"Custom\" for the different [Configuration](./Configuration.md) scenarios. Practical experience with web cams has shown that these do not support other image sizes as those defined in the discrete formats (sensor modes). The camera would always use the smallest standard format which entirely contains the specified area. Therefore, it does not make sense allowing the specification of a custom format, if the camera will not use it.\n- For USB cameras, the aspect ratio for *Stream Size* of different [Configuration](./Configuration.md) use cases is not synchronized. For these cameras, the checkbox [Sync Aspect Ratio](./Configuration.md#configuration-tab) is unchecked and disabled.\n\n## V4.0.3\n\n### Extension\n\n- In the [Info](./Z_Legacy_Information.md) dialog, the section [Raspberry Pi](./Z_Legacy_Information.md#installed-cameras) was extended with information on the software stack, which can be helpful analyzing errors which could occur with specific versions or installation details.\n\n## V4.0.2\n\n### Bugfixes\n\n- Fixed error which occurred when changing the *Active Camera* in [Settings](./Settings.md) to a USB camera\n\n## V4.0.1\n\n### Bugfixes\n- Fixed error \"Error ```<class 'ValueError'>``` : Class Camera has no element when_motion_detected\"   \nCause of the error was a wrong configuration for [Triggers](./TriggerTriggers.md) with *Source* 'Camera', which had included an event 'when_motion_detected'. This event is only available for *Source* 'MotionDetector'.    \nThe wrong configuration is now corrected.    \nIf you have configured a Trigger for event 'when_motion_detected' with *Source* 'Camera', you need to delete it and replace it with a trigger with *Source* 'MotionDetector'    \nThis covers part of [raspi-cam-srv Discussion #77](https://github.com/signag/raspi-cam-srv/discussions/77).\n\n## V4.0.0\n\n### New Features\n\n- Seamless integration of **USB cameras** with CSI cameras (see [Info](./Z_Legacy_Information.md) and [Multi Cam](./CamMulticam.md))<br>USB cameras are accessed through OpenCV which must have been installed (see [Installation](./installation.md) Step 11.).<br>The use of USB cameras can be activated or deactivated in the [Settings](./Settings.md).<br>USB cameras are handled in exactly the same way as CSI cameras and they can be used in all operational functions, except [Focus Stacks](./PhotoSeriesFocus.md) and [Exposure Series](./PhotoSeriesExp.md).<br>**NOTE**: Currently [Controls](./CameraControls.md) (Focus, Zooming, Ecposure and Image control) cannot be used with USB cameras. However, images can be flipped and resolution can be adjusted with [Configuration](./Configuration.md).\n- Hot-Plug of USB cameras is supported. See button **Reload Cameras** in [Settings / Configuration](./SettingsConfiguration.md).\n- **No-Camera** Mode is supported.<br>Before, when no cameras were installed, **raspiCamSrv** could not be started.<br>Now, it is possible to use **raspiCamSrv** also without cameras, for example in order to control [GPIO Devices](./SettingsDevices.md) through the [Action Buttons](./ConsoleActionButtons.md) or through the [Event Handling Infrastructure](./Trigger.md) or in order to use the [Versatile Buttons](./ConsoleVButtons.md) Console to issue freely configurable OS commands and see their output.\n- [Switching Cameras](./CamMulticam.md#switch-cameras) in the [Multi Cam](./CamMulticam.md) dialog now requires confirmation when Camera [Confoguration](./Configuration.md) and/or [Controls](./CameraControls.md) were changed and not yet saved for camera switch. This shall avoid unintended loss of configuration work when switching the cameras.\n- [Installation Procedure](./installation.md) has been updated for use with **Debian-Trixie**, the successor of **Bullseye**.<br>Tests have so far only be made with a Raspberry Pi Zero 2 system with connected CSI and USB camera.<br>Please report any issues, you may run into, in other configurations.\n\n### Bugfixes\n\n- Made [Photo Display](./Phototaking.md#photo-display) robust against [deletion of photos](./PhotoViewer.md).   \nBefore, when a photo was deleted, the entire display buffer was cleared.   \nNow, only the deleted photos are removed from the display buffer.   \nAlso, when the server is started with stored configuration, it is checked whether to stored buffer information is still consistent with the existing photos or videos.\n\n\n## V3.7.1\n\n### Bugfixes\n\n- Fixed [Notification](./TriggerNotification.md) for [Motion Capturing](./TriggerMotion.md).    \nIn special cases of parameters for [Trigger Control](./TriggerControl.md), [Camera Actions](./TriggerCameraActions.md) or Video/Photos to be included in the message (see [Notification Settings](./TriggerNotification.md)), notification mails were not sent or images were not included in the mails.   \nThis fix resolves [raspiCamSrv Issue #76 (Notification email)](https://github.com/signag/raspi-cam-srv/issues/76)\n\n## V3.7.0\n\n### New Features\n\n- [Camera Calibration](./CamCalibration.md) uses an image series of a calibration pattern for calibration of a stereo camera pair.\n- [Stereo Vision](./CamStereo.md) uses a stereo camera setup for creation of 3D videos or depth maps.\n- The [Multi-Cam](./CamMulticam.md) dialog has a new function to [Synchronize Configurations](./CamMulticam.md#synchronize-configurations).\n\n### Bugfixes\n\n- Fixed behavior when [starting server with stored configuration](./Settings.md#configuration) after the camera or one of the cameras has been replaced by a different model.    \nIn the previous version, the settings for the second camera have been taken from the stored configuration.   \nNow, if a camera has been changed, the corresponding configuration is reset to default.    \nThis will also be indicated with the yellow indicator for [unsaved configuration changes](./UserGuide.md#elements)\n- Layout of [Multi-Cam](./CamMulticam.md) dialog has been corrected. In the previous version the two columns had a different width.\n- Fixed wrong status of [Start server with stored Configuration](./Settings.md#configuration) after a [Reset Server](./Settings.md#configuration)\n- Fixed image distortion when the stream size of a [Configuration](./Configuration.md) has been changed to an aspect ratio which is not that of the default [ScalerCrop](./ScalerCrop.md).     \nIn this case, scalerCrop is now activated in the controls, so that it will also be included in the streaming configuration and will also be synchronized with the second camera, if this is the same model as the active one.\n- Fixed inappropriate status indicators and [Multi-Cam](./CamMulticam.md) menu for systems having just one camera.\n\n### Note\n\n- For **Raspberry Pi Zero** running the latest builds of **Bookworm**, there seems to be an issue with MJPEG streaming. It has been observed that, after some time of streaming (a few minutes), the entire system gets stalled and does no longer react without rebooting. The reasons are not yet understood and need to be analyzed.    \n**This problem does not occur on Bullseye systems!**\n**The issue is no longer observed in Bookworm systems when updated after about mid September 2025!**\n\n## V3.6.2\n\n### Bugfixes\n\n- Fixed \"UnboundLocalError\" which occurred when creating a new [Trigger](./TriggerTriggers.md) or a new [Action](./TriggerActions.md).\n\n## V3.6.1\n\n### Bugfixes\n\n- Fixed \"NameError: name 'photoViewConfig' is not defined\"\n\n## V3.6.0\n\n### New Features\n\n- Now supporting alternate or simultaneous photo taking and video recording with both cameras for systems which allow connecting two (non-USB) cameras (currently Raspberry Pi 5).\n- [API](./API.md) extended with new web services supporting photo taking and video recording with both cameras.\n- The new [Multi-Cam](./CamMulticam.md) dialog gives access to photo taking and video recording functions for both cameras, if available.\n\n### Bugfixes\n\n- Fixes \"KeyError: 'GPIO'\" which could occur when the [event handling system](./Trigger.md) was stopped without having fired a GPIO trigger during its life time.\n- Fixed an issue where [Test Motion Detection](./TriggerMotion.md#testing-motion-capturing) did not work if it was started within a period where [Trigger Operation](./TriggerControl.md) was not active.\n\n## V3.5.6\n\n### Bugfixes\n\n- Fixed an issue, where a request did not return when trying to take a photo in cases, where the [configuration](./Configuration.md) for Live View and Photo both use the same stream but with different stream size.\n- Fixed missing Live view after an error has occurred during photo taking or video recording.    \nIn some cases, the Live view needs to be deactivated while taking a photo or recording a video because of incompatible [configuration](./Configuration.md).    \nThe deactivation flag is now cleared so that the Live view will show up after an error has occurred.    \n**NOTE:** that you need to push the *Live* menu button for refreshing the screen.\n- Fixed wrong [Status Indicator](./UserGuide.md#process-status-indicators) for [Trigger](./TriggerTriggers.md) thread after [starting server with stored configuration](./SettingsConfiguration.md)\n- Improved exception handling for camera access:    \nException occurring during camera configuration are now not only logged but also shown within the UI.    \nLow level error messages, indicating the error source as reported by Picamera2, are no longer overwritten by higher level more general messages.\n\n### New Features\n\n- The [Settings/Configuration](./SettingsConfiguration.md) screen now shows all unsaved configuration changes which have been made during a session. This can help to decide whether or not the current configuration needs to be saved.    \nThus, whenever the [Configuration Status Indicator](./UserGuide.md#process-status-indicators) is switched on, a new entry for unsaved configuration changes is made.\n- [StepperMotor](./gpioDevices/StepperMotor.md) has got new functionality.   \n[wipe](./gpioDevices/StepperMotor.md#wipeangle_from-angle_to-speed-count) will rotate back and forth between given angles for a certain nomber of cycles or until [stop](./gpioDevices/StepperMotor.md#stop) is called.\n\n### Changes\n\n- Adapted [Installation procedure](./installation.md) step 11 for version of [matplotlib](https://matplotlib.org/).    \nAs discussed in [picamera2 issue \\#1211](https://github.com/raspberrypi/picamera2/issues/1211), Picamera2 relies on the numpy version coming with Debian installation, which is numpy 1.x.  \nOn the other hand, matplotlib version 3.8 and later seem to require numpy 2.x, which is binary incompatible to version 1.x.\n- For [StepperMotor](./gpioDevices/StepperMotor.md), the minimum speed (which is achieved by setting *speed=0*) has been reduced by a factor of 10.    \nThe angular velocity is now:\nFor speed=0: 164.20 seconds for 360°    \nFor speed=1: 4.42 seconds for 360°\n- Extended capabilities of ```GET api probe``` [API](./API.md) WebService endpoint:    \nNow ```Camera.streamOutput``` and ```Camera.stream2Output``` are accessible which represent the streaming output of the primary and secondary camera.\n\n## V3.5.5\n\n### Bugfixes\n\n- Thread-safe handling of last live stream access. The last access of a client to a camera stream controls automatic shutdown of the streaming server after 10 sec of inactivity (see [Streaming](./UserGuide.md#streaming)).    \nSince streaming clients and servers are executed in different threads, it could happen in rare cases that a client has tried to access a stream in a phase where the server has started but not yet completed to shut down because of inactivity.    \nSince the camera is closed when streaming is shut down, different camera errors could occur, depending on camera shutdown status.   \nThis could occur in particular when taking photos or taking [Photo Snapshots](./CamWebcam.md#photo-snapshot) through the Web URL.   \nNow, access to the time of last stream access has been made thread-safe by holding locks while a process is evaluating or changing this value and, in cases where inactivity is detected by the server, the lock is only released after the server has completely shut down.    \n(Fixes [raspi-cam-srv issue #61](https://github.com/signag/raspi-cam-srv/issues/61))\n\n- After an OS upgrade, *Kernel Version* and *Debian Version* in the [Info/Installed Cameras screen](./Z_Legacy_Information.md) did not show the correct values if the server was configured to [start with the stored configuration](./SettingsConfiguration.md). Instead, the values from the stored configuration were shown.   \nThis has been fixed.   \nThe entry in the [server configuration storage file](./SettingsConfiguration.md#server-configuration-storage) will have the old value until the configuration has been stored.\n\n### Changes\n\n- Information on the *Debian Version* in the [Info/Installed Cameras screen](./Z_Legacy_Information.md) now includes information on the system architecture (32-/64-bit) of the installed OS.\n\n### New Features\n\n- A new [API](./API.md) WebService endpoint is provided:   \n```GET api probe``` allows probing oject properties of live objects of an active **raspiCamSrv** server.   \n**NOTE:** This service is mainly intended for error analysis within a live system and requires detailed knowledge of the raspiCamSrv object model.   \nYou can specify a set of object attributes for which attribute values shall be queried.   \nAs objects, you select from the base singleton objects {Camera(), CameraCfg(), MotionDetector(), PhotoSeriesCfg() or TriggerHandler()} and then specify valid properties with dot-notation.    \nThe result is returned in JSON format. Error messages are shown if an attribute is not JSON serializable.\n\n## V3.5.4\n\n### Bugfixes\n\n- Corrected deprecated log level for Picamera2 logging from ```Picamera2.ERROR``` to ```logging.ERROR```   \n(See [raspi-cam-srv Issue #62](https://github.com/signag/raspi-cam-srv/issues/62))\n\n## V3.5.3\n\n### Changes\n\nIn dialog [Web Cam](./CamWebcam.md), the button **Memorize Configuration and Controls for Camera Change** was renamed to **Save Active Camera Settings for Camera Switch** in order to more clearly express its functionality.   \nSee [raspi-cam-srv Issue #60](https://github.com/signag/raspi-cam-srv/issues/60)\n\n## V3.5.2\n\n### Bugfixes\n\n- Fixed a bug which caused [motion detection](./TriggerMotion.md) to be stalled after booting the Raspberry Pi.<br>The error was caused by a race condition between start of the raspiCamSrv server and syncronization of system time with a time server.<br>When time synchronization resets the system time while the live view is already active, this will signal inactivity for more than 10 sec and, therefore, immediately stop the live stream and subsequently close the camera.<br>Since motion detection relies on an active live stream, this could cause blocking of the motion detection thread which then cannot be stopped except by restarting the server.<br>Now, the system checks the system time synchronization status at startup and, if necessary, waits until time is syncronized with the time server.<br>This resolves [raspiCamSrv Issue #28: raspi-cam-srv seems to be frozen sometimes](https://github.com/signag/raspi-cam-srv/issues/28)<br>**NOTE**: This fix does not currently work when running raspiCamSrv as [Docker Container](./SetupDocker.md)\n\n- Fixed missing [Config](./Configuration.md) screen for [Docker installations](./SetupDocker.md)\n\n### New Feature\n\n- The [Info](./Z_Legacy_Information.md) screen now shows the time when **raspiCamSrv** has recognized time synchronization, which can be considered as the server start time.\n\n## V3.5.1\n\n### Bugfixes\n\n- Fixed video recording during motion tracking for Raspberry Pi models 1, 2, 3:<br>For models Pi 4 and lower, video recording during motion tracking should use the *Live View* [Configuration](./Configuration.md) because this is usually set to lower resolution and should, therefore, not cause memory issues with these devices.<br>Only for Pi 5, the *Video* Configuration should be used.<br>Unfortunately, in versions up to V3.5.0, this was only applied to Pi 4 and Pi Zero, whereas for Pi 1, Pi 2 and Pi 3 the *Video* configuration was used.<br>This is now corrected and for all models <= Pi 4 the *Live View* configuration is used.\n\n## V3.5.0\n\n### New Features\n\n- [StepperMotor](./gpioDevices/StepperMotor.md) has got new functionality. In addition to the new methods ```swing()``` and ```rotate_to(angle)```, the ```current_angle``` of rotation is tracked and can be set and queried.<br>```swing()``` allows stepwise rotations within given boundaries whereas ```rotate_to(angle)``` rotates to a specified angle.<br>This functionality relies on [Calibration](./SettingsDevices.md#calibrating-a-device) and device status tracking.<br>**NOTE**: If you have already a StepperMotor configured, it will not inherit the new functions. You will need to recreate it.\n- [Device Configuration](./SettingsDevices.md) for StepperMotor allows [Calibration](./SettingsDevices.md#calibrating-a-device) to set a specific orientation as zero reference.\n- For devices requiring [Calibration](./SettingsDevices.md#calibrating-a-device) (currently only StepperMotor), the status is continuously tracked and memorized so that it can be restored after a restart or a renewed device access.<br>**NOTE**: This may not work if the **raspiCamSrv** server is stopped while the StepperMotor is active.\n- A new Camera [Trigger] is available: ```when_series_photo_taken``` allows assigning [Actions](./TriggerActions.md) when a photo has been taken within a [Photo Series](./PhotoSeries.md).<br>The trigger does not fire in case of an [Exposure Series](./PhotoSeriesExp.md) or a [Focus Stack](./PhotoSeriesFocus.md)\n\n### Changes\n\n- Camera [Actions](./TriggerActions.md) do no longer trigger events. This means: if, for example, a [Trigger](./TriggerTriggers.md) has been specified when a Photo is taken, this trigger will no longer fire if the photo is taken as an action of another trigger.\n\n### Bugfixes\n\n- [API](./API.md) endpoint ```api/start_triggered_capture``` now also starts event handling and not only motion detection\n- [API](./API.md) endpoint ```api/stop_triggered_capture``` now also stops event handling and not only motion detection\n- Fixed error ```Camera.frames - Exception: argument of type 'NoneType' is not iterable``` which could occur if button [Load Stored Configuration](./SettingsConfiguration.md) has been pressed.\n- Fixed [Load Stored Configuration](./SettingsConfiguration.md): Now, all background processes are stopped before the stored configuration is loaded, and they are restarted afterwards, if they had been active before.\n- Fixed [Reset Server](./SettingsConfiguration.md): Now, all background processes are stopped before the configuration is set to default, and they are restarted afterwards, if they had been active before. Some missing configurations which may have led to errors have also been fixed.\n- Made ```table-layout:fixed``` for [Versatile Buttons](./ConsoleVButtons.md) and [Action Buttons](./ConsoleActionButtons.md) so that all columns have equal width and empty rows are shown.\n- Multiple SMTP mails for the same [Trigger](./TriggerTriggers.md) are avoided.\n- Fixed event notification. Due to a timing issue, sent mails could be incomplete and attachments may have been missing.\n\n## V3.4.0\n\n### New Features\n\n- Under [Actions](./TriggerActions.md), it is now possible to configure camera actions:<br>take_photo<br>start_video<br>stop_video<br>record_video with a configurable duration.\n- [Actions](./TriggerActions.md) now support SMTP action for sending a mail in case of an event.\n- [Trigger](./TriggerTriggers.md) allow configuring *MotionDetector* trigger for *CAM-1*: *when_motion_detected*.<br>This trigger fires when a motion is detected by the cameras [motion detection](./TriggerMotion.md) algorithms.\n- [Device Types](./SettingsDevices.md#device-type-configuration) for GPIO devices include now additional GPIO base classes which allows integrating more general devices:<br>- DigitalInputDevice<br>- DigitalOutputDevice<br>- OutputDevice\n- An [indicator](./UserGuide.md#title-bar) has been added which indicates unsaved configuration changes.\n- The [event log](./TriggerActive.md#log-file) can now be downloaded from the [Calendar view](./TriggerCalendar.md)\n\n### Changes\n\n- For [Triggers](./TriggerTriggers.md) with *Source* \"Camera\" the device names were changed:<br>\"Active Camera\" -> \"CAM-1\"<br>\"Second Camera\" -> \"CAM-2\"<br>If triggers have been created with the old *Device* names, they will be renamed automatically when data are loaded from the stored configuration.\n- For [Versatile Buttons](./SettingsVButtons.md) and [Action Buttons](./SettingsAButtons.md) the maximum number of rows and columns was changed from 9 to 99.\n- Added favicon for browser tab\n\n### Bugfixes\n\n- Fixed error<br>```ERROR in camCfg: Error loading from /home/pi/server/raspi-cam-srv/raspiCamSrv/static/config/serverConfig.json: 'NoneType' object is not subscriptable```<br>reported in [raspi-cam-srv Issue #55](https://github.com/signag/raspi-cam-srv/issues/55)\n- Bugfixes and improvements for Class [StepperMotor](./gpioDevices/StepperMotor.md):<br>For full-step mode, the waiting time is now doubled while speed range is unchanged. (1ms does not work)<br>New methods *step(steps)* and *rotate(angle)* have been added, which allow positive and negative arguments.<br>Rotations are now with higher precision. They are now in integers of motor steps rather than geared steps.<br>For full-step mode, now two coils are activated in each step, instead of only one, which increases torque.\n- Several tables which can become large, can now be scrolled with fixed headers.\n\n## V3.3.0\n\n### New Features\n\n- Configuration of access to [GPIO-connected devices](./SettingsDevices.md) through the [gpiozero](https://gpiozero.readthedocs.io/en/stable/index.html) library.\n- Extension of ```gpiozero.OutputDevice``` for support of [Stepper Motors](./gpioDevices/StepperMotor.md)\n- Configuration of [Triggers](./TriggerTriggers.md) for events from GPIO-connected input devices, such as sensors and buttons.\n- Implementation of callback hooks for photo-related events ('when_photo_taken', 'when_recording_starts', 'when_recording_stops', 'when_streaming_*_starts', 'when_streaming_*_stops') for integration with the new event handling infrastructure.\n- Configuring of [Actions](./TriggerActions.md) for GPIO-connected output devices.\n- [Configuration of Action Buttons](./SettingsAButtons.md) and [Console / Action Buttons](./ConsoleActionButtons.md) for the direct manual execution of [Actions](./TriggerActions.md) with GPIO-connected output devices such as LEDs, motors and servos. This includes also the control of Stepper Motors.\n- An option *On Dial Marks* has been added to [Photo Series](./PhotoSeries.md) which assures that photos will be taken exactly (within tolerances) at every full hour, half hour, quarter, minute, ..., depending on the chosen interval.\n\n## V3.2.0\n\n### New Features\n\n- The [Settings for Versatile Buttons](./SettingsVButtons.md) now allows setting the commandline in [Versatile Buttons](./Console.md) to be interactive, which allows entering commands directly.\n\n- Access to Online Help added to the different application screens.<br>The *Online Help* button opens the document page on GitHub related to the active dialog.\n\n## V3.1.0\n\n### New Features\n\n- Functionality has bee added for configuration of [Versatile Buttons](./Console.md) for execution of commands on the level of the Raspberry Pi's Linux OS.<br>This covers part of the request in [Discussion #47](https://github.com/signag/raspi-cam-srv/discussions/47).\n\n## V3.0.0\n\n## Package version upgrade\n\n- Released for Flask 3.1.0<br>raspiCamSrv can use the current Flask version 3.x<br>Upgrading Flask in an existing installation is not mandatory.<br>In order to upgrade from Flask 3.0.0 to the latest version 3.x, proceed as follows:<br>```cd prg/raspi-cam-srv/```<br>```source .venv/bin/activate```<br>```pip install --upgrade \"Flask>=3,<4\"```\n\n## V2.13.0\n\n### New Feature\n\n- RaspiCamSrv can now also be deployed in Docker.<br>See [Running raspiCamSrv as Docker Container](./SetupDocker.md)\n\n## V2.12.0\n\n### New Features\n\n- The [Info Screen](./Z_Legacy_Information.md) has been extended by a section [Streaming Clients](./Z_Legacy_Information.md#streaming-clients) which shows a list of clients which are currently using any of the camera streams. \n\n### Bugfixes\n\n- Fixed TypeError which could occur if a paused [Photo Series](./PhotoSeries.md), for which no photos had been taken, was continued. \n\n- Fixed an error where for a [Photo Series](./PhotoSeries.md) with *Interval* > 60 sec. an additional photo could have been taken about 1.5 sec. before the expected time when the regular photo is taken. If the waiting time between successive photos is > 60 sec. and if no other process requires the camera, the camera is closed to minimize resource consumption. The waiting time is then shortened by 1.5 sec., to account for the time required for camera wakeup. If, however, the live stream is activated within 60 sec. before the time for the next photo, the camera is already active and this additional time is not required. \n\n- Fixed an error for [Photo Series](./PhotoSeries.md) with *Interval* >> 60 sec. for the case when the series was started while the live stream was still active. In this case, the photo series did not stop the camera during the waiting period because it was required by the live stream. If the photo series continued taking the next photo, it did not recognize that the camera was closed in the meantime. An attempt to take a photo caused the thread to stop without error notification.<br>As a result, the series seemed to be active while it was actually dead.\n\n- Fixed an error for [Photo Series](./PhotoSeries.md) with *Interval* > 60 sec. where controls (e.g. zoom/ScalerCrop) were not applied to the photos of the series, except probably for the first one.<br>The reason was that the camera is closed if the waiting time for the next photo is > 60 sec. and after restart the camara wasn't given time to pick up the controls settings. Now, an additional waiting time of 1 sec. has been added which resolves the issue.\n\n## V2.11.4\n\n### Bugfix\n\n- Fixed [initialization of the raspiCamSrv API](./SettingsAPI.md) which did not work when a secrets file did not yet exist.\n\n## V2.11.3\n\n### Bugfix\n\n- Fixed \"TypeError: can only concatenate str\", which might occur in special cases for a [Sun-controlled timelapse series](./PhotoSeriesTimelapse.md#).\n\n- Fixed wrong display of *Sunset* in [Timelapse series](./PhotoSeriesTimelapse.md#).\n\n- Fixed \"KeyError: 'UnitCellSize'\" for cases where camera_properties do not include information on the physical size of the sensor’s pixels\n\n### Doc\n\n- Added [description for setup of stanalone systems](./bp_PiZero_Standalone.md)\n\n## V2.11.2\n\n### Bugfix\n\n- Fixed an issue where photos and videos could not be taken if the [Transform](./Configuration.md#transform) settings for the different configuration were different.<br>Now, when modifying the *Transform (flip <> or flip v)* are changed in one configuration this change is also applied to all other configurations.<br>This covers raspiCamSrv Issue #33 [Errors after changing Transform settings](https://github.com/signag/raspi-cam-srv/issues/33)\n\n## V2.11.1\n\n### Bugfix\n\n- Fixed an import error which occurred after having upgraded to V2.11 when package ```flask-jwt-extended``` has not yet been installed.\n\n## V2.11.0\n\n### New Features\n\n- V2.11.0 introduces the new [raspiCamSrv API](./API.md) for interoperability of raspiCamSrv with other software packages.<br>This resolves the feature request raspi-cam-srv issue #34 [API?](https://github.com/signag/raspi-cam-srv/discussions/34)\n\n- Required installation actions:<br>In order to allow API support, it is necessary to install an additional package.<br>This can be done before or after the [Update Procedure](./updating_raspiCamSrv.md):<br>```cd ~/prg/raspi-cam-srv```<br>```source .venv/bin/activate```<br>```pip install flask-jwt-extended```\n\n### Changes\n\n- The [Settings](./Settings.md) screen has been restructured to incorporate the additional settings required for the API\n\n\n## V2.10.5\n\n### Bugfixes\n\n- Allowed port range in [Trigger - Notification](./TriggerNotification.md) extended to [1 ... 65535]<br>Partly resolves raspi-cam-srv issue #42 [SMTP port issue](https://github.com/signag/raspi-cam-srv/issues/42)\n\n- Fixed [Trigger Notification](./TriggerNotification.md) for SMTP servers which do not require authentication.<br>It can now be specified whether or not the server requires authentication.<br>Within the connection test it is checked whether the SMTP server requires SSL and authentication.<br>If the requirements are not consistent with the settings on the [Notification](./TriggerNotification.md) screen, an error message is shown.<br>Resolves raspi-cam-srv issue #42 [SMTP port issue](https://github.com/signag/raspi-cam-srv/issues/42)\n\n\n## V2.10.4\n\n### Bugfix\n\n- Fixed function [Load Stored Configuration](./SettingsConfiguration.md) on the [Settings](./Settings.md) screen.<br>After execution of this function, values shown on the [Settings](./Settings.md) screen were only updated to the values loaded from the stored configuration after the page has been refreshed. <br>Resolves raspi-cam-srv issue #39 [load_config route assumes LiveStream2 exists (causes crash if non-existent)](https://github.com/signag/raspi-cam-srv/issues/39)\n\n## V2.10.3\n\n### Bugfixes\n\n- Fixed function [Load Stored Configuration](./SettingsConfiguration.md) on the [Settings](./Settings.md) screen.<br>This function failed in cases when only a single camera is connected to a Raspberry Pi.<br>Resolves raspi-cam-srv issue #39 [load_config route assumes LiveStream2 exists (causes crash if non-existent)](https://github.com/signag/raspi-cam-srv/issues/39)\n\n- Fixed [Switch Cameras](./CamMulticam.md#switch-cameras) on the [Web Cam](./CamWebcam.md) screen.<br>If working with two cameras, this function caused an error when [Triggered Capture of Videos and Photos](./Trigger.md), a [Photo Series](./PhotoSeries.md) or [Video Recording](./Phototaking.md#video) is currently active.<br>The user is now asked to stop either of these processes before switching cameras.\n\n## V2.10.2\n\n### New Features\n\n- Added kernel version and Debian version to [Info](./Z_Legacy_Information.md) screen.\n\n## V2.10.1\n\n### Bugfix\n\n- Fixed an issue with platform-specific search of tuning files.\n\n## V2.10.0\n\n### New Features\n\n- Support of [Camera Tuning](./Tuning.md) by selection of alternate tuning files.<br>Resolves raspi-cam-srv issue #26 [NoIR camera settings](https://github.com/signag/raspi-cam-srv/issues/26)<br>**Note:** There is still an issue when streaming two cameras. (See Picamera2 Issue #1103 [Tuning file support not thread-safe?](https://github.com/raspberrypi/picamera2/issues/1103))\n\n### Bugfixes\n\n- Fixed error ```The browser (or proxy) sent a request that this server could not understand.``` which ocurred when pressing *Submit* in the [Control](./TriggerControl.md) tab of the [Trigger](./Trigger.md) menu.<br>Resolves raspi-cam-srv issue #27 [Trigger Control Submit make server error](https://github.com/signag/raspi-cam-srv/issues/27)\n\n## V2.9.2\n\n### Bugdixes\n\n- Disallow changing parameters of a [Photo Series](./PhotoSeries.md) after it had already been started.\n\n## V2.9.1\n\n### New Feature\n\n- [Photo Series](./PhotoSeries.md) can be downloaded (see [Downloading a Series](./PhotoSeries.md#downloading-a-series))\n\n## V2.9.0\n\n### New Features\n\n- The [Photo Viewer](./PhotoViewer.md) has been enabled to download photos and to delete photos from the Raspberry Pi.\n\n## V2.8.4\n\n### Bugfixes\n- When a [Sun-controlled Photo Series](./PhotoSeriesTimelapse.md) was started, it could happen that the series end was recalculated without considering the configured time periods.<br>Typically, this happened when the configured start time was earlier than the time when the series was actually started.<br>Because this end time was in many cases earlier than the start of the next time slot, the series may have immediately stopped when the next period started.<br>This is now fixed.\n- In some cases when a series was finished because the configured end time has been reached, the background process did not stop and continued to produce photos with zero interval until the configured number of shots has been reached.\n- When a [Photo Series](./PhotoSeries.md) was paused and continued afterwards, the **Current shots** was incremented without taking a photo with this number. Therefore **current shots** did not represent the real number of photos and the series would be stopped before reaching the configured **Number of shots**<br>Now, **Current shots** is only incremented if a photo has been taken.\n- When the waiting time for the next shot in a [Photo Series](./PhotoSeries.md) is larger than 60 sec, RaspiCamSrv stops the camera and restarts it when the time is reached.<br>However, the restart requires 1.5 sec waiting time to allow the camera to collect statistics for auto-exposure algos.<br>Therefore, the phototaking is delayed by at least 1.5 sec with respect to the expected times.<br>This is now compensated.<br>The observed delay is now considerable smaller and ranges from ~0.2 sec for the first photo to ~0.04 sec for the next ones.\n\n## Changes\n\n- When a new [Photo Series](./PhotoSeries.md) is created, the start time is delayed by 1 minute with respect to the current time to give time for further configurations.\n\n## V2.8.3\n\n### Bugfix\n- Fixed layout issues in screen [Settings](./Settings.md) for cases where **Show Histograms** and/or **Ext Motion Detection** are not supportet.\n\n## V2.8.2\n\n### Bugfix\n- The Bugfix introduced in [V2.8.0](#v280) caused an error on systems like Pi Zero where modules cv2, matplotlib or numpy cannot be installed.<br>This error is now fixed.<br>If in your system the [Settings](./Settings.md) screen shows that **Ext. Motion Detection supported** is checked and in screen [Trigger/Motion](./TriggerMotion.md) the **Motion Detection Algorithm** list only shows \"Mean Square Diff\", you can try the following:<br>Edit file ```prg/raspi-cam-srv/raspiCamSrv/static/config/triggerConfig.json```<br>Remove the part highlighted in the following screenshot, if it exists:<br>![Fix282](./img/RN282_img1.jpg)\n\n## V2.8.1\n\n### Changes\n- Removed alternate type hints in module sun.<br>These were introduced in Python 3.10.<br>However in Raspberry Pi Zero systems Python 3.9 is installed.\n\n## V2.8.0\n\n### New Features\n\n- For [Photo Series](./PhotoSeries.md), the page [Timelapse Series](./PhotoSeriesTimelapse.md) now allows specification of up to two daily periods depending on sunrise and sunset.<br>Refers to [Discussion #21](https://github.com/signag/raspi-cam-srv/discussions/21).\n- On the [Settings](./Settings.md) screen, new parameters for geographical latitude, longitude and elevation as well as a time zone selector have been added.<br>Non-zero settings for these parameters are required for using [Sun-controlled Photo Series](./PhotoSeriesTimelapse.md)\n\n### Bugfixes\n- For [Motion Detection](./TriggerMotion.md), the list of supported algorithms had shown only \"Mean Square Diff\", even if **Ext. Motion Detection supported** was activated in the [Settings](./Settings.md) screen.<br>Now all available algorithms can be selected and used if the modules cv2, matplotlib and numpy are installed on the system (see [RaspiCamSrv Installation](./installation.md), 11)\n\n## V2.7.1\n\n### Bugfixes\n\n- Images from a photo snapshot URL (see [Web Cam](./CamWebcam.md)) could not be saved using 'save as' from the context menu.   \nThe reason was that these images still contained the framing and mime type from MJPEG streaming.   \nThis is now fixed.   \nThis solves [raspi-cam-srv Issue #22](https://github.com/signag/raspi-cam-srv/issues/22)\n\n## V2.7.0\n\n### New Features\n\n- For streaming access, it can now be configured in the [Settings](./Settings.md) screen whether authentication is required or not.   \nThe default is that authentication is not required, as before.   \nThis modification has been made for Feature Request [#20](https://github.com/signag/raspi-cam-srv/issues/20)\n\n### Changes\n- The default log level for libcamera was set to ERROR instead of WARNING in order to suppress V4L2 pixel format warnings.\n\n## V2.6.3\n\n### Bugfixes\n\n- Fixed ```Error starting camera: main stream should be a dictionary``` which accurred at server start, if an active [Photoseries](./PhotoSeries.md) with *Photo Type* \"raw+jpg\" was configured to be *Continued on Server Start*.\n- When a [Photoseries](./PhotoSeries.md) is automatically continued on server start/restart, previous versions did not allow seeing the live stream while the Photoseries was active. Now, if the photo series configuration is compatible with live stream configuration, you will see the live stream also after automatic continuation of the series.\n\n## V2.6.2\n\n### Bugfixes\n\n- Fixed ```Exception: 'NoneType' object has no attribute 'get'``` which occurred when taking a video which requres exclusive camera access.     \nThe reason was that, while stopping the live stream, it was not recognized that video recording was intended. As a result, the camera was closed and access to the camera during video recording lead to this error.   \nRefers to: [raspi-cam-srv Issue #18](https://github.com/signag/raspi-cam-srv/issues/18).\n- Fixed ```AttributeError: 'NoneType' object has no attribute 'requestStop'``` which could occur after applying *Reset Server* in the [Settings](./Settings.md) screen.\n\n### Changes\n\n- For Raspberry Pi models lower than model 5 (Zero, 1, 2, 3, 4),    \nthe [Configuration](./Configuration.md) for *Photo* is initialized with the lowest *Sensor Mode*    \nand the *Buffer Count* for *Video* is set to 2, identical with *Live View*.    \nThis makes all configurations compatible and allows for the Live Stream parallel to Video Recording, when using the default configuration.    \nRefers to: [raspi-cam-srv Issue #18](https://github.com/signag/raspi-cam-srv/issues/18).\n\n## V2.6.1\n\n### Bugfixes\n\n- With deactivated [Sync Aspect Ratio](./Configuration.md), the aspect ratio of different configurations was nevertheless synced. This is now fixed.\n- When activating [Sync Aspect Ratio](./Configuration.md), after it was previously deactivated, all aspect ratios were set to the one of *Live View* and not to the currently selected configuration.\n- When activating [Sync Aspect Ratio](./Configuration.md), after it was previously deactivated, ScalerCrop was not automatically updated.\n\n## V2.6.0\n\n### New Features\n\n- [Zoom and Pan](./ZoomPan.md) has been completely reworked.   \nIt now takes regard of the [ScalerCrop](./ScalerCrop.md) specifics of Raspberry Pi cameras.    \nThis allows full control of image areas also for cases with extreme aspect ratios.    \n**Note**: For cases where the height of the *Stream Size* is considerably larger than its width, the live stream in the [Live](./LiveScreen.md) screen may exceed the page height. This cannot currently be avoided without loosing the capability of graphical [focus](./FocusHandling.md#autofocus-windows) and [zoom](./ZoomPan.md#graphically-setting-the-zoom-window).\n- The [Config](./Configuration.md) screen now has an option to synchronize aspect reatios of *Stream Size*s across all configurations.    \nIf this is activated and a non-standard aspect ratio is configured, for example, for the *Live View*, the *Stream Size*s for the other configurations will be adjusted to the same aspect ratio.    \nThen the Live Stream will no longer be distorted because the camera system will select a *ScalerCrop* with the same aspect ratio.\n- The [Info](./Z_Legacy_Information.md) screen in section [Camera x](./Z_Legacy_Information.md#camera-x) now shows the Sensor Mode in which the camera is currently operating if the camera is currently started.\n\n### Changes\n\n- Camera [Configuration](./Configuration.md#) for *Raw Photo* now allows *Custom* *Stream Size*.    \nHowever, if a *Stream Size* is specified which does not correspond to the output_size of one of the cameras Sensor Modes, the camera will automatically select a suitable Sensor Mode and produce a .dng file with the corresponding size.    \nThe reason for this change was that the new option to automatically syncronize aspect ratios across configurations can lead to a *custom* *Stream Size* also for the *Raw Photo* configuration.\n- The parameter *Stream Size aligned with Sensor Modes* in the [Configuration](./Configuration.md#) now dafaults to False.    \nThe reason for this change is that, when specifying identical *Stream Sizes* for *lores* and *main* streams, the camera could produce an error ```Error starting camera: lores stream dimensions may not exceed main stream``` because the automatic alignment might produce effective *Stream Sizes* which violate tis restriction.\n\n### Bugfixes\n- [Zooming](./ZoomPan.md) did not preserv image center\n\n\n## V2.5.4\n\n### Bugfixes\n\n- Avoid ```Error starting camera: lores stream dimensions may not exceed main stream```   \nNow, when specifying any [Camera Configuration](./Configuration.md), it is checked whether the specified *Stream Size* for the different use cases obey the restriction that stream size for 'lores' must be less than stream size for 'main' stream.    \nIf the restriction is violated, an error message is shown and the previous values are restored.\n- In [Camera Configuration](./Configuration.md) for *Photo*, *Stream* could be changed to 'lores' in the dialog, but this change has not been stored. Now fixed\n- Fixed: --- Logging error --- ... camera_pi.py\", line 793, in clearConfig\n\n\n## V2.5.3\n\n### Bugfixes\n\n- The previous fix was not robust enough and really worked only with debugging activated..    \nNow, the camera is given a second more time after different steps of switching.\n\n## V2.5.2\n\n### Bugfixes\n\n- Switching the camera caused ```RuntimeError: Unable to stop preview.``` (see [raspi-cam-srv Issue #14](https://github.com/signag/raspi-cam-srv/issues/14)).    \nThis is now fixed. Switching the camera can be done from the [Settings](./Settings.md#switching-the-active-camera) screen as well as from the [WebCam](./CamMulticam.md#switch-cameras) screen.\n\n## V2.5.1\n\n### New Features\n\n- During [Motion Capture](./TriggerMotion.md), framerates are also reported for the *Mean Square Diff* algorithm.    \nSee [Testing Motion Capturing](./TriggerMotion.md#testing-motion-capturing)\n\n## V2.5.0\n\n### New Features\n\n- [Extended Motion Capturing Algorithms](./TriggerMotion.md) are available, including [Frame Differencing](./TriggerMotion.md#test-for-frame-differencing-algorithm), [Optical Flow](./TriggerMotion.md#test-for-optical-flow-algorithm) and [Background Subtraction](./TriggerMotion.md#test-for-background-subtraction-algorithm)\n- The [Extended Motion Capturing Algorithms](./TriggerMotion.md) can be run in a testing mode, showing live views of intermediate image processing results which can help for a better understanding of the algorithms and adjustment of their variable parameters.\n\n### Changes\n\n- For Motion Detection, Trigger Parameters (see [Database for Events](./TriggerActive.md#database)) have been changed from format \"string\" to Python Dictionary, allowing multiple parameters for the [Extended Motion Capturing Algorithms](./TriggerMotion.md).    \nExisting database entries with string format are still supported.\n\n## V2.4.3\n\n### Bugfixes\n\n- When data for an ACTIVE [Photo Series](./PhotoSeries.md) were changed, the [status](./PhotoSeries.md#photo-series-state-chart) of the series was set back to \"READY\" but the thread was still active.    \nNow, for an ACTIVE or PAUSED series, the *Photo Type* and *Start* can no longer be changed.    \nThe status will be promoted only for a NEW series.    \nFor a series in status FINISHED, data can no longer be modified.\n- The ERROR ```Could not import SensorConfiguration from picamera2.configuration```, which occured on Bullseye systems was changed to INFO\n\n## V2.4.2\n\n### Bugfixes\n\n- If livestream terminates, camera is closed if a [Photo Series](./PhotoSeries.md) is active (no [Exposure Series](./PhotoSeriesExp.md) or [Focus Stack](./PhotoSeriesFocus.md)) and if the time to the next shot is larger than 60 sec.    \nIn the previous version, the camera would not have been closed if a Photo Series was active at the time when the livestream terminated.    \nIf, for example, the interval for the Photo Series would have been 1 hour and if the livestream would have been activated shortly after a shot was taken, the camera would have been open and started for about one hour and only be closed after the next shot of the series.\n\n## V2.4.1\n\n### New Features\n\n- Process information for the Flask server process and its threads has been added to the [Info screen](./Z_Legacy_Information.md) \n- Camera status information has been added to the [Info screen](./Z_Legacy_Information.md)\n\n### Improvements\n- Cameras are now stopped and closed in times when they are not active.   \nAs a consequence, the number of active threads and CPU utilization is reduced in phases when cameras are not streaming and no other background processes (video recording, photo series, motion capturing) are active.    \nFor more details, see [Camera Status and Number of Threads](./Z_Legacy_Information.md#camera-status-and-number-of-threads)\n\n\n### Bugfixes\n\n- [Code Generation](./Troubelshooting.md#generation-of-python-code-for-camera) did not generate import statements.\n- Error status for [Triggered Capture of Videos and Photos](./Trigger.md) which had been stored with [Store Configuration](./SettingsConfiguration.md) are now cleared if server is started with stored configuration\n\n## V2.4.0\n\n### New Features\n\n- Photo Series can be set to be [automatically continued](./PhotoSeries.md#series-configuration) on server start if they had been interrupted by a server stop or system shotdown or reboot.\n\n### Bugfixes\n\n- The active [Photo Series](./PhotoSeries.md) had always been set to the alphabetically last series in case of a server start/restart, even if another series had been active at the time when the server was stopped.    \nNow, if a series with status \"ACTIVE\" is found when the server is started, this series will be set as active series.\n\n## V2.3.6\n\n### Bugfixes\n\n- Fixed error ```[Errno 12] Cannot allocate memory``` for Raspberry Pi 3.    \n(See [raspi-cam-srv Issue #9](https://github.com/signag/raspi-cam-srv/issues/9))    \nLower values for buffer_count are now also used for Pi 3, Pi 2 and Pi 1. in the same way as for Pi 4 and Pi Zero.\n\n## V2.3.5\n\n### Bugfixes\n\n- Fixed issue with **endpoints photo_feed and photo_feed2**:    \nThese endpoints use the live streams for the available cameras. However, if the live stream was not active at the time when a client requested this endpoint, no photo was shown. Only when live streams were activated through the **raspiCamSrv** Web UI, photos were shown.   \nNow, when these endpoints are requested, the system automatically starts a live stream if it is currently not active and delivers a photo.\n\n## V2.3.4\n\n### New Features\n\n- e-Mail notification on motion capturing events (see [Notification](./TriggerNotification.md))\n\n## V2.3.3\n\n### Bugfixes\n\n- Starting server with stored Configuration ([Settings](./SettingsConfiguration.md)) did not correctly set a previously configured [Zoom](./ZoomPan.md) (*ScalerCrop*). Instead, *ScalerCrop* was set to the active camera's pixel array size (see [raspiCamSrv Issue #7](https://github.com/signag/raspi-cam-srv/issues/7)). This was done only during initial system start and not after manually applying **Load Stored Configuration** in [Settings](./SettingsConfiguration.md).   \nNow, the stored *ScalerCrop* is no longer overwritten, if a zoom (<>100%) has been explicitely applied (\"include_scalerCrop\": true in controls.json).\n\n\n## V2.3.2\n\n### Improvements\n\n- Error handling has been improved. Server errors, also from background threads, are routed to the web client.   \nThis does not apply to errors occurring in encoders which are running in own threads. Exceptions thrown in these threads are currently not handled by **raspiCamSrv**.   \nError reasons are mostly invalid combinations of [Configuration](./Configuration.md) parameters, especially with *Stream Format*\n\n### Bugfixes\n\n- After applying **Swith Cameras** in page *Web Cam*, Title and metadata for the second camera were identical to those of the first camera.\n- [Reset Server](./SettingsConfiguration.md) may have caused errors in streaming or other functions\n- In [Config](./Configuration.md), *raw* stream can no longer be configured for *Live View*, *Photo*, and *Video*\n\n## V2.3.1\n\n### Bugfixes\n\n- Avoid flooding with console error message \"Motion detection thread did not stop within 5 sec\".    \nNow assuming that thread does no longer exist.\n- Fixed error ```TypeError: can only concatenate str (not \"NoneType\") to str``` which could occur in ```motionDetector.py``` if video recording failed after motion detection.   \nIn this case, there has been an error message in [events logfile](./TriggerActive.md#log-file)\n- Encoder Bitrate is no longer specified when recording a video (before it was set to 10000000)\n- Changed loglevel from ```debug``` to ```error``` when an exception occurred during video recording\n- Added error log when encoder could not be started after motion capture.   \nPreviously, the error was only shown in the [events logfile](./TriggerActive.md#log-file)\n- For Raspberry Pi 4, the default sensor mode is set to 0 (lowest resolution) in order to avoid encoder errors.\n- For Raspberry Pi 4, motion capture videos are recorded from the *lowres* stream with *Live View* configuration\n- For Raspberry Pi 4, default buffer count was reduced to 2 for live view and 4 for video\n\n## V2.3.0\n\n### New Features\n\n- Streaming of second camera added (see [Webcam](./CamWebcam.md) page). A single **raspiCamSrv** server can now simultaneously stream both cameras connected to a Raspberry Pi 5.\n- The camera configuration and controls for the active camera can be preserved also for a situation when this camera acts as \"other\" camera.\n- Streaming configurations for both cameras are stored together with the entire configuration (see [Settings](./SettingsConfiguration.md)) and can be loaded on server restart.\n\n## V2.2.3\n\n### Bugfixes\n\n- For Raspberry Pi Zero, use the *lowres* stream (Live View Configuration) for recording videos during motion capture.   \nDuring motion capture, the *Live View* camera configuration is used because the live stream is required for detecting motion. However, the *Buffer Count* of 2, used for this configuration for Pi Zero (see [V2.1.2](#v212)), is too small for video recording with the resolution of the *Video* configuration.\n- Fixed an error which could occur when [viewing events](./TriggerEventViewer.md) when placeholder photos for videos were not yet read from the database.\n\n## V2.2.2\n\n### New Feature\n\n- Added an option to automatically start motion capture with the Flask server.   \nThus, if server start is done in a service, motion capturing will automatically be active if the device is booted.   \n(See [Triggered Capture of Videos and Photos](./Trigger.md))\n\n## V2.2.1\n\n### Bugfixes\n\n- Prevent changing settings while the trigger-capture process is active\n- Prevent changing camera configuration while the trigger-capture process is active\n- Prevent starting an Exposure Series or a Focus Stack Series while the trigger-capture process is active\n- Fixed \"ValueError: could not convert string to float: ''\" which may have ocurred for Exposure Series or Focus Stack Series with a camera having no focus support\n\n## V2.2.0\n\n### Installation Hints\n\nThis version has a new database schema with tables used for captured events.\n\nAfter an update with ```git pull```, you need to initialize the database with   \n```flask --app raspiCamSrv init-db```   \nbefore starting the server.   \nThis will also recreate the user database and requires new registration.\n\nServices should be stopped during upgrade\n\n### New Feature\n\n- Introduced basic motion capturing (see [Triggered Capture of Videos and Photo](./Trigger.md))\n\n## V2.1.2\n\n### Bugfix\n\n- For Raspberry Pi Zero, the \"Buffer Count* in the [Configuration](./Configuration.md) for *Live View* and *Video* has been reduced to 2 and 4, respectively because of memory issues.   \nAlso, the default *Sensor Mode* for *Video* has been set to the lowest (0) mode, rather than to the highest.\n\n## V2.1.1\n\n### Known issues\n\n- On Pi Zero, there seems to be issues with parallel live stream on *lores* and video recording or phototaking on *main*.   \nGot ```Camera frontend has timed out!``` exception.   \nProbably, this feature needs to be deactivated on these platforms. Need to study in more details.\n\n### New Features\n\n- The Camera [Information](./Z_Legacy_Information.md) screen now shows also information on the Raspberry Pi version and board version.\n\n### Bugfix\n\n- For Raspberry Py systems Pi 4 and earlier, the *Stream Format* for *Live view* is initialized with \"YUV420\".    \nAccording to the [Picamera2 Manual](./picamera2-manual.pdf) ch. 4.2, p. 16, this format must be used for these systems for the *lowres* stream which is now the default for *Live View*.   \nThe list of values for the *lowres* stream in the [Config](./Configuration.md) dialog is not restricted to YUV format, however, if an other format is selected, an error message is shown and the parameter remains at \"YUV420\".\n- On Bullseye systems (Pi Zero), the package *picamera2.configuration* does not currently include the class *SensorConfiguration*. Also the *CameraConfiguration* class does not contain the element *sensor*.   \nThis caused an \"Import Error\" when starting the server.   \nThis error is now captured and, if it occurs, the *sensor* element in the configuration is ignored.\n\n\n## V2.1.0\n\n### New Features\n\n- Added endpoint for photo snapshots ([raspi-cam-srv Issue #5](https://github.com/signag/raspi-cam-srv/issues/5))  \n(see [Web Cam](./CamWebcam.md))\n\n## V2.0.0\n\n\n### New Features\n\n- Major modification of camera control to allow non-exclusive access to the camera from parallel tasks.   \nPhototaking, video recording and photoseries do no longer interrupt the live stream if the required camera configurations are compatible.    \n(See [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md))\n- Added code generation to the camera module.   \nThe code used for interaction of **raspiCamSrv** with Picamera2 is logged into a file specific for each server run. This generates executable Python code, suitable to 'replay' the entire camera interaction of a raspiCam Server run.    \nThis can be used for testing and error analysis.   \n(See [Generation of Python Code for Camera](./Troubelshooting.md#generation-of-python-code-for-camera))\n\n### Changes\n\n- The camera configuration for VIDEO is now initialized with the sensor mode with the largest stream size in order to allow simultaneous use of main stream for Photo and Video.\n\n### Refactoring\n\n- General refactoring of \"Timelapse series\" to \"Photo Series\".   \nTimelapse series are now just a special kind of photo series.\n- The folder ```raspi-cam-srv/raspiCamSrv/static/timelapse``` is no longer used.   \nInstead, photo series are now stored in folder ```raspi-cam-srv/raspiCamSrv/static/photoseries```   \nThis folder will be automatically created at the first server start.   \nIf you have stored photoseries under the ```timelapse``` folder, you can move them to the ```photoseries``` folder and then delete the ```timelapse``` folder.   \nFor each series, you need to exchange ```/timelapse/``` with ```/photoseries/``` in the ```*_cfg.json``` files\n"
  },
  {
    "path": "docs/ScalerCrop.md",
    "content": "# Image Cropping and Sensor Modes\n\n[![Up](img/goup.gif)](./ZoomPan.md)\n\n\n## Pixel Array Size and Sensor Modes\n\nRaspberry Pi cameras have sensors with various Pixel Array Sizes which are shown in the [Camera Properies](./Information_CamPrp.md) section of the [Info](./Information.md) screen.\n\nFor example, the V3 camera (Imx708) has a PixelArraySize of 4608 x 2592 pixels.\n\n![SensorModes](./img/Cropping_SensorModes.jpg)\n\nA camera can operate in a limited number of **Sensor Modes** (e.g. Sensor Modes 0, 1, 2 for the Imx708).\n\nInformation on Sensor Modes is shown in the [Sensor Mode x](./Information_Sensor.md) section of the [Info](./Information.md) screen.\n\nEach Sensor Mode is characterized by (among others) \n\n- a Bit Depth\n- a Frame Rate\n- a specific field of view (Crop Limits) which either spans the entire PixelArraySize or a subarea of it.\n- an output size which specifies the resolution of the image obtained with this Sensor Mode.   \nHere, an image pixel corresponds to a single sensor pixel or a group of 2x2 pixels.   \nThe output sizes for Imx708 are    \nSensor Mode 0: 1536 x 864    \nSensor Mode 1: 2304 x 1296    \nSensor Mode 2: 4608 x 2592\n\n## Cropping\n\nRaspberry Pi cameras can deliver images from a subarea of the sensor.    \nThis area is specified by the [Camera Controls](./CameraControls.md) parameter [ScalerCrop](./ZoomPan.md#current-scalercrop-zoom) which can be specified in the [Zoom and Pan](./ZoomPan.md) section of the [Live](./LiveScreen.md) screen of **raspiCamSrv**.\n\nThe effective ScalerCrop rectangle (ScalerCrop Zoop/Pan) is restricted by parameters which can be obtained from the ```camera_controls``` (see [Picamera2 Manual](./picamera2-manual.pdf), Appendix B)\n\n**NOTE**: All rectangles are specified by a tuple (xOffset, yOffset, width, height)\n\n- ScalerCrop Maximum    \nThis is the largest rectangle in which the effective ScalerCrop rectangle must be completely enclosed.    \nThis rectangle is limited by the Crop Limits of the Sensor Mode.\n- Scaler Crop Minimum   \nThis is the smallest area which can be delivered by the camera.    \nOnly width and height are relevant and width and height of the effective ScalerCrop rectangle must not be smaller.\n- Scaler Crop Default     \nThis is the default ScalerCrop rectangle which will always be chosen by the camera if an effective ScalerCrop rectangle is not requested within the Camera Properties.    \n\nWhereas in a standard case, ScalerCrop Maximum and ScalerCrop Default cover the entire PixelArraySize, the pictures below show the situation for two extreme cases:\n\n![ScalerCrop Sensor Mode 0](./img/Cropping_ScalerCrop_0.jpg) &nbsp; ![ScalerCrop Sensor Mode 0](./img/Cropping_ScalerCrop_2.jpg)\n\n## Strategy\n\nThe strategies, by which the camera operates, are not fully documented in detail.   \nHowever, systematic experiments with the relevant parameters show the following bahavior:\n\n- Use the [Camera Configuration](./Configuration.md) to specify the **Stream Sizes** for different use cases.    \nThe most 'relevant' configuration seems to be the **raw** stream.    \nA special option (*Sync Aspect Ratio*) assures consistent aspect ratios for all configurations.\n- Depending on the 'relevant' Stream Size, the camera will automatically choose a suitable Sensor Mode.    \nThe active Sensor Mode can be seen in the [Installed Cameras](./Information_Cam.md) screen for the active camera if it is currently open and started.    \n**NOTE**: **raspiCamSrv** will normally use configurations where all 3 streams (raw, main, lores) are configured in order to allow simultaneous camera access with different intents.\n- From the Crop Limits of the Sensor Mode the ScalerCrop Maximum rectangle is determined. \n- The ScalerCrop Default is the largest rectangle,    \nwhich has the aspect ratio of the 'relevant' Stream Size,    \nand which is fully inside the Crop Limits of the active Sensor Mode,   \nand which is horizontally and vertically centered.\n- Zooming and Panning allows scaling the rectangle and panning it within the area of the ScalerCrop Maximum rectangle.    \nZooming and Panning with **raspiCamSrv** always preserves the aspect ratio.\n- Finally, the effective ScalerCrop area is scaled to the *Stream Size* of the different streams\n\nBecause the aspect ratio of the ScalerCrop Zoom/Pan rectangle is determined from the aspect ratio of the 'relevant' stream, the Live Stream will be distorted if the aspect ratio for the *Live View* configuration is different from that of the configuration for the 'relevant' stream."
  },
  {
    "path": "docs/Settings.md",
    "content": "# raspiCamSrv Settings\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe Parameters section of the Settings screen is used for specification of general parameters.\n\nOther sections focus on\n\n- [Server Configuration](./SettingsConfiguration.md)\n- [User Management](./SettingsUsers.md)\n- [API Management](./API.md)\n- [Versatile Buttons](./SettingsVButtons.md)\n- [Action Buttons](./SettingsAButtons.md)\n- [Live Buttons](./SettingsLButtons.md)\n- [Devices](./SettingsDevices.md)\n- [Update](./SettingsUpdate.md)\n\n*Users* and/or *API* may be invisible, depending on context.    \nFor the case that no camera is connected, see [Settings (No Camera)](./Settings_NoCam.md).\n\n![Settings](img/Settings.jpg)\n\nThe General Paramenters include\n\n- *Active Camera* allows explicitely [setting the active camera](#switching-the-active-camera) for systems with multiple cameras.\n- *Use USB Cameras*, when activated will allow connected USB cameras to be used as *Active* or *Second Camera* (see also [Multi Cam](./CamMulticam.md))<br>**NOTE**: When deactivating *Use USB Cameras*, make sure that none of the USB cameras is currently streaming.\n- [Audio settings](#recording-audio-along-with-video) for systems with microphones if sound is to be recorded along with videos\n- *Path for Photos/Videos* shows the path where media will be stored.\n- File types for *Photo*, *Raw* Photo and *Video*\n- *Use Stereo Vision* allows activating [Stereo capabilities](#activating-and-deactivating-stereo-vision) for systems having 2 non-USB cameras of the same type connected.\n- *Show Histograms* allows [activatig/deactivating Histograms](#activating-and-deactivating-histograms) display of histograms\n- *Ext. Motion Detection supported* shows whether the actually installed libraries allow support of [Extended Motion Tracking Algoritms](#extended-motion-detection-support)\n- *Req. Auth for Streaming* controls whether [streaming requires authentication](#configuring-authentication-for-streaming)\n- *Allow access through API* shows whether the installed libraries allow secure [API access](#api-access).<br>Also if it is supported, it can be deactivated.\n- The geo-coordinates *Latitude*, *Longitute*, *Elevation* as well as the *Time Zone* are required for sun-calculations in [Sun-controlled Timelapse Photo Series](./PhotoSeriesTimelapse.md).\n\n\n## Switching the active Camera\n\nOn systems which allow connection of multiple cameras (e.g. Pi 5), it is possible to switch the active camera.   \n(see also [Information / Cameras](./Information_Cam.md))\n\n![Camera Switch](img/Settings_CamSel.jpg)\n\n## Disabling Use of USB Cameras\n\nIf you have connected one or more USB cameras to a Raspberry Pi, you can exclude them from being available in **raspiCamSrv**.   \nThey will still be shown in the [Info](./Information.md) screen but they are no longer offered as choice for camera selection in the Settings or [Multi Cam](./CamMulticam.md) screen.\n\nIf a USB camera was set as *Active Camera* or as *Second Camera*, this will be replaced by a CSI camera.\n\nIf no CSI cameras are present, the UI will switch to the \"No-Camera\" mode ([Settings (No Camera)](./Settings_NoCam.md)).\n\n## Configuring Authentication for Streaming\n\nIt can be configured whether streaming of videos or photos requires authentication:\n\n![Settings](img/Settings_Auth_Streaming.jpg)\n\n- If the checkbox is not checked, the system allows access to video streams or photos for everybody without authentication.\n- If the checkbox is checked, video streams or photos can only be accessed if a valid session is active.   \nIf a streaming URL is entered in a browser and there is no valid Flask session, the login screen is shown and, after having entered valid credentials, the [Live](./LiveScreen.md) screen is shown. Now, the desired streaming URL can be directly entered or selected from the [Web Cam](./CamWebcam.md) screen.   \nA valid Flask session exists, if login has been passed once within an active browser instance, either in another tab of the browser window intended for streaming or within another window of the **same** browser.   \nClosing all windows of a browser kills the session. \n\n## Activating and Deactivating Stereo Vision\n\nIf your system has 2 non-USB cameras of the same model connected, you can use these as [stereo camera system](https://en.wikipedia.org/wiki/Computer_stereo_vision).\n\n**raspiCamSrv** supports basic stereo features such as [3D Videos and Depth Maps](./CamStereo.md) as well as [Calibration and Rectification](./CamCalibration.md).\n\nThese features can only be activated if it has been recognized at system startup that 2 non-USB cameras of the same model are connected to the system.    \nIn addition, it is required that OpenCV, and numpy are installed (see [RaspiCamSrv Installation](./installation.md) Step 11).    \n\nIf any of these conditions is not met, the reason will be indicated:\n\n![NoStereo](img/Settings_noStereo.jpg)\n\n**NOTE**: *Stereo Vision* should only be activated if both cameras are mounted (or at least arranged) in a typical stereo camera setup. <br>I am using a 3D-printed [Raspberry Pi Camera 3 Stereo Case](https://makerworld.com/en/models/1742837-raspberry-pi-camera-3-stereo-case)\n\n![Stereo](img/Pi_Camera_3_Case_Stereo_front.JPG)\n\n## Activating and Deactivating the use of Camera AI Features\n\nAI features are currently only supported with the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) which uses the Sony IMX500 imaging sensor to provide low-latency, high-performance AI capabilities.\n\nThe camera can be used like any other Raspberry Pi CSI camera.    \nHowever to exploit the AI capablities of the camera, specific packages need to be installed.\n\nIf any of the required packagas is missing or if no AI camera is connected, this will be indicated and the checkbox is deactivated:\n\n![NoAI](img/Settings_no_AI.jpg)\n\nIn order to enable AI features for an active AI camera, activate the checkbox.\n\nThen, an additional dialog [Camera AI Configuration](./Configuration_AI.md) will be available where specific AI features can be configured and activated.\n\nIf *Use Camera AI* is deactivated and a specific AI model is currently enabled for the Active Camera or/and for the Second Camera, this will be disabled automatically.\n\n\n## Activating and Deactivating Histograms\n\n**raspiCamSrv** can show histograms for photos.   \nHistograms are generated with [OpenCV](https://de.wikipedia.org/wiki/OpenCV).  \nThis requires that the packages OpenCV, numpy and matplotlib are installed (see [RaspiCamSrv Installation](./installation.md) Step 9)   \n\n\nIf these packages are installed, you can select whether or not to *Show Histograms*.   \nThe default on first server start is to show histograms.\n\nIt may be necessary on smaller systems (Raspberry Pi Zero W, Raspberry Pi Zero 2 W) to deactivate this option because of memory restrictions.   \nIf the option is deactivated, the modules are not loaded and histograms will not be displayed, even if all packages are installed.\n\nThe system will automatically detect whether or not the required packages are installed and accessible. If this is not the case, this will be indicated and the checkbox will be disabled:\n\n![NoHistograms](img/Settings_noHistogram.jpg)\n\n## Extended Motion Detection Support\n\nIn all installations, [Motion Capturing](./TriggerMotion.md) with the *Mean Square Difference* algorithm are supported.\n\nIn order to also be able to use the extended algorithms, the following modules must be installed (see [Installation procedure, Step 11](./installation.md)):\n\n- OpenCV\n- numpy\n- matplotlib\n\nWhen the server starts up, it will be checked whether these modules can be imported.   \nIf the import had failed, this will be indicated on the Settings screen in the same way as for [Histograms](#activating-and-deactivating-histograms), above.   \nThen, only the *Mean Square Difference* algorithm will be offered  for choice on the [Trigger/Motion](./TriggerMotion.md) tab.\n\n## Recording Audio along with Video\n\n### Preconditions\n\nIf a microphone, such as a USB microphone is connected to the Raspberry Pi, it is possible to record audio along with videos.\n\nPicamera2 accesses the microphone through [PulseAudio](https://wiki.archlinux.org/title/PulseAudio).\nPulseAudio daemons (```pulseaudio.socket``` and ```pulseaudio.service```) are running as [user units](https://wiki.archlinux.org/title/Systemd/User) and not as system units.\n\nIn order to access the microphone, **raspiCamSrv** needs to run in the user environment, too.   \nThis is automatically the case when the Flask service is directly started from the command line in the **raspiCamSrv** virtual environment with   \n```flask --app raspiCamSrv run --debug --host=0.0.0.0```\n\nAlternatively, **raspiCamSrv** can be configured as **user** service as described in [README / Service Configuration for Audio Support](./service_configuration.md#service-configuration-for-audio-support)\n\n### Configuration\n\n**raspiCamSrv** will automatically detect whether a microphone is connected and accessible through PulseAudio.\n\nIf this is the case, the default microphone will be shown in the Settings screen:\n![SettingsMic](img/Settings_microphone.jpg)    \nAlso the checkbox *Record Audio along with Video* is enabled for change.\n\nIf the checkbox is checked, audio will be recorded when a video will be recorded.\n\nIf no microphone is connected or the microphone is not accessible through PulseAudio (because **raspiCamSrv** runs as system service), this will be indicated as   \n![SettingsMic](img/Settings_no_microphone.jpg)    \nand the *Record Audio along with Video* checkbox is disabled.\n\nMicrophones can be plugged in/out without stopping the system. After a refresh of the *Settings* screen, the system will detect the changed setup.\n\nIf multiple microphones are plugged in, PulseAudio will automatically select a default microphone.   \nIf the selected microphone is not the intended one, plug it out temporarily. Pulse Audio will automatically select another default and keep it.\n\n\n### Audio/Video Synchronization\n\nDue to timing issues of audio and video subsystems, there may be a delay between video and audio.   \nThe discrepancy is typically in subsecond range.\n\nTest videos should be made with something like a clapperboard. In case of delays, the *Audio Timeshift* value should be adjusted (it can be positive or negative) until video and audio are in sync.\n\n## API Access\n\nAPI access to **raspiCamSrv** is protected through JSON Web Tokens (JWT).<br>This requires the module ```flask_jwt_extended```, which is first used in **raspiCamSrv V2.11**.\n\nIf the upgrade to this version has been done without installing this module (see [Release Notes V2.11](./ReleaseNotes.md#v2110)), the system will show a hint\n![SettingsAPI](./img/Settings_API_na.jpg)\nand also hide the *API* section\n\nIn this case, the module can be installed (see [Release Notes V2.11](./ReleaseNotes.md#v2110)) and after the server has been restarted, it shows as \n![SettingsAPI](./img/Settings_API_a.jpg)\nwhich now allows activating or deactivating API support.\n\nIf the setting is changed, it is necessary to\n\n1. [Store the configuration](./SettingsConfiguration.md)\n2. Make sure that the server is configured to [Start with stored Configuration](./SettingsConfiguration.md)\n3. Restart the server (Button *Restart Server* in [Settings/Configuration](./SettingsConfiguration.md))\n\nThis will be indicated through the hint\n\n![SettingsAPI](./img/Settings_API_change.jpg)\n"
  },
  {
    "path": "docs/SettingsAButtons.md",
    "content": "# Settings - Action Buttons\n\n[![Up](img/goup.gif)](./Settings.md)\n\nOn this screen, you can 'design' the [Console Action Buttons](./ConsoleActionButtons.md) by assigning visual attributes and [Actions](./TriggerActions.md) to Buttons arranged on a grid.\n\n![ActionButtons](./img/Settings_AButtons.jpg)\n\nThe general usage of this screen is similar to that for configuring [Versatile Buttons](./ConsoleVButtons.md), except that an [Actions](./TriggerActions.md) needs to be assigned to a button."
  },
  {
    "path": "docs/SettingsAPI.md",
    "content": "# Settings API\n\n[![Up](img/goup.gif)](./Settings.md)\n\nIn this section of the Settings screen, parameters for protection of the API through [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) can be configured.\n\n**raspiCamSrv** uses the [Flask implementation of JWT](https://flask-jwt-extended.readthedocs.io/en/stable/index.html), requiring installation of the ```flask_jwt_extended``` package (see [Release Notes for 2.11](./ReleaseNotes.md#v2110))\n\nThis section will only be visible if this package is installed and if the [Geleral Parameter](./Settings.md) *Allow access through API* is checked.\n\n![Settings API](./img/Settings_API_1.jpg)\n\nJWT requires a secret private key to sign tokens.\n\n**raspiCamSrv** will store this key in a secrets file and will not publish it in [configuration exports](./SettingsConfiguration.md#server-configuration-storage).<br>Therefore, in order to activate API support, the location of the secrets file needs to be specified. It is recommended using the folowing path:<br>```/home/<user>/.secrets/raspiCamSrv.secrets```<br>This file will usually also be used to store credentials of the mail server for [notification](./TriggerNotification.md) on motion detection.\n\nThe following parameters need to be configured:\n\n- *JWT Secret Key File Path*: <br>The full path to the secrets file<br>The path as well as the file will be automatically created if they do not exist.\n- *Access Token Expiration in Minutes*<br>The Access Token is used for normal access to API endpoints.<br>For higher security, it is recommended to limit the livetime of this token.<br>This requires, however, that clients are able to react on an expiration error with refreshing the token.<br>If this is not possible, expiration can be deactivated by specifying the value 0.\n- *Refresh Token Expiration in Days*<br>The refresh token can be used to authenticate for receiving a fresh Access Token.<br>Both, Access Token and Refresh Token are obtained with the ```/api/login``` endpoint.<br>If an expiration period > 0 for the Refresh Token is specified, a new login is required if the refresh token has expired.\n\n## API Status Information\n\nThe status of API support is indicated by the colored status line.\n\nA status change will only take effect for clients if the server is restarted with the necessary configuration:\n\n1. [Store the configuration](./SettingsConfiguration.md)\n2. Make sure that the server is configured to [Start with stored Configuration](./SettingsConfiguration.md)\n3. Restart the server (see [Update Procedure, step 4](./updating_raspiCamSrv.md))\n\n\n### API enabled but not active\n\n![Settings API](./img/Settings_API_2.jpg)\n\nThis is the initial status before specification of the path to the secrets file.\n\n### API configuration completed but not yet active\n\n![Settings API](./img/Settings_API_3.jpg)\n\nThis status is obtained after valid JWT data have been submitted and if the server has not yet been restarted with these data.\n\n### API active\n\n![Settings API](./img/Settings_API_4.jpg)\n\nThis status is shown after the server has been started with valid JWT settings.\n\n## Generation of Access Token\n\nIf a client system is not able to correctly obtain valid tokens from the server, it is possible to use an Access Token which does not expire and generate it with the **raspiCamSrv** UI:\n\n![Settings API](./img/Settings_API_5.jpg)\n\nThis token can be copied into the clipboard and used as bearer token in API calls.<br>**raspiCamSrv** will never persist or publish this token, except once on this screen after it has been generated."
  },
  {
    "path": "docs/SettingsConfiguration.md",
    "content": "# Settings / Server Configuration\n\n[![Up](img/goup.gif)](./Settings.md)\n\n\nThe *Settings* screen includes a *Configuration* section with functions to control the **raspiCamSrv** configuration.\n\nThe list of *Unsaved Configuration Changes* lists all actions with their time of execution, which have been made during the current session and which have not yet been saved to the server.\n\n![Configuration](./img/Settings_Config.jpg)\n\n- Button *Store Configuration* generates a set of JSON files which include the entire configuration of the **raspiCamSrv** server (see [below](#server-configuration-storage)).<br>**NOTE**: This does not include [Photo Series](./PhotoSeries.md). These are persisted automatically and independently. It also does not include [Events](./TriggerActive.md).\n- Button *Load Stored Configuration* replaces the current configuration with the previously stored configuration.<br>[Photo Series](./PhotoSeries.md) and [Events](./TriggerEventViewer.md) are not affected.<br>**NOTE**: If you had activated [API](./SettingsAPI.md) access before, this will be deactivated when the stored configuration is loaded. You need to restart the server to activate it again.\n- Button *Reload Cameras* resets and reloads the entire camera configuration (see [Reloading Cameras](#reloading-cameras)). This fuction must be applied when USB cameras have been unplugged or new USB cameras plugged in while the server was active (hot plug). This will adjust the entire camera configuration to the new setup.<br>**NOTE**: Use this function **immediately** after unplugging a USB camera. Otherwise errors can occur when using other functions<br>**NOTE**: This has no effect when CSI cameras have been plugged in or out. This requires rebooting the Raspberry Pi, to be effective.\n- Button *Reset Server* stops any background activity (live stream, video, photo series, motion capturing and event handling) and replaces the current configuration with the default configuration.<br>[Photo Series](./PhotoSeries.md) and [Events](./TriggerEventViewer.md) are not affected. Any associated resources remain unchanged. However, an active [Photo Series](./PhotoSeries.md) will be paused and needs to be continued.<br>**NOTE**: If you had activated [API](./SettingsAPI.md) access before, this will no longer be available when the configuration is reset.<br>The same applies to [Notification Settings](./TriggerNotification.md) which need to be reconfigured.<br>**NOTE**: If you had activated *Start Server with Stored Configuration*, this will be deactivated. Probably, you might want to store the new configuration bofore activating this again.\n- *Start server with stored Configuration* controls whether a server start shall use the default configuration or the stored configuration.\n- Button *Backup Stored Data*   \nWith this button, you can create a [backup](#backups) of all data currently stored in the file system.   \nBefore pressing the button, you need to enter a unique name for the backup.\n- Button *Restore Backup*    \nWith this button, you can restore the selected backup.   \nAfter restore is completed and confirmed by the status message, you need to restart the server with the *Restart Server* button.\n- Button *Remove Backup*   \nWith this button, you can remove the selected Backup.\n- Button *Restart Server*   \nwill restart the raspiCamSrv Flask server.    \nThe system will automatically detect whether the server was started as system unit, as user unit or from the command line.    \nIn the latter case, you are asked to stop the server manually.   \nWhen the server restarts, the browser will lose connection.    \nPress the browser's **Back** button until you see the recently used raspiCamSrv screen and then push any of the upper menu options to reconnect with the restored configuration.\n\n#### Server Configuration Storage\n\nWhen the configuration is stored with the *Store Configuration* button, a set of files is created/replaced in the ```raspi-cam-srv/raspiCamSrv/static/config``` folder:\n\n![Config](./img/Settings_ConfigStore.jpg)\n\n- _loadConfigOnStart.txt<br>This is just an empty marker file. If the file exists, the server will initiate its configuration with configuration data stored in the other files.<br>Otherwise, default configuration settings will be applied.\n- cameraConfigs.json<br>This is currently not used\n- cameraProperties.json<br>This file contains the camera properties of the actice camera, which are shown in [Camera Properties](./Information_CamPrp.md).<br>Camera properties are always read directly from the camera.\n- cameras.json<br>This file contains the installed cameras with information shown in [Installed Cameras](./Information_Cam.md)<br>Installed cameras are always directly queried from the camera system.\n- controls.json<br>This file includes all the camera configuration settings as shown in the upper right part of the Live screen [Camera Controls](./LiveScreen.md#top-right-quarter)\n- LiveViewConfig.json, photoConfig.json, rawConfig.json, videoConfig.json<br>contain the camera configuration settings for the different use cases as shown in the [Config screen](./Configuration.md)\n- rawFormats.json<br>contain a list of formats which can be used for raw photos.<br>This information is extracted from the different [Sensor Modes](./Information_Sensor.md and is always directly obtained from the camera system.\n- serverConfig.json<br>This file includes configuration settings for the **raspiCamSrv** dialog system, such as information included in the [Settings](./Settings.md) dialog, or the configuration of the [Display Buffer](./LiveScreen.md#bottom-left-quarter) and some navigation details.\n- streamingCfg.json contains, for each camera, the [Tuning](./Tuning.md) configuration, the [Live View Configuration](./Configuration.md) settings and the [Camera Controls](./CameraControls.md) which will be used for streaming. The included Video Configuration is stored because Picamera2 always requires the *main* stream to be configured. This will not be used for streaming.\n- triggerConfig.json contains the configuration settings for triggered capture of videos and photos (motion capture)\n- tuningConfig.json contains the settings maintained in the [Tuning](./Tuning.md) dialog\n\n## Reloading Cameras\n\nWhen the function **Reload Cameras** is applied, the system will \n\n1. [Detect](./Information_Cam.md#detection-of-cameras) the currently connected cameras\n2. [Identify](./Information_Cam.md#identification-of-usb-cameras) USB cameras, if connected \n3. Then determine Camera Properties (e.g. model) and Sensor Modes for CSI and [USB](./Information_Cam.md#determining-camera-properties-for-usb-cameras) cameras.\n4. Create a list of supported cameras, considering whether [Use of USB cameras is disabled](./Settings.md#disabling-use-of-usb-cameras).\n\nBefore applying the function the list of supported cameras may look like\n\n| Num | Model                  | USB | Device      |\n|-----|------------------------|-----|-------------|\n| 0   | imx708                 | No  |             |\n| 1   | imx219                 | No  |             |\n| 2   | Logi 4K Stream Edition | Yes | /dev/video0 |\n| 3   | C922 Pro Stream Webcam | Yes | /dev/video4 |\n\nAfter remofing the *imx219* and unplugging the *Logi 4K*, the list will be: \n\n| Num | Model                  | USB | Device      |\n|-----|------------------------|-----|-------------|\n| 0   | imx708                 | No  |             |\n| 1   | C922 Pro Stream Webcam | Yes | /dev/video4 |\n\nWhen comparing the lists, the system will look for matching Num, Model, USB and Device.   \nIf one of these parameters differs, the camera with that number will be initialized based on the current Camera Properties and Sensor Modes. Any previously specified [Configuration](./Configuration.md) or [Controls](./CameraControls.md) will be reset to default values for the respective camera type.\n\nIn the example above, Camera 0 will keep their settings and Camera 1 will be reset.\n\n## Backups\n\nBackups preserve the currently stored data structures of **raspiCamSrv** so that they can be consistently restored later.\n\nThis includes:\n\n- all [configuration data](#server-configuration-storage) stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/config```\n- all photos stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/photos```\n- all [photo series](./PhotoSeries.md#photo-series-in-the-file-system) stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/photoseries```\n- all [event data](./TriggerActive.md#event-data) stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/events```\n- The [Camera calipration photos](./CamCalibration.md#calibration-data-storage) for a [Stereo Camera System](./CamStereo.md) stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/calib_photos```\n- The Stereo Camera calibration parameters stored under folder ```~/prg/raspi-cam-srv/raspi-cam-srv/static/calib_data```\n- The SQLite database with [User data](./Authentication.md) and [Event data](./TriggerActive.md#database) stored as ```~/prg/raspi-cam-srv/instance/raspiCamSrv.sqlite```\n\nWhen a backup is created, all these data are stored underneath ```~/prg/raspi-cam-srv/backups/<backup-name>``` where ```<backup-name>``` is the name given to the backup:\n\n![Backup](./img/Settings_Backup.jpg)"
  },
  {
    "path": "docs/SettingsConfiguration_NoCam.md",
    "content": "# Settings / Server Configuration (No Camera)\n\nThis is a variant of the [Settings / Server Configuration](./SettingsConfiguration.md) screen for the case when no camera is available.\n\n[![Up](img/goup.gif)](./Settings_NoCam.md)\n\n\nThe *Settings* screen includes a *Configuration* section with functions to control the **raspiCamSrv** configuration.\n\nThe list of *Unsaved Configuration Changes* lists all actions with their time of execution, which have been made during the current session and which have not yet been saved to the server.\n\n![Configuration](./img/Settings_Config_no_cam.jpg)\n\nFor details, see [Settings / Server Configuration](./SettingsConfiguration.md)\n\nWhen currently no camera is available in **raspiCamSrv** but one or more USB cameras have been plugged in while raspiCamSrv was active, button **Reload Cameras** will reset the entire camera configuration and usually detect new cameras.   \nA confirmation is required:\n\n![Conf](./img/Settings_ConfigReloadCamConf.jpg)"
  },
  {
    "path": "docs/SettingsDevices.md",
    "content": "# Settings - Devices\n\n[![Up](img/goup.gif)](./Settings.md)\n\nOn this Settings screen you can configure devices connected to the Raspberry Pi through GPIO of the [40-pin header](#40-pin-gpio-header).\n\n![Devices](./img/Settings_Devices.jpg)\n\n\n**IMPORTANT**: To preserve any configurations over server restart, you need to [store the configuration and activate *Start Server with stored Configuration*](./SettingsConfiguration.md)\n\n## Creating a Device\n\nTo create a new device, select one of the preconfigured Device Types and enter a unique ID by which the device will be identified within **raspiCamSrv**.\n\nPressing *Create* will open the *Device Configuration* where the device can be configured in detail.\n\nA graphic with wiring information in the upper right area will help connecting the device.\n\n## Configuring a Device\n\nIn **raspiCamSrv**, GPIO-connected devices are controlled using the [gpiozero](https://gpiozero.readthedocs.io/en/stable/index.html) library which is deployed with Raspberry Pi OS.\n\n**raspiCamSrv** supports all 'regular' input and output device classes of gpizero as well as StepperMotor which is an own extension of ```gpiozero.OutputDevice```.\n\n- *Config Status*<br>shows whether the device is completely configured.<br>This requires normally, that valid numbers have been set for all pin parameters.\n- *Device Type*<br>shows the class name through which the device is accessible.\n- *Usage*<br>shows whether the device is an Input or Output device.<br>This information is used when [Triggers](./TriggerTriggers.md) and [Actions](./TriggerActions.md) are configured where either one or the other can be selected.\n- *gpiozero Doc*<br>A link to the *gpiozero* page for the specific class can help understanding the meaning of the different interface parameters as well as for the specific behavior of the class.<br>Only the link for the StepperMotor links to a **raspiCamSrv** page with information about this class.\n\nTypically, most of the device types have an individual set of parameters with different data types and value ranges.\n\nThese parameters are preconfigured in **raspiCamSrv** (see [Device Type Configuration](#device-type-configuration)) and are shown with their default values from *gpiozero*.\n\nIn almost all cases, only the parameters for the GPIO pins need to be configured.\n\nOnly, when all GPIO pins are configured, the *Config Status* is set to \"OK\", which is a precondition that the device can be used.\n\n## Device Overview\n\nThe right side of the dialog shows an overview of all configured devices with their associated GPIO pins and their *Config Status*\n\nThe information about *Unused GPIO Pins* can help finding places for new devices.<br>This information only considers the devices configured in **raspiCamSrv**.\n\n## Modifying Device Configuration\n\nYou can modify configuration parameters for a device after selecting the device ID in the *Device Configuration* section.\n\nIf a device is selected, also the device Type and the image in the upper area are adjusted.\n\nThe ID of a device can not be modified.\n\n## Deleting a Device\n\nThe *Delete* button allows deletion of the selected device. This requires an additional confirmation.\n\nIf a device is used in one of the [Triggers](./TriggerTriggers.md) or [Actions](./TriggerActions.md) configurations, it cannot be deleted. An error message will be shown only after deletion has been confirmed.\n\n## Testing a Device\n\nAfter a device has been configured, it should be tested that it is working correctly.\n\nWhen the *Config Status* is \"OK\", the *Device Test* section is shown, which initially consists only of the *Test* button.\n\nWhen it is pressed, a preconfigured set of test steps will be executed.\n\n**IMPORTANT**: Before pressing *Test*, make sure that any moving devices (e.g. Motors, Servos) can move freely.\n\nAfter the test is completed, the return values of the configured test methods will be shown.\n\nFor a test either the entire test or the individual steps may have preconfigured durations. So you need to wait until the test is completed and observe the device.\n\n## Calibrating a Device\n\nSome devices require state tracking and calibration.\n\n### Calibration Types\n\nDifferent devices may require different calibration procedures.\n\nThis applies currently to\n\n#### StepperMotor:\n\nThe StepperMotor itself does not have knowledge about its current position and when the class is instantiated, the *current_angle* is set to zero.   \nFor usage of the StepperMotor it is, however, essential to know the position at any time.\n\nIt is, therefore, necessary to\n\n1. set a certain state as reference \"Zero\"\n2. track and memorize any movements\n3. set the last state whenever the device class is instantiated\n\n#### ServoPWM\n\nFor Servos, the situation is slightly different. Servos (except 360° Servos) have a limited range of operation\nwith a minimum and maximum angle. The actual position is controlled through the pulse width of a PWM signal and the position is kept when the signal is switched off.\n\nIn order to avoid unexpected movements when a servo is activated, we need to keep track of the last position and set this position when the servo is started.\n\nFurthermore, we usually want to have a certain position as reference or \"Zero\". The exact choice of this position should be adustable by calibration.\n\nThe choice of a specific position as \"Zero\" does not change the range of operarion.\n\n### Calibration Support\n\nraspiCamSrv supports both types of calibration which can be configured for a device type in the [Device Type Configuration](#device-type-configuration).\n\nFor device types requiring calibration, a **Calibrate** button will be shown when a device with this type is created or modified.\n\nPressing **Calibrate** will show additional buttons for calibration as well as the current \"internal\" state (```current_angle``` for StepperMotor, ```current_angle-calibration``` for ServoPWM):\n\n![Calibration](./img/Settings_Devices_Calibration.jpg)\n\nYou can now change the device status using the arrow buttons until you reach the desired zero.   \nPushing **OK** will then\n\n- for StepperMotor\n<br>set the current state as reference\n\n- for ServoPWM\n<br>set the parameter *calibration* to the current \"internal\" state\n\n- in both cases hide the calibration buttons.\n\n## State Tracking\n\n**raspiCamSrv** will track all status changes in a JSON file named after the device ID:     \n![Status](./img/Settings_Devices_Calibration_State.jpg)\n\n**ATTENTION**: If you are using such devices in any triggered [Actions](./TriggerActions.md) you should avoid shutting down the server while event handling is activated. The system might not be able to memorize the latest device state and may, therefore start with an incorrect state.\n\n## Device Type Configuration\n\nThe device types, supported by **RaspiCamSrv** are preconfigured in the file ```gpioDeviceTypes.py```.\n\nBelow is an example for the ```DistanceSensor```:\n\n```json\ngpioDeviceTypes = [\n    {\n        \"type\":\"DistanceSensor\",\n        \"usage\":\"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#distancesensor-hc-sr04\",\n        \"image\": \"device_DistanceSensor.jpg\",\n        \"params\": {\n            \"echo\": {\n                \"value\": \"\",\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 27,\n                \"isPin\": True\n                },\n            \"trigger\": {\n                \"value\": \"\",\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 27,\n                \"isPin\": True\n                },\n            \"queue_len\": {\n                \"value\": 9,\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 99\n                },\n            \"max_distance\": {\n                \"value\": 1.0,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 100.0\n                },\n            \"threshold_distance\": {\n                \"value\": 0.3,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 100.0\n                },\n            \"partial\": {\n                \"value\": False,\n                \"type\": \"bool\"\n                }\n        },\n        \"testMethods\":[\n            \"distance\",\n            \"value\"\n        ],\n        \"events\":[\n            \"when_in_range\",\n            \"when_out_of_range\"\n        ],\n        \"eventSettings\":{\n            \"threshold_distance\": 0.0\n        },\n        \"control\":{\n            \"bounce_time\": 0.0\n        }\n    },\n]\n\n```\n\nThe different elements are used for different purposes:\n\n- *type*<br>identifies the class name for the device type.\n- *usage*<br>distinguishes Input and Output devices\n- *docURL*<br>is the URL for class documentation\n<br>If ```/latest/``` is found in the URL, this will be replaced by the document version for the current software version, encapsulated with \"/\".\n- *image*<br>identifies the image shown in the dialog\n- *params*<br>characterizes the class constructor interface with parameter name, default value, type (with some non-Python declarations) as well as the valid range.<br>The \"isPin\" sub-element identifies parameters which correspond to GPIO pins.\n- *testMethods*<br> is a list of test methods, if necessary with parameters, which are executed during the test.\n- *testDuration* or *testStepDuration* specify the duration of the entire test or of every test step.\n- *events*<br>occur in Input devices and identify events which are captured by the device class and to which callback routines can be assigned.<br>This will be used in the specification of [Triggers](./TriggerTriggers.md).\n- *eventSettings*<br>is a list of parameter assignments which will be set **before** callbacks are assigned to the event parameters.<br>An example is the ```threshold_distance``` which is required for ```DistanceSensor``` to distinguish between *in_range* and *out_of_range*.\n- *actionTargets*<br>occur in Output devices and identify methods which can be used in [Actions](./TriggerActions.md).<br>Whereas the sub-element \"method\" identifies the method name (which can be a method or a property which **raspiCamSrv** determines through inspection), the \"params\" element specifies the parameters which can be configured for the method call or the property assignment.<br>These parameters serve also as 'templates' for type checks which are done during [Action](./TriggerActions.md) configuration.\n- *control* elements<br>which can occur as sub-elements of \"actionTarget\" for Output devices as well as for Input devices, are not part of the class interface but are used to control specific behavior in **raspiCamSrv**.<br>For input devices it can, for example specify the ```bounce_time``` for software controlled bouncing suppression beyond that what might already be provided by *gpiozero*.<br>For Output devices and a specific action target, it can, for example specify a duration for which the action shall last, for example how long an LED shall be enlighted.\n\n### Calibration Configuration\n\nDevices requiring calibration, have an element ```\"calibration\"``` in their Device Type Configuration:\n\n```\n        \"calibration\": {\n            \"fbwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": -10.0},\n            },\n            \"bwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": -1.0},\n            },\n            \"calibrate\": {\n                \"param\": \"calibration\",\n            },\n            \"fwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": 1.0},\n            },\n            \"ffwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": 10.0},\n            },\n        },\n```\n\nHere you find 5 sub-elemnts which control the function of the 5 calibration buttons in the Settings/Devices dialog:\n\n- fbwd\n<br>defines the method to be called (or attribute to be set) for \"fast backward\" (```<<```)\n- bwd\n<br>defines the method to be called (or attribute to be set) for \"backward\" (```<```)\n- fwd\n<br>defines the method to be called (or attribute to be set) for \"foreward\" (```>```)\n- ffwd\n<br>defines the method to be called (or attribute to be set) for \"fast foreward\" (```>>```)\n- calibrate\n<br>specifies the calibration procedure to be applied when the ```OK``` button is pressed\n\n#### Calibration Procedures\n\nThe following alternatives are supported for specifying the calibration procedure:\n\n##### \"Servo-like\" Calibration\n\n```\n            \"calibrate\": {\n                \"param\": \"calibration\",\n            },\n```\n\nThis specifies that a specific class parameter (here ```calibration```) needs to be set to the current (intrinsic) value.\n\nThis must be a parameter of the class constructor which cannot be changed for an existing object.\n\n##### \"Stepper-motor-like\" Calibration\n\n```\n            \"calibrate\": {\n                \"method\": \"value\",\n                \"params\": {\"value\": 0.0},\n            },\n```\n\nThis specifies that a specific attribute is set to the given value (typically 0.0) or that a specific method is called with the specified parameter signature.\n\nThis method (or attribute setter) must not physically change the position (or state) of the device but only change the internal variable, representing this position, to the given value.\n\n\n## 40-Pin GPIO Header\n\n![GPIO Header](./img/GPIO_Pins.jpg)"
  },
  {
    "path": "docs/SettingsLButtons.md",
    "content": "# Settings - Live Buttons\n\n[![Up](img/goup.gif)](./Settings.md)\n\nOn this screen, you can 'design' the [Live Ctrl Buttons](./CameraControls_Ctrl.md) by assigning visual attributes and [Actions](./TriggerActions.md) or OS commands to Buttons arranged on a grid.\n\n![LiveButtons](./img/Settings_LButtons.jpg)\n\nThe general usage of this screen is similar to that for configuring [Versatile Buttons](./ConsoleVButtons.md), except that you can assign either an OS Command or an [Action](./TriggerActions.md) to a button."
  },
  {
    "path": "docs/SettingsUpdate.md",
    "content": "# Settings - Update\n\n[![Up](img/goup.gif)](./Settings.md)\n\nThis dialog can be used to update raspiCamSrv to the latest version available on [GitHub](https://github.com/signag/raspi-cam-srv)\n\n![Update 2](./img/Settings_Upd_2.jpg)\n\nIf the installed version is lower than the latest version on GitHub, the version number on the title bar is shown in yellow.\n\nThe Update dialog shows the following fields:\n\n- *Check for Updates*<br>This switch can be used to activate or deactivate checking for updates.\n- *Latest Version*<br>This is the latest version available on GitHub. raspiCamSrv determines the latest version by analyzing the [Release Notes](./ReleaseNotes.md).<br>GitHub is checked for the latest version at every start of the Flask server or with the specified time interval when executing any function of the [Live](./LiveScreen.md) dialogs.\n- *See Release Notes*<br>The link opens the latest release notes for inspection of new features.\n- *Installed Version*<br>This field shows the currently installed version.\n- *Update to Vx.y.z*<br>This button is only visible if you can update to a newer version.<br>Pressing the button will issue a ```git pull origin main --depth=1``` command to update raspiCamSrv.<br>After the update has completed successfully, the button will turn to (see below):\n- *Restart Server*<br>This button will restart the raspiCamSrv Flask server to switch to the new version.\n- *Notify on Version later than*<br>Initially, this field shows the current version.<br>If you want to skip updates to the indicated latest version, you can press the *Ignore ...* button and the field will show the latest version.<br>Information on possible updates will only be given, if the latest version on GitHub is later than the version shown in this field.\n- *Latest Version checked at*<br>This shows the time when the indicated *Latest Version* was retrieved from GitHub.\n- *Check Interval (Hours)*<br>Is the interval with which raspiCamSrv will check for updates.\n- *Check Now*<br>Check GitHub for a new version now.\n\n## Dialog after successfull Update\n\n![Update 2](./img/Settings_Upd_3.jpg)\n\nNow, you need to restart the server with the *Restart Server* button.\n\n**NOTE**: After completion of the update, the active server is still on the old version.<br>When the server restarts, the browser will get an empty response. You should press the *Back* button of the browser to return to the previous screen and then press any menu options of the **black** menu bar.\n\nIf the server is still on the old version after restart, you can follow the [manual update procedure](./updating_raspiCamSrv.md).\n\n## Dialog with *Check for Updates* deactivated\n\n![Update 1](./img/Settings_Upd_1.jpg)\n"
  },
  {
    "path": "docs/SettingsUsers.md",
    "content": "# Settings Users\n\n[![Up](img/goup.gif)](./Settings.md)\n\n\nFor management of users, the *Settings* screen has an additional section *Users* which is visible only for the SuperUser:  \n\n![User Management](./img/Auth_UserManagement.jpg)   \n\nThe list shows all registered users with\n- unique user *ID*\n- user *Name*\n- *Initial*, indicating whether the user has been initially created by the SuperUser and needs to change password on first log-in.\n- *SuperUser*, indicating the user registered as SuperUser\n\nThe SuperUser can\n- register new users using the *Register New User* button\n- remove users which have been selected in the list\n"
  },
  {
    "path": "docs/SettingsVButtons.md",
    "content": "# Settings - Versatile Buttons\n\n[![Up](img/goup.gif)](./Settings.md)\n\nThis Settings screen allows configuration of function buttons which will be shown on the [Console](./Console.md) screen.\n\n![vButtons](./img/SettingsVButtons.jpg)\nThe screenshot above is an example layout. Initially, the *Button Settings* area is empty.\n\n## Button Layout\n\nButtons on the [Console](./Console.md) screen are arranged in an N x M grid where the *Number of Rows* and *Number of Columns* can be configured.   \n\nOnce non-zero values have been specified and submitted, the *Buttons Settings* area will show a list of parameters for specification of button properties.\n\nIf an existing grid layout is modified by changing either the number of rows and/or the number of columns, the configured rows and columns of buttons will be preserved if the new values are larger than the old ones. Superfluous rows or columns will be removed in case that new dimensions are smaller than the old ones.\n\nThe checkbox *Interactive Commandline* controls whether [Console](./Console.md) will show an interactive commandline where commands can be directly entered. \n\n**IMPORTANT**: You need to [Store Configuration](./SettingsConfiguration.md) if you want the button settings to survive a server restart!\n\n## Button Settings\n\n- *Row*<br>The row in which the button will be placed.\n- *Col*<br>The column in which the button will be placed.\n- *Visible*<br>When the checkbox is activated, a button will be shown in the given grid cell, otherways the grid cell will remain empty.\n- *Shape*<br>The shape of each button can be selected from a small set of standard shapes (Rectangle, Rounded, Circular, Square).<br>The example layout of the above configuration is shown for the [Console](./Console.md) screen.\n- *Color*<br>The Color of each button can be selected from a small set of standard Colors (Black, Red, Green, Yellow, Blue).<br>The example layout of the above configuration is shown for the [Console](./Console.md) screen.\n- *Button Text*<br>Text to be shown on the button.\n- *Command*<br>Linux command to be executed on OS level.<br>You may use available Linux commands or run your own scripts.<br>It is recommended to test these commands on an OS prompt before configuring and running them out of **raspiCamSrv**<br>The working directory is that of the service (see [Service Configuration](./service_configuration.md)).\n- *Conf*<br>If the checkbox is checked, the respective button will require a confirmation before the command will be executed.\n\nAny changes for these settings need to be submitted with the button underneath the table\n\n## User Scripts and Programs\n\nAny scripts or Python programs can be put inside a folder     \n```~/prg/raspi-cam-srv/user_code```    \nor any sudirectory structure.\n\nThis folder structure is excluded from Git and will, therefore, not be touched when upgrading or updating.\n\nFor addressing these, it is sufficient to use the path relative to the root ```~/prg/raspi-cam-srv```, e.g. ```user_code/my_program.py```\n\nPython programs can use all packages installed in the virtual environment when called out of the Flask server, for example when pressing a [Versatile Button](./ConsoleVButtons.md).\n\n"
  },
  {
    "path": "docs/Settings_NoCam.md",
    "content": "# raspiCamSrv Settings (No Camera)\n\n[![Up](img/goup.gif)](./UserGuide_NoCam.md)\n\nThis is a variant of the general [Settings](./Settings.md) screen, which shows up when no camera is available.\n\nOther sections focus on\n\n- [Server Configuration](./SettingsConfiguration.md)\n- [User Management](./SettingsUsers.md)\n- [API Management](./API.md)\n- [Versatile Buttons](./SettingsVButtons.md)\n- [Action Buttons](./SettingsAButtons.md)\n- [Devices](./SettingsDevices.md)\n\n*Users* and/or *API* may be invisible, depending on context.\n\n![Settings](img/Settings_no_cam.jpg)\n\nThe General Paramenters include\n\n- *Use USB Cameras* This option is only visible if the system has detected at least one USB camera (see [Info](./Information.md)).   \nActivating the checkbox will activate the connected cameras for **raspiCamSrv**.\n- *Allow access through API* shows whether the installed libraries allow secure [API access](#api-access).<br>Also if it is supported, it can be deactivated.\n- The geo-coordinates *Latitude*, *Longitute*, *Elevation* as well as the *Time Zone* are not currently used when there are no cameras.\n\n## Enabling Use of USB Cameras\n\nIf this option is shown, **raspiCamSrv** has identified at least one USB camera, but currently this is not available.\n\nIf you enable use of USB cameras, the system will select one of the USB cameras as *Active Camera* and another one, if present, as *Second Camera*\n\nThe UI will then automatically switch to the mode with cameras ([Information](./Information.md))\n\n## API Access\n\nAPI access to **raspiCamSrv** is protected through JSON Web Tokens (JWT).<br>This requires the module ```flask_jwt_extended```, which is first used in **raspiCamSrv V2.11**.\n\nIf the upgrade to this version has been done without installing this module (see [Release Notes V2.11](./ReleaseNotes.md#v2110)), the system will show a hint\n![SettingsAPI](./img/Settings_API_na.jpg)\nand also hide the *API* section\n\nIn this case, the module can be installed (see [Release Notes V2.11](./ReleaseNotes.md#v2110)) and after the server has been restarted, it shows as \n![SettingsAPI](./img/Settings_API_a.jpg)\nwhich now allows activating or deactivating API support.\n\nIf the setting is changed, it is necessary to\n\n1. [Store the configuration](./SettingsConfiguration.md)\n2. Make sure that the server is configured to [Start with stored Configuration](./SettingsConfiguration.md)\n3. Restart the server (see [Update Procedure, step 4](./updating_raspiCamSrv.md))\n\nThis will be indicated through the hint\n\n![SettingsAPI](./img/Settings_API_change.jpg)\n"
  },
  {
    "path": "docs/SetupDocker.md",
    "content": "# Running **raspiCamSrv** as Docker Container\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\nA container image is available for **raspiCamSrv** at [https://hub.docker.com/repository/docker/signag/raspi-cam-srv](https://hub.docker.com/repository/docker/signag/raspi-cam-srv)\n\n**ATTENTION**: Running raspiCamSrv in Docker is still somehow 'experimental'. Successful tests have been done only on Pi 4 and Pi 5. However, not all functions have so far been systematically tested. On Pi Zero W and Pi Zero 2 W, deployment of the image was not successful, probably because of its size (~840 MB).     \nLimited memory may force using lower resolutions, especially with two CSI cameras on a Pi 5.\n\n**1. Preconditions**\n\n- [Installation of Docker on a Raspberry Pi](#installation-of-docker-on-a-raspberry-pi)\n- [Check Contiguous Memory (CMA)](#checking-contiguous-memory-cma)\n- [Deactivate raspiCamSrv Service from manual Installation](#deactivate-raspicamsrv-service-from-manual-installation)\n\n**2. Compose Service Definition**\n\nIn an arbitrary working directory (e.g. ```~/docker```), create\n\n```compose.yaml```:\n\n```yml\nservices:\n  raspi-cam-srv:\n    image: signag/raspi-cam-srv\n    container_name: raspi-cam-srv\n    network_mode: \"host\"\n    ports:\n      - \"5000:5000\"\n    devices:\n      - /dev/video0:/dev/video0\n      - /dev/gpiochip0:/dev/gpiochip0\n    volumes:\n      # Uncomment resource mappings, if required\n      # Configure and prepare container-external folders\n      #- ./resources/database/:/app/instance/\n      #- ./resources/calib_data/:/app/raspiCamSrv/static/calib_data/\n      #- ./resources/calib_photos/:/app/raspiCamSrv/static/calib_photos/\n      #- ./resources/config/:/app/raspiCamSrv/static/config/\n      #- ./resources/events/:/app/raspiCamSrv/static/events/\n      #- ./resources/photos/:/app/raspiCamSrv/static/photos/\n      #- ./resources/photoseries/:/app/raspiCamSrv/static/photoseries/\n      #- ./resources/tuning/:/app/raspiCamSrv/static/tuning/\n      - /dev:/dev\n      - /sys:/sys\n      - /run/udev/:/run/udev:ro\n      - /etc/timezone:/etc/timezone:ro\n      - /etc/localtime:/etc/localtime:ro\n    environment:\n      - GPIOZERO_PIN_FACTORY=lgpio\n      - SYSTEMD_BUS_ADDRESS=unix:path=/run/systemd/private\n    restart: unless-stopped\n    privileged: true\n```\n\nThe *volumes* attribute includes prepared mappings of resource folders for database, [camera calibration](./CamCalibration.md), [configuration](./SettingsConfiguration.md), [events](./TriggerActive.md), [photos](./PhotoViewer.md), [photoseries](./PhotoSeries.md#photo-series-in-the-file-system) and [tuning](./Tuning.md), which can/should be mapped to container-external folders in order to allow easy access and preserve the data also in case the container is removed or reset after a new image version has been pulled.<br>**NOTE**: For a quick test, the container can also be run without these mappings.\n\nConsider that the database holds [user data](./Authentication.md) as well as [event data](./TriggerActive.md) which will be lost if the database exists only within the container and if the container is removed.\n\n**3. Pull raspi-cam-srv Image**\n\n```docker compose pull raspi-cam-srv```\n\nWait until all pulls are completed.\n\n**4. Create Container**\n\n```docker compose create raspi-cam-srv```\n\n![Create Container](./img/docker_CreateContainer.jpg)\n\n**5. Start Container**\n\n```docker compose start raspi-cam-srv```\n\n![Start Container](./img/docker_StartContainer.jpg)\n\n**6. Initialize Database**\n\nThis step is only required if the ```/app/instance/``` folder, containing the database, has been mapped to a container-external folder.\n\nThe image includes already an initialized database. However, if the ```/app/instance/``` is mapped, this database is not visible for the container and an empty database is created which needs to be initialized.\n\nThis can be done on an interactive command prompt for the container:\n\n```docker compose exec raspi-cam-srv sh```\n\n```flask --app raspiCamSrv init-db```\n\n![Initialize DB](./img/docker_InitDb.jpg)\n\n\n**7. Connect to raspiCamSrv**\n\nFor usage of **raspiCamSrv** see the [User Guide](./UserGuide.md)\n\n## Useful Docker commands\n\nSee [Docker Reference](https://docs.docker.com/reference/cli/docker/compose/)\n\n- Pull latest image<br>```docker compose pull raspi-cam-srv```\n- Start container<br>```docker compose start raspi-cam-srv```\n- Show server logs<br>```docker compose logs raspi-cam-srv```\n- Open shell for interactive prompt<br>```docker compose exec raspi-cam-srv sh```\n- List containers<br>```docker container ls```\n- List images used by the created containers<br>```docker compose images```\n- Stop container<br>```docker compose stop raspi-cam-srv```\n- Remove container<br>```docker compose rm raspi-cam-srv```\n- List images<br>```docker image ls```\n- Remove an image with a given ID<br>```docker image rm IMAGE_ID```\n- Show docker disk usage<br>```docker system df```\n- Remove unused data<br>```docker system prune```\n\n\n## Update Procedure\n\nChanges in the [raspi-cam-srv Git repository](https://github.com/signag/raspi-cam-srv) will automatically trigger a new build of the [raspi-cam-srv Docker Image](https://hub.docker.com/repository/docker/signag/raspi-cam-srv).\n\nTo update to the latest version, proceed as follows:\n\n1. cd to your working directory, e.g. ```cd ~/docker```\n2. ```docker compose pull raspi-cam-srv```\n3. ```docker compose stop raspi-cam-srv```\n4. ```docker compose rm raspi-cam-srv```\n5. ```docker compose create raspi-cam-srv```\n6. ```docker compose start raspi-cam-srv```\n\n## Installation of Docker on a Raspberry Pi\n\nThe [Docker](https://www.docker.com/) documentation includes descriptions on how to [Install Docker Engine on Raspberry Pi OS](https://docs.docker.com/engine/install/raspberry-pi-os/)\n\nThe most convenient way is using the [convenience script](https://docs.docker.com/engine/install/raspberry-pi-os/#install-using-the-convenience-script) provided by the Docker team:\n\n\n1. Connect to the Pi using SSH: <br>```ssh <user>@<host>```<br>with ```<user>``` and ```<host>``` as specified during setup with Imager.\n2. Update the system<br>```sudo apt update``` <br>```sudo apt full-upgrade```\n3. Install Docker using the [convenience script](https://docs.docker.com/engine/install/raspberry-pi-os/#install-using-the-convenience-script):<br>```curl -sSL https://get.docker.com \\| sh```\n4. Add current user to the ```docker``` group:<br>```sudo usermod -aG docker $USER```\n5. Log out and log in to activate the modified group assignment:<br>```logout```<br>```ssh <user>@<host>```\n6. Check that Docker is working correctly:<br>```docker run hello-world```\n\n## Checking Contiguous Memory (CMA)\n\nCameras on Raspberry Pi use CMA memory (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), chapter 8.3).\nThe default size of CMA memory is different for different Raspberry Pi models and can be shown with     \n```cat /proc/meminfo```\n\nOn a Pi 5, the available CMA memory (~65 MB) was found to be too small for accessing cameras from a Docker container when larger image sizes are used.\n\nThe value for ```CmaTotal``` should be at least the value found for Raspberry Pi Zero of 262144 kB.\n\nIf the value on your system is smaller, it needs to be increased:\n\n1. Edit the Raspberry Pi configuration file:<br>```sudo nano /boot/firmware/config.txt```\n2.  Find the line<br>```dtoverlay=vc4-kms-v3d```<br>and replace it with<br>```dtoverlay=vc4-kms-v3d,cma-512``` for > 2GB systems<br>```dtoverlay=vc4-kms-v3d,cma-384``` for > 1GB systems<br>```dtoverlay=vc4-kms-v3d,cma-320``` for smaller systems\n3. Reboot<br>```sudo reboot```\n\n## Deactivate raspiCamSrv Service from manual Installation\n\nIf you have installed raspiCamSrv manually, you need to deactivate the service:\n\n\n1. Stop the service:<br>```sudo systemctl stop raspiCamSrv.service```<br>or<br>```systemctl --user stop raspiCamSrv.service```\n2. Disable the service so that it does not automatically start with boot:<br>```sudo systemctl disable raspiCamSrv.service```<br>or<br>```systemctl --user disable raspiCamSrv.service```\n\n"
  },
  {
    "path": "docs/Trigger.md",
    "content": "# Introduction to Event Handling and Triggered Capture of Videos and Photos\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\n**raspiCamSrv** can capture events from camera and GPIO input devices and let these process actions by the camera and GPIO output devices.\n\n##### Supported Triggers\n- [Triggers](./TriggerTriggers.md) from GPIO-connected sensors\n\n\n##### Additional Triggers when a Camera is available\n- [Triggers](./TriggerTriggers.md) from camera events such as start and stop of video recording or streaming or [detection of motion](./TriggerMotion.md).\n- [Motion Capturing](./TriggerMotion.md) through image analysis\n- [Active Motion Capture](./TriggerActive.md)\n\n##### Supported Actions\n- [Actions](./TriggerActions.md) with GPIO-connected devices, such as LEDs, motors, servos or sound devices.\n- SMTP [actions](./TriggerActions.md) for sending an eMail to the [configured recipient](./TriggerNotification.md).\n- [Trigger-Actions](./TriggerTriggerActions.md) define which trigger will execute which action(s).\n- Under [Notification](./TriggerNotification.md), you configure the general mail recipient as well as specifics for notification on [motion detection through the camera](./TriggerMotion.md). \n\n##### Additional Actions when a Camera is available\n- Camera [actions](./TriggerActions.md), such as taking photos, starting or stopping video recording or recording a video with a given length.\n- [Camera Actions](./TriggerCameraActions.md) specify the camera actions in case of [motion detection through the camera](./TriggerMotion.md).\n- Under [Notification](./TriggerNotification.md), you configure whether mail notification shall include photos or videos from [motion detection through the camera](./TriggerMotion.md). \n\n##### Event Dashboard\n\n- [Event Viewer](./TriggerEventViewer.md)\n- [Calendar](./TriggerEventViewer.md)\n\n\n## Event Handling Infrastructure\n\n**raspiCamSrv** comes with two different types of event handling:\n\n### 1. Motion Capturing\n\nOriginally supported was [Motion Capturing](./TriggerMotion.md) with photo-taking and video recording actions as well as notification by mail.    \nThe relevant dialogs for configuration are [Motion](./TriggerMotion.md), [Camera](./TriggerCameraActions.md) and [Notification](./TriggerNotification.md).\n\n### 2. General Event Handling\n\nSince version V3.3.0, **raspiCamSrv** supports a more general approach to event handling which includes not only the camera but also various kinds of input and output devices which can be connected to the Raspberry Pi's GPIO pins.\n\nIf you have no devices connected to your Raspberry Pi, you just stay with [Motion Capturing](./TriggerMotion.md).\n\nIf you have input devices, such as sensors or buttons and/or output devices, such as LEDs, buzzers, relais or motors connected to your Pi, in addition to a camera, you can benefit from the fully integrated powerful event handling of **raspiCamSrv**.\n\n- You start with configuring the connected devices in the [Settings/Devices](./SettingsDevices.md) screen.    \n- Then, for the configured input devices, you configure [Triggers](./TriggerTriggers.md) which can also be based on camera events, such as start or stop of video recording, streaming or motion capturing.\n- As next, you configure any type of [actions](./TriggerActions.md) which you want to see, such as LEDs being switched on, a stepper motor executing a certain number of steps or photos and/or videos being taken with the camera.<br>In addition, you can also configure an SMTP action for being informed about an event by mail.\n- Once this is done, you need to specify for each of the triggers, which actions shall be processed once an event has been triggered. This is done in dialog [Trigger Actions](./TriggerTriggerActions.md).\n- In the [Triggers](./TriggerTriggers.md) and [Actions](./TriggerActions.md) dialogs, you also have the possibility to deactivate or activate triggers and actions, respectively.\n\n### Integration\n\nThe two types of event handling exist independently from each other and can be used separately or simultaneously.    \nEvents from [Triggers](./TriggerTriggers.md) defined as part of the [General Event Handling](#2-general-event-handling) can also be [logged](./TriggerActive.md#log-file) and visualized in the [Event Viewer](./TriggerEventViewer.md) if their *event_log* control parameter is set to \"True\".\n\nSMTP actions for mailing use the [Notification](./TriggerNotification.md) settings. Whether photos and/or videos, created during action processing, shall be included in a mail, can be configured independently for each SMTP [action](./TriggerActions.md).\n\nIn order to receive a mail on an event, you just activate one of your configured SMTP actions for the intended trigger.   \nWhile all actions are started simultanously in individual threads, the thread for the SMTP action will wait for completion of all other actions of the same trigger and include any information and rosources from the other action threads.\n\n### Where do Photos and Videos go?\n\nPhotos and videos taken in case of [motion detection](./TriggerMotion.md) will always be stored in the [event folder](./TriggerActive.md#event-data) while events are logged in the [log file](./TriggerActive.md#log-file) and the [database](./TriggerActive.md#database).    \nOld data can be removed with the [Cleanup](./TriggerCalendar.md#cleanup) function.\n\nThe same applies to triggers and actions of [General Event Handling](#2-general-event-handling) if the control parameter *event_log* for the trigger is set to \"True*.\n\nIf this parameter is \"False\", photos and videos will be stored at the same location as if they were taken on the [Live](./LiveScreen.md) screen and they can be inspected in the [Photo Viewer](./PhotoViewer.md).\n\n### Restrictions\n\nYou can use [motion capturing through the camera](./TriggerMotion.md) as trigger.   \nWhereas you can associate any kind of GPIO actions with such a trigger, you can not associate any camera or SMTP action.    \nThis is because camrea- and SMTP actions are handled already in the [Motion Capturing](#1-motion-capturing) infrastructure, which must be activated in order to capture motion detection events.\n"
  },
  {
    "path": "docs/TriggerActions.md",
    "content": "# Actions\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nThis screen is used to specify actions which can be started by **raspiCamSrv**, either as a reaction on a [Trigger](./TriggerTriggers.md) or manually through an [Action Button](./ConsoleActionButtons.md).\n\n![Actions1](./img/Trigger_Actions1.jpg)\n\n**IMPORTANT**: To preserve any configurations over server restart, you need to [store the configuration and activate *Start Server with stored Configuration*](./SettingsConfiguration.md).\n\n## Creating an Action\n\n1. In field *Action Source*, select the source system for which the action is defined:    \n(Camera will be available as option only if a camera is available)    \n![Action2](./img/Trigger_Actions2.jpg)\n1. This will open a list of devices defined for the chosen source system:    \n![Action3](./img/Trigger_Actions3.jpg)    \nFor the GPIO system, these are the **Output** devices configured on [Settings/Devices](./SettingsDevices.md)<br>**NOTE**: For SMTP, a device will only be shown if a mail account has been specified and verified in dialog [Notification](./TriggerNotification.md).<br>If this is the case, the configured *SMTP Server* will be shown as device.\n1. After a device has been selected, the system will show the device type with a link to related gpiozero documentation as well as the action methods which can be executed for this device (this information is taken from the [fixed configuration for the device type](./SettingsDevices.md#device-type-configuration)):    \n![Action4](./img/Trigger_Actions4.jpg)    \n1. When the method has been chosen, the sytem will display any parameters which may be required for this method:    \n![Action5](./img/Trigger_Actions5.jpg)    \nNow you need to specify values for these parameters, unless you leave the defaults, and enter a unique name for the action.    \nIn this step, the *Submit* button will be activated.    \n1. Pressing the *Submit* button will create the action and show it in the *Action Overview*.\n\n### Parameters\n\nThe parameters, for which values can be specified, are parameters of the method signature for the device class.    \nInformation about their type and value range, as well as about their function can be obtained from the *gpiozero* class documentation accessible through the link.\n\n**raspCamSrv** will check the data type by analyzing the datatype of the [configured template](./SettingsDevices.md#device-type-configuration). However, the allowed value range is currently not checked. You need to consult the *gpiozero* documentation.\n\nSometimes, the 'Action Method' is in fact a property and not a callable method. In this case, **raspiCamSrv** will just assign the value to the property and ignore the parameter name.\n\n### Control\n\nControl parameters are not part of the class interfaces but they can affect how **raspiCamSrv** processes an action method:\n\n- *duration*<br>With duration, you can specify the length of the time interval, during which the device will stay in the state achieved through the method, for example the 'on' state of an LED.<br>After this time, the system will check, whether the device object has a method off() (which is the case for LEDs and Buzzer) or a method stop() (which is the case for Motor and TonalBuzzer).<br>If either of these methods is found, it is applied.<br>In effect, the device will be in an inactive state afterwards.\n- *steps*<br>This is the number of steps in which the device shall reach the intended state within the given duration.<br>The intention here is that one might want a smooth rather than an abrupt movement, for example for a Servo.<br>**NOTE**This feature is currently not yet supported.\n- *burst_count*<br>This is a a parameter for method \"take_photo\". You can specify the number of photos which shall be taken as part of a photo burst in a series with a given interval.\n- *burst_intvl*<br>This is the interval you can specify for a photo burst.\n- *attach_photo*, *attach_video*<br>for an SMTP action, you can specify whether or not photos and/or videos shall be attached to the mail which have been created as part of the actions of the triggered event.\n\n### Restrictions\n\nAt a given time, only one action can be executed on a specific device type.\n\n### Timing of Action Execution\n\nWhereas action execution is synchronous, when invoked through an [Action Button](./ConsoleActionButtons.md) (the user needs to wait until the action is completed), this is different for the case when an action is triggered by a [Trigger](./TriggerTriggers.md).\n\nIn the latter case, action execution is done in an own thread which allows the action to be completed independently from the event handling thread which can treat other events in the meantime.\n\nThis means that actions are always completed and not interrupted.\n\nThis applies to actions with a configured duration, such as an LED which shall be 'on' for a certain time or a video with a given duration.    \nHowever, it applies also to actions with an inherent time consumtion. For example the movement of a StepperMotor can consist of hundreds of steps with a waiting time of 1 to 4 ms after each step. This requires in total several seconds to complete.\n\nIf for such an action a new action is requested before the previous action is completed, it will wait until the device is no longer busy.\n\nWhen stopping the event handling system, **raspiCamSrv** will wait for active actions to complete.\n\n## Activation of Actions\n\nIf the event-handling thread is currently active:\n- The *Active* check boxes are locked.\n\nIf the event-handling thread is not active:\n- The *Active* check boxes are active\n\nYou can activate/deactivate Actions by changing the *Active* check box and submitting the change.\n\n## Deletion of Actions\n\nYou can select one or multiple actions for deletion in the *Del* column and submit the selection.\n\nThe *Del* column will only be accessible for change if the event-handling thread is currently not active.\n\nYou cannot delete an action if it is used in an [Action Button](./SettingsAButtons.md).\n\nWhen an action is deleted, also its reference in the [Trigger-Actions](./TriggerTriggerActions.md) will be removed.\n\n## Testing an Action\n\nIt is recommended to test a new action, before it is used in a [Trigger assignment](./TriggerTriggerActions.md) or in an assignment to an [Action Button](./SettingsAButtons.md) or a [Live Button](./SettingsLButtons.md).\n\nFor testing, you select a single action in the *Test* column and submit the selection.\n\n## Changing Actions\n\nChanging of actions is currently not possible.\n\nHowever, you can easily create a new similar one with different parameters and deactivate or delete the old one.\n\n\n\n"
  },
  {
    "path": "docs/TriggerActive.md",
    "content": "# Triggered Capture of Videos and Photos\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\n# Active Motion Capturing\n\nThe *Start* button on the *Control* page starts the trigger capturing process.\n\n![ActiveCapture](./img/Trigger_Active.jpg)\n\nAn active capture process is indicated with red [Process Status Indicator](./UserGuide.md#process-status-indicators)\n\nThe *Start* button has changed to *Stop*, which allows stopping the process.\n\nIf motion capturing is currently active but operation paused because of schedule settings, this is indicated by a yellow process status indicator:\n\n![Capturepaused](./img/ProcessIndicator8.jpg)\n\n## Event Data\n\nEvent data are stored in directory ```./prg/raspi-cam-srv/raspiCamSrv/static/events```:\n\n![Eventstorage](./img/Trigger_Storage.jpg)\n\nStorage includes a log file as well as video and photo files.\n\n## Parallel Activities\n\nWhile motion capturing is active, the live stream process will be kept active because this is used for motion detection.\n\nWhile motion capturing is active, you may continue working with **raspiCamSrv**.   \nYou may even take photos, videos or photo series.\n\nHowever, you should avoid changing camera controls or configuration because this might restart the camera.\n\n### Dos and Don'ts\n\n#### Blocked:\n\n- Changing [Settings](./Settings.md)\n- Starting a [Exposure Series](./PhotoSeriesExp.md) or a [Focus Stack Series](./PhotoSeriesFocus.md)\n- Changing [Camera Configuration](./Configuration.md)\n\n#### Changing Zoom\n\nThis can be done while trigger capturing is active.   \nHowever the moment when the new zoom setting is activated will be registered as motion event.\n\n#### Changing Focus\n\nTo improve the focus for camera model 3, you may change [Focus Settings](./FocusHandling.md) and [Trigger Autofocus](./FocusHandling.md#trigger-autofocus)\n\n#### Changing Camera Controls\n\nIn order to change the quality of videos and photos, you may change any [Camera Controls](./CameraControls.md) while motion capturing is active.\n\n## Log File\n\nWhile events are registered and videos and photos are taken, the system maintains a log file (```events/_events.log```) with antries for all events and the times when photos are captured or videos are started and stopped:\n\n![EventLog](./img/Trigger_Logfile.jpg)\n\nThe log file of the above screenshot shows examples without delayed actions as well as with configurations with 4 photos in the *Photo Burst*.    \n\n## Database\n\nEvents and event actions are also stored in the SQLite3 database stored at   \n```./prg/raspi-cam-srv/instance/raspiCamSrv.sqlite```.\n\nThe primary purpose of the database is providing fast access to event data over a longer period for the [Event Viewer](./TriggerEventViewer.md)\n\n![TriggerDB](./img/Trigger_DB.jpg)\n\nTable *events* holds all individual events:\n\n![DBEvents](./img/Trigger_DB_Events.jpg)\n\nTable *eventactions* holds the actions taken for each event:\n\n![DBEventactions](./img/Trigger_DB_Eventactions.jpg)"
  },
  {
    "path": "docs/TriggerCalendar.md",
    "content": "# Trigger / Event Calendar\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nThe calendar gives an overview on the number of events which have been registered for a specific day:\n\n![EventCalendar](./img/Trigger_Calendar.jpg)\n\nClicking on a red field navigates to the [Events](./TriggerEventViewer.md) display for this specific day.\n\nYou can change the active month using the date control and navigation arrows, or return to the current month with the *Now* button.\n\n## Download Log\n\nYou can download the [Log file](./TriggerActive.md#log-file) including a timeline of all events and associated actions.<br>\nNote that only those triggers and their associated actions will be included in the log, for which the [control parameter](./TriggerTriggers.md#control) *event_log* has the value \"True\".\n\n## Cleanup\n\nThe *Cleanup* button can be used for removing old events.   \nThis requires that the process is stopped.\n\nAfter pressing the button, a confirmation is required:   \n![CleanupConfirm](./img/Trigger_ConfirmCleanup.jpg)    \nThe *Retention Period* for cleanup, shown in this confirmation, has been specified on the [Trigger/Control](./TriggerControl.md) page.\n\nFor all events older than the *Retention Period*, cleanup will\n\n- remove all log file entries\n- delete all photo and video files\n- delete related database entries\n\n"
  },
  {
    "path": "docs/TriggerCameraActions.md",
    "content": "# Camera Actions\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\n\n![Action](./img/Trigger_Action.jpg)\n\nThis section allows specification of aspects for photos and/or videos recorded in reaction an an  event:\n\n- *Video Recording Type*   \nWith *Normal*, video recording starts with the event or, if configured, after a specified dalay.   \nWith \"Circular*, the system continuesly captures video in a circular buffer with a capacity of a few seconds. In case of an event, also the seconds before the event will be available in the video.   \nCurrently, only *Normal* is supported.\n- *Pre-Record Length (sec)* is the number of seconds, the system shall look 'backwards' from the time of an event.\n- *Video Duration* specifies the length of videos captured in case of an event.    \nIf a new event is registered while video recording from the previous event is still active, this will be stopped before recording for the new event starts.\n- *Photo Burst - Number of Photos* allows specifying a number of photos which will be successively captured in case of an event.   \nIf video is recorded, at least one photo must be specified.\n- *Photo Burst Interval* is the interval after the previous photo when the system will capture a new photo if there is still motion detected. If no motion is detected after this interval, no photo will be taken.\n- *Action data path* is the path where pictures and logs for events will be stored.\n"
  },
  {
    "path": "docs/TriggerControl.md",
    "content": "# Trigger / Control\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nWith this screen, you control scheduling of event handling and motion detection.\n\n![Triggercontrol](./img/Trigger_Control.jpg)\n\nIn the *Control* section, you may specify basic aspects of triggered actions:\n\nWhere not otherwise stated, this applies to 'legacy' [motion capturing](./Trigger.md#1-motion-capturing) as well as to [General Event Handling](./Trigger.md#2-general-event-handling).\n\n- Under *Triggers*, you select the triggers to be used.   \nYou can activate Motion detection and/or the other *Configured Triggers*\n- Under *Actions* you specify the actions to be taken in case of [motion detection through the camera](./TriggerMotion.md).   \nYou may select among video recording and photo taking.   \nIn case *Record Video* is selected, also at least one photo must be taken. This photo will serve as placeholder for the video in the [Event Viewer](./TriggerEventViewer.md).    \nWith *Notification* you specify whether or not you want to be informed by e-Mail about an event. The details need to be specified on the [Notification](./TriggerNotification.md) tab.    \n**ATTENTION**: If you have chosen to only activate *Configured Triggers* without *Motion Detection*, the listed Actions will be deactivated because they are only supported with *Motion Detection*.<br>From the *Configured Triggers*, the configured [Trigger-Actions](./TriggerTriggerActions.md) will be executed, which may also include camera actions.\n- With *Operation Weekdays*, you specify the weekdays when triggering shall be active.\n- *Operation Start* specifies the daytime when triggering is activated on each active weekday.\n- *Operation End* specifies the daytime when triggering is paused.\n- *Automatic Start with Server*   \nWhen activated, the trigger capturing process can be automatically started with the server.   \nWhen you change this parameter, you need to go to [Settings](./Settings.md) and store the current [Server Configuration](./SettingsConfiguration.md)   \nIf you want automatic start, you also need to select *Start Server with stored Configuration*.    \n**Note** In case you start the Flask server manually, do not use the ```--debug``` option. This will cause an exception (see [Flask Issue #5437](https://github.com/pallets/flask/discussions/5437)).\n- *Detection Delay* allows specifying a delay in seconds. When an event is triggered, the configured action (video and/or photo, Notification) will be delayed by the specified number of seconds. Normally, this will be 0.<br>This applies to motion-captured events only.    \n- *Detection Pause* specifies a 'dead time' after an event has been registerd. Within this time no new event will be registered although the system will not stop detecting motion.    \nThis setting prevents from being flooded with registered events, for example if motion persists for a longer time.    \nDetection pause (and alse *Detection Delay*), configured here, does not apply to the configured [Triggers](./TriggerTriggers.md). For these, it is possible to specify *bouncing-time* individually for every trigger.\n- *Retention Period* specifies the number of days  for which event data will be retained when a [cleanup](./TriggerCalendar.md#cleanup) is done.<br>This does not apply for photos or videos which have been taken on triggers for which *event_log* was set to \"False\"\n\nData changes will not be persisted unless the **Submit** button has been pressed.\n\n## Starting Trigger capturing\n\nTrigger- and event handling is activated using the *Start* button.\n\nDepending on the selected Triggers, *Motion Detection* and/or *Configured Triggers* are started.\n\nWhich process is currently active is indicated by the [status indicators](./UserGuide.md#process-status-indicators):\n\n- Motion detection only:    \n![Proc13](./img/ProcessIndicator14.jpg)\n- Configured Triggers only:    \n![Proc13](./img/ProcessIndicator15.jpg)\n- Both    \n![Proc13](./img/ProcessIndicator13.jpg)\n\nNote that, whenever Motion Detection is active, also the live stream will be kept active because this is used to detect motion.\n\nFor active of motion capturing, see [Active Motion Capturing](./TriggerActive.md) \n\n"
  },
  {
    "path": "docs/TriggerEventViewer.md",
    "content": "# Trigger / Events\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nEvent Details are shown in the Event Viewer for a specific day:\n\n![Event Viewer](./img/Trigger_Events.gif)\n\nIn the top area, you may \n\n- change the active day using the date control or the arrow buttons.   \nSingle arrows shift by day, double arrows by week.   \nIn each case the starting hour is set to 00:00h.\n- change the start time from which on events will be shown.   \nHere, single arrows shift by a quarter of an hour, double arrows by an hour.\n- You can select whether you want to see videos, photos, both or none.   \nIn case of videos, the first photo is always shown instead of the video on the left side, however clicking on the photo will open the video viewer.\n- whether a video or a photo is represented by the small picture can be distinguished by information on the video length.\n\nSelecting a video or photo shows it in the detail area on the right side.\n\n## Event Card\n\nThe Event Card for each event     \n![EventCard](./img/Trigger_EventCard.jpg)    \nshows, from top to bottom:\n\n- Event Type\n- Event date\n- Event time\n- Event trigger\n- Event trigger algorithm   \n[Mean Square Diff](./TriggerMotion.md)    \n[Frame Diff.](./TriggerMotion.md#test-for-frame-differencing-algorithm)    \n[Optical Flow](./TriggerMotion.md#test-for-optical-flow-algorithm)    \n[BG Subtraction](./TriggerMotion.md#test-for-background-subtraction-algorithm)\n- Trigger parameter (see [Motion](./TriggerMotion.md) tab)   \ncam : Camera Num by which motion was detected    \nroi : Index of the [Region of Interest](./TriggerMotion.md#regions-of-interest-and-regions-of-no-interest) in which motion was detected   \n**NOTE**: Motion detection analysis is stopped whenever motion has been detected in one of the RoIs. The index of this ROI is reported here.    \nmsd : *Mean Square Threshold*    \nBBox_thr : *Bounding Box Threshold*    \nIOU_thr : *IOU Threshold*     \nMotion_thr : *Motion Threshold*    \nModel : *Background Subtraction Model* (1=MOG2, 2=KNN)\n\nYou may use the information to fine tune the algorithm parameters on the [Motion](./TriggerMotion.md) tab.\n\n## Photos/Videos with ROI/RoNI\n\nWhen [Photos/Videos with RoI/RoNI](./TriggerMotion.md) has been activated, borders of RoIs/RoNIs are shown on photos and videos (for \"Mean Square Diff\" not no videos):\n\n- Red borders represent *Regions of Interest* where motion was detected first.\n- Green borders represent the other specified *Regions of Interest.\n- Blue borders represent *Regions of NO Interest.\n\nIn case that photos have been taken together with videos, this is represented as shown below.   \nVideos show the video length in the footer.\n\n![EventsVodeoPhoto](./img/Trigger_Events_Photo.jpg)"
  },
  {
    "path": "docs/TriggerMotion.md",
    "content": "# Trigger / Motion Detection Configuration\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\n![Motion](./img/Trigger_Motion.jpg)\n\n## Algorithms\n\nWith Motion Capturing, you may trigger actions in case that **raspiCamSrv** has detected changes in the visual area of the active camera.    \nSensitivity of detection is strongly dependent on the algorithm used for detection.\n\n**raspiCamSrv** currently supports 4 different algorithms:\n\n- *Mean Square Difference* between pixel color levels of successive frames.\n- *Frame Differencing*\n- *Optical Flow*\n- *Background Subtraction*\n\nThe latter 3 variants are implementations of algorithms proposed by Isaac Berrios in [Introduction to Motion Detection](https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2).\n\nWhereas the *Mean Square Difference* is available in general, the other algorithms require [special preconditions for Extended Motion Capturing](./Settings.md#extended-motion-detection-support).\n\n## Configuration\n\nThis screen allows specification of motion capturing aspects:\n\n- *Motion Detection Algorithm*    \nallows selecting the algorithm by which the system will recognize motion through its camera.   \n![Motion Algos](./img/Trigger_Motion_Algos.jpg)    \nDepending on the selected algorithm, the relevant parameters are editable and can be adjusted.\n- *Mean Square Threshold*    \nis the value of the mean square difference above which the system detects a motion event.\n- *Bounding Box Threshold*    \nis the threshold for acceptable contour sizes (see [IB-1](https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2))\n- *IOU Threshold*    \nis the Threshold for \"Intersection Over Union or IOU\" of overlapping bounding boxes (see [IB-1](https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2))\n- *Motion Threshold*    \nis the minimum flow threshold for motion used in *Optical Flow* algorithm (see [IB-2](https://medium.com/@itberrios6/introduction-to-motion-detection-part-2-6ec3d6b385d4))\n- *Background Subtraction Model*    \nis the model used for generating a background model in *Background Subtraction* algorithm. (see [IB-3](https://medium.com/@itberrios6/introduction-to-motion-detection-part-3-025271f66ef9))\n- *Video with Bounding Boxes*    \nallows selection of the type of video recorded for a motion event, if activated on the [Control](./TriggerControl.md) tab.   \nIf activated, the video will show bounding boxes around areas for which motion has been detected.   \nOtherwise, normal videos will be recorded.\n- *Use Regions of Interest*    \nIf selected (and submitted) it will be possible to specify a set of rectangular areas which serve as *Regions of Interest* / *Regions of NO Interest*.    \nFor *Regions of Interest* (RoI), motion will only be detected when occurring within these regions (see [below](#regions-of-interest-and-regions-of-no-interest)).    \n*Regions of NO Interest* (RoNI) will be generally excluded from motion detection.\n- *Regions of Interest*   \nThis field holds the *Regions of Interest* as a tuple of tuples where each tuple represents one region with (x offset, y offset, width, height). The values refer to the [Pixel Array Size](./Information_CamPrp.md) of the sensor.    \nThe field is not editable; it is populated when *Regions of Interest* are drawn.\n- *Regions of No Interest*   \nThis field holds the *Regions of No Interest* as a tuple of tuples where each tuple represents one region with (x offset, y offset, width, height). The values refer to the [Pixel Array Size](./Information_CamPrp.md) of the sensor.    \nThe field is not editable; it is populated when *Regions of No Interest* are drawn.\n- *Photos/Videos with RoI/RoNI*    \nThis switch decides whether or not *Regions of Interest* and/or *Regions of NO Interest* are drawn on photos or videos captured while *Motion Detection* is active.   \n**NOTE**: When *Algorithm* \"Mean Square Diff\" is used, RoIs/RoNIs can only be shown on photos but not on videos.\n\n\nAny changes must be submitted with the **Submit** button.   \nChanges will be effective after the Motion Capturing Process has been started the next time.    \nFor example, trees or leaves moving in the wind are normally not of interest.\n\n## Regions of Interest and Regions of NO Interest\n\n**NOTE**: This feature is only supported if OpenCV is installed.\n\nIn many cases, it is desirable to restrict the motion-sensitive region of the camera view to specific areas.\n\n**raspiCamSrv** supports two type of areas:\n- *Regions of Interest* (RoI) restrict motion detection to these areas\n- *Regions of NO Interest* (RoNI) exclude motion detection from specific areas.\n\nBoth can be used simultaneously or alternatively.    \nWhen no RoI is defined, the entire cropping area will be the Region of Interest.   \nRoNIs will only have an effect if at least a part of them is within a RoI.\n\n**NOTE**: Regions of Interest may be automatically adjusted when the Live View is [zoomed or panned/tilted](./ZoomPan.md)\n\nAfter *Use Regions of Interest* has been activated (and submitted), a Live View will be shown with a canvas activated on which the intended regions can be drawn:\n\n![Motion RoI](./img/Trigger_Motion_RoI.gif)\n\nYou first need to select the type of region to be drawn. Afterwards, you draw the region with left mouse key pressed.\n\n*Regions of Interest* are shown with green border line.  \n*Regions of No Interest* are shown as blue filled rectangle\n\nWhile drawing a new region, the borders of previously drawn regions are invisible.    \nAll borders are shown as soon as the mouse pointer has left the drawing canvas.\n\nTo remove previously drawn regions, just deactivate and activate the *Use Regions of Interest* checkbox without submitting.\n\nAfter all intended regions are finally drawn, submit the Motion Detection Configuration settings.\n\n## Testing Motion Capturing\n\nIn order to optimize the parameters for the intended application, **raspiCamSrv** allows test runs for the selected algorithm.    \nDuring a test run, no events are generated. Instead, a preview of different aspects of the chosen algorithm is shown.    \nDetected motion events are indicated by the occurrence of bounding boxes.    \nAn active test run is indicated by the turquoise status indicator.\n\n- For the *Mean Square Difference* algorithm, there is no test. However, pressing the **Test Motion Detection** button will show the current framerate in the message line, if motion detection is active.\n- For the other algorithms, pressing the **Test Motion Detection** button will stop an active Motion Detection server and start a test run.    \nA set of 4 intermediate images are presented which are calculated from the last, or the last two, frames. \n\nIf Regions of Interest are defined, these will be considered and visualized during the test.\n\n### Test for *Frame Differencing* Algorithm\n\nFor a detailed description of this algorithm, see [Introduction to Motion Detection: Part 1](https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2) by Isaac Berrios.\n\n![Frame Difference Test](./img/Trigger_Motion_FrameDiff_Test_l.gif)\n\n### Test for *Optical Flow* Algorithm\n\nFor a detailed description of this algorithm, see [Introduction to Motion Detection: Part 2](https://medium.com/@itberrios6/introduction-to-motion-detection-part-2-6ec3d6b385d4) by Isaac Berrios.\n\n![Optical Flow Test](./img/Trigger_Motion_OpticalFlow_Test_l.gif)\n\n### Test for *Background Subtraction* Algorithm\n\nFor a detailed description of this algorithm, see [Introduction to Motion Detection: Part 3](https://medium.com/@itberrios6/introduction-to-motion-detection-part-3-025271f66ef9) by Isaac Berrios.\n\n![Background Suntraction Test](./img/Trigger_Motion_BGSubtract_Test_l.gif)\n\nThis algorithm can normally be expected to give best results.   \nThe fact that the shown example has a lower quality compared to *Optical Flow* may be attributed to the 'stationary' movement of the object which 'burns' itself into the background model because each frame contributes to the model.\n\n## Performance\n\nThe performance requirements for the different algorithms have an impact on the frame rates which can be achieved during testing and an active Motion Capturing process.    \n\nIn order to get information on the frame rates, these are measured during a test and displayed at the bottom of the screen.   \nReliable values can only be obtained after the test has run some time. Therefore, it is necessary to refresh the screen using the button aside of the displayed value.\n\nUnfortunately, framerates had not been refreshed before recording the GIFs, above.\n\nThe the following table shows framerates observed with a Pi 5 and a camera model 3:\n\n| Algorithm              | Stream | Sensor Mode | Stream Size | Framerate |\n|------------------------|--------|-------------|-------------|-----------|\n| Mean Square Diff       | lores  | default     |  640 x 360  | ~14       |\n| Frame Differencing     | lores  | default     |  640 x 360  | ~14       |\n| Optical Flow           | lores  | default     |  640 x 360  | ~5        |\n| Background Subtraction | lores  | default     |  640 x 360  | ~14       |\n|                        |        |             |             |           |\n| Mean Square Diff       | lores  | 0           | 1536 x 864  | ~14       |\n| Frame Differencing     | lores  | 0           | 1536 x 864  | ~14       |\n| Optical Flow           | lores  | 0           | 1536 x 864  | ~1        |\n| Background Subtraction | lores  | 0           | 1536 x 864  | ~5        |\n|                        |        |             |             |           |\n| Mean Square Diff       | lores  | 1           | 2304 x 1296 | ~10       |\n| Frame Differencing     | lores  | 1           | 2304 x 1296 | ~6        |\n| Optical Flow           | lores  | 1           | 2304 x 1296 | ~0.3      |\n| Background Subtraction | lores  | 1           | 2304 x 1296 | ~2        |\n   \n   \nBelow is a load profile taken wit a Pi5, 8GB memory, built in a standard case with fan.   \nSteaming was never active and no motion was tracked during recording.   \n![Load Profile](./img/Trigger_LoadProfile.jpg)\n\nMotion tracking with different algorithms was run for 30 minutes in the following sequence:\n- 18:15 - System idle\n- 18.30 - Mean Square Diff\n- 19:00 - Frame Differencing\n- 19:30 - Background Subtraction\n- 20:00 - Optical Flow\n- 20:30 - Motion tracking stopped\n- 20:45 - raspiCamSrv stopped\n\nAlthough there is a significant impact on CPU utilization, especially for the *Optical Flow* algorithm, CPU temperature is within reasonable ranges.\n\n## Recorded Videos\n\nIf the option to record videos with bounding boxes has been chosen, the videos are generated frame by frame within the algorithm.   \nThe framerate needs to be specified before frames are added to the video.    \nCurrently, the video is started when a motion event has been detected (final number of detected bounding boxes > 0). However, at this time, the real frame rate is not yet known. Therefore a rough estimate is used for the individual algorithms which is close to the values shown in figures, above.\n\nThis results in videos with motion speed close to real life.\n\nIf, however, the achieved rates on a specific system differ from these values, the video speed may be timelapse or slow-motion.\n"
  },
  {
    "path": "docs/TriggerNotification.md",
    "content": "# Trigger / Notification\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nOn this tab, you specify the details required for notification on an event by e-Mail:\n\n![TriggerNotification0](./img/Trigger_Notification0.jpg)\n\n## Mail Server Settings\n\n- *SMTP Server* is the server address, for example \"smtp.gmail.com\"\n- *Port* is the server port to be used\n- *Use SSL* specifies whether or not SSL (Secure Sockets Layer) is to be used\n- *Server requires Authentication* must be checked if the mail server requires authentication with user and password.\n- *User* is the user name to be used for login to the server. This is typically identical with the e-Mail address.\n- *Password* is the password required for authentication\n\n## Handling of Mail Server Credentials\n\nCredentials for authentication to the mail server are not part of the normal raspiCamSrv configuration.    \nThey ere never exported to JSON files when configuration is stored ([Settings/Configuration](./SettingsConfiguration.md))   \nTherefore, they can also not be imported when the server restarts.\n\n**raspiCamSrv** offers two alternatives for secure handling:\n\n### 1. Storage in a Secrets File\n\nThis is activated by checking *Store Credentials in File* and by specifying the full path of that file in *Credentials File Path*.   \nFor example, the path could be ```/home/<user>/.secrets/raspiCamSrv.secrets```.   \nIf the path and the file do not exist, they will be automatically created.  \nAccess to this file should be restricted to the user running **raspiCamSrv** as service or from the command line.\n\nThe advantage of this method is, that on server restart the system can automatically load user name and password for connection to the mail server. Thus, notification will automatically be activated, provided that Triggered Capture is automatically started with server start.\n\n### 2. Manual Entry\n\nAlternatively to storage in a secrets file, *User* and *Password* can be manually entered.   \n**raspiCamSrv** will keep this information in memory during the server livetime.   \nAfter server restart, these credentials are initally not available and, therefore, notification will not be active until the credentials have been entered once on this screen.\n\n## Mail Settings\n\n- *From e-Mail* is the e-Mail address to be shown in the *From* field. Mail servers may replace this with the e-Mail address of the account.\n- *To e-Mail* is the e-Mail address of the recipient to whom the notification is to be sent.\n- *Subject* Is the text for the *Subject* field of the mail to be sent.\n- With *Notification Pause*, you can specify a pause in seconds in which no further notification mail wil be sent.   \nA value smaller than the *Detection Pause* (see [Trigger Control](./TriggerControl.md)) will have no effect, so that every event will be notified.    \nIf the value is chosen as a multiple (N) of the *Detection Pause*, only every Nth event will be notified.\n- *Include Video* specifies whether or not the event video will be included in the mail.    \nIf this is selected, the mail will be sent not earlier than video recording has terminated.    \nThis is determined by *Video Duration* (see [Camera Actions](./TriggerCameraActions.md)).   \nKeep in mind that the video size should not exceed the maximum mail size allowed by the provider.\n- *Include Photos* specifies whether or not photos should be attached to the mail.   \nIf this is selected and a number > 1 has been specified for *Photo Burst* (see [Camera Actions](./TriggerCameraActions.md)), the mail will not be sent before the last photo has been taken or the next event has been registered.\n\n## Submitting Configuration Settings\n\n![TriggerNotification1](./img/Trigger_Notification1.jpg)\n\nWhen submitting configuration entries, the system will automatically try to connect to the mail server using the specified credentials.\n\n![TriggerNotification3](./img/Trigger_Notification3.jpg)\n\n![TriggerNotification3a](./img/Trigger_Notification3a.jpg)\n\nIf the connection test was successful, this is indicated in the message area, otherwise, an error message is shown.   \nUser and password are removed from the screen and need to be entered again, if necessary.\n\nWhether or not connection to the mail server has been verified, is allways shown in the top of the screen.\n\n## Entering Credentials after Server Restart\n\nAfter the server has been restarted and credentials are not stored in a secrets file, they need to be entered once:\n\n![TriggerNotification2](./img/Trigger_Notification2.jpg)\n\n\n## Notification Errors\n\nSending a mail may require several seconds, especially if the mail includes larger attachments.   \nTherefore an additional thread is started for each mail to be sent, in order not to block capturing events.\n\nIf an error occurs while trying to send a mail, an error status is set in the Trigger configuration settings.   \nThe error message is shown on the *Control* Tab of the *Trigger* screen:   \n\n![TriggerNotificationError](./img/Trigger_NotificationError.jpg)\n\nThe message will vanish when Triggered Capture is restarted or when the server is restarted.   \n\nErrors occurring in the sending process do not stop motion capturing."
  },
  {
    "path": "docs/TriggerOverview.md",
    "content": "# Trigger - Triggers and Actions\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\n![Cam Menu](img/TriggerMenu.jpg)\n\n**raspiCamSrv** can capture events from camera and GPIO input devices and let these process actions by the camera and GPIO output devices.\n\nFor example, pressing a button could trigger photo taking, or starting/stopping video recording could trigger switching an LED, or detection of motion could cause e-Mail notification.\n\n**NOTE**: If no camera is connected, the camera-related menu items are missing.\n\n\n- [Introduction](./Trigger.md) \n- [Control](./TriggerControl.md)\n- [Triggers](./TriggerTriggers.md)\n- [Actions](./TriggerActions.md)\n- [Trigger-Actions](./TriggerTriggerActions.md)\n- [Motion](./TriggerMotion.md)\n- [Camera](./TriggerCameraActions.md)\n- [Notification](./TriggerNotification.md)\n- [Events](./TriggerEventViewer.md)\n- [Calendar](./TriggerCalendar.md)"
  },
  {
    "path": "docs/TriggerTriggerActions.md",
    "content": "# Trigger-Actions\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nOn this page, you specify which actions are invoked in case of a specific Trigger event.\n\n![TriggerActions](./img/Trigger_TriggerActions.jpg)\n\n**IMPORTANT**: To preserve any configurations over server restart, you need to [store the configuration and activate *Start Server with stored Configuration*](./SettingsConfiguration.md)\n\n\nThe dialog will only allow changes when the event handling thread is not active.\n\nJust activate the check box for every event you want to be triggered by a specific trigger."
  },
  {
    "path": "docs/TriggerTriggers.md",
    "content": "# Triggers\n\n[![Up](img/goup.gif)](./TriggerOverview.md)\n\nThis page is used for spacification of triggers.     \nTriggers are registered events occurring for the camera system or for GPIO input [devices](./SettingsDevices.md) which can be used to initiate one or multiple [actions](./TriggerActions.md).\n\n![Trigger0](./img/Trigger_Trigger1.jpg)\n\n\n**IMPORTANT**: To preserve any configurations over server restart, you need to [store the configuration and activate *Start Server with stored Configuration*](./SettingsConfiguration.md)\n\n## Creating a Trigger\n\n1. In field *Trigger Source*, select the source system for which the trigger is defined:    \n(*Camera* and *Motion Detector* will be available as option only when a camera is available)    \n![Trigger1](./img/Trigger_Trigger2.jpg)\n\n1. This will open a list of devices defined for the chosen source system:    \n![Trigger1](./img/Trigger_Trigger3.jpg)    \nfor the GPIO system, these are the **Input** devices configured on [Settings/Devices](./SettingsDevices.md)\n1. After a device has been selected, the system will show the device type with a link to related gpiozero documentation as well as the events which can be trapped for this device (this information is taken from the [fixed configuration for the device type](./SettingsDevices.md#device-type-configuration)):    \n![Trigger1](./img/Trigger_Trigger4.jpg)    \n1. When the event has been chosen, the sytem will display any parameters which may be required for this event:    \n![Trigger1](./img/Trigger_Trigger5.jpg)    \nNow you need to specify values for these parameters, unless you leave the defaults, and enter a unique name for the trigger.    \nIn this step, the *Submit* button will be activated.    \n1. Pressing the *Submit* button will create the trigger and show it in the *Trigger Overview*.\n\n### Parameters\n\nThe parameters, for which values can be specified, are properties or methods of the device class.    \nInformation about their function can be obtained from the gpiozero class documentation accessible through the link.\n\n### Control\n\nControl parameters are not part of the class functionality but they can affect how **raspiCamSrv** processes a captured event:\n\n- *bounce_time*<br>This is a time interval given in seconds.<br>After an event has been processed, other events occurring within this interval will be ignored.<br>This shall avoid jitter in triggered actions. For some devices (e.g. Button), bounce time can already be specified for the device and will be handled by gpiozero.\n- *event_log*<br>The value of this parameter decides whether or not a trigger and its [associated actions](./TriggerTriggerActions.md) will be treated as events and included in [event logging](./TriggerActive.md) and [event viewer](./TriggerEventViewer.md).<br>If **False**, events triggered by the trigger will not be logged in [event logging](./TriggerActive.md) and they will also not be visible in the [event viewer](./TriggerEventViewer.md). Instead, if camera actions are associated with the trigger, the resulting photos and videos will be visible in the normal [Photo Viewer](./PhotoViewer.md).\n\n### Restrictions\n\nOnly one active trigger can be configured for a specific device-event.\n\nIf another trigger is configured for the same event, only one trigger will remain active.\n\nIf you want multiple actions on a specific trigger, you will specify this in [Trigger-Actions](./TriggerTriggerActions.md).\n\n## Activation of Triggers\n\nIf the event-handling thread is currently active:\n- The *Active* check boxes are locked.\n- If this is the only trigger for the chosen device-event, the trigger will be activated.\n- If another trigger exists for the same device-event, the new trigger will not be activated.\n\nIf the event-handling thread is not active:\n- The *Active* check boxes are active\n- If this is the only trigger for the chosen device-event, the trigger will be activated\n- If another trigger exists for the same device-event, this will be deactivated and the new trigger will be activated.\n\nYou can activate/deactivate triggers by changing the *Active* check box and submitting the change.\n\nIf you tried to activate several triggers for the same device-event, the system will leave only one of them active.\n\n## Deletion of Triggers\n\nYou can select one or multiple triggers for deletion in the *Delete* column and submit the selection.\n\nThe *Delete* column will only be accessible for change if the event-handling thread is currently not active.\n\n## Changing Triggers\n\nChanging of triggers is currently not possible.\n\nHowever, you can easily create a new similar one with different parameters and deactivate the old one.\n"
  },
  {
    "path": "docs/Troubelshooting.md",
    "content": "# raspiCamSrv Troubleshooting\n\n## Errors during Installation\n\nThis section deals with errors which may occur while running the [Automated Installer](./installation.md) or while [Installing manually](./installation_man.md)\n\n**ERROR: pip's dependency resolver**     \n```\nERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\ntypes-flask-migrate 4.0 requires Flask-SQLAlchemy>=3.0.1, which is not installed.    \n```     \nThis type of messages, mentioning different missing packages, may occur, for example, in\n<br>- Step 10: Installing Flask ...\n<br>- Step 11.2: Installing numpy ...\n<br>- Step 11.3: Installing matplotlib ...\n<br>- Step 11.4: Installing flask-jwt-extended ...\n<br>These are actually not real errors but just warnings, which say that some other packages have unmet dependencies.\n<br>As long as the installation of the intended package is successful, this can be ignored.\n<br>Successful installation is, for example, confirmed through\n<br>```Successfully installed numpy-2.4.2```\n<br>In case of failing installation, the automated installer will stop at this point.\n\n**OSError: [Errno 12] Cannot allocate memory**\n<br>This error can occur when Picamera2 tries to allocate CMA memory (See also [Checking Contiguos Memory (CMA)](./SetupDocker.md#checking-contiguous-memory-cma)).\n<br>The context is usually\n```\nFile \"/usr/lib/python3/dist-packages/picamera2/dma_heap.py\", line 98, in alloc\n    ret = fcntl.ioctl(self.__dmaHeapHandle.get(), DMA_HEAP_IOCTL_ALLOC, alloc)\n```\nDuring installation, this error can occur in\n<br>- Step 12: Initializing database ...\n<br>The error has mainly been observed in RPI Zero and RPI Zero 2 systems.\n<br>Experience has shown that this is a temporary issue and that memory allocation can be successful later.\n<br>Therefore the [Automated Installer](./installation.md) uses up to 5 attemts to initialize the database with a pause of 5 sec inbetween.\n<br>Should execution of this step not be successful, the installer will stop.\n<br>You can then try to run the installer again later\n\n\n\n## Errors during Operation\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThis section intends to collect information on how to deal with errors or problems which may occur while running **raspiCamSrv**.\n\n- **Password forgotten**    \nIf you have your password forgotten, there are two alternatives:<br>1. Somone else is Superuser:<br>Ask him to remove your user entry and create a new one (See [Settings / Users](./SettingsUsers.md)).<br>2. You are the Superuser.<br>You need to reset the database where user entries are stored.<br>You do this with with ```flask --app raspiCamSrv init-db``` (see [RaspiCamSrv Installation](./installation.md) Step 11).<br>At the next Login, you need to Register as new Superuser (see [Authorization](./Authentication.md))\n\n- **ERROR in motionDetector: Exception in _motionThread: OpenCV(4.6.0)**   \nThis error may occur when trying to use [extended motion capturing](./TriggerMotion.md) while the 'YUV420' stream format is set for the [Live View Configuration](./Configuration.md). <br>It seems that OpenCV is not capable to handle images with this format.    \nThis error is typically observed on Pi3 and Pi4 where the YUV stream format is mandatory for the lores stream according to the [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), ch. 4.2.<br>\nAs a workaround, you may try setting the \"main\" stream for the Live View configuration with \"RGB888\" Stream Format.   \nTo avoid performance issues, also a low Stream Size (e.g. 640x400) should be chosen.<br>\nSee [raspi-cam-srv Issue #48](https://github.com/signag/raspi-cam-srv/issues/48)\n\n- **No Connection to server although server has been started as service**.    \nThis may happen (see [raspi-cam-srv Issue #8](https://github.com/signag/raspi-cam-srv/issues/8)) if the service has been started before the network interfaces are ready.   \nThe systemd journal will indicate that the Flask server is only listening to *localhost* (127.0.0.1)   \nIn this case, more restrictive settings in the *After* clause of the [service configuration](./service_configuration.md) file may be required (see [systemd Network Configuration Synchronization Points](https://systemd.io/NETWORK_ONLINE/)) \n- **SystemError: No cameras were found on the server's device**   \nSee [raspi-cam-srv Issue #6](https://github.com/signag/raspi-cam-srv/issues/6)\n- **ERROR in camera_pi: Could not import SensorConfiguration from picamera2.configuration. Bypassing sensor configuration**   \nThis message may occur when running on Bullseye systems.   \nCurrently, it can be ignored because the missing *SensorConfiguration* class has currently no impact on **raspiCamSrv** functionality.   \n*SensorConfiguration* is a class in Picamera2 which is referenced in the CameraConfiguration (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf) chapter 4.3)    \nIt includes information on the output size and bit depth of a stream.    \nIn Bullseye systems, this class is missing.         \nCurrently, raspiCamSrv does not require the *SensorConfigiuration* but it is included in the data model because Picamera2 uses it.   \nThe error occurs when trying to import the class.   \n\n- **WARN RPiSdn sdn.cpp:39 Using legacy SDN tuning - please consider moving SDN inside rpi.denoise**   \nThis is just a warning from the libcamera system that the tuning file should be updated.      \nIt is currently not known that there is an impact on raspiCamSrv functionality.\n\n\n- **ERROR V4L2 v4l2_videodevice.cpp:1906 /dev/video4[16:cap]: Failed to start streaming: Broken pipe**  \nSee [picamera2 Issue #104](https://github.com/raspberrypi/libcamera/issues/104) from Feb 1, 2024   \nThe recommended solution was to go back to kernel release 6.1.65 with ```sudo rpi-update d16727d```\n- **ModuleNotFoundError: No module named 'picamera2'**   \nSee [raspi-cam-srv Issue #4](https://github.com/signag/raspi-cam-srv/issues/4)\n- **TypeError: memoryview: casts are restricted to C-contiguous views**   \nSee [picamera2 Issue #959](https://github.com/raspberrypi/picamera2/issues/959)\n\n## Logging\n\nThe **raspiCamSrv** server uses Python logging.\n\nLogging is initialized in module ```__init__.py```.   \nAll lines controlling the way of logging or [code generation](#generation-of-python-code-for-camera) are preceeded with a comment line, starting with   \n```#>>>>>```\n\nBy default, a StreamingHandler is added to all loggers which outputs log information to sys.stderr.   \nIf desired, the prepared FileHandler can be activated.\n\nThe log level for all loggers is initialized with level ERROR.   \nThis can be modified for all or for specific modules.\n\n### Flask logging\n\nFlask logging is controlled by ```app.logger```\n\n### Werkzeug logging\n\nWerkzeug implements WSGI, the standard Python interface between applications and servers.\n\nWerkzeug logs basic request/response information.\n\nWerkzeug logging is controlled by ```logging.getLogger(\"werkzeug\")```.\n\nThe log level is initialized in ```__init__.py``` with INFO, in order to enable informative logging during server start.\n\nAfter the server has been started, the log level is raised to ERROR.   \nThis is done in ```auth.py``` in function ```login_required(view)```.\n\n### raspiCamSrv Logging\n\nLogging can be controlled individually for each module.\n\n### libcamera logging\n\nThe libcamera library is the basic C++ camera library on which Picamera2 is based.\n\nThe log level is controlled through an environment variable LIBCAMERA_LOG_LEVELS.   \nThis is set in ```__init__.py``` to WARNING.   \nOther allowed log levels are listed in the comment.\n\nFor more details, see [Picamera2 manual](./picamera2-manual.pdf), chapter 8.6.2\n\n### Picamera2 logging\n\nPicamera2 logging is initialized in ```__init__.py``` with ERROR\n\nFor more details, see [Picamera2 manual](./picamera2-manual.pdf), chapter 8.6.1\n\n## Generation of Python Code for Camera\n\nThe system can generate a file with Python code including the entire interaction of **raspiCamSrv** with Picamera2.   \nThis file can then be used for debugging and error analysis.\n\nA specific logger (\"pc2_prg\") with with level DEBUG is used for code generation.   \nThe logger can be activated by setting   \n```prgLogger.setLevel(logging.DEBUG)```   \nin ```__init___.py```\n\nThe code file is located in   \n```/home/<user>/prg/raspi-cam-srv/logs```   \nwith name   \n```prgLog_YYYYMMDD_hhmmss.log```\n\nA new file will be generatet at every server start.\n\nTo run the files, you neet to change the file type from ```.log``` to ```.py```   \nGenerating the files with ```.py``` extension does not work because Flask seems to recognize these files and does strange things.\n\nAll photo and video output generated by these files will be located at    \n```/home/<user>/prg/raspi-cam-srv/output```   \nwith the same file names as in the original session.\n"
  },
  {
    "path": "docs/Tuning.md",
    "content": "# raspiCamSrv Camera Tuning\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThe algorithms of Raspberry Pi cameras are affected by tuning files which are specific for each camera type and Raspberry Pi version. These files are automatically loaded when the camera system is instantiated.   \nA set of default tuning files for all camera models are part of the Raspberry Pi OS distribution.     \n(For details, see the [Raspberry Pi Camera Algorithm and Tuning Guide](https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf))\n\nWith **raspiCamSrv**, you can control which files are used and you may tweak specific features within these files to optimize the camera behavior for your specific needs.\n\nThis functionality can be accessed through menu *Config*, submenu *Tuning*:\n\n![Tuning1](./img/Tuning1.jpg)\n\nThe configuration provides three parameters:\n\n- *Name of Tuning File*<br>This is the name of the tuning file to be loaded for the active camera.<br>Raspberry Pi OS distributions include default tuning files with '.json' type and a name which is identical to the name of the camera model.<br>If you provide own tuning files, you may choose an arbitrary name. If you are using multiple camera models simultaneously, you should be able to correctly associate a file with a camera model.\n- *Full path to folder with tuning files*<br>If this is empty (None), Picamera2 will search a system-specific list of likely installation folders for the required tuning file.<br>raspiCamSrv supports usage of a custom folder (```/home/<user>/prg/raspi-cam-srv/raspiCamSrv/static/tuning```) which allows keeping own tuning files separate from the default ones.\n- *Load Tuning File*<br>If this checkbox is activated, raspiCamSrv will request Picamera2 to load the specified tuning file instead of the default one.<br>Otherwise, Picamera2 will load the default tuning file.\n\nAny changes of one of these parameters needs to be submitted with the *Submit & Apply* button.    \nIf *Load Tuning File* is checked, a restart of the live stream will be requested so that the result of the chosen tuning file can be directly seen in the [Live](./LiveScreen.md) view.\n\n## Switching between Custom and Default Folder\n\n- Button *Custom Folder* will switch the folder for tuning files to the custom folder ```/home/<user>/prg/raspi-cam-srv/raspiCamSrv/static/tuning```<br>If there is already a tuning file with the specified name in this folder, it will be used.<br>If the tuning file does not yet exist in the custom folder, the standard tuning file will be copied to the custom folder.\n- Button *Default Folder* (toggled) will switch to the default folder.<br>This folder is system-specific, e.g. ```/usr/share/libcamera/ipa/rpi/pisp```\n\n![Tuning2](./img/Tuning2.jpg)\n\n## Modification of Tuning Files\n\nModification of tuning files with raspiCamSrv can only be done in the custom folder.\n\nIf you are not directly accessing the tuning files on the Raspberry Pi, you can download a file, rename it, if desired, and modify it on the client machine.     \nAfterwards you can upload it to the custom folder.    \nWhen done, the file will be available for selection.\n\n### Deleting a Tuning File\n\nThe button *Delete Tuning File* will only be active, if\n\n- the custom folder is activated\n- *Load Tuning File* is unchecked\n- No parameter changes have beenn made after the last *Submit & Apply*\n\nThis restriction avoids inadvertently deleting the wrong file if the file has been changed without submitting.\n\n### Download Tuning File\n\nA tuning file can be downloaded from the default folder or from the custom folder.\n\n### Uploading Tuning Files\n\nThe buttons for upload will only be active if the custom folder is selected.\n\n1. You start pushing the *Select Tuning File for Upload* button<br>![Tuning3](./img/Tuning3.jpg)\n2. If a single file has been selected, its name will be shown on the button:<br>![Tuning4](./img/Tuning4.jpg)\n3. If a multiple files have been selected, the number of selected files will be shown on the button:<br>![Tuning5](./img/Tuning5.jpg)\n4. Finally, you need to the *Upload selected File* button to upload:<br>![Tuning6](./img/Tuning6.jpg)\n\n## Using a different Custom Folder\n\nIf you have already prepared tuning files in a specific folder on the Raspberry Pi, which is different from the Custom Folder used by raspiCamSrv, you can also refer to this folder.\n\nYou first need to manually enter the *Full Path* to this folder and push *Submit & Apply*.\n\nAfterwards, the .json files in this folder will be available for selection as *Name of Tuning File*\n\n## Tuning with Multiple Cameras\n\nTuning files are specific for a camera and not for a camera model.\n\nIf two cameras are used with a Pi5, the tuning file needs to be individually specified for each camera, also if both are the same model.\n\nTo preserve the tuning configuration for a camera, it must be memorized using the *Memorize Configuration and Controls for Camera Change* in screen [Multi Cam](./CamMulticam.md)\n\nBecause of a Threading issue in Picamera2 (see Picamera2 Issue #1103 [Tuning file support not thread-safe?](https://github.com/raspberrypi/picamera2/issues/1103)), the streams from the two cameras in dialog [Web Cam](./CamWebcam.md) may look as if the tuning file of the first camera would have also been applied to the second one.\n\nNevertheless, the handling in raspiCamSrv is correct.   \nYou may proof this in the following way:\n\n1. Activate a dialog without live view, e.g. [Info](./Information.md).\n2. Wait at least 10 sec. until both streaming threads have terminated (refresh the screen from time to time)\n3. In another browser window, stream just one camera (endpoint video_feed or video_feed2).<br>You should then see the effect of the correct tuning file.<br>However, if you later start streaming the other camera, you may see that the tuning file for the previously started camera has been applied.\n"
  },
  {
    "path": "docs/UserGuide.md",
    "content": "# RaspiCamSrv User Guide\n\n[![Up](img/goup.gif)](./index.md)\n\nThe variant of the user interface, described on this page, refers to the case where **at least one camera** (CSI or USB) is available.   \nIf this is not the case, refer to the [reduced user interface](./UserGuide_NoCam.md).\n\n#### Startup\nWhen the server on a Pi is running and the Pi is reacheable through the network (usually WiFi), you can connect with a browser using the Pi address ('raspi05' in the example below) with the Flask port number, usually 5000, e.g.:  \n```http://raspi05:5000```\n\n#### Login\n\nThe system will request an initial [registration and a login](./Authentication.md) and subsequently open the **Live** application screen.\n\nFor error handling, see [raspiCamSrv Troubleshooting](./Troubelshooting.md)   \nFor interoperability, **raspiCamSrv** provides an [API](./API.md) which allows access to selected functions through web services.\n\n#### CSI-/USB-Cameras\n\nIn addition to CSI cameras (2 for Pi 5), you can connect as many USB cameras as physical USB ports are available.    \nHowever, at a time, **raspiCamSrv** will only operate two of them simultaneously.   \nRelated to usability in the UI, you will almost see no difference between CSI and USB cameras. Where they are important, they are handled in the UI and described in their context.\n\n#### AI Camera\n\n**raspiCamSrv** supports AI features with the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html). See [AI Camera Support](./AiCameraSupport.md).\n\n## Application Screen\n![Main Screen](img/Live_start.jpg)\n\n### Elements\n\n#### Title bar\n\nOn the left side, the title bar shows\n\n- the current version of raspiCamSrv<br>If an update to this version is available, the version number is shown in yellow:<br>![New Release Indicator](./img/CanUpdate_Indicator.jpg)<br>\n(See [Settings/Update](./SettingsUpdate.md) dialog)\n\nOn the right side, the title bar shows\n\n- the current server connection\n- the active camera as advertised by Picamera2\n- the active user\n- A special icon will indicate unsaved configuration changes:<br>![Changes](./img/UnsavedChangesIndicator.jpg)<br>It will vanish after changes have been saved with [Settings/Configuration/Store Configuration](./SettingsConfiguration.md) or after a stored configuration has been loaded.   \nThe icon can be pressed (2 times) to save unsaved changes.\n\nOn the left side, the title bar shows the application name (raspiCamSrv) and the current screen.\n\n#### Main Menu\n\nThe main menu (black background) allows navigation to different screens:\n\n- **Live** shows the [Live Screen](./LiveScreen.md) which includes functionality for image control as well as photo- and video taking\n- **Config** gives access to camera [Tuning](./Tuning.md) and camera [Configuration](./Configuration.md) where basic camera configurations can be specified for different scenarios.\n- **Info** opens the [Camera Information](./Information.md) page with information on the system and installed cameras as well as Properties and Sensor Modes of the active camera.\n- **Photos** shows the [Photos](./PhotoViewer.md) where the currently available photos and videos can be browsed and inspected in detail.\n- **Photoseries** opens the [Photo Series](./PhotoSeries.md) page for definition and control of photo series.\n- **Trigger** Allows configuring and controlling [triggered actions](./Trigger.md), based on [Motion Capturing](./TriggerMotion.md) as well as on configured [events from GPIO-connected sensors](./TriggerTriggers.md)\n- **Cam** gives access to the dialogs for [Web Cam](./CamWebcam.md) access; [Multi Cam](./CamMulticam.md) for multi-camera control.    \nIf a [Stereo Camera configuration](./Settings.md#activating-and-deactivating-stereo-vision) is activated, it hosts also the dialogs for [Camera Calibration](./CamCalibration.md) as well as for [Stereo Vision](./CamStereo.md).\n- **Console** is dedicated to manually controlled interactions with the [Raspberry Pi OS](./ConsoleVButtons.md) or with [GPIO-connected Actors](./ConsoleActionButtons.md), such as motors, servos or LEDs.\n- **Settings** opens the [Settings](./Settings.md) page for all kinds of static configurations for **raspiCamSrv**.\n- **Log Out** will log the active user out and direct to the [Log-In Screen](./Authentication.md)\n\n**NOTE:** Selecting an option on the main menu will issue a request to the server with a specific URL and, thus, refresh the screen.\n\n#### Submenu\n\nMany of the **raspiCamSrv** pages, selected by a [Main Menu](#main-menu) option have a submenu.    \nSubmenues are indicated by a green background.\n\n**NOTE:** Selecting an option on a submenu will **not** issue a new request and, thus, will **not** refresh the screen with new information from the server.    \nInstead, submenu options activate different sections of the currently loaded page.   \nHowever, *Submit* buttons on a page section apply only to data shown on the active section and not to data on other sections of the same page.\n\n\n#### Process Status indicators\n\nOn the right side of the menu bar there is a group of status indicators for the different [background processes](./Background%20Processes.md):\n\n![Status Indicators](./img/ProcessIndicator1.jpg)\n\n![Status Indicators](./img/ProcessIndicator3.jpg)\n\nFrom right to left, these indicate the status of\n\n- Live stream thread for active camera\n- Video thread for the active camera\n- Recording [audio](./Settings.md#recording-audio-along-with-video) along with video for the active camrera\n- [Photo Series](./PhotoSeries.md) thread\n- [Motion Capture](./Trigger.md) thread\n- [Trigger](./TriggerTriggers.md) thread\n- Live stream thread for the second camera, if available (see [Web Cam](./CamWebcam.md) or [Multi-Cam](./CamMulticam.md))\n- Video thread for the second camera, if available (see [Multi-Cam](./CamMulticam.md))\n\nRed color indicates that a process is active whereas gray indicates that it is inactive.   \n\nIn the case of [motion capture](./TriggerMotion.md) or [event handling](./Trigger.md),    \n\n- yellow color indicates that the process is active but currently not scheduled to register events\n- turquoise color indicates that the motion capture process runs in [test mode](./TriggerMotion.md#testing-motion-capturing)\n\n![MotionPaused](./img/ProcessIndicator8.jpg)     \n![MotionPaused](./img/ProcessIndicator12.jpg)\n\n\n#### Message Line\nAt the bottom of the screen, there is a message line where application messages will be shown when necessary.\n\n## Streaming\n\n**raspiCamSrv** supports streaming MJPEG video.\n\nThe straming URLs are   \n```http://<server>:<port>/video_feed``` for MJPEG video with Active Camera   \n```http://<server>:<port>/photo_feed``` for photo snapshots with Active Camera and low resolution     \n```http://<server>:<port>/photo_feed_hr``` for photo snapshots with Active Camera and high resolution     \n```http://<server>:<port>/video_feed2``` for MJPEG video with Second Camera   \n```http://<server>:<port>/photo_feed2``` for photo snapshots with Second Camera and low resolution     \n```http://<server>:<port>/photo_feed2_hr``` for photo snapshots with Second Camera and high resolution     \nAll URLs can be accessed without authentication if the checkbox *Req. Auth for Streaming* on the [Settings](./Settings.md) screen is deactivated.   \nIf this checkbox is activated, a user must have logged in to raspiCamSrv once in the same browser session which shall be used for streaming. A streaming or snapshot request in a browser session without login will redirect to the login screen.\n\nIn the web client, an active streaming server is indicated with the process status indicators as    \n![ProcessStatusIndicator](./img/ProcessIndicator1.jpg) if only the active camera is streaming or   \n![ProcessStatusIndicator](./img/ProcessIndicator10.jpg) if both cameras are streaming or     \n![ProcessStatusIndicator](./img/ProcessIndicator11.jpg) if if only the second camera is streaming     \n\nA live stream is shown in in the [Live Screen](./LiveScreen.md) for the active camera or on the [Web Cam](./CamWebcam.md) or [Multi-Cam](./CamMulticam.md) pages for both cameras.\n\nIf [Stereo Vision](./CamStereo.md) is active, both cameras are streaming:             \n![ProcessStatusIndicator](./img/ProcessIndicator21.jpg)      \n![ProcessStatusIndicator](./img/ProcessIndicator20.jpg)      \nWhen these indicators turn yellow, this indicates that the additional stereo camera process is active, serving the stereo vision stream.\n\n\nThe streaming servers are automatically shut down if no client has been streaming within the last 10 seconds.   \nThis is independently controlled for both cameras as well as for the stereo camera process.   \nFor example if one is working in other dialogs rather than *Live Screen*, straming is not used and the streaming servers are shut down, which is indicated by   \n![ProcessStatusIndicator](./img/ProcessIndicator0.jpg)   \nStreaming is automatically reactivated, if a streaming client connects, for example if the *Live Screen* is activated.\n\nOther clients, either connecting directly through the streaming URL or by using the **raspiCamSrv** web client, will also activate the streaming servers.\n\nStreaming of the active camera can be deactivated, if a **raspiCamSrv** task is executed which requires exclusive access to the camera because of a specific [Configuration](./Configuration.md) which is not compliant with the configuration required for streaming (for more details, see [raspiCamSrv Tasks and Background Processes](./Background%20Processes.md)).\n\n**NOTE**     \nFor a full understanding of application details, users should familiarize with the official document [Raspberry Pi - The Picamera2 Library](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf).  \nThe document version, on which this raspiCamSrv release is based, is also included in this documentation: [picamera2-manual.pdf](./picamera2-manual.pdf)\n\n## Media Viewer\n\nIn several dialogs, the raspiCamSrv UI shows photos or videos which have been taken with the camera system.    \nAlthough most of these are larger than typical thumbnails, they may be too small for more detailed inspection of quality or image details.\n\nTherefore, raspiCamSrv provides a Media Viewer which can be started by clicking on the image or video.\n\nThe availability of a Media Viewer is indicated by a modified cursor when hovering over the image.\n\n![MediaViewer Start](./img/MediaViewer_1.jpg)\n\nKlicking on the image, will open a new browser tab with the selected image or video:\n\n![MediaViewer Start](./img/MediaViewer_2.jpg)\n\nThe tab can be separated from the main browser window and zoomed to screen size or full screen mode.\n\nThe file name of the image/video is shown as tab title.\n\n### Media Viewer for videos\n\nWhen videos are shown with controller icons for play/stop etc., only the upper area is sensitive for starting the media viewer whereas the lower area is used for controlling the video playback.\n\n![MediaViewer Start](./img/MediaViewer_3.jpg)\n\nThe Media Viewer browser tab will include its own set of controls:\n\n![MediaViewer Start](./img/MediaViewer_4.jpg)\n\n### Live Stream\n\nThe live stream, available in dialogs [Live](./LiveScreen.md), [Cam](./Cam.md) and [Trigger/Motion](./TriggerMotion.md) are not enabled for Media Viewer activation. Instead, the live stream can be directly opened in a separate window using the [Streaming URL](./CamWebcam.md).\n\n\n### Live Stream at Camera Start\n\nWhen a camera starts up, there is usually a short delay of a few seconds until the first frames are delivered by the camera.   \nThis time is required by different algorithms (e.g. auto exposure or automatic white balance) to collect information on the scene.\n\nFor the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html), if AI features are enabled (see [Camera AI Configuration](./Configuration_AI.md)), the camera needs to load the specified neural network model. This can take significantly more time compared to the start of normal cameras and may lead to irritation when waiting for the live stream to start.\n\nThe Picamera2 IMX500 package therefore issues a warning note about long startup times or a progress bar when running the [imx500 demo programs](https://github.com/raspberrypi/picamera2/tree/main/examples/imx500) on the Debian UI.\n\n![imx500 Start](./img/Live_imx500_start_journal.jpg)\n\n**raspiCamSrv** shows an animation while the camera is starting up until the first frames are delivered for the Live stream:\n\n![Live animation](./img/Live_Animation.gif)\n\nThis animation is also shown for other cameras.\n\nOnly for systems where OpenCV is not installed, the system will wait for the first frames until the Live stream is shown.\n\nFor the imx500 camera, the system log will show activities while the model is being loaded to the camera.   \nExperience has shown that, especially when the model is changed, the process may take a long time, even on a Raspberry Pi 5."
  },
  {
    "path": "docs/UserGuide_NoCam.md",
    "content": "# RaspiCamSrv User Guide (No Camera)\n\n[![Up](img/goup.gif)](./index.md)\n\nThis is a special variant of the general **raspiCamSrv** [User Interface](./UserGuide.md) for the case when no camera is available.\n\nThis applies to the following situations:\n\n- There is currently no camera at all connected to the Raspberry Pi, neither CSI camera nor USB camera.\n- Currently, only one or more USB cameras are connected and have been recognized by the system (see [Info/Cameras](./Information_Cam.md)) but usage of USB cameras has been deactivated in [Settings](./Settings_NoCam.md).\n\n## Application Screen\n\nInitailly, the system starts up with the [Info/System](./Information_Sys.md) dialog activated:\n![Main Screen](img/Live_start_no_cam.jpg)\n\n### Elements\n\n#### Title bar\n\nOn the right side, the title bar shows\n\n- the current server connection\n- An information that currently no camera is available\n- the active user\n- A special icon will indicate unsaved configuration changes:<br>![Changes](./img/UnsavedChangesIndicator_no_cam.jpg)<br>It will vanish after changes have been saved with [Settings/Configuration/Store Configuration](./SettingsConfiguration.md) or after a stored configuration has been loaded.    \nThe icon can be pressed to save unsaved changes.\n\nOn the left side, the title bar shows the application name (raspiCamSrv) and the current screen.\n\n#### Main Menu\n\nThe main menu (black background) allows navigation to different screens:\n\n- **Info** opens the [Camera Information](./Information.md) page with information on installed cameras as well as Properties and Sensor Modes of the active camera.\n- **Trigger** Allows configuring and controlling [triggered actions](./Trigger.md), based on configured [events from GPIO-connected sensors](./TriggerTriggers.md)\n- **Console** is dedicated to manually controlled interactions with the [Raspberry Pi OS](./ConsoleVButtons.md) or with [GPIO-connected Actors](./ConsoleActionButtons.md), such as motors, servos or LEDs.\n- **Settings** opens the [Settings](./Settings.md) page for all kinds of static configurations for **raspiCamSrv**.\n- **Log Out** will log the active user out and direct to the [Log-In Screen](./Authentication.md)\n\n**NOTE:** Selecting an option on the main menu will issue a request to the server with a specific URL and, thus, refresh the screen.\n\n#### Submenu\n\nMost of the **raspiCamSrv** pages, selected by a [Main Menu](#main-menu) option have a submenu.    \nSubmenues are indicated by a green background.\n\n**NOTE:** Selecting an option on a submenu will **not** issue a new request and, thus, will **not** refresh the screen with new information from the server.    \nInstead, submenu options activate different sections of the currently loaded page.   \nHowever, *Submit* buttons on a page section apply only to data shown on the active section and not to data on other sections of the same page.\n\n\n#### Process Status indicators\n\nOn the right side of the menu bar there is space status indicators for [background processes](./Background%20Processes.md):\n\nWhen a camera is not available, there is only the status indicator for the [Trigger](./TriggerTriggers.md) thread:\n\n![Status Indicators](./img/ProcessIndicator0_no_cam_1.jpg)\n\nGray color indicates that a process is inactive whereas red indicates that it is active.   \n![Status Indicators](./img/ProcessIndicator0_no_cam_2.jpg)\n\nYellow color indicates that the process is active but currently not scheduled to register events\n![TriggerPaused](./img/ProcessIndicator0_no_cam_3.jpg)\n\n#### Message Line\n\nAt the bottom of the screen, there is a message line where application messages will be shown when necessary.\n"
  },
  {
    "path": "docs/Z_Legacy_Information.md",
    "content": "# raspiCamSrv Information on Camera System\n\n[![Up](img/goup.gif)](./UserGuide.md)\n\nThis screen contains several tabs with information on the camera system:   \n\n## Installed Cameras\n\n![Cameras](img/Info-Cameras.jpg)\n\n### Raspberry Pi\n\nThis section shows information on the server hardware with *Model* and *Board Revision*\n\nFor the operating system, the kernel version (result of ```uname -r```), the Debian version (result of *Description* from ```lsb_release -a``` and ```cat /etc/debian_version```) and the system architecture (32-/64-bit) (result from ```dpkg-architecture --query DEB_HOST_ARCH```) are shown.\n\n*Process Info* shows current process information for the raspiCamSrv server process (result of Linux ```ps -eLf``` command)\n\n- *PID*: Process ID of Flask process (PID)\n- *Start*: Process start time (STIME): either start time (HH:MM) at current day or day (MonDD) when process was started.\n- *#Threads*: Number of threads (NLWP)\n- *CPU Process*: CPU time of process (TIME for LWP == PID) in HH:MM:SS\n- *CPU Threads*: Sum of CPU time for threads ((TIME for LWP != PID)) in %H:MM:SS\n\n*FFmpeg Info* shows information on an ffmpeg process if encoding of .mp4 videos is currently active.   \nRecording of .mp4 videos may have been [started manually](./Phototaking.md) or as an action within [motion capturing](./Trigger.md)\n\n*raspiCamSrv Start* shows the time when the raspiCamSrv server has been started.   \nAt server start, raspiCamSrv checks whether or not the Raspberry Pi system time is synchronized with the time server.   \nWhen the device is booted and raspiCamSrv is automatically started, the time synchronization will occasionally be done after the Flask server has already been started.    \nIn this case, in order to avoid timing issues, raspiCamSrv will wait at startup until time synchronization is completed.   \nThe time shown here is the system time at the moment when the check for time synchronization was successful.   \nraspiCamSrv analyzes the output of command ```timedatectl``` to check the system clock synchronization status.    \nIf this check fails or times out (60 sec), raspiCamSrv will start nevertheless. In this case, the information \"System time not synced at raspiCamSrv start\" will be shown here.     \nIf the server is running in a Docker container (see [Running raspiCamSrv as Docker Container](./SetupDocker.md)), the time is assumed to be synchronized and the check will be skipped. This is indicated through     \n![Container](img/Info-Container.jpg)\n\n\n*Software Stack* shows information on installed packages with Version (*Ver*) and the path from which the packages were loaded (*Loc*).\n\n### Camera x\n\nThe tab lists all cameras currently connected to the system.\n\nEach camera has an identifying number (0, 1, ...) shown in the title above each parameter list.   \nThe assignment of camera number to physical camera may change when CSI cameras are plugged into a different CSI port (Pi 5) or when USB cameras are plugged into a different USB port.\n\nWhen the server starts up, the first camera in the [list of cameras](#detection-of-cameras) is selected as active camera, unless a specific camera is activated when a [stored configuration](./SettingsConfiguration.md) is loaded at startup..\n\nYou may later switch to another camera on the [Settings](./Settings.md) screen or the [Multi Cam](./CamMulticam.md) screen\n\nThe active camera is indicated in the list.\n\nThe active camera will also be shown in the title bar of the application after log-in.\n\nFor USB cameras, the device through which the camera is accessible is also shown (See [Detection of Cameras](#detection-of-cameras)).\n\nThe information \"(Not in use)\" for a USB camera indicates that the camera has been detected by **raspiCamSrv** but it is not in use because USB cameras have been deactivated in the [Settings](./Settings_NoCam.md). In this case, the USB camera cannot be selected as active or second camera in the [Settings](./Settings.md) screen or the [Multi Cam](./CamMulticam.md) screen.\n\n#### Status\n\n*Current Status* shows the status of the camera:\n\n- open / closed\n- started / stopped\n- current [Sensor Mode](#sensor-modes)   \nThis is only shown for the currently active camera if it is started.    \nIf the Sensor Mode cannot currently be determined, 'unknown' is shown.    \nThe Sensor Mode is usually automatically selected by the camera and normally corresponds to the largest [Stream Size](./Configuration.md#stream-size-width-height), requested by one of the [Camera Configurations](./Configuration.md).\n- inactive<br>is shown for USB cameras which are currently not in use as active or second camera.\n- excluded<br>is shown for a USB camera which is, in principle, available for being used with **raspiCamSrv**, but currently excluded in the [Settings](./Settings.md)\n- not supported (OpenCV missing)<br>Is shown if a detected USB camera cannot be used within **raspiCamSrv** because OpenCV is not installed.\n\nSee [Camera Status and Number of Threads](#camera-status-and-number-of-threads)\n\nUnder *Tuning File*, you can see whether the Default or a custom tuning file are currently in use.    \nSee [raspiCamSrv Camera Tuning](./Tuning.md).\n\n#### AI Features\n\nThis shows whether AI Features of a camera are available and active.    \nCurrently, this applies only to the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) with Sony IMX500 sensor.\n\n- Not Available\n<br>Indicates that the camera has no AI capabilities\n- Available\n<br>Indicates that the camera has AI capabilities and that AI features are enabled in the [Settings](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features).\n<br>Whether or not the camera is currently running a neural network model can be controlled in the [Camera AI Configuration](./Configuration_AI.md)\n- Disabled in Settings\n<br>Indicates that the camera has AI capabilities. However AI features are disabled in the [Settings](./Settings.md#activating-and-deactivating-the-use-of-camera-ai-features)\n\n#### Camera connected but not in the list?\n\nIf you have a USB camera connected which does not show up in the list, you may have plugged in the camera while **raspiCamSrv** was running.   \nIn this case, you can use function [Reload Cameras](./SettingsConfiguration.md) to identify hot-plugged cameras.\n\n### Streaming Clients\n\n![Streaming Clients](./img/Info-StreamingClients.jpg)\n\nThe tab lists the clients which are currently using one of the camera streams.   \nAlong with the IP address of the client, a list of streams is shown which the client is using:\n\n- *live_view*<br> [The Live View](./LiveScreen.md) stream<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLiveActive.jpg)\n- *video_feed*<br>The [video Stream](./CamWebcam.md#video-stream) for the active camera<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLiveActive.jpg)\n- *video_feed2*<br>The [video Stream](./CamWebcam.md#video-stream) for the second camera, if available<br>indicated by [Process Status Indicator](./UserGuide.md#process-status-indicators) ![indicator_live](./img/ProcessIndicatorLive2Active.jpg)\n\n## Camera Properties\n\n![Camera Properties](img/Info-CamProps.jpg)\n\nThese are the properties of the camera which is currently active.\n\n(See [Determining Camera Properties for USB Cameras](#determining-camera-properties-for-usb-cameras))\n\n\n## Sensor Modes\n\nThe camera system advertises the supported Sensor Modes with their characteristics.\n\nThese are referred to within the [Camera Configuration](./Configuration.md).\n\nThe characteristics vor every Sensor Mode are shown on an individual tab:\n\n![Sensor Mode](img/Info_SensorMode.jpg)\n\nUSB cameras may advertise a large number of sensor modes. In this case, the remaining buttons for sensor mode selection can be found in a drop-down list:\n![Sensor Mode](img/Info_SensorModeUsb.jpg)\n\n\n## Camera Status and Number of Threads\n\nThe number of threads used by the server process depends on the status of the camera(s).\n\n- When all cameras are closed, there is just the server process and, in case of Bookworm systems, 2 threads which are started with the import of Picamera2.    \nThus, there is a minimum of three threads (1 for Bullseye).\n- Opening a camera starts additional threads which remain active while the camera is open.   \nThe number of threads may depend on the camera infrastructure specific for the operating system.\n- Starting a camera and/or starting an encoder starts additional threads depending on the chosen camera function and encoder.\n- **raspiCamSrv** also uses threads for background processes, such as live stream, video recording, photo series and motion detection. These ramain active while these processes are running.\n- Stopping and closing a camera will also stop the dependent threads and thus reduce the number of active threads.\n- If .mp4 video is currently recorded ([started manually](./Phototaking.md) or as an action within [motion capturing](./Trigger.md)), there will be an additional ffmpeg process with additional threads.\n- In case of .mp4 video recording with H264Encoder and FfmpegOutput there seems to be an issue with threads:    \nIn this case, there may be threads surviving when the encoder is stopped (see [picamera2 Issue #1023](https://github.com/raspberrypi/picamera2/issues/1023)).   \nSo, when .mp4 videos have been recorded, the number of threads may not go down to 3 (1 for Bullseye) after all camaras have been closed.   \nExperience shows that such threads may survive for a longer time but typically, they show only minor or no CPU utilization.   \nOften, they vanish after the camera has been closed after live stream has stopped.\n\n**raspiCamSrv** closes the camera in case it is not used:\n\n- When the [live stream](./LiveScreen.md) stops after 10 seconds of inactivity, the camera used for the live stream will be stopped and closed.\n- After [photos have been taken or videos have been recorded](Phototaking.md), the camera will be stopped and closed.\n- For [Photo Series](./PhotoSeries.md), the camera will be stopped and closed after a shot if the interval to the next shot is >60 sec.   \nThis does not apply to [Exposure Series](./PhotoSeriesExp.md) and [Focus Stacks](./PhotoSeriesFocus.md).\n- If [motion detection](./Trigger.md) is active, the live stream is kept activated which keeps the camera open and started.\n- In case of [Stereo Vision](./CamStereo.md), the live streams for both cameras are kept active,\n\n## Detection of Cameras\n\n**raspiCamSrv** uses the ```Picamera2.global_camera_info()``` list (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), ch. 8.7) to identify the currently connected cameras.\n\nFor each camera, the information provided by Picamera2 includes\n\n- ```Num```: The camera number by which a camera is identified within Picamera2 as well as in raspiCamSrv.\n- ```Model```: The model name of the camera, as advertised by the camera driver\n- ```Location```: A number reporting how the camera is mounted, as reported by libcamera.\n- ```Rotation```: How the camera is rotated for normal operation, as reported by libcamera\n- ```ID```: An identifier string for the camera, indicating how the camera is connected. <br>You can tell from this value whether the camera is accessed using I2C or USB.\n\n\n\n## Identification of USB Cameras\n\nA camera is identified as USB camera, if ```usb``` is found in the ```ID```.\n\nIn **raspiCamSrv**, USB cameras are accessed through [OpenCV](https://opencv.org/) rather than through Picamera2, which provides only very limited support for USB cameras.\n\nHowever, with OpenCV, a camera cannot be accessed through the Picamera2 camera number (```Num```).    \nInstead, the ```/dev/videoX``` of the Linux kernel must be used.\n\nFor mapping of the Picamera2 camera number (```Num```) to the device number, **raspiCamSrv** uses the following algorithm:\n\nAssuming that the ```ID``` is structured in the following way:\n\ne.g.:   \n```/base/axi/pcie@1000120000/rp1/usb@200000-2:1.0-046d:085c```\n\n| Component                       | Meaning \n|---------------------------------|------------\n| ```/base/axi/pcie@1000120000``` | Root of the system-on-chip’s PCIe controller\n| ```/rp1/usb@200000```           | The RP1 I/O controller’s USB host controller (i.e. USB root hub)\n| ```-2:1.0```                    | USB device address and interface: port 2, interface 1.0\n| ```-046d:085c```                | Vendor ID : Product ID (046d = Logitech, 085c = C922 Pro Stream Webcam)\n\nNow, with Video for Linux (V4L2), we can list all video devices:\n\n```v4l2-ctl --list-devices``` reveals, for example:\n\n```\n...\nrpi-hevc-dec (platform:rpi-hevc-dec):\n        /dev/video19\n        /dev/media1\n\nLogi 4K Stream Edition (usb-xhci-hcd.0-1):\n        /dev/video2\n        /dev/video3\n        /dev/video4\n        /dev/video5\n        /dev/media4\n\nC922 Pro Stream Webcam (usb-xhci-hcd.0-2):\n        /dev/video0\n        /dev/video1\n        /dev/media3\n \n```\n\nEach header within the list shows the camera's model and port (```(usb-xhci-hcd.0-2)``` indicates port 2)\n\nNow, by mapping model and port from the Picamera2 ```ID``` with corresponding information from ```v4l2-ctl```, we can identify the group for each USB camera.\n\nThe first entry in the list of devices for this group is assigned to the camera number, assuming that this represents the main camera stream, whereas subsequent entries are for alternative functions.\n\n## Determining Camera Properties for USB Cameras\n\nWhereas camera properties of CSI cameras are directly provided by Picamera2 (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), Appendix D), this is not the case for USB cameras.\n\n**raspiCamSrv** maps properties of USB cameras as far as possible to the Picamera2 datastructure for seamless integration of USB cameras.\n\nThe USB camera properties are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --all```\n\ngiving:\n\n```\nDriver Info:\n        Driver name      : uvcvideo\n        Card type        : C922 Pro Stream Webcam\n        Bus info         : usb-xhci-hcd.0-2\n        Driver version   : 6.12.47\n        Capabilities     : 0x84a00001\n                Video Capture\n                Metadata Capture\n                Streaming\n                Extended Pix Format\n                Device Capabilities\n        Device Caps      : 0x04200001\n                Video Capture\n                Streaming\n                Extended Pix Format\nMedia Driver Info:\n        Driver name      : uvcvideo\n        Model            : C922 Pro Stream Webcam\n        Serial           : A9382BFF\n        Bus info         : usb-xhci-hcd.0-2\n        Media version    : 6.12.47\n        Hardware revision: 0x00000016 (22)\n        Driver version   : 6.12.47\nInterface Info:\n        ID               : 0x03000002\n        Type             : V4L Video\nEntity Info:\n        ID               : 0x00000001 (1)\n        Name             : C922 Pro Stream Webcam\n        Function         : V4L2 I/O\n        Flags            : default\n        Pad 0x01000007   : 0: Sink\n          Link 0x0200001f: from remote pad 0x100000a of entity 'Processing 3' (Video Pixel Formatter): Data, Enabled, Immutable\nPriority: 2\nVideo input : 0 (Camera 1: ok)\nFormat Video Capture:\n        Width/Height      : 640/480\n        Pixel Format      : 'YUYV' (YUYV 4:2:2)\n        Field             : None\n        Bytes per Line    : 1280\n        Size Image        : 614400\n        Colorspace        : sRGB\n        Transfer Function : Rec. 709\n        YCbCr/HSV Encoding: ITU-R 601\n        Quantization      : Default (maps to Limited Range)\n        Flags             :\nCrop Capability Video Capture:\n        Bounds      : Left 0, Top 0, Width 640, Height 480\n        Default     : Left 0, Top 0, Width 640, Height 480\n        Pixel Aspect: 1/1\nSelection Video Capture: crop_default, Left 0, Top 0, Width 640, Height 480, Flags:\nSelection Video Capture: crop_bounds, Left 0, Top 0, Width 640, Height 480, Flags:\nStreaming Parameters Video Capture:\n        Capabilities     : timeperframe\n        Frames per second: 30.000 (30/1)\n        Read buffers     : 0\n\nUser Controls\n\n                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128\n                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128\n                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128\n        white_balance_automatic 0x0098090c (bool)   : default=1 value=1\n                           gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=0\n           power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2 (60 Hz)\n                                0: Disabled\n                                1: 50 Hz\n                                2: 60 Hz\n      white_balance_temperature 0x0098091a (int)    : min=2000 max=6500 step=1 default=4000 value=4000 flags=inactive\n                      sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=128 value=128\n         backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=0 value=0\n\nCamera Controls\n\n                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 (Aperture Priority Mode)\n                                1: Manual Mode\n                                3: Aperture Priority Mode\n         exposure_time_absolute 0x009a0902 (int)    : min=3 max=2047 step=1 default=250 value=250 flags=inactive\n     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=1\n                   pan_absolute 0x009a0908 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                  tilt_absolute 0x009a0909 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                 focus_absolute 0x009a090a (int)    : min=0 max=250 step=5 default=0 value=0 flags=inactive\n     focus_automatic_continuous 0x009a090c (bool)   : default=1 value=1\n                  zoom_absolute 0x009a090d (int)    : min=100 max=500 step=1 default=100 value=100\n\n```\n\nBy parsing this information, relevant data for camera properties can be retrieved and mapped to camera property elements.\n\nThe ```PixelArraySize``` is determined as the maximum size of the Sensor Modes found (see [below](#determining-sensor-modes-for-usb-cameras))\n\n\n## Determining Sensor Modes for USB Cameras\n\nWhereas sensor modes of CSI cameras are directly provided by Picamera2 (see [Picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), ch. 4.2.2.3), this is not the case for USB cameras.\n\n**raspiCamSrv** maps video formats of USB cameras as far as possible to the Picamera2 sensor mode datastructure for seamless integration of USB cameras.\n\nThe USB camera video formats are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --list-formats-ext```\n\ngiving:\n\n```\nioctl: VIDIOC_ENUM_FMT\n        Type: Video Capture\n\n        [0]: 'YUYV' (YUYV 4:2:2)\n                Size: Discrete 640x480\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n                Size: Discrete 160x90\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n...\n\n                Size: Discrete 2304x1536\n                        Interval: Discrete 0.500s (2.000 fps)\n        [1]: 'MJPG' (Motion-JPEG, compressed)\n                Size: Discrete 640x480\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n...\n\n                Size: Discrete 1920x1080\n                        Interval: Discrete 0.033s (30.000 fps)\n                        Interval: Discrete 0.042s (24.000 fps)\n                        Interval: Discrete 0.050s (20.000 fps)\n                        Interval: Discrete 0.067s (15.000 fps)\n                        Interval: Discrete 0.100s (10.000 fps)\n                        Interval: Discrete 0.133s (7.500 fps)\n                        Interval: Discrete 0.200s (5.000 fps)\n\n```\n\nFrom this output the list of sensor modes is generated with information on Size and Format.\n\nThe FPS, stored for each sensor mode is the maximum value of fps found for each format.\n\n## Determining supported Controls\n\nEvery USB Camera advertises a list of controls which can be used to adjust focus or image appearance.\n\nSupported controls are determined with the v4l2 through (e.g.):\n\n```v4l2-ctl --device=/dev/video12 --list-ctrls```\n\ngiving:\n\n```\nUser Controls\n\n                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128\n                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128\n                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128\n        white_balance_automatic 0x0098090c (bool)   : default=1 value=0\n                           gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=0\n           power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2 (60 Hz)\n      white_balance_temperature 0x0098091a (int)    : min=2000 max=7500 step=10 default=4000 value=3000\n                      sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=128 value=128\n         backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=1 value=1\n\nCamera Controls\n\n                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 (Aperture Priority Mode)\n         exposure_time_absolute 0x009a0902 (int)    : min=3 max=2047 step=1 default=250 value=312 flags=inactive\n     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=0\n                   pan_absolute 0x009a0908 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                  tilt_absolute 0x009a0909 (int)    : min=-36000 max=36000 step=3600 default=0 value=0\n                 focus_absolute 0x009a090a (int)    : min=0 max=255 step=5 default=0 value=10 flags=inactive\n     focus_automatic_continuous 0x009a090c (bool)   : default=1 value=1\n                  zoom_absolute 0x009a090d (int)    : min=100 max=500 step=1 default=100 value=100\n```\n\n**raspiCamSrv** analyzes this list with respect to a limited set of controls and registers\n\n- control name\n- value data type\n- minimum value\n- maximum value\n- default value\n\nFor the active camera, this information is appended to the Camera Controls data structure (see [Server Configuration Storage](./SettingsConfiguration.md#server-configuration-storage)), which will also be transferred to the Streaming Configuration in case of multiple cameras.\n\nIn order to establish this data structure, USB cameras need to be activated once as *Active Camera*.\n\nThe controls information is cused to customize the [Camera Controls](./CameraControls_UsbCams.md) screens when a USB camera is the *Active Camera*."
  },
  {
    "path": "docs/ZoomPan.md",
    "content": "# raspiCamSrv Zoom & Pan\n\n[![Up](img/goup.gif)](./CameraControls.md)\n\n![ZoomAndPan](img/Zoom.jpg)\n\nThis tab allows zooming and panning the image area within the dimensions supported by the camera pixel array size and Sensor Modes.\n\nFor more details, see [Image Cropping and Sensor Modes](./ScalerCrop.md)\n\n**NOTE**: When zoom or pan/tilt changes the cropping window of the Live View, [Regions of Interest](./TriggerMotion.md#regions-of-interest-and-regions-of-no-interest) which may heve been defined for [Motion Capturing](./TriggerMotion.md) may be adjusted to the new cropping window.\n\n## Current zoom factor in %\n\nThis value shows the current zoom factor.   \nIt cannot be modified manually but only through the **Zoom in**, **Zoom out** or **Full** buttons.\n\nThe value is given in % of the [ScalerCrop Default](./ScalerCrop.md#cropping) size.\n\n## Zoom & pan step in %\n\nThis value can be adjusted.   \nIt specifies the step size by which every click on **Zoom in** or **Zoom out** will change the *Current zoom factor*.\n\n## Current ScalerCrop (Zoom)\n\nThis rectangle, given in pixels, specifies the ScalerCrop rectangle which will be requested as part of the [Camera Controls](./CameraControls.md).   \n\nThe rectangle is given as tuple (x_offset, y_offset, width, height).\n\nWhen drawing the zoom window (see [below](#graphically-setting-the-zoom-window)), the rectangle parameters will be updated.\n\nAfter Submitting, the entry should be identical to the *Current ScalerCrop (Live View)*.\n\n## Current ScalerCrop (Live View)\n\nThis shows the scaler crop rectangle which is currently active for the live view.\n\nThe *Current ScalerCrop (Live View)* is the base for determining offset and size of the *Autofocus Windows* when drawing rectangles on the canvas (see [Focus](./FocusHandling.md)).\n\nThe *Current ScalerCrop (Live View)* is also the base for [Regions of Interest or Regions of No Interest](./TriggerMotion.md#regions-of-interest-and-regions-of-no-interest), which can be defined for [Motion Capturing](./TriggerMotion.md).\n\n## Zoom\n\nThe following buttons allow zooming:\n\n- Zoom in   \nzooms into the image (reduces the viewport), keeping the center.\n- Zoom out   \nzooms out (enlarges the viewport), keeping the center.    \nIf the image had been panned before, the center will be shifted, if necessary, to keep the ScalerCrop rectangle within the [ScalerCrop Maximum](./ScalerCrop.md#cropping) area.\n- Full    \nZooms to 100% keeping the center, unless a shift is required to keep the ScalerCrop rectangle within the [ScalerCrop Maximum](./ScalerCrop.md#cropping) area.\n\n## Pan\n\nPanning can be done with the following buttons:\n\n- Pan up / Pan down    \nMove the ScalerCrop rectangle up/down until the upper/lower border of the [ScalerCrop Maximum](./ScalerCrop.md#cropping) rectangle is reached.\n- Pan left / Pan right    \nMove the ScalerCrop rectangle left/right until the left/right border of the [ScalerCrop Maximum](./ScalerCrop.md#cropping) rectangle is reached.\n- Center   \nMove the ScalerCrop rectangle to the center of the [ScalerCrop Maximum](./ScalerCrop.md#cropping) rectangle, keeping the zoom factor.\n- Default   \nSet the ScalerCrop rectangle to the [ScalerCrop Default](./ScalerCrop.md#cropping) rectangle.\n\n\n## Graphically setting the Zoom Window\n\nPushing the **Draw** button will switch into graphical mode where the zoom window can be drawn on a canvas over the Live Stream area.   \nAll other buttons, except **Full** will be disabled in this mode.\n\n![ZoomGraphically](img/Zoom_Graph.jpg)\n\n**Attention:** With Safari (e.g. on an iPad), due to the issue with onload events, the canvas will not be directly visible. It needs to trigger window resize by shortly 'pulling' down the window.\n\nWhile drawing a rectangle for the intended image section, the original aspect ratio will be preserved.   \nAfter drawing is finished, the *Current ScalerCrop (Zoom)* will be updated with offset and dimensions of the zoom window.\n\nAfter pressing **Draw**, the button has changed to **Submit** which must be pressed in order to apply the ScalerCrop setting to the preview and store it for later photo or video taking.\n\nPressing **Submit** terminates the graphic mode.\n\nWhen the **Full** button is pressed in the graphic mode, the dialog returns to normal mode without applying a previously drawn zoom window.\n"
  },
  {
    "path": "docs/api/postman/raspiCamSrv.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"5f679e3f-97eb-491c-8265-7ebfe598603c\",\n\t\t\"name\": \"raspiCamSrv\",\n\t\t\"description\": \"API for Raspberry Pi Camera Server (raspiCamSrv) [https://github.com/signag/raspi-cam-srv](https://github.com/signag/raspi-cam-srv)\\n\\nSecurity: JSON Web Tokens (JWT)\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\",\n\t\t\"_exporter_id\": \"36794475\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"api login\",\n\t\t\t\"event\": [\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"let response = pm.response.json();\\r\",\n\t\t\t\t\t\t\t\"pm.collectionVariables.set(\\\"access_token\\\", response.access_token);\\r\",\n\t\t\t\t\t\t\t\"pm.collectionVariables.set(\\\"refresh_token\\\", response.refresh_token);\"\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"packages\": {}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disabledSystemHeaders\": {}\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n    \\\"username\\\": \\\"{{user}}\\\",\\r\\n    \\\"password\\\": \\\"{{pwd}}\\\"\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/login\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"login\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Client login.\\n\\nReturns: Access Token and Refresh Token\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api refresh\",\n\t\t\t\"event\": [\n\t\t\t\t{\n\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\"let response = pm.response.json();\\r\",\n\t\t\t\t\t\t\t\"pm.collectionVariables.set(\\\"access_token\\\", response.access_token);\\r\",\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\"packages\": {}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disabledSystemHeaders\": {}\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{refresh_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/refresh\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"refresh\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Refresh of Access Token\\n\\nAuthentication: Refresh Token\\n\\nResponse: Access Token\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api protected\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/protected\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"protected\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Dummy API for testing purposes\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_photo\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_photo\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_photo\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take photo with active camera\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_photo 2\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_photo2\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_photo2\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take photo with second (non-active) camera\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_photo both\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_photo_both\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_photo_both\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take photos simultaneously with both cameras.\\n\\nThe photos will have the same file name, but are stored in camera-specific subfolders.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_raw_photo\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_raw_photo\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_raw_photo\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take raw photo with active camera.\\n\\nIn addition to the raw photo, also a normal photo is stored.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_raw_photo2\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_raw_photo2\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_raw_photo2\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take raw photo with second (non-active) camera.\\n\\nIn addition to the raw photo, also a normal photo is stored.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api take_raw_photo both\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/take_raw_photo_both\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"take_raw_photo_both\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Take raw photos simultaneously with both cameras.\\n\\nThe photos will have the same file name, but are stored in camera-specific subfolders.\\n\\nIn addition to the raw photo, also a normal photo is stored.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n    \\\"duration\\\": {{video_duration}}\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Record video with fixed duration using the active camera.\\n\\nData: video duration.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video until stop\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Start recording a video with active camera.\\n\\nRecording must be stopped with **api stop video.**\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api stop video\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/stop_video\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"stop_video\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Stop video recording with the active camera.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video 2\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n    \\\"duration\\\": {{video_duration}}\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video2\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video2\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Record video with fixed duration using the second (non-active) camera.\\n\\nData: video duration.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video 2 until stop\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video2\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video2\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Start recording a video with the second (non-active) camera.\\n\\nRecording must be stopped with **api stop video2.**\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api stop video 2\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/stop_video2\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"stop_video2\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Stop recording a video with the second camera.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video both\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n    \\\"duration\\\": {{video_duration}}\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video_both\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video_both\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Simultaneously record fixed-length videos with both cameras.\\n\\nThe videos will get the same name but will be stored in camera-specific subdirectories.\\n\\nData: video duration.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api record video both until stop\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/record_video_both\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"record_video_both\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Start simultaneously recording videos with both cameras.\\n\\nThe videos will get the same name but will be stored in camera-specific subdirectories.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api stop video both\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODk1Nzc1MSwianRpIjoiNDNiMDM0MWEtNjk3Zi00MjFlLTkwNmUtZjgxMTJlY2VlNzI0IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNuIiwibmJmIjoxNzM4OTU3NzUxLCJjc3JmIjoiZTlmZDZmY2UtYjA2NS00NzUwLTk4NTQtNDBiM2ZhNjZkNGUzIiwiZXhwIjoxNzM4OTYxMzUxfQ.P3bXCCEoZGIwR9l-00l4xwuQYR05Q9ELqC66DhqCX7M\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/stop_video_both\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"stop_video_both\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Stop recording videos with both cameras\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api switch cameras\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/switch_cameras\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"switch_cameras\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Switch cameras for systems with 2 cameras.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api start motion detection\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/start_triggered_capture\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"start_triggered_capture\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Start motion detection\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api stop motion detection\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/stop_triggered_capture\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"stop_triggered_capture\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Stop motion detection\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api info\",\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/info\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"info\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Get status information from server:\\n\\n```\\n{\\n    \\\"message\\\": {\\n        \\\"active_camera\\\": \\\"Camera 0 (imx708)\\\",\\n        \\\"cameras\\\": [\\n            {\\n                \\\"active\\\": true,\\n                \\\"is_usb\\\": false,\\n                \\\"model\\\": \\\"imx708\\\",\\n                \\\"num\\\": 0,\\n                \\\"status\\\": \\\"open - started - current Sensor Mode: 2\\\"\\n            },\\n            {\\n                \\\"active\\\": false,\\n                \\\"is_usb\\\": false,\\n                \\\"model\\\": \\\"imx477\\\",\\n                \\\"num\\\": 1,\\n                \\\"status\\\": \\\"closed\\\"\\n            }\\n        ],\\n        \\\"operation_status\\\": {\\n            \\\"audio_recording\\\": false,\\n            \\\"event_handling\\\": false,\\n            \\\"livestream2_active\\\": false,\\n            \\\"livestream_active\\\": true,\\n            \\\"motion_capturing\\\": false,\\n            \\\"photoseries_recording\\\": false,\\n            \\\"video_recording\\\": false,\\n            \\\"video_recording2\\\": false\\n        },\\n        \\\"server\\\": \\\"raspi06:5000\\\",\\n        \\\"version\\\": \\\"raspiCamSrv V3.6.0\\\"\\n    }\\n}\\n\\n ```\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t},\n\t\t{\n\t\t\t\"name\": \"api probe\",\n\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\"disableBodyPruning\": true\n\t\t\t},\n\t\t\t\"request\": {\n\t\t\t\t\"auth\": {\n\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\"value\": \"{{access_token}}\",\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\"header\": [],\n\t\t\t\t\"body\": {\n\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\"raw\": \"{\\r\\n    \\\"properties\\\": [\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().event\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().event2\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().last_access\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().last_access2\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().threadLock.locked()\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"Camera().thread2Lock.locked()\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"CameraCfg().serverConfig.error\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"CameraCfg().serverConfig.error2\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"CameraCfg().serverConfig.errorc2\\\"\\r\\n        },\\r\\n        {\\r\\n            \\\"property\\\": \\\"CameraCfg().serverConfig.errorc22\\\"\\r\\n        }\\r\\n    ]\\r\\n}\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"url\": {\n\t\t\t\t\t\"raw\": \"{{base_url}}/api/probe\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"{{base_url}}\"\n\t\t\t\t\t],\n\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\"probe\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"description\": \"Probe a set of object properties.\\n\\nYou need to specify an object through one of the singleton base classes (Camera(), CameraCfg(), MotionDetector(), PhotoSeriesCfg() or TriggerHandler()) and then specify valid properties with dot-notation.\\n\\nNote: Not all properties might be JSON-serializable.\"\n\t\t\t},\n\t\t\t\"response\": []\n\t\t}\n\t],\n\t\"event\": [\n\t\t{\n\t\t\t\"listen\": \"prerequest\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"packages\": {},\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"listen\": \"test\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"packages\": {},\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t],\n\t\"variable\": [\n\t\t{\n\t\t\t\"key\": \"base_url\",\n\t\t\t\"value\": \"<base_url>\",\n\t\t\t\"type\": \"default\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"user\",\n\t\t\t\"value\": \"<user>\",\n\t\t\t\"type\": \"default\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"pwd\",\n\t\t\t\"value\": \"<password>\",\n\t\t\t\"type\": \"default\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"access_token\",\n\t\t\t\"value\": \"<access_token>\",\n\t\t\t\"type\": \"default\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"refresh_token\",\n\t\t\t\"value\": \"<refresh_token>\",\n\t\t\t\"type\": \"default\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"video_duration\",\n\t\t\t\"value\": \"30\",\n\t\t\t\"type\": \"default\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "docs/bp_Hotspot_Bookworm.md",
    "content": "# Hotspot Configuration for 'Bookworm' OS\n\n[![Up](img/goup.gif)](./bp_PiZero_Standalone.md)\n\nThis section describes how to configure a Raspberry Pi as hotspot if the OS is *Debian Bookworm*.\n\nIn the following description, you will need to replace\n\n- ```<SSID>``` with the intended hotspot SSID, e.g. \"RaspiCamSrv01\"\n- ```<passphrase>``` with the passphrase to protect hotspot access\n\nThe connection ID, used in [NetworkManager](https://networkmanager.dev/docs/api/latest/nmcli.html) commands is chosen as \"RaspiCamSrv\"\n\n## 1. Install required packages\n\n```\nsudo apt install dnsmasq iptables\n```\n\n## 2. Configure Hotspot\n\n```\nsudo nmcli con add type wifi ifname wlan0 con-name RaspiCamSrv autoconnection yes ssid <SSID>\n\nsudo nmcli con modify RaspiCamSrv 802-11-wireless.mode ap 802-11-wireless.band bg\n\nsudo nmcli con modify RaspiCamSrv wifi-sec.key-mgmt wpa-psk\n\nsudo nmcli con modify RaspiCamSrv wifi-sec.psk \"<passphrase>\"\n\n```\n\n## 3. Assign a Fixed IP Address\n\n```\nsudo nmcli con modify RaspiCamSrv ipv4.method manual ipv4.addresses 192.168.1.1/24\n\nsudo nmcli con modify RaspiCamSrv ipv4.gateway 192.168.1.1\n\nsudo nmcli con modify RaspiCamSrv ipv4.dns 192.168.1.1\n\n```\n\n## 4. Activate Hotspot\n\n```\nsudo nmcli con up RaspiCamSrv\n```\n\nIf your SSH session uses the Wi-Fi Adapter, connection will now be lost.\n\nIf you reconnect, the ethernet adapter will be used.\n\nAt this time, a TCP/IP connection through the hotspot is not yet possible. \n\n## 5. Configure DHCP for hotspot\n\n```\nsudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.orig\n\nsudo nano /etc/dnsmasq.conf\n```\n\nEnter the following code:\n\n```\ninterface=wlan0 \nno-dhcp-interface=eth0\ndhcp-range=192.168.1.100,192.168.1.200,255.255.255.0,24h\ndhcp-option=option:router,192.168.1.1 \ndhcp-option=option:dns-server,192.168.1.1\n```\n\n## 6. Check and start DHCP Server and DNS-Cache\n\n```\ndnsmasq --test -C /etc/dnsmasq.conf\n```\n\n## 7. Enable dnsmasq for automatic start\n\n```\nsudo systemctl restart dnsmasq\n\nsudo systemctl status dnsmasq\n\nsudo systemctl enable dnsmasq\n```\n\n## 8. Enable IP Forwarding\n\n```\nsudo nano /etc/sysctl.conf\n```\n\nFind and uncomment the following line:\n```\nnet.ipv4.ip_forward=1\n```\n\n## 9. Set up NAT\n\n```\nsudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n\nsudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT\n\nsudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT\n\n```\n\n## 10. Save Firewall Rules\n\n```\nsudo sh -c \"iptables-save > /etc/iptables.rules\"\n```\n\n## 11. Load firewall rules on boot\n\n```\nsudo nano /etc/network/interfaces\n```\n\nAdd the following line:\n\n```\npost-up iptables-restore < /etc/iptables.rules\n```\n\n## 12. Extend hosts\n\n```\nsudo nano /etc/hosts\n```\n\nAdd the following line:\n\n```\n192.168.1.1 <hostname>\n```\n\nwhere ```<hostname>``` must be replaced by the host name specified during OS setup.\n\n\n## 13. Reboot the system\n\n```\nsudo reboot\n```\n\n## 14. Check processes\n\nAfter reconnecting with SSH\n\n```\nnmcli con show --active\n\nip addr show wlan0\n```\n\n## 19. Test Hotspot access\n\n- Unplug the network cable\n- Switch Off/On the power supply\n- From a mobile device, wait for the hotspot and try to connect."
  },
  {
    "path": "docs/bp_Hotspot_Bullseye.md",
    "content": "# Hotspot Configuration for 'Bullseye' OS\n\n[![Up](img/goup.gif)](./bp_PiZero_Standalone.md)\n\nThis section describes how to configure a Raspberry Pi as hotspot if the OS is *Debian Bullseye*.\n\n## 1. Install required packages\n\n```\nsudo apt install dnsmasq hostapd iptables\n```\n\n## 2. Configure WLAN\n\n```\nsudo nano /etc/dhcpcd.conf\n```\n\nCopy/Paste the following code:\n\n```\ninterface wlan0\nstatic ip_address=192.168.1.1/24\nnohook wpa_supplicant\n```\n\nThis will configure the Wi-Fi adapter with a static IP address 192.168.1.1\n\n## 3. Restart DHCP\n\n```\nsudo systemctl restart dhcpcd\n```\n\nIf your client is connected through Wi-Fi, it will now lose connection\nand you need to reconnect, which will now use the ethernet connection.\n\n## 4. Check interfaces\n\n```\nip l\n```\n\nCheck that both, the ethernet interface (eth0) and Wi-Fi adapter (wlan0) are available.\n\n## 5. Setup DHCP server and DNS-Cache\n\n```\nsudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf_orig\n\nsudo nano /etc/dnsmasq.conf\n```\n\nEnter the following code:\n\n```\ninterface=wlan0\nno-dhcp-interface=eth0\ndhcp-range=192.168.1.100,192.168.1.200,255.255.255.0,24h\ndhcp-option=option:dns-server,192.168.1.1\n```\n\n## 6. Test & start DHCP server and DNS Cache\n\n```\ndnsmasq --test -C /etc/dnsmasq.conf\n```\n\nThe response should be\n```\ndnsmasq: syntax check OK.\n```\n\n## 7. Restart DNSMASQ and enable it for automatic start\n\n```\nsudo systemctl restart dnsmasq\n\nsudo systemctl status dnsmasq\n\nsudo systemctl enable dnsmasq\n```\n\n## 8. Setup WLAN-AP-Host (hostapd)\n\n```\nsudo nano /etc/hostapd/hostapd.conf\n```\n\nReplace the content with the following code\n\n```\ninterface=wlan0\nssid=<SSID>\nchannel=1\nhw_mode=g\nieee80211n=1\nieee80211d=1\ncountry_code=<Country Code>\nwmm_enabled=1\nauth_algs=1\nwpa=2\nwpa_key_mgmt=WPA-PSK\nrsn_pairwise=CCMP\nwpa_passphrase=<wpa_passphrase>\n```\n\nwhere you need to replace\n\n- <SSID> with the intended SSID for the hotspot (e.g.: RaspiCamSrv)\n- <Country Code> with your [A-2 ISO 3166-1 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)\n- <wpa_passphrase> with the passphrase to secure hotspot access\n\n## 9. Test & start WLAN-AP-Host\n\n```\nsudo hostapd -dd /etc/hostapd/hostapd.conf\n```\n\nAt the end of the output you should find:\n\n```\n...\nwlan0: interface state COUNTRY_UPDATE->ENABLED\n...\nwlan0: AP-ENABLED\n...\n\n```\n\n## 10. Test Hotspot Access\n\nWith a mobile device try to access the hotspot.\n\nThis will generate log output in the SSH session.\n\n## 11. Enable hostapd process for automatic start\n\nIn the SSH session stop the active process<br>\n<Ctrl+C>\n\n```\nsudo systemctl unmask hostapd\nsudo systemctl start hostapd\nsudo systemctl enable hostapd\n```\n\n## 12. Check that hostapd is active\n\n```\nsudo systemctl status hostapd\n```\n\n## 13. Activate routing\n\n```\nsudo nano /etc/sysctl.conf\n```\n\nFind and uncomment the following line:\n\n```\nnet.ipv4.ip_forward=1\n```\n\n## 14. Activate NAT\n\n```\nsudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n\nsudo sh -c \"iptables-save > /etc/iptables.ipv4.nat\"\n```\n\n## 15. Assure NAT activation at system start\n\n```\nsudo nano /etc/rc.local\n```\n\nBefore the last line with ```exit 0``` enter\n\n```\niptables-restore < /etc/iptables.ipv4.nat\n```\n\n## 16. Reboot the system\n\n```\nsudo reboot\n```\n\n## 18. Check processes\n\nAfter reconnecting with SSH\n\n```\nsudo systemctl status hostapd\n\nps ax | grep hostapd\n\nsudo systemctl status dnsmasq\n\nps ax | grep dnsmasq\n```\n\n## 19. Test Hotspot access\n\n- Unplug the network cable\n- Switch Off/On the power supply\n- From a mobile device, wait for the hotspot and try to connect."
  },
  {
    "path": "docs/bp_Hotspot_Trixie.md",
    "content": "# Hotspot Configuration for *Trixie* OS\n\n[![Up](img/goup.gif)](./bp_PiZero_Standalone.md)\n\nThis section describes how to configure a Raspberry Pi as a **standalone Wi-Fi hotspot**\nusing **NetworkManager**, as provided by default in *Debian Trixie / Raspberry Pi OS (Trixie)*.\n\n**Note**: This configuration is **not compatible with older Bookworm-style setups** that relied on\n`dnsmasq`, `iptables`, or `/etc/network/interfaces`.\n\n---\n\n## Overview\n\nNetworkManager provides built-in support for:\n\n- Access Point (AP) mode\n- DHCP server for hotspot clients\n- NAT (masquerading)\n- IP forwarding\n\nNo additional network services or firewall rules are required for a basic hotspot.\n\n## Naming Conventions\n\nIn the following description, replace:\n\n- `<SSID>` with the intended hotspot SSID  \n  e.g. `RaspiCamSrv01`\n- `<passphrase>` with the WPA2 passphrase\n\nThe NetworkManager **connection ID** used throughout this document is:   \nRaspiCamSrv\n\n## 1. Create the hotspot connection\n\n```\nsudo nmcli con add type wifi ifname wlan0 con-name RaspiCamSrv ssid <SSID> \\\n  connection.autoconnect yes\n```\n\n## 2. Configure hotspot parameters\n\nEnable Access Point mode and WPA2 security:\n\n```\nsudo nmcli con modify RaspiCamSrv \\\n  802-11-wireless.mode ap \\\n  802-11-wireless.band bg \\\n  wifi-sec.key-mgmt wpa-psk \\\n  wifi-sec.psk \"<passphrase>\"\n\n```\n\n(Optional) Lock the Wi-Fi channel for improved stability:\n\n```\nsudo nmcli con modify RaspiCamSrv 802-11-wireless.channel 6\n```\n\n## 3. Enable shared IPv4 networking (recommended)\n\n```\nsudo nmcli con modify RaspiCamSrv ipv4.method shared\n```\n\nThis automatically enables:\n\n- DHCP for hotspot clients\n- NAT (masquerading)\n- IPv4 forwarding\n\n## 4. Activate the hotspot\n\n```bash\nsudo nmcli con up RaspiCamSrv\n```\n\n**NOTE**: If your SSH session uses Wi-Fi, the connection will now be lost.    \nReconnection will use Ethernet.\n\n## 5. Verify hotspot status\n\nCheck active connections:\n\n```bash\nnmcli con show --active\n```\n\nCheck IP address assigned to the hotspot interface:\n```bash\nip addr show wlan0\n```\nYou should see an address in the range assigned by NetworkManager   \n(e.g. 10.42.0.1/24 or similar)\n\n## 6. Setup mDNS\n\nTo provide host name resolution to clients, [mDNS (Multicast DNS)](https://en.wikipedia.org/wiki/Multicast_DNS) can be used instead of a full-fledged DNS server.\n\nInstall and enable [Avahi](https://en.wikipedia.org/wiki/Avahi_(software)):\n```\nsudo apt install avahi-daemon\nsudo systemctl enable avahi-daemon\nsudo systemctl start avahi-daemon\n```\n\nWith Avahi, you will need to connect to the Raspberry Pi with    \n```<hostname>.local``` instead of ```<hostname>``` or IP address,    \nwhere ```<hostname>``` is the name given during [OS Installation](./bp_PiZero_Standalone.md#1-install-os-on-microsd-card)\n\n## 7. Test hotspot access\n\n1. Disconnect the Ethernet cable\n2. Power-cycle the Raspberry Pi\n3. From a mobile device:   \n-- Search for the Wi-Fi network ```<SSID>```    \n-- Connect using the configured passphrase    \n-- Verify internet or local access   \n-- With a Ping tool, try to ping ```<hostname>.local```"
  },
  {
    "path": "docs/bp_PiZero_Standalone.md",
    "content": "# Setup of Raspberry Pi Zero as Standalone System\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\nThis section describes how to set up a Raspberry Pi **Zero W** or **Zero 2 W** as standalone system.\n\nThis will allow you placing the Raspi camera anywhere, independently from an accessible Wi-Fi.\n\nIt will act as hotspot to which you can connect from a mobile client to gain access to **raspiCamSrv**.\n\nThe subsequent descriptions can, in principle, also be applied for other Raspberry Pi models.\n\n## Headless Setup\n\nDuring the setup of such a system, the Raspberry Pi requires a cabled network connection because the Wi-Fi adapter will be configured as hotspot and is, therefore, not available for access by the configuration client.\n\nConnections to a display, keyboard and mouse are not required.\n\n![Standalone Setup](./img/bp_PiZero_Connect.jpg)\n\nRequired [cables and adapters](https://www.raspberrypi.com/products/#power-supplies-and-cables):\n\n- USB A to Ethernet adapter\n- Micro USB/Male to USB A/Female cable\n- Power supply with Micro USB plug\n\n## 1. Install OS on microSD Card\n\nFollow the instructions for [Install using Imager](https://www.raspberrypi.com/documentation/computers/getting-started.html#raspberry-pi-imager).\n\n- **Model**<br>\nMake sure to select the correct model\n- **Bullseye, Bookworm or Trixie?**<br>\nraspiCamSrv can be used with all of these systems.<br>\nUnless other reasons force taking one of the older OS, it is recommended to use the officially recommended, which is currently Trixie.\n- **Full or lite system?**<br>\nIt is recommended to install the full system although the desktop environment will not be required.    \nHowever, raspiCamSrv can also be installed on Lite variants.\n- **OS Customisation**<br>\nMake sure to *Configure wireless LAN*, although the Wi-Fi Adapter will later not be run in client mode,<br>\nhowever this will assure that the *Wireless LAN Country* will be set.\n\n## 2. Power Up\n\n1. If camera applications are intended, connect the camera (the small CSI-2 port of Pi Zero and Pi 5 require a special cable)\n2. Insert the microSD card into the card slot\n3. If available, encapsulate the Pi into a case\n4. Connect the network USB cable through an ethernet adapter to a switch of your local network\n5. Connect the power supply\n\n## 3. Connect and upgrade\n\nThe system may take some time until it is visible within the network.\n\nFrom a client device connect via SSH<br>\n\n```\nssh <user>@<hostname>\n\n...\n\nsudo apt update\nsudo apt full-upgrade\n```\n\n## 4. Configure Hotspot\n\nThe process of configuration is slightly different, depending on the cosen OS:\n\n- [Hotspot Configuration for 'Trixie' OS](./bp_Hotspot_Trixie.md)\n- [Hotspot Configuration for 'Bookworm' OS](./bp_Hotspot_Bookworm.md)\n- [Hotspot Configuration for 'Bullseye' OS](./bp_Hotspot_Bullseye.md)\n\n## 5. Install raspiCamSrv\n\nFor installation, you will need to connect through ethernet.\n\nThen, follow the [raspiCamSrv Installation Procedure](./installation.md), which will also do the Service configuration.\n\n## 6. Test\n\nAfter rebooting, first test using the client connected with ethernet cable.\n\nIf this is successfull, you can shutdown and unplug the ethernet cable.\n\nAfter restart, connect from a mobile client to the hotspot and connect to raspiCamSrv from a browser window.    \n**NOTE**, that for the [Trixie setup](./bp_Hotspot_Trixie.md), you need to use ```<hostname>.local``` instead of ```hostname```.\n\n\n## Updating a Stanalone RaspiCamSrv System\n\nSince the standalone system has no internet connection, you will not be notified on new **raspiCamSrv** versions (see [Update notification through coloured version number](./UserGuide.md#title-bar)).\n\nWhen you are aware of an update, you need to connect the standalone Raspberry Pi to a network with internet access using an ethernet cable.\n\nThen, you can use the [Update function](./SettingsUpdate.md) to check for updates and for updating **raspiCamSrv**."
  },
  {
    "path": "docs/features.md",
    "content": "\n# Features V4.10.x\n\n[![Up](img/goup.gif)](./index.md)\n\nFor more details, see the [User Guide](./UserGuide.md).    \n\n![Live Overview](./img/Live.jpg)\n\n## Feature Overview\n\n### Platform Support\n\n- raspiCamSrv can be run on all currenly known Raspberry Pi **hardware platforms** (except microcontroller boards) from Raspberry [Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) to [Raspberry Pi 5](https://www.raspberrypi.com/products/raspberry-pi-5/)\n- Supported **Operating Systems** are the **Raspberry Pi** OS versions Bullseye, Bookworm and Trixie.\n- The recommended variant for all of these is the full 64-Bit variant recommended by [**Raspberry Pi** Imager](https://www.raspberrypi.com/software/)\n- Setup can be done alternatively using an [automatic installer](./installation.md) or by [deployment as Docker Container](./SetupDocker.md)\n\n\n### WSGI Server Support\n\n- raspiCamSrv is based on [Flask](https://flask.palletsprojects.com/) and, as such, a [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) (Web Server Gateway Interface) application.   \nIt requires a [WSGI Server](https://www.fullstackpython.com/wsgi-servers.html) as middleware between the WSGI application and a Web Server.\n- raspiCamSrv can run with the [Werkzeug](https://werkzeug.palletsprojects.com/) WSGI server built-in in Flask. This is usually used for development and testing purposes, but it is also fine for private environments.\n- For publicly accessible systems, raspiamSrv can be run with [Gunicorn](https://gunicorn.org/) ('Green Unicorn') with [specific settings](./installation_man.md#gunicorn-settings).    \nThe [automatic installer](./installation.md) as well as the [Docker Image](https://hub.docker.com/repository/docker/signag/raspi-cam-srv/general) use this as default.\n\n### Camera Support\n\n- raspiCamSrv supports the currently available [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/accessories/camera.html).\n- With the [Raspberry Pi AI Camera](https://www.raspberrypi.com/documentation/accessories/ai-camera.html) (imx500), you can study inference of neural network models for specific tasks (Classification, Object Detection, Pose Estimation, Segmentation) within the Web UI of **raspiCamSrv** (see [AI Camera Support](./AiCameraSupport.md)).\n- CSI Cameras from other providers can be used as long as they are supported by Picamera2.\n- USB cameras connected through the Pi's USB ports are seamlessly integrated, however control options are limited, depending on their capabilities.\n\n### Camera Management\n\n- raspiCamSrv can detect and use all **CSI** and **USB** cameras connected to a Raspberry Pi, as long as they are identified by Picamera2, which is usually the case.\n- One of these cameras must be selected as *Active Camera*.    \n[Camera configuration](./Configuration.md) (e.g. stream size colour space or flipping) as well as [controls](./CameraControls.md) (e.g. focus, zoom/pan/tilt, exposure- and image-control) can be actively modified only for the *Active Camera*.\n- Another camera, if available, can be selected as *Second Camera* by [Multi Camera Control](./CamMulticam.md).\n- All settings for the *Active Camera* can be preserved before it is replaced by another camera (e.g. by switching cameras). They will be restored/applied when this camera is set as *Active Camera* or as *Second Camera*.\n- Function [Reload Cameras](./SettingsConfiguration.md) allows hot plug-in/-out of USB cameras without server restart\n\n### Camera Configuration\n\n- raspiCamSrv supports all [camera configuration options](./Configuration.md#configuration-tab) which are foreseen by Picamera2.\n- Individual configuration sets can be specified for 4 different use-cases: Live View, Photo, Raw Photo and Video.\n- Before the camera is started, raspiCamSrv configures all three camera streams (lowres, main, raw) for the most likely use-cases. This allows to keep the live stream (lowres) active while the camera is being used for phototaking (main), raw photo taking (raw) or video recording (main)\n- If necessary, specific applications can request the camera for exclusive use.\n- Support of Camera [Tuning](./Tuning.md) by selection and management of tuning files.\n\n### Camera Control\n\n- raspiCamSrv supports all [Camera Control options](./CameraControls.md) foreseen by Picamera2.\n- [Focus control](./FocusHandling.md) if supported by the camera (e.g. camera module 3 or specific USB cameras).\n- Graphically drawing *Autofocus Windows* for CSI cameras.\n- [Pan / Tilt / Zoom](./ZoomPan.md) for CSI as well as for USB cameras.\n- [Auto Exposure Control](./CameraControls_AutoExposure.md) for CSI cameras.\n- [Exposure Control](./CameraControls_Exposure.md) for CSI cameras.\n- [Image Control](./CameraControls_Image.md) for CSI cameras as well as for USB cameras (if supported by the camera).\n- **NEW**: Automatic White Balance using Nueral Networks can be activated through [Modification of Tuning Files](./tutorials/AWB_with_neural_networks.md)\n- Panel for [Direct Control](./LiveDirectControl.md) of numeric control parameters.\n- Configurable [Live View buttons](./CameraControls_Ctrl.md) to be used for physical pan/tilt, light or control of other devices\n\n### Photo Taking / Video Recording\n\n- Taking [Photos / Raw Photos](./Phototaking.md).\n- Recording [Video](./Phototaking.md#video).\n- Recording [Audio along with the Video](./Settings.md#recording-audio-along-with-video).\n- Photo/Video [metadata](./Phototaking.md#metadata) display.\n- Photo [histogram](./Phototaking.md) generation and display.\n- [Display buffer](./Phototaking.md#photo-display) for comparison of photos and metadata/histogram.\n- [Photo Viewer](./PhotoViewer.md)\n- [Photo Download](./PhotoViewer.md)\n- Photos/videos are enabled for being inspected in a separate [Media Viewer](./UserGuide.md#media-viewer) window.\n\n### Streaming\n\n- Endpoint for [streaming](./CamWebcam.md) (MJPEG) the active camera.\n- Endpoint for [streaming](./CamWebcam.md) the second camera.\n- Endpoints for photo snapshots of active and second camera with low resolution.\n- Endpoints for photo snapshots of active and second camera with high resolution.\n- Option for activating / deactivating authentication for streaming and snapshots.\n\n### Multi-Camera Features\n\n- [Selection](./CamMulticam.md) of *Active Camera* and *Second Camera* out of connected CSI and USB cameras.\n- [Simultaneous streaming](./CamWebcam.md) of both cameras.\n- [Simultaneous photo taking or video recording](./CamMulticam.md#buttons) for both cameras.\n- [Camera switch](./CamMulticam.md#switch-cameras).\n- [Preserving active camera configuration and controls](./CamMulticam.md#configuring-mjpeg-stream-and-jpeg-photo) for later reuse.\n- [Stereo vision support](./Settings.md#activating-and-deactivating-stereo-vision) for two cameras of same model.\n- [Synchronization of settings](./CamMulticam.md#synchronize-configurations) for stereo cameras\n- [Camera calibration](./CamCalibration.md) for stereo cameras.\n- [Depth Maps](./CamStereo.md#depth-maps)\n- [3D Video](./CamStereo.md#3d-video)\n\n### Photo Series\n\n- [Definition of Photo Series](./PhotoSeries.md) (# shots, interval etc.).\n- [Control of Photo Series](./PhotoSeries.md) (start, stop, pause, resume).\n- [Download of Photo Series](./PhotoSeries.md).\n- [Timelapse Series](./PhotoSeriesTimelapse.md) with optional sunrise/sunset restrictions.\n- **NEW**: [Sun-Controlled Timelapse Series](./PhotoSeriesTimelapse.md#azimuth-mode) with well-defined sun azimuth.\n- [Exposure Series](./PhotoSeriesExp.md) with varying exposure time or gain (ISO).\n- [Exposure Series Result](./PhotoSeriesExp.md#result) showing histograms.\n- [Focus Stack Series](./PhotoSeriesFocus.md) iterating through a range of focus settings.\n- Capability for [auto restart](./PhotoSeries.md#series-configuration) of series when Server or Raspi is restarted.\n\n### Motion Detection\n\n- [Scheduled Detection of Motion](./TriggerActive.md).\n- Support for [different algorithms](./TriggerMotion.md) for motion detection.\n- [Adjustable Sensitivity](./TriggerMotion.md) for motion detection.\n- Support for [Regions of Interest](./TriggerMotion.md#regions-of-interest-and-regions-of-no-interest).\n- Support for [Regions of NO Interest](./TriggerMotion.md#regions-of-interest-and-regions-of-no-interest)\n- [Test Mode for Motion Detection](./TriggerMotion.md#testing-motion-capturing).\n\n### GPIO Device Management\n\n- Configuration of [GPIO Devices](./SettingsDevices.md).\n- [Testing](./SettingsDevices.md#testing-a-device) of GPIO Devices\n- [Device Calibration](./SettingsDevices.md#calibrating-a-device) for devices which rquire state tracking (e.g. stepper motor)\n- Device control through [gpiozero](https://gpiozero.readthedocs.io/en/stable/index.html)\n- All gpiozero device types are supported in raspiCamSrv\n- Own device types can be added by [configuration](./SettingsDevices.md#device-type-configuration)\n\n#### Additional Device Types\n - [Stepper Motor](./gpioDevices/StepperMotor.md)\n - [ServoPWM](./gpioDevices/ServoPWM.md) for jitter-free servo control with hardware PWM\n\n### Event Handling - Triggers and Actions\n\n- [Configuration of Triggers](./TriggerTriggers.md)\n- Triggering by [GPIO Input Devices](./SettingsDevices.md) (button, sensors)\n- Triggering by [Motion Detection](./TriggerMotion.md)\n- Triggering by Camera events (photo taken, video start, video stop)\n- [Configuration of Actions](./TriggerActions.md)\n- [Testing of Actions](./TriggerActions.md#testing-an-action)\n- Actions by [GPIO Output Devices](./SettingsDevices.md) (LED, buzzer, servo, motor)\n- All action methods of all [gpiozero Output Devices](https://gpiozero.readthedocs.io/en/stable/api_output.html#regular-classes) are supported\n- Actions by Camera (take photo, start/stop video)\n- [Camera Actions](./TriggerCameraActions.md) in case of motion detection (video duration, photo burst)\n- [Notification](./TriggerNotification.md) actions (mail, mail attachments)\n- [Action-to-Trigger Association](./TriggerTriggerActions.md)\n- [Event Viewer](./TriggerEventViewer.md)\n- [Event Calendar](./TriggerEventViewer.md)\n- [Detailed Event Information](./TriggerEventViewer.md)\n- Event Photos / Videos with motion detection frame\n- Event Photos / Videos with RoI RoNI\n\n### Console Functions\n\n- Freely configurable [Array of Versatile Buttons](./ConsoleVButtons.md)\n- Freely configurable [Array of Action Buttons](./ConsoleActionButtons.md) for execution of configured [Actions](./TriggerActions.md).\n\n### API\n\n- Selected functions of raspiCamSrv are accessible through specific [Web Service End Points](./API.md)\n- API access is secured through JSON Web Tokens (JWT).\n- A Postman collection is available for testing\n- A specific API (probe) is available for 'probing' attribute values of raspiCamSrv live objects.\n\n### Privacy Protection\n\n- raspiCamSrv access requires registered [users](./Authentication.md)\n- The Superuser can manage other users: create, remove, reset password\n- Login requires a password\n- For streaming, it is possible to disable the necessity of authentication\n- API access is secured through JSON Web Tokens (JWT).\n- Secrets (mail account, JWT secret key) are held in a separate secrets store which is not part of the persisted configuration data.\n\n### Configuration Management\n\n- Configuration Management refers to the way how raspiCamSrv handles its operational data which may be modified during user sessions.\n- [On request](./SettingsConfiguration.md), all data of the raspiCamSrv server can be [persisted as JSON files](./SettingsConfiguration.md#server-configuration-storage)\n- Optionally, the server can start with the stored configuration or with an initialized setup.\n- An [indicator](./UserGuide.md#elements) shows when configuration data have been modified during a session.\n- All modifications, which have not yet been saved, are [listed in a dialog](./SettingsConfiguration.md).\n- You can create backups of entire configuration sets and restore them at another time.\n\n### System Information\n\n- Information on the Raspberry Pi System, the raspiCamSrv software stack as well as the active processes is shown on the [Info / System](./Information_Sys.md) screen.\n- Information on the connected cameras is shown on the [Info / Cameras](./Information_Cam.md) screen.\n- [Properties of the Active Camera](./Information_CamPrp.md) are also shown.\n- In addition, the Info Menu provides also details for the individual [Sensor Modes](./Information_Sensor.md) of the *Active Camera*.\n\n### No Camera\n\n- raspiCamSrv can operate in a special mode when [no camera is connected](./UserGuide_NoCam.md).\n- In this case, all camera-related features are invisible.\n- Functions which do not require a camera, remain available: [GPIO devices](./SettingsDevices.md), [Event Handling](./Trigger.md), [Console](./Console.md).\n\n### Supervision\n\n- For error analysis, [Logging](./Troubelshooting.md#logging) can be activated on module level.\n- In order to inspect the interface of raspiCamSrv with Picamera2, it is possible to activate [Generation of Python Code for the Camera](./Troubelshooting.md#generation-of-python-code-for-camera). This will create an executable Python file including all Picamera2 calls.\n\n\n\n## Known Issues\n\n- In **Safari** (e.g. on an iPad), there is still an issue with the Live Screen:    \n Due to the specific timing of the onload event, [AF Windows](./FocusHandling.md#autofocus-windows) may not be visible immediately after the page has been loaded. If you just 'pull' the entire window down for a short time (don't touch the AF Windows canvas), they will show up.   \n If the Live stream does not show up (e.g. after visiting another screen), take a photo and then push **Hide**/**Show**. This will show the live stream.\n - There may be an issue configuring specific sensor modes or stream sizes for the *Live View* in [Config](./Configuration.md). As a result, the live view will not show up and the server log will show an exception. You may need to reset the server (see [Reset Server](./SettingsConfiguration.md))<br>This is already fixed but may not yet be available in your environment (see [picamera2 Issue #959](https://github.com/raspberrypi/picamera2/issues/959))\n\n## Limitations\nThe software is still being tested and extended.\n\n- Hot plug of CSI cameras is not supported. This will require rebooting the Raspberry Pi (Hot plug of USB cameras is supported).\n- Hot plug of USB cameras is possible but requires to [Reload Cameras](./SettingsConfiguration.md#reloading-cameras).     \nHot plug-out of a USB camera should be avoided when the camera is active. This will produce exceptions.\n- Although the layout is responsive, it may not be \"good-looking\" with all sizes of browser windows\n\n## Credits\n- Most technical information on Picamera2 (<https://github.com/raspberrypi/picamera2>) has been taken from the [Raspberry Pi - The Picamera2 Library](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf) document.\n- The implementation of live streaming with Flask has been inspired by <https://blog.miguelgrinberg.com/post/video-streaming-with-flask>\n- The detailed solution for the mjpeg_server is based on the example [mjpeg_server.py](https://github.com/raspberrypi/picamera2/blob/main/examples/mjpeg_server.py) of the [picamera2 repository](https://github.com/raspberrypi/picamera2)\n- The solution for drawing on the canvas for definition of AF Windows has been inspired by <https://codepen.io/AllenT871/pen/GVyXKp>\n- The [Extended Motion Capturing Algorithms](./TriggerMotion.md) are based on work done by Isaac Berrios, published under [Introduction to Motion Detection: Part 1 - 3](https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2)   \nThe algorithm code has been taken from this source as well as its [GitHub Repository](https://github.com/itberrios/CV_projects/tree/main/motion_detection) and integrated into the **raspiCamSrv** environment.\n- raspiCamSrv uses the [gpiozero](https://gpiozero.readthedocs.io/en/stable/index.html) library for interfacing GPIO-connected devices.\n"
  },
  {
    "path": "docs/getting_started_overview.md",
    "content": "# Getting started with raspiCamSrv\n\n[![Up](img/goup.gif)](./index.md)\n\n1. [Check requirements](requirements.md)\n2. [Setup the system](./system_setup.md) for raspiCamSrv\n3. [Install raspiCamSrv](./installation.md)<br>or [run raspiCamSrv as Docker Container](./SetupDocker.md)\n\n## Manual Step-by-Step Installation\n\nThe following procedures can be applied if an individual installation is intended or if the automatic installation fails:\n\n1. [Manual installation of raspiCamSrv](./installation_man.md)\n2. [Manual Service Configuration](./service_configuration.md)\n\n## Alternatives\n\n- [Running raspiCamSrv as Docker container](./SetupDocker.md)\n- [Setup of Raspberry Pi Zero as Standalone System](./bp_PiZero_Standalone.md)\n\n## Troubleshooting\n\n- [Trouble Shooting Guide](./Troubelshooting.md)"
  },
  {
    "path": "docs/gpioDevices/ServoPWM.md",
    "content": "# ServoPWM\n\n## Overview\n\n```\nclass ServoPWM(*args, **kwargs)\n```\n\nimplements control of servo motors with hardware PWM.\n\nDevelopment of this class was motivated by the fact that software-based PWM,\nas provided by the default RPi.GPIO pin factory in gpiozero, results in significant jitter for servo motors.\nThe alternative pigpio pin factory does support hardware PWM, but it is currently not compatible\nwith the latest Debian release (Trixie) for Raspberry Pi. \n\nIn this class, hardware PWM support makes use of the [rpi-hardware-pwm](https://github.com/Pioreactor/rpi_hardware_pwm) library which needs to be installed with     \n```pip install rpi-hardware-pwm```\n\nOn Raspberry Pi, hardware PWM is only supported for the GPIO pins 12, 13, 18 and 19.\n\nRouting of the PWM signal to one or several of these pins needs to be configured through device tree\noverlays in ```/boot/firmware/config.txt``` (Trixie or Bookworm) or ```/boot/config.txt``` (Bullseye):   \nfor example:    \n```\n[all]\ndtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\n```\n\nOn RPI 4 and earlier only up to 2 pins can be configured for PWM simultaneously.\n\nOn RPI 5, you can configure up to 4 pins, e.g.\n```\n[pi5]\ndtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\ndtoverlay=pwm-2chan,pin=18,func=2,pin2=19,func2=2\n```\n\nYou need to reboot after dtoverlay changes.\n\nWhich pins are enabled for PWM can be checked with    \n```pinctrl get 12,13,18,19```\n\nYou will get something like     \n```\n12: a0    pd | lo // GPIO12 = PWM0_CHAN0\n13: a0    pd | lo // GPIO13 = PWM0_CHAN1\n18: no    pd | -- // GPIO18 = none\n19: no    pd | -- // GPIO19 = none\n```\n\nServoPWM checks PWM enabling for the specified pin.    \nServoPWM will translate the GPIO pin number to the correct PWM channel.\n\nSetup:\n\n1. Connect the signal cable of the servo to the chosen GPIO pin\n2. Connect the power input of the servo to Raspberry Pi 5V and GND pins with the correct polarity.\n\nThe following code will rotate the servo clockwise (from the perspective of the servo) by 15°:\n```\nfrom raspiCamSrv.gpioDevices import ServoPWM\nservo = ServoPWM(12)\nservo.rotate(15)\nservo.stop()\n```\n\nThe class was tested with a KY66 servo.\n\n## Parameters\n\n- **pin** (*int*) - The GPIO pin that the servo signal input is connected to\n- **min_angle** - (*float*) The minimum angle, the servo can drive to. Defaults to -90.0\n- **max_angle** - (*float*) The maximum angle, the servo can drive to. Defaults to  90.0\n- **min_pulse_width_us** - (*int*) The minimum PWM pulse width in microseconds. Defaults to 500\n- **max_pulse_width_us** - (*int*) The maximum PWM pulse width in microseconds. Defaults to 2500\n- **frame_width_us** - (*int*) The frame width of the PWM signal in microseconds. Defaults to 20000\n<br>(frequency = 1000000/frame_width_us)\n- **speed** (*float*) - Speed of the servo: time (in sec) required for a 360° turn. Defaults to 2.8\n<br>This parameter is only used in case of *idle_off*=True to calculate the time for a requested rotation before the signal can be set to low.\n- **idle_off** - (*bool*) If True, the PWM pulse width is set to 0 after a requested rotation has been finished. Defaults to False\n<br>If jitter occurs in idle phases, this can be activated to eliminate jitter.\n<br>Normally, this is not required for hardware PWM.\n- **calibration** (*float*) - Calibration angle which will be considered as 0 position of the servo. Defaults to 0.0\n<br>Must be between *min_angle* and *max_angle*.\n\n## Properties and Methods\n\n### *property* **current_angle**\n\nReturns or sets the current angle relative to *calibration* angle.    \nAny rotations are limited to the range within *min_angle* and *max_angle*.\n\n### *property* **value**\n\nSynonym for *current_angle*\n\n### **min**()\n\nRotate to the minimum position of the servo.     \nThe minimum position is the position to which the servo drives with the specified *min_pulse_width_us*\nThe absolute angle at this position (without calibration) is assumed to be *min_angle*\n\n### **max**()\n\nRotate to the maximum position of the servo.     \nThe maximum position is the position to which the servo drives with the specified *max_pulse_width_us*\nThe absolute angle at this position (without calibration) is assumed to be *max_angle*\n\n### **mid**()\n\nRotate to the mid position of the servo.    \nThis is the position halfway between *min_angle* and *max_angle*.\n\nNote that for non-zero *calibration*, this position is different from *current_angle*=0.0.\n\n### **rotate_to**(*angle=?*)\n\nRotate to the given angle relative to *calibration*.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate to\n\n### **rotate_by**(*angle=?*)\n\nRotate by the given angle relative to *current_angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive or negative)\n\n### **rotate_right**(*angle=?*)\n\nRotate right (clockwise from the pespective of the servo) by the given angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive)\n\n### **rotate_left**(*angle=?*)\n\nRotate left (anti-clockwise from the pespective of the servo) by the given angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive)\n\n\n### **stop()***\n\nStop PWM.\n\n### **close()***\n\nStop PWM.\n"
  },
  {
    "path": "docs/gpioDevices/StepperMotor.md",
    "content": "# StepperMotor\n\n## Overview\n\n```\nclass StepperMotor(*args, **kwargs)\n```\n\nextends ```gpiozero.OutputDevice``` and represents a generic stepper motor connected to a stepper motor driver.    \nAn example combination, for which this class has been developped and tested, is the stepper motor **28BYJ-48** with the motor driver **ULN2003A**.\n\n1. Plug in the 5-cable jack of the motor into the socket of the motor driver.   \n2. Connect the 4 inputs on the motor driver (IN1 ... IN4) to 4 GPIO pins of the Raspberry Pi.<br>It is important to correctly memorize which pin is connected to which input.\n3. Connect the power input of the motor driver to Raspberry Pi 5V and GND pins with the correct polarity.\n\nThe following code will rotate the motor counter-clockwise (from the perspective of the motor) by 15°:\n```\nfrom raspiCamSrv.gpioDevices import StepperMotor\nstepper = StepperMotor(6, 13, 19, 26)\nstepper.rotate(-15)\nstepper.close()\n```\n\n## Parameters\n\n- **in1** (*int*) - The GPIO pin that the motor drivers **IN1** pin is connected to\n- **in2** (*int*) - The GPIO pin that the motor drivers **IN2** pin is connected to\n- **in3** (*int*) - The GPIO pin that the motor drivers **IN3** pin is connected to\n- **in4** (*int*) - The GPIO pin that the motor drivers **IN4** pin is connected to\n- **mode** (*int*) The mode in which the motor is operated.<br>Can be ```0``` (the default) for selecting half step mode with a resolution of 8 steps per full turn or ```1``` for full step mode with 4 steps per turn.\n- **speed** (*float*) - The speed with which the motor is operated. The speed is controlled through waiting times between successive steps.<br>A value of ```0.0``` results in the lowest speed with a waiting time of 4 ms and a value of ```1.0``` (the default) results in the highest speed with a waiting time of 1ms.<br>Values outside of this interval will be set to the nearest interval border.\n- **current_angle** - (*float*) The current angle of the motor. Defaults to 0.0\n- **swing_from** - (*float*) left boundary angle for swinging. Defaults to -45.0\n- **swing_to** - (*float*) right boundary angle for swinging. Defaults to 45.0\n- **swing_step** - (*float*) step width for swinging. Defaults to 9.0\n- **swing_direction** - (*int*) current swing direction. 1 (default) clockwise, 0 counter-clockwise.\n- **stride_angle** - (*float*) The angle incremet for a single step after gearing.<br>The default value of 5.625 is the value for the **28BYJ-48** motor.\n- **gear_reduction** (*int*) - The inverse of the transmission ratio of the gear box.<br>The default value of 64 is the value of the 1/64 ratio for the **28BYJ-48** motor.\n\n## Properties and Methods\n\n### *property* **mode**\n\nReturns or sets the mode of operation.\n\n### *property* **speed**\n\nReturns or sets the speed.\n\nAllowed values range from 0 for lowest speed (164.20 seconds for 360°) to 1 for highest speed (4.24 seconds for 360°)\n\n### *property* **stride_angle**\n\nReturns the stride angle.\n\n### *property* **gear_reduction**\n\nReturns the gear_reduction\n\n### *property* **current_angle**\n\nReturns or sets the current angle.    \nWhen the class is initiated, the angle is set to zero.    \nEvery motor movement will update the current angle.   \nFor **step**, **step_forward** and **step_backward**, the current angle will stay within (-360 <= *current_angle* <= 360).    \nFor the **rotate*** and **swing** methods, *current_angle* is not restricted to these limits, which allows tracking of multiple turns.\n\n### *property* **value**\n\nReturns or sets the current angle.    \n\n### *property* **swing_from**\n\nReturns or sets the left boundary for swinging in degree (-360 - 0).    \n\n### *property* **swing_to**\n\nReturns or sets the right boundary for swinging in degree (0 - 360).    \n\n### *property* **swing_step**\n\nReturns or sets the step width for swinging in degree (0 - 360).\n\n### *property* **swing_direction**\n\nReturns or sets the current swinging direction. 1=right, -1=left\n\n### **step**(*steps=?*)\n\nSteps forward for positive and backward for negative argument by the given number of steps.    \nThus, the angle is changed by *steps x stride_angle*.   \nWhen using this method, \n\n**Parameters**:\n\n- **steps** (*int*) Number of steps to move (positive or negative)\n\n### **step_forward**(*steps=?*)\n\nSteps forward (clockwise rotation) by the given number of steps.    \nThus, the angle is increased by *steps x stride_angle*\n\n**Parameters**:\n\n- **steps** (*int*) Number of steps to move forward (positive value)\n\n### **step_backward**(*steps=?*)\n\nSteps backward (anti-clockwise rotation) by the given number of steps.    \nThus, the angle is decreased by *steps x stride_angle*\n\n**Parameters**:\n\n- **steps** (*int*) Number of steps to move backward (positive palue)\n\n### **rotate**(*angle=?*)\n\nRotate clockwise (for positive angle) or counter-clockwise (for negative angle) by the given angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive or negative)\n\n### **rotate_right**(*angle=?*)\n\nRotate right (clockwise from the pespective of the motor) by the given angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive)\n\n### **rotate_left**(*angle=?*)\n\nRotate left (anti-clockwise from the pespective of the motor) by the given angle.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive)\n\n### **rotate_to**(*target=?*)\n\nRotate to the given angle.\n\n**Parameters**:\n\n- **target** (*float*) Angle to rotate to\n\n### **swing**()\n\nDo one swing step in the current *swing_direction* with the current *swing_step*.\nIf the *current_angle* would exceed *swing_from* or *swing_to*, rotation will reverse its direction at the border.\n\n**Parameters**:\n\n- **angle** (*float*) Angle to rotate (positive)\n\n### **wipe**(*angle_from=?, angle_to=?, speed=?, count=?*)\n\nWipe back and forth within a given range of angles with the given speed.\n\nWith the *count* parameter you can specify a certain number of cycles.    \nin case of *count=0*, swiping will continue until **stop** is called.\n\nAfter termination, the motor returns to the starting position.\n\n**Parameters**:\n\n- **angle_from** (*float*) Start angle (defaults to -45.0)\n- **angle_to** (*float*) End angle (defaults to 45.0)\n- **speed** (*float*) Speed to be used for swiping. Can be in the range from 0 (slow) to 1 (fast). Default is 0.\n- **count** (*int*) Number of cycles. A value of 0 will cause infinite swiping which needs to be stopped with **stop()**\n\n### **stop()***\n\nTerminate swiping after the active cycle is finished.\n\n### **close()**\n\nShut down the device and release all associated resources (such as GPIO pins). \n"
  },
  {
    "path": "docs/index.md",
    "content": "**raspiCamSrv** is a Web server for Raspberry Pi systems providing an App for control and streaming of CSI and USB cameras as well as for controlling a large variety of connected [GPIO devices](./SettingsDevices.md).\n\nWhile all currently connected cameras are accessible by the system, up to two cameras can be operated simultaneously at a time, supporting multi-camera features like [Stereo Vision](./CamStereo.md).\n\nInteroperability between Cameras and GPIO devices is achieved through the freely configurable [event handling infrastructure](./Trigger.md).\n\n**raspiCamSrv** supports all Raspberry Pi platforms from Pi Zero to Pi 5, running Bullseye, Bookworm or Trixie OS.\n\nBesides the currently available Raspberry Pi cameras, also compatible CSI cameras from other providers can be used. USB web cams are seamlessly integrated.\n\n**raspiCamSrv** is built with [Flask 3.x](https://flask.palletsprojects.com/en/stable/) and uses the [Picamera2 library](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf).    \nDue to responsive layout from [W3.CSS](https://www.w3schools.com/w3css/), all modern browsers on PC, Mac or mobile devices can be used as clients.\n\nFor resources and latest version, refer to the [raspiCamSrv GitHub Repository](https://github.com/signag/raspi-cam-srv)\n\n![Live Overview](./img/Live.jpg)"
  },
  {
    "path": "docs/installation.md",
    "content": "# RaspiCamSrv Installation\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\n## Installation Steps\n\nThe following description refers to the initial installation.    \nIf you want to update an existing installation to the latest version, see [Update Procedure](./updating_raspiCamSrv.md).\n\n\n1. Connect to the Pi using SSH: <br>```ssh <user>@<host>```<br>with ```<user>``` and ```<host>``` as specified during setup with Imager.\n2. Make sure that the system is up to date<br>```sudo apt update``` <br>```sudo apt full-upgrade```\n3. Run the automatic installer with:      \n```bash <(curl -fsSL https://raw.githubusercontent.com/signag/raspi-cam-srv/main/scripts/install_raspiCamSrv.sh)```    \nFollow instructions given by the installer (see [below](#installer))\n4. When the installer has finished successfully, open a browser and connect to raspiCamSrv using the indicated URL.\n5. Before you can login, you first need to [register](./Authentication.md).<br>The first user will automatically be SuperUser who can later register other users ([User Management](./Authentication.md#user-management))\n6. After successful log-in, the [Live screen](./LiveScreen.md) will be shown, if at least one camera is connected, otherwise the [Info](./Information.md) screen.\n7. For usage of **raspiCamSrv**, please refer to the [User Guide](./UserGuide.md)\n\n## Installer\n\n**NOTE**: You can run the installer multiple times without any risk, also over an existing installation.\n\nSo, if you want to switch the [WSGI server](./Information_Sys.md#wsgi-server) or need more [threads for Gunicorn](./installation_man.md#gunicorn-settings), or if you want to extend the softwarestack with a missing package, just rerun the installer. It will not touch your existing data, but just update the installation to the latest version and adjust the service configuration to your requirements.\n\nUnder normal circumstances, the installer will finish successfully.\nYou will probably see some red <span style=\"color: red;\">**ERROR**</span> messages (See [Trouble Shooting Guide](./Troubelshooting.md#errors-during-installation)) which you can ignore as long as the installer continues and [finalizes](#finalization).\n\n### Starting the Installer\n\n#### For Fresh Installation\n\n```\n==========================================\n=== raspiCamSrv Automated Installer    ===\n===                                    ===\n=== Exit at any step with Ctrl+C       ===\n==========================================\n\nRPI Model           : Raspberry Pi 4 Model B Rev 1.1\nDetected OS codename: trixie full\nHostname            : raspi03\n\nRunning as user     : sn\nInstalling at       : /home/sn/prg\n\n=====================\nInstallation Defaults\n=====================\nInstallation Path   : /home/sn/prg/raspi-cam-srv\nWSGI Server         : Gunicorn\nGunicorn Threads    : 6\nService Port        : 5000 (default, will be adjusted if already in use)\nAudio Recording     : Disabled (Installing system service)\nAdvanced Features   : Enabled\n                      USB Cams, Histograms, Stereo Vision, extended Motion Detection\n                      (Requires OpenCV, numpy, matplotlib)\nAI Camera Support   : Disabled\nHardware PWM Support: Disabled\n                      Hardware PWM is required for jitter-free servo control\n\nDo you want to install with these settings? [Y/n]:\n\n\nNo more questions! Ready to start installation? [Y/n]:\n```\n\nConfirming both times with ```y``` or ```[Enter]``` will run the installer with the default settings.\n\nConfirming with ```n``` will allow for individual settings.\n\n#### Fresh Installation with existing Backup from previous Installation\n\nIf saved backups from a previous installation exist at ```~/prg/raspi-cam-srv_backups``` (see [Retaining backups when uninstalling](#retaining-backups)), the installer will automatically restore these for the new installation. If they are not required, you can remove them in dialog [Settings/Configuration](./SettingsConfiguration.md).\n\n```\n==========================================\n=== raspiCamSrv Automated Installer    ===\n===                                    ===\n=== Exit at any step with Ctrl+C       ===\n==========================================\n\nRPI Model           : Raspberry Pi Zero 2 W Rev 1.0\nDetected OS codename: bookworm lite\nHostname            : raspi05\n\nRunning as user     : sn\nInstalling at       : /home/sn/prg\n\n=====================\nInstallation Defaults\n=====================\nInstallation Path   : /home/sn/prg/raspi-cam-srv\nBackup              : Restoring backup from a previous installation\nWSGI Server         : Gunicorn\nGunicorn Threads    : 6\nService Port        : 5000 (default, will be adjusted if already in use)\nAudio Recording     : Disabled (Installing system service)\nAdvanced Features   : Enabled\n                      USB Cams, Histograms, Stereo Vision, extended Motion Detection\n                      (Requires OpenCV, numpy, matplotlib)\nAI Camera Support   : Disabled\nHardware PWM Support: Disabled\n                      Hardware PWM is required for jitter-free servo control\n\nDo you want to install with these settings? [Y/n]:\n\n\n```\n\n#### Installing over existing Installation\n\nIf the installation path ```~/prg/raspi-cam-srv``` exists already, it is assumed that a raspiCamSrv installation exists already on the system.\n\n```\n==========================================\n=== raspiCamSrv Automated Installer    ===\n===                                    ===\n=== Exit at any step with Ctrl+C       ===\n==========================================\n\nRPI Model           : Raspberry Pi Zero 2 W Rev 1.0\nDetected OS codename: bookworm lite\nHostname            : raspi05\n\nRunning as user     : sn\nInstalling at       : /home/sn/prg\n\n=====================\nInstallation Mode\n=====================\nInstallation Path   : /home/sn/prg/raspi-cam-srv (exists)\nService Status      : raspiCamSrv.service (running, will be stopped)\n\nA raspiCamSrv installation exists already.\n\nDo you want to skip update of raspiCamSrv and software stack and only reconfigure the service[Y/n]:\n\n\nOnly installing/replacing service for existing installation\n\n=====================\nInstallation Defaults\n=====================\nWSGI Server         : Gunicorn\nGunicorn Threads    : 6\nService Port        : 5000 (default, will be adjusted if already in use)\nAudio Recording     : Disabled (Installing system service)\n\nDo you want to install with these settings? [Y/n]:\n\n\nNo more questions! Ready to start installation? [Y/n]:\n\n```\n\nConfirming all questions with ```y``` or ```[Enter]```  will\n\n- stop a running raspiCamSrv service\n- skip updating the raspiCamSrv repository\n- skip installation of software packages\n- try to initialize the database in case this did not complete in the previous installation run\n- reconfigure a system service (no audio recording)\n\nAlternatively, you can allow updating raspiCamSrv and software stack and/or run a customized installation.\n\n### Custom Installation\n\nIf a custom installation is required, necessary information is requested step by step:\n\n```\nDo you want to install with these settings? [Y/n]: n\n\n\nAvailable WSGI servers:\n1) Gunicorn (recommended for publicly accessible systems) - default\n2) Flask built-in server (OK for testing and private networks)\nChoose WSGI server [1/2]:\n\nUsing WSGI server: gunicorn\n\nHow many parallel video streams do you require? [default: 6]:\n\nUsing 6 threads for Gunicorn worker process\n\nDo you need to record audio along with videos? [y/N]:\n\nAudio recording enabled: false\n\nDo you want to enable advanced features (USB Cams, Histograms, Stereo Vision, extended Motion Detection)? [Y/n]:\n\nAdvanced features enabled: true\n\nDo you intend to use the Raspberry Pi AI Camera (imx500)? [y/N]: y\n\nAI Camera support enabled: true\n\nDo you intend to use Hardware PWM for jitter-free servo control? [y/N]: y\n\nHardware PWM support enabled: true\n\nNo more questions! Ready to start installation? [Y/n]:\n```\n\n- For WSGI server selection, see the [WSGI Server section](./Information_Sys.md#wsgi-server) of the [Info/System](./Information_Sys.md) screen.\n- For Gunicorn, especially setting *Number of Threads*, see [Gunicorn Settings](./installation_man.md#gunicorn-settings).\n- For recording audio, see [Recording Audio along with Video](./Settings.md#recording-audio-along-with-video).\n- For support of AI features for the imx500 camera, see [AI Camera Support](./AiCameraSupport.md).    \n(This is not available for Bullseye systems)\n- Hardware PWM support is only required if you intend to use [ServoPWM](./gpioDevices/ServoPWM.md) as [Device](./SettingsDevices.md) for controlling servos through [Actions](./TriggerActions.md).\n\n\n### Installation Process\n\nThe installer will automatically execute the procedure described for [manual installation](./installation_man.md) as well as [service configuration](./service_configuration.md).\n\n\nThe steps shown in the installer protocol correspond to the steps of [manual installation](./installation_man.md).\n\nIn case of problems during installation and usage, see [Troubleshooting](./Troubelshooting.md) or try the [manual installation procedure](./installation_man.md).\n\n#### Step 12: Initializing database for Raspberry Pi Zero Systems\n\nRecent (per ~02/2026) updates of Bookworm and Trixie on Raspberry Pi Zero / Zero 2 devices seem to have an issue with allocation of CMA memory by Picamera2 (see also [Checking Contiguous Memory (CMA)](./SetupDocker.md#checking-contiguous-memory-cma)). The issue may be due to CMA fragmentation or a previous process not releasing its buffers cleanly or fast enough. This issue has not been found for Bullseye systems on RPI Zero and also not for RPI 1, ... 5.\n\nWhen the Flask raspiCamSrv application is created (which is also the case while initializing the database during installation), raspiCamSrv instantiates the active camera through Picamera2. During this process, Picamera2 tries to allocate CMA memory.\n\nIt has been observed on RPI Zero Bookworm and Trixie systems, that this allocation may fail at one time and be successful later.\n\nTherefore, in this specific setup, the raspiCamSrv installer will try database initialization for up to 5 times with a pause of 5 sec in case of a failure.\n\nThe example, below, shows success in the second attempt:\n\n```\nStep 12: Initializing database ...\nAttempt 1 of 5...\nTraceback (most recent call last):\n  File \"<frozen runpy>\", line 198, in _run_module_as_main\n  File \"<frozen runpy>\", line 88, in _run_code\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/__main__.py\", line 3, in <module>\n    main()\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/cli.py\", line 1131, in main\n    cli.main()\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/click/core.py\", line 1406, in main\n    rv = self.invoke(ctx)\n         ^^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/click/core.py\", line 1867, in invoke\n    cmd_name, cmd, args = self.resolve_command(ctx, args)\n                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/click/core.py\", line 1914, in resolve_command\n    cmd = self.get_command(ctx, cmd_name)\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/cli.py\", line 631, in get_command\n    app = info.load_app()\n          ^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/cli.py\", line 349, in load_app\n    app = locate_app(import_name, name)\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/cli.py\", line 262, in locate_app\n    return find_best_app(module)\n           ^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/.venv/lib/python3.11/site-packages/flask/cli.py\", line 72, in find_best_app\n    app = app_factory()\n          ^^^^^^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/raspiCamSrv/__init__.py\", line 155, in create_app\n    cam = Camera()\n          ^^^^^^^^\n  File \"/home/sn/prg/raspi-cam-srv/raspiCamSrv/camera_pi.py\", line 1713, in __new__\n    cls.initCamera()\n  File \"/home/sn/prg/raspi-cam-srv/raspiCamSrv/camera_pi.py\", line 1953, in initCamera\n    cls.loadCameraSpecifics()\n  File \"/home/sn/prg/raspi-cam-srv/raspiCamSrv/camera_pi.py\", line 2945, in loadCameraSpecifics\n    sensorModes = Camera.cam.sensor_modes\n                  ^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3/dist-packages/picamera2/picamera2.py\", line 599, in sensor_modes\n    self.configure(temp_config)\n  File \"/usr/lib/python3/dist-packages/picamera2/picamera2.py\", line 1221, in configure\n    self.configure_(\"preview\" if camera_config is None else camera_config)\n  File \"/usr/lib/python3/dist-packages/picamera2/picamera2.py\", line 1193, in configure_\n    self.allocator.allocate(libcamera_config, camera_config.get(\"use_case\"))\n  File \"/usr/lib/python3/dist-packages/picamera2/allocators/dmaallocator.py\", line 43, in allocate\n    fd = self.dmaHeap.alloc(f\"picamera2-{i}\", stream_config.frame_size)\n         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3/dist-packages/picamera2/dma_heap.py\", line 98, in alloc\n    ret = fcntl.ioctl(self.__dmaHeapHandle.get(), DMA_HEAP_IOCTL_ALLOC, alloc)\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nOSError: [Errno 12] Cannot allocate memory\nFailed. Waiting 5s before retry...\nAttempt 2 of 5...\nInitialized the database.\n\n```\n\nA similar behavior may be observed at server start.    \nHowever, since the raspiCamSrv service is configured to restart automatically, server start will usually be successful after some time.\n\n### Finalization\n\n```\nStep 13: Checking Flask service port ...\nTrying port 5000 ...\nUsing port 5000\n\nCleaning up existing service before reinstalling ...\nSystem service 'raspiCamSrv.service' disabled.\nSystem service 'raspiCamSrv.service' configuration removed.\n\nInstalling 'raspiCamSrv.service' as user unit for WSGI Server werkzeug ...\nUser service installed and started.\n\n==========================================\n=== raspiCamSrv installation completed ===\n===                                    ===\n=== Access via http://raspi06:5000\n==========================================\n\n```\n\n#### Check for Hardware PWM Support\n\nIf you have selected to enable Hardware PWM support, the following information will be displayed:\n\n```\n=============================================================================================================\nChecking for Hardware PWM support on GPIO pins 12, 13, 18, 19 ...\npinctrl get 12,13,18,19\n12: a0    pd | lo // GPIO12 = PWM0_CHAN0\n13: ip    -- | lo // GPIO13 = input\n18: ip    -- | lo // GPIO18 = input\n19: ip    -- | lo // GPIO19 = input\n\nIf you see 'PWM0' or 'PWM1' in the output above, Hardware PWM support is available for the indicated pins.\nOtherwise, you need to specify device tree overlays in /boot/firmware/config.txt and reboot your Raspberry Pi.\nDepending on your RPI model and the required pins, add the following lines to /boot/firmware/config.txt:\n[all]\ndtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\n[pi5]\ndtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\ndtoverlay=pwm-2chan,pin=18,func=2,pin2=19,func2=2\n=============================================================================================================\n```\n\n## Supervision\n\nYou can check the system logs with    \n```sudo journalctl -ef```\n\n### For 'Werkzeug' WSGI server:\n\nWhen the Flask server starts up, it will show a warning that this is a development server.   \nThis is, in general, fine for private environments.   \nHow to deploy with a production WSGI server, is described in the [Flask documentation](https://flask.palletsprojects.com/en/stable/deploying/)\n\n```\nDec 09 18:49:19 raspi06 python[9642]:  * Serving Flask app 'raspiCamSrv'\nDec 09 18:49:19 raspi06 python[9642]:  * Debug mode: off\nDec 09 18:49:19 raspi06 python[9642]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\nDec 09 18:49:19 raspi06 python[9642]:  * Running on all addresses (0.0.0.0)\nDec 09 18:49:19 raspi06 python[9642]:  * Running on http://127.0.0.1:5000\nDec 09 18:49:19 raspi06 python[9642]:  * Running on http://192.168.178.72:5000\nDec 09 18:49:19 raspi06 python[9642]: Press CTRL+C to quit\n\n```\n\n### For Gunicorn WSGI server\n\nWhen Gunicorn starts, it will output Info messages like\n\n```\nFeb 18 14:09:29 raspi06 systemd[1197]: Started raspiCamSrv.service - raspiCamSrv.\nFeb 18 14:09:30 raspi06 gunicorn[7906]: [2026-02-18 14:09:30 +0100] [7906] [INFO] Starting gunicorn 25.1.0\nFeb 18 14:09:30 raspi06 gunicorn[7906]: [2026-02-18 14:09:30 +0100] [7906] [INFO] Listening at: http://0.0.0.0:5000 (7906)\nFeb 18 14:09:30 raspi06 gunicorn[7906]: [2026-02-18 14:09:30 +0100] [7906] [INFO] Using worker: gthread\nFeb 18 14:09:30 raspi06 gunicorn[7906]: [2026-02-18 14:09:30 +0100] [7906] [INFO] Control socket listening at /home/sn/prg/raspi-cam-srv/gunicorn.ctl\nFeb 18 14:09:30 raspi06 gunicorn[7908]: [2026-02-18 14:09:30 +0100] [7908] [INFO] Booting worker with pid: 7908\n\n```\n\nThe PID of the worker process is also shown in the [Info/System screen](./Information_Sys.md#process-info)\n\n**NOTE**: When starting, the Gunicorn master process creates a control socket file ```gunicorn.ctl``` in the working directory, which will be removed when the server stops.\n\n## Manually Starting the Server\n\n1. Stop the service    \n```sudo systemctl stop raspiCamSrv```  (In case of a system unit)    \n```systemctl --user stop raspiCamSrv```  (In case of a user unit)\n2. Go to the install directory    \n```cd ~/prg/raspi-cam-srv```\n3. Activate the virtual environment    \n```.venv/bin/activate```\n4. Start raspiCamSrv    \neither with the Flask built-in development server (werkzeug):    \n```python -m flask --app raspiCamSrv run --port 5000 --host=0.0.0.0```    \nor with the Gunicorn production server:     \n```gunicorn -b 0.0.0.0:5000 -w 1 -k gthread --threads 6 --timeout 0 --log-level info 'raspiCamSrv:create_app()```\n\n## Uninstalling raspiCamSrv\n\n1. Connect to the Pi using SSH: <br>```ssh <user>@<host>```\n2. Run the automatic uninstaller with:    \n```bash <(curl -fsSL https://raw.githubusercontent.com/signag/raspi-cam-srv/main/scripts/uninstall_raspiCamSrv.sh)```\n\n### Uninstaller\n\nThe uninstaller will request confirmation:\n\n```\n==========================================\n=== raspiCamSrv Automated Uninstaller  ===\n===                                    ===\n=== Exit at any step with Ctrl+C       ===\n==========================================\n\nRPI Model           : Raspberry Pi Zero 2 W Rev 1.0\nDetected OS codename: bookworm lite\nHostname            : raspi05\n\nRunning as user     : sn\nUninstalling from   : /home/sn/prg/raspi-cam-srv\n\nraspiCamSrv will be completely removed from raspi05. Continue? [yes/NO]:\n```\n\nTo uninstall, you need to reply with ```yes```.\n\n### Retaining Backups\n\nIf you had created [backups](./SettingsConfiguration.md#backups), these can be preserved for a possible reuse in a new installation.\n\n```\nBackups found in /home/sn/prg/raspi-cam-srv/backups:\ntotal 8\ndrwxr-xr-x 4 sn sn 4096 Feb 28 16:50 2026-02-28-16:49\ndrwxr-xr-x 4 sn sn 4096 Feb 28 17:05 2026-02-28-17:05\n\nDo you want to keep these backups? [y/N]:y\n\nBackups saved at /home/sn/prg/raspi-cam-srv_backups\n\nUninstalling raspiCamSrv service ...\n\n```\n"
  },
  {
    "path": "docs/installation_man.md",
    "content": "# Manual raspiCamSrv Installation\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\n\nThe following procedure describes a manual step by step installation.    \nFor automatic installation, see [RaspiCamSrv Installation](./installation.md).\n\nIf you want to update an existing installation to the latest version, see [Update Procedure](./updating_raspiCamSrv.md).\n\nIn case of problems during installation and usage, see [Troubleshooting](./Troubelshooting.md)\n\n**NOTE**: For Debian-**Trixie**, some of the required packages are already preinstalled. To ensure everything is consistently installed in and run from the **raspiCamSrv** virtual environment, the respective ```pip install``` commands, below, have been extended with a ```--ignore-installed``` clause and the Flask server is started with ```python -m flask ...```\n\n## Step by Step\n\n1. Connect to the Pi using SSH: <br>```ssh <user>@<host>```<br>with <user> and <host> as specified during setup with Imager.\n2. Update the system<br>```sudo apt update``` <br>```sudo apt full-upgrade```\n3. If you intend to take videos and have installed a *lite* version of the OS, you may need to install *ffmpeg*:<br>Check whether ffmpeg is installed with<br>```which ffmpeg```<br>If you get an empty response, install with<br>```sudo apt install ffmpeg```\n4. Create a root directory under which you will install programs (e.g. 'prg')<br>```mkdir prg```<br>```cd prg```\n5. Check that git is installed (which is usually the case in current Bullseye, Bookworm or Trixie distributions)<br>```git --version```<br>If git is not installed, install it with<br>```sudo apt install git```\n6. Clone the raspi-cam-srv repository:<br>```git clone --branch main --single-branch --depth 1 https://github.com/signag/raspi-cam-srv```\n7. Create a virtual environment ('.venv') on the 'raspi-cam-srv' folder:<br>```cd raspi-cam-srv```<br>```python -m venv --system-site-packages .venv```<br>For the reasoning to include system site packages, see the [picamera2-manual.pdf](./picamera2-manual.pdf), chapter 9.5.\n8. Activate the virtual environment<br>```cd ~/prg/raspi-cam-srv```<br>```source .venv/bin/activate```<br>The active virtual environment is indicated by ```(.venv)``` preceeding the system prompt.<br>(If you need to leave the virtual environment at some time, use ```deactivate```)\n9. Make sure that picamera2 is available on the system:\n<br>```python```\n<br>```>>>import picamera2```\n<br>```>>>quit()```\n<br>If you get a 'ModuleNotFoundError', see the [picamera2 Manual](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf), chapter 2.2, how to install picamera2.\n<br>For **raspiCamSrv** it would be sufficient to install without GUI dependencies:\n<br>For Bullseye and Bookworm:\n<br>```sudo apt install -y python3-picamera2 --no-install-recommends```\n<br>For Trixie:\n<br>```sudo apt install -y python3-libcamera python3-picamera2 --no-install-recommends```\n10. Install Flask 3.x **with the virtual environment activated (Step 8)**.<br>Raspberry Pi OS distributions come with Flask preinstalled, however we need to run Flask from the virtual environment in order to see other packages which will be located there.<br>```pip install --ignore-installed \"Flask>=3,<4\"```<br><br>Make sure that Flask is really installed in the virtual environment:<br>```which flask``` should output<br>```/home/<user>/prg/raspi-cam-srv/.venv/bin/flask```\n11. **Optional** installations:\n<br>The following installations are only required if you need to visualize histograms of photos or if you are interested in using [Extended Motion Capturing Algorithms](./TriggerMotion.md) or [Stereo Vision](./CamStereo.md).<br>For use of USB cameras, OpenCV is required.\n<br>\n<br>All installations must be done with the virtual environment activated (Step 8)\n<br>\n<br>Install [OpenCV](https://de.wikipedia.org/wiki/OpenCV):\n<br>```sudo apt-get install python3-opencv```\n<br>\n<br>Install [numpy](https://numpy.org/):\n<br>For RPI Zero 2, RPI 1 ... 5:\n<br>```pip install --ignore-installed numpy```\n<br>(There may be errors, which normally can be ignored)\n<br>For RPI Zero W:\n<br>```sudo apt-get install -y python3-numpy```\n<br>\n<br>Install [matplotlib](https://de.wikipedia.org/wiki/Matplotlib):\n<br>For RPI Zero 2, RPI 1 ... 5:\n<br>**Trixie**:```pip install --ignore-installed matplotlib```\n<br>(There may be errors, which normally can be ignored)\n<br>**Bookworm**: ```pip install --ignore-installed \"matplotlib<3.8\"```\n<br>(The version restriction assures compatibility with numpy 1.x which is [required for Picamera2](https://github.com/raspberrypi/picamera2/issues/1211))\n<br>For RPI Zero W:\n<br>```sudo apt-get install -y python3-matplotlib```\n<br>\n<br>The following installation is required for enabling the [raspiCamSrv API](./API.md)\n<br>Install [flask-jwt-extended](https://flask-jwt-extended.readthedocs.io/en/stable/)\n<br>```pip install --ignore-installed flask-jwt-extended```\n<br>(There may be errors, which normally can be ignored)\n<br>\n<br>The following installation is only required if you are using a Lite variant of the Debian OS:\n<br>For RPI Zero 2, RPI 1 ... 5:\n<br>```pip install --ignore-installed psutil```\n<br>For RPI Zero W:\n<br>```sudo apt-get install -y python3-psutil```\n<br>\n<br>The following installations are only required if you intend to use a Raspberry Pi AI Camera:\n<br>\n<br>Install the imx500-all package:\n<br>```sudo apt install imx500-all```\n<br>\n<br>Install [munkres](https://pypi.org/project/munkres/)\n<br>```pip install --break-system-packages munkres```\n<br>(There may be errors, which normally can be ignored)\n<br>\n<br>If you intend to use the [Gunicorn](https://gunicorn.org/) WSGI server instead of the development WSGI server integrated in Flask ([Werkzeug](https://werkzeug.palletsprojects.com/en/stable/)), you need to install Gunicorn:\n<br>```pip install --break-system-packages gunicorn```\n<br>(There may be errors, which normally can be ignored)\n<br><br>\n12. Initialize the database for Flask <br>(with ```raspi-cam-srv``` as active directory and the virual environment activated - see step 8):<br>```python -m flask --app raspiCamSrv init-db```\n13. Check that the Flask default port 5000 is available<br>```sudo netstat -nlp | grep 5000```<br>If an entry is shown, find another free port (e.g. 5001) <br>and replace ```port 5000``` by your port in all ```flask``` commands, below and also in the URL in step 12.\n14. Start the server<br>(with ```raspi-cam-srv``` as active directory and the virual environment activated - see step 8):<br>Either use the Flask built-in development server:<br>```python -m flask --app raspiCamSrv run --port 5000 --host=0.0.0.0```<br>or use [Gunicorn](https://gunicorn.org/) as productive server:<br>```gunicorn -b 0.0.0.0:5000 -w 1 -k gthread --threads 6 --timeout 0 --log-level info 'raspiCamSrv:create_app()'```\n15. Connect to the server from a browser:<br>```http://<raspi_host>:5000```<br>This will open the [Login](./Authentication.md) screen.\n16. Before you can login, you first need to [register](./Authentication.md).<br>The first user will automatically be SuperUser who can later register other users ([User Management](./Authentication.md#user-management))\n17. After successful log-in, the [Live screen](./LiveScreen.md) will be shown, if at least one camera is connected, otherwise the [Info](./Information.md) screen.\n18. Done!\n19. For usage of **raspiCamSrv**, please refer to the [User Guide](./UserGuide.md)\n\n\nWhen the Flask server starts up, it will show a warning that this is a development server.   \nThis is, in general, fine for private environments.   \nHow to deploy with an alternative production WSGI server, is described in the [Flask documentation](https://flask.palletsprojects.com/en/stable/deploying/)\n\n## Gunicorn Settings\n\nWhen using the [Gunicorn](https://gunicorn.org/) WSGI server, specific settings must be used with the raspiCamSrv Flask application.\n\n### Number of Workers\n\n**Command line**: ```-w 1```\n\nOnly a single worker must be configured.\n\nEach worker will be a separate process. Multiple workers would run multiple raspiCamSrv Flask processes in parallel.    \nThis would cause conflicts when accessing Raspberry Pi resources, such as cameras and GPIO devices.    \n\n### Port Binding\n\nBy default, Gunicorn binds to ```127.0.0.1:8080```\n\nIt is recommended using the same port as the Flask-internal WSGI server (5000).\n\n**Command line**: ```-b 0.0.0.0:5000```\n\n### Worker Type\n\nGunicorn supports different [Worker Types](https://gunicorn.org/design/?h=design#worker-types) to be used.   \nFrom these, only ```gthread``` works for raspiCamSrv, because\n\n- keep-alive connections are supported, which is essential for MJPEG streaming\n- it uses real OS threads which is essential for multi-threading in raspiCamSrv, Picamera2 and OpenCV\n\n**Command line**: ```-k gthread```\n\n### Numer of Threads\n\nWith the ```gthread``` worker type, it is necessary to specify the number of threads which can be simultaneously active, because each request will be handled by an own worker thread.\n\nTherefore, the number of threads limits the number of simultaneous MJPEG streams.\n\nOn the other hand, when a worker is started, the worker process will pre-create the specified number of threads, regardless of how many clients are connected.\n\n**Command line**: ```--threads 6```\n\nEvery MJPEG stream uses one thread, for example:\n\n- [Live screen](./LiveScreen.md): 1 thread\n- [Web Cam screen](./CamWebcam.md) with 2 cameras: 2 threads\n- [Stereo Cam screen](./CamStereo.md): 3 threads\n- Every client streaming from a ```video_feed``` endpoint uses 1 thread\n\nSo, with ```--threads 4``` and 2 clients showing the [Web Cam screen](./CamWebcam.md), the 4 threads are used up and another client, trying to stream from a ```video_feed``` endpoint, would wait.\n\n### Timeout\n\nGunicorn kills and restarts worker threads which are silent for more than the number of seconds specified in the ```timeout``` option.\n\nThe default is 30 seconds.\n\nFor raspiCamSrv, timout should be avoided.\n\n**Command line**: ```--timeout 0```\n\n### Logging\n\n**Command line**: ```--log-level info```\n\n'info' is the default log level.\n\nOther valid level names are:\n\n- debug\n- warning\n- error\n- critical\n\n### WSGI APP\n\nraspiCamSrv uses a factory pattern to create the Flask application.\n\nTherefore, the raspiCamSrv WSGI app needs to be ecposed to Gunicorn in the form\n\n**Command Line**: ```'raspiCamSrv:create_app()'```"
  },
  {
    "path": "docs/picamera2_manual.md",
    "content": "[The Picamera2 Library](https://pip-assets.raspberrypi.com/categories/652-raspberry-pi-camera-module-2/documents/RP-008156-DS-2-picamera2-manual.pdf?disposition=inline)"
  },
  {
    "path": "docs/requirements.md",
    "content": "# Requirements\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\n- A **Raspberry Pi** ([Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/), [Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/), [Pi 1](https://www.raspberrypi.com/products/raspberry-pi-1-model-b-plus/), [Pi 3](https://www.raspberrypi.com/products/raspberry-pi-3-model-b-plus/), [Pi 4](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/), [Pi 5](https://www.raspberrypi.com/products/raspberry-pi-5/))\n- A [Raspberry Pi camera](https://www.raspberrypi.com/documentation/accessories/camera.html)\n- A suitable **camera cable** <br>(Pi Zero W, Pi Zero 2 W and Pi 5 have the small CSI-2 camera port, requiring a special cable which is usually not shipped with the camera)\n- A **microSD card**\n- A suitable **power supply**<br>For Pi Zero W or Pi Zero 2 W, a normal mobile phone charger is sufficient as long as it has a Micro-USB connector\n- Optionally, a **case** for the specific model may ease handling.<br>For Pi Zero W or Zero 2 W, offerings for the official case (e.g. [here](https://www.reichelt.de/gehaeuse-fuer-raspberry-pi-zero-rot-weiss-rpiz-case-whrd-p223607.html?PROVID=2788&gclid=EAIaIQobChMI2JfM3vjcgwMVSxYGAB1pSQBOEAYYASABEgL_GPD_BwE)) should include a special short (38 mm) camera cable.<br>The cover for the camera is fine for camera models 1 and 2. For camera model 3, some handwork is necessary to enlarge the hole to a square for the camera body.<br>Alternatively, there are various 3D printable variants, e.g. [Case for Raspberry Pi Zero Models with Camera](https://makerworld.com/en/models/1804527-case-for-raspberry-pi-zero-models-with-camera#profileId-1924557).\n- A **Wifi network** with internet access and known access credentials (**SSID**, **password**)\n- A **PC or Mac** with network access and **(micro)SD** card reader\n\nThe [setup description](./system_setup.md), assumes a completely autonomous or 'headless' setup, where the Raspberry Pi requires nothing but a power supply cable without any necessity to ever connect it to a display, keyboard or mouse.   \n\n|   |  | \n|--------|---|\n|![Pi Zero Cover](./img/pi_zero_cover.jpg)|![Pi Zero Cover](./img/pi_zero_cover_3dp.jpg)|\n|Official Pi Zero Case with Camera módel 2| [3D-printed case](https://makerworld.com/en/models/1804527-case-for-raspberry-pi-zero-models-with-camera#profileId-1924557) suitable for tripod-mounting\n"
  },
  {
    "path": "docs/service_configuration.md",
    "content": "# Service Configuration\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\n**NOTE**: This installation step is included in the [automatic installation](./installation.md)\n\nWhen the Flask server is started in a SSH session as described in [Installation Step 11](./installation.md), it will terminate with the SSH session.\n\nInstead, you may want the server to start up independently from any user sessions, restart after a failure and automatically start up when the device is powered up.\n\nIn order to achieve this, the Flask server start can be configured as service under control of systemd.\n\n## No Audio Recording required\n\nThe following procedure is for the case where **audio recording** with video is **not required**. Otherwise, see [next](#service-configuration-for-audio-support) section.\n\n1. Open a SSH session on the Raspberry Pi\n2. Copy the service template *raspiCamSrv.service* which is provided with **raspiCamSrv** to your home directory.\n<br>When using the Flask built-in development server (werkzeug):\n<br>```cp ~/prg/raspi-cam-srv/config/raspiCamSrv.service ~```\n<br>When using the Gunicorn production server:\n<br>```cp ~/prg/raspi-cam-srv/config/raspiCamSrv_gunicorn.service ~/raspiCamSrv.service```\n3. Adjust the service configuration:\n<br>```nano ~/raspiCamSrv.service```\n<br>Replace all (4) occurrences of '```<user>```' with the user ID, specified during [System Setup](./system_setup.md)\n<br>If you need a port different from 5000 (see [RaspiCamSrv Installation](./installation.md), step 11), replace also ```port 5000``` by your port.\n<br>If you are using the Gunicorn WSGI server, you can adjust ```--threads 6``` to a value suitable for your use case (See [Gunicorn Settings / Number of Threads](./installation_man.md#numer-of-threads))\n4. Stage the service configuration file to systemd:\n<br>```sudo cp ~/raspiCamSrv.service /etc/systemd/system```\n5. Start the service:\n<br>```sudo systemctl start raspiCamSrv.service```\n6. Check that the Flask server has started as service:\n<br>```sudo journalctl -ef```\n7. Enable the service so that it automatically starts with system boot:\n<br>```sudo systemctl enable raspiCamSrv.service```\n8. Reboot the system to test automatic server start:\n<br>```sudo reboot```\n\n## Service Configuration for Audio Support\n\nIf it is intended to record audio along with videos, a slightly different setup is required (see [Settings](./Settings.md#recording-audio-along-with-video)):   \nInstead of installing the service as a system unit, it needs to be installed as user unit (see [systemd/User](https://wiki.archlinux.org/title/Systemd/User)) in order to get access to [PulseAudio](https://wiki.archlinux.org/title/PulseAudio).\n\n### Trixie and Bookworm Systems\n\nIf your system is a trixie or a bookworm system (```lsb_release -a```) follow these steps:\n\n1. Open a SSH session on the Raspberry Pi\n2. Copy the service template *raspiCamSrv.service* which is provided with **raspiCamSrv** to your home directory\n<br>When using the Flask built-in development server (werkzeug):\n<br>```cp ~/prg/raspi-cam-srv/config/raspiCamSrv.service ~```\n<br>When using the Gunicorn production server:\n<br>```cp ~/prg/raspi-cam-srv/config/raspiCamSrv_gunicorn.service ~/raspiCamSrv.service```\n3. Adjust the service configuration:\n<br>```nano ~/raspiCamSrv.service```\n<br>Replace all (4) occurrences of '```<user>```' with the user ID, specified during [System Setup](./system_setup.md)\n<br>If necessary, raplace also the standard port 5000 with your port.<br>Remove the entry User=```<user>``` from the [System] section\n<br>If you are using the Gunicorn WSGI server, you can adjust ```--threads 6``` to a value suitable for your use case (See [Gunicorn Settings / Number of Threads](./installation_man.md#numer-of-threads))\n<br>In section [Install], change ```WantedBy=multi-user.target``` to ```WantedBy=default.target```\n4. Create the directory for systemd user units<br>```mkdir -p ~/.config/systemd/user```\n5. Stage the service configuration file to systemd for user units:<br>```cp ~/raspiCamSrv.service ~/.config/systemd/user```\n6. Start the service:<br>```systemctl --user start raspiCamSrv.service```\n7. Check that the Flask server has started as service:<br>```journalctl --user -ef```<br>If you get ```No journal files were found.```, try<br>```sudo journalctl -ef```\n8. Enable the service so that it automatically starts with a session for the active user:<br>```systemctl --user enable raspiCamSrv.service```\n9. Enable lingering in order to start the unit right after boot and keep it running independently from a user session<br>```loginctl enable-linger```\n10. Reboot the system to test automatic server start:<br>```sudo reboot```\n\n\n### Bullseye Systems\n\nIf your system is a bullseye system (```lsb_release -a```), which is currently still the case for Pi Zero, follow these steps:\n\n1. Open a SSH session on the Raspberry Pi\n2. Clone branch 0_3_12_next of Picamera2 repository<br>```cd ~/prg```<br>```git clone -b 0_3_12_next https://github.com/raspberrypi/picamera2```\n3. Copy the service template *raspiCamSrv.service* which is provided with **raspiCamSrv** to your home directory<br>```cp ~/prg/raspi-cam-srv/config/raspiCamSrv.service ~``` \n4. Adjust the service configuration:<br>```nano ~/raspiCamSrv.service```<br>- Replace '```<user>```' with the user ID, specified during [System Setup](./system_setup.md)<br>- If necessary, raplace also the standard port 5000 with your port.<br>- Add another Environment entry: ```Environment=\"PYTHONPATH=/home/<user>/prg/picamera2\"```<br>- Remove the entry User=```<user>``` from the [System] section<br>- In section [Install], change ```WantedBy=multi-user.target``` to ```WantedBy=default.target```<br>For an example of the final .service file, see below\n5. Create the directory for systemd user units<br>```mkdir -p ~/.config/systemd/user```\n6. Stage the service configuration file to systemd for user units:<br>```cp ~/raspiCamSrv.service ~/.config/systemd/user```\n7. Start the service:<br>```systemctl --user start raspiCamSrv.service```\n8. Check that the Flask server has started as service:<br>```journalctl --user -e```\n9. Enable the service so that it automatically starts with a session for the active user:<br>```systemctl --user enable raspiCamSrv.service```\n10. Enable lingering in order to start the unit right after boot and keep it running independently from a user session<br>```loginctl enable-linger```\n11. Reboot the system to test automatic server start:<br>```sudo reboot```\n\n### Example Service Configuration\n\n#### For user \"sn\" with ```werkzeug``` WSGI server:\n\n```\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=/home/sn/prg/raspi-cam-srv/.venv/bin/flask --app raspiCamSrv run --port 5000 --host=0.0.0.0\nEnvironment=\"PATH=/home/sn/prg/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nWorkingDirectory=/home/sn/prg/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\n\n[Install]\nWantedBy=default.target\n```\n\n#### For user \"sn\" wth ```gunicorn``` WSGI server:\n\n```\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=/home/sn/prg/raspi-cam-srv/.venv/bin/gunicorn -b 0.0.0.0:5000 -w 1 -k gthread --threads 6 --timeout 0 --log-level info 'raspiCamSrv:create_app()'\nEnvironment=\"PATH=/home/sn/prg/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nWorkingDirectory=/home/sn/prg/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\n\n[Install]\nWantedBy=default.target\n```\n"
  },
  {
    "path": "docs/system_setup.md",
    "content": "# System Setup for raspiCamSrv\n\n[![Up](img/goup.gif)](./getting_started_overview.md)\n\nFollow the instructions of the [Raspberry Pi Getting Started Documentation](https://www.raspberrypi.com/documentation/computers/os.html#get-raspberry-pi-os) for OS installation using [Imager](https://www.raspberrypi.com/documentation/computers/getting-started.html#raspberry-pi-imager).   \n\n**NOTE**: It is recommended to install the Debian distribution proposed by [Imager](https://www.raspberrypi.com/documentation/computers/getting-started.html#raspberry-pi-imager) for your Raspberry Pi device. This is usually the full 64-bit version including desktop.   \nThe latest distribution is currently Trixie.\n\nRaspiCamSrv can also be installed on Bullseye or Bookworm systems, updated to the latest version.\n\nLite variants of these systems, without desktop, can be used for RPI Zero models which are usually only operated in headless mode. For these, the [Automated Installer](./installation.md) will install all required features which are not included in this distribution.\n\nMake sure that SSH is enabled on the Services tab.\n\nOnce the SD card is written, insert it into the Raspberry Pi and power it up.   \nInitially, it will take several minutes until it is visible in the network.\n"
  },
  {
    "path": "docs/tutorials/AWB_with_neural_networks.md",
    "content": "# Tutorial: Automatic White Balance with Neural Networks\n\n[![Up](../img/goup.gif)](./Tutorials_Overview.md)\n\nThis tutorial describes how to activate neural network-based AWB (AWB-NN).\n\nThis is a relatively new feature provided by the Raspberry Pi team which is currently only documented in the Forum article [AWB with Neural Networks](https://forums.raspberrypi.com/viewtopic.php?p=2365911).\n\nAWB-NN can currently only be activated through a modification of the tuning file for the camera.     \nIf activated, it overrules *AWB* settings in the [Camera Controls](../CameraControls_Image.md).\n\n## Precondition\n\nAWB-NN is only available in Trixie with libcamera version 0.7.0+rpt20260205 or later.\n\nTo check, open the [Info / System](../Information_Sys.md) dialog and check \n\n- Hardware and OS / Debial Version\n- Software Stack / libcamera\n\n## Procedure\n\n1. Open the [Config / Tuning](../Tuning.md) dialog\n<br>The dialog will show the tuning file for the active camera located in the standard folder ```/usr/share/libcamera/ipa/rpi/pisp```.\n2. Push button **Custom Folder**\n<br>which will create a custom folder for tuning files if it does not yet exist (```/home/<user>/prg/raspi-cam-srv/raspiCamSrv/static/tuning```) and copy the tuning file to this folder:\n<br>![Image1](./img/AWB-NN_01.jpg)\n<br>The reason for this is that standard tuning files in the standard folders must not be edited.\n3. Push button **Download Tuning File**\n<br>This will download the tuning file to your Downloads folder\n4. It is recommended to rename the tuning file to indicate that it is used for AWB-NN:\n<br>![Image2](./img/AWB-NN_02.jpg)\n5. Open the tuning file in your preferred text editor\n6. Search for node ```rpi.awb``` and change the element ```\"enabled\"``` to ```false```:\n<br>![Image3](./img/AWB-NN_03.jpg)\n7. Now, search for node ```rpi.nn.awb``` and change the element ```\"enabled\"``` to ```true```:\n<br>![Image4](./img/AWB-NN_04.jpg)\n8. Save the file\n9. In dialog **Tuning**, press button **Select Tuning File for Upload**,\n<br>navigate to your Downloads folder and select the modified tuning file.\n<br>The button will now show the name of the selected tuning file (if just one file has been selected).\n10. Now, push button **Upload selected File**\n<br>![Image6](./img/AWB-NN_06.jpg)\n<br>This will upload the file to the custom folder for tuning files\n11. Change the seleted tuning file to the modified file. Make sure that it is the file for the active camera:\n<br>![Image7](./img/AWB-NN_07.jpg)\n12. Check *Load Tuning File* and push button **Submit and Apply**\n<br>![Image8](./img/AWB-NN_08.jpg)\n\n## Result\n\nIf the modified tuning file is selected, Picamera2 will load this file when the camera is started.\n\nCited from [AWB with Neural Networks](https://forums.raspberrypi.com/viewtopic.php?p=2365911#p2365911):        \n\"The AWB NN algorithm definitely produces different results to the default Bayesian algorithm, the nature of which depends on the training data.    \nGenerally, we find it produces significantly better results, though your mileage may vary. Obviously this is quite early days still, \nso we're interested in people's experiences.\"\n\n\n## Deactivate AWB-NN\n\nTo return to the standard AWB, deactivate *Load Tuning File* and push button **Submit and Apply**\n\n\n\n"
  },
  {
    "path": "docs/tutorials/Tutorials_Overview.md",
    "content": "# RaspiCamSrv Tutorials\n\n[![Up](../img/goup.gif)](../index.md)\n\n- [Automatic White Balance with Neural Networks](./AWB_with_neural_networks.md)\n"
  },
  {
    "path": "docs/updating_raspiCamSrv.md",
    "content": "# Updating raspiCamSrv\n\n[![Up](img/goup.gif)](./index.md)\n\nBefore updating, make sure that\n\n- [video recording](./Phototaking.md#video) is stopped\n- there are no active [photoseries](./PhotoSeries.md)\n- [triggered capture](./Trigger.md) (motion tracking) is stopped\n- server will not [start with stored configuration](./SettingsConfiguration.md)\n\nThe [Settings/Update](./SettingsUpdate.md) dialog is the easiest way for updating. \n\nAlternatively, you can configure [Versatile Buttons](./ConsoleVButtons.md) with similar commands as described in the following, so that update and server restart can be initiated directly from the Web UI.  \n(Note that commands issued through Versatile Buttons execute from the root directory in the virtual environment)\n\nFor update, proceed as follows:    \n(If running a Docker container see [Update Procedure for Docker Container](./SetupDocker.md#update-procedure))\n\n1. Within a SSH session go to the **raspiCamSrv** root directory    \n```cd ~/prg/raspi-cam-srv```\n2. If you have made local changes (e.g. logging), you may need to reset the workspace with   \n```git reset --hard```\n3. If you have created unversioned files, you may need to clean the workspace with   \n```git clean -fd```\n4. Use [git fetch](https://git-scm.com/docs/git-fetch) to update to the latest version     \n(normally you need to fetch only the ```main``` branch)     \n```git fetch origin main --depth=1```    \nAs a result, you will see a summary of changes with respect to the previously installed version.\n5. Use [git reset](https://git-scm.com/docs/git-reset) to reset the current branch head to origin/main    \n```git reset --hard origin/main```    \nAs a result, you will see the new HEAD version.\n6. Restart the service, depending on [how the service was installed](./service_configuration.md)    \n```sudo systemctl restart raspiCamSrv.service```    \nor    \n```systemctl --user restart raspiCamSrv.service```\n7. Check that the service started correctly     \n```sudo journalctl -e```    \nor    \n```journalctl --user -e```\n8. If you used [start with stored configuration](./SettingsConfiguration.md) before updating, you may now try to activate this again.<br>In cases where configuration parameters were not modified with the update, this will usually work.<br>If not, you will need to prepare and store your preferred configuration again.\n\nIn case that the server did not start correctly or if you see an unexpected behavior in the UI, you may have forgotten to deactivate [start with stored configuration](./SettingsConfiguration.md)<br>In this case, you can do the following:\n\n- ```cd ~/prg/raspi-cam-srv/raspiCamSrv/static/config```\n- Check whether a file ```_loadConfigOnStart.txt``` exists in this folder.\n- If it exists, remove it:<br>```rm _loadConfigOnStart.txt```\n- Then repeat step 4, above\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: raspiCamSrv V4.10.x Documentation\nplugins:\n    - search\n    - mike\ntheme:\n  name: material\n  favicon: img/favicon.ico\n  features:\n    - content.code.copy\n\nnot_in_nav: |\n  Z_Legacy*.md\n\nnav:\n  - Home: index.md\n  - Getting Started: \n      - Overview: getting_started_overview.md\n      - Requirements: requirements.md\n      - System Setup: system_setup.md\n      - Installation: installation.md\n      - Alternatives:\n          - Manual Step by Step Installation: installation_man.md\n          - Service Configuration: service_configuration.md\n          - Running raspiCamSrv as Docker container: SetupDocker.md\n          - Setup of Raspberry Pi Zero as Standalone System: bp_PiZero_Standalone.md\n          - Configuring as Hotspot:\n              - Hotspot Setup for Trixie: bp_Hotspot_Trixie.md\n              - Hotspot Setup for Boookworm: bp_Hotspot_Bookworm.md\n              - Hotspot Setup for Bullseye: bp_Hotspot_Bullseye.md\n      - Trouble Shooting Guide: Troubelshooting.md\n  - Features: features.md\n  - References:\n      - Picamera2 Manual: picamera2_manual.md\n  - Releases:\n      - Release Notes: ReleaseNotes.md\n      - Updating a raspiCamSrv Installation: updating_raspiCamSrv.md\n  - User Guide:\n      - Overview: UserGuide.md\n      - Authentication: Authentication.md\n      - Live Menu:\n          - Live Screen: LiveScreen.md\n          - Camera Controls:\n               - Overview: CameraControls.md\n               - Focus Handling: FocusHandling.md\n               - Zoom/Pan/Tilt: ZoomPan.md\n               - Auto-Exposure: CameraControls_AutoExposure.md\n               - Exposure: CameraControls_Exposure.md\n               - Image: CameraControls_Image.md\n               - Ctrl: CameraControls_Ctrl.md\n               - Camera Controls for USB Cameras: CameraControls_UsbCams.md\n               - Cropping: ScalerCrop.md\n          - Direct Control Panel: LiveDirectControl.md\n      - Photo / Video Taking: Phototaking.md\n      - Config Menu:\n          - raspiCamSrv Camera Configuration: Configuration.md\n          - raspiCamSrv Camera AI Configuration: Configuration_AI.md\n          - Tuning: Tuning.md\n      - Info Menu:\n          - Overview: Information.md\n          - System Information: Information_Sys.md\n          - Installed Cameras: Information_Cam.md\n          - Active Camera Properties: Information_CamPrp.md\n          - Active Camera Sensor Modes: Information_Sensor.md\n      - Photos Menu:\n          - Photo Viewer: PhotoViewer.md\n      - Photo Series Menu:\n          - Photo Series: PhotoSeries.md\n          - Timelapse Series: PhotoSeriesTimelapse.md\n          - Exposure Series: PhotoSeriesExp.md\n          - Focus Stack Series: PhotoSeriesFocus.md\n      - Trigger Menu:\n          - Overview: TriggerOverview.md\n          - Introduction: Trigger.md\n          - Event Handling Control: TriggerControl.md\n          - Trigger Configuration: TriggerTriggers.md\n          - Action Configuration: TriggerActions.md\n          - Trigger/Action Configuration: TriggerTriggerActions.md\n          - Motion Detection Configuration: TriggerMotion.md\n          - Camera Action Configuration: TriggerCameraActions.md\n          - Notification Configuration: TriggerNotification.md\n          - Active Motion Capturing: TriggerActive.md\n          - Event Viewer: TriggerEventViewer.md\n          - Event Calendar: TriggerCalendar.md\n      - Cam Menu:\n          - Overview: Cam.md\n          - Web Cam Dialog: CamWebcam.md\n          - Multi-Cam Dialog: CamMulticam.md\n          - Camera Calibration Dialog: CamCalibration.md\n          - Stereo Camera Dialog: CamStereo.md\n      - Console Menu:\n          - Overview: Console.md\n          - Versatile Buttons: ConsoleVButtons.md\n          - Action Buttons: ConsoleActionButtons.md\n      - Settings Menu:\n          - Settings Parameters: Settings.md\n          - Configuration Management: SettingsConfiguration.md\n          - User Management: SettingsUsers.md\n          - API Settings: SettingsAPI.md\n          - Settings for Versatile Buttons: SettingsVButtons.md\n          - Settings for Action Buttons: SettingsAButtons.md\n          - Settings for Live Buttons: SettingsLButtons.md\n          - GPIO Device Configuration: SettingsDevices.md\n          - GPIO Devices:\n              - Stepper Motor: gpioDevices/StepperMotor.md\n              - Servo Motor: gpioDevices/ServoPWM.md\n          - Update: SettingsUpdate.md\n      - AI Camera Support: AiCameraSupport.md\n      - Background Processes: Background Processes.md\n  - No Camera Mode:\n      - User Guide: UserGuide_NoCam.md\n      - System Information: Information_Sys.md\n      - Settings: Settings_NoCam.md\n      - Configuration Management: SettingsConfiguration_NoCam.md\n\n  - API: API.md\n  - Tutorials:\n      - Overview: tutorials/Tutorials_Overview.md\n      - Automatic White Balance with Neural Networks: tutorials/AWB_with_neural_networks.md\n\n\n\n\n"
  },
  {
    "path": "raspiCamSrv/__init__.py",
    "content": "import os\nfrom pathlib import Path\nfrom flask import Flask\nimport logging\nfrom flask.logging import default_handler\nfrom picamera2 import Picamera2\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.motionDetector import MotionDetector\nfrom raspiCamSrv.triggerHandler import TriggerHandler\nimport json\nimport datetime\nimport time\nfrom werkzeug.serving import is_running_from_reloader\n\ndef create_app(test_config=None):\n    # create and configure the app\n    app = Flask(__name__, instance_relative_config=True)\n    app.config.from_mapping(\n        SECRET_KEY=\"dev\",\n        DATABASE=os.path.join(app.instance_path, \"raspiCamSrv.sqlite\"),\n    )\n\n    # ensure the instance folder exists\n    try:\n        os.makedirs(app.instance_path)\n    except OSError:\n        pass\n\n    # Configure loggers\n    logsPath = os.path.dirname(app.instance_path) + \"/logs\"\n    os.makedirs(logsPath, exist_ok=True)\n    logFile = logsPath + \"/raspiCamSrv.log\"\n    Path(logFile).touch(exist_ok=True)\n    filehandler = logging.FileHandler(logFile)\n    filehandler.setFormatter(app.logger.handlers[0].formatter)\n    for logger in (\n        app.logger,\n        logging.getLogger(\"werkzeug\"),\n        logging.getLogger(\"raspiCamSrv.db\"),\n        logging.getLogger(\"raspiCamSrv.auth\"),\n        logging.getLogger(\"raspiCamSrv.auth_su\"),\n        logging.getLogger(\"raspiCamSrv.camCfg\"),\n        logging.getLogger(\"raspiCamSrv.camera_pi\"),\n        logging.getLogger(\"raspiCamSrv.config\"),\n        logging.getLogger(\"raspiCamSrv.home\"),\n        logging.getLogger(\"raspiCamSrv.images\"),\n        logging.getLogger(\"raspiCamSrv.info\"),\n        logging.getLogger(\"raspiCamSrv.settings\"),\n        logging.getLogger(\"raspiCamSrv.photoseries\"),\n        logging.getLogger(\"raspiCamSrv.photoseriesCfg\"),\n        logging.getLogger(\"raspiCamSrv.trigger\"),\n        logging.getLogger(\"raspiCamSrv.motionDetector\"),\n        logging.getLogger(\"raspiCamSrv.motionAlgoIB\"),\n        logging.getLogger(\"raspiCamSrv.triggerHandler\"),\n        logging.getLogger(\"raspiCamSrv.gpioDevices\"),\n        logging.getLogger(\"raspiCamSrv.webcam\"),\n        logging.getLogger(\"raspiCamSrv.console\"),\n        logging.getLogger(\"raspiCamSrv.sun\"),\n        logging.getLogger(\"raspiCamSrv.api\"),\n        logging.getLogger(\"raspiCamSrv.stereoCam\"),\n    ):\n        logger.setLevel(logging.ERROR)\n\n    # >>>>> Uncomment the following line in order to log to the log file\n    # app.logger.addHandler(filehandler)\n\n    # >>>>> Explicitely set specific log levels. Leave \"werkzeug\" at INFO\n    logging.getLogger(\"werkzeug\").setLevel(logging.INFO)\n    # logging.getLogger(\"raspiCamSrv.auth\").setLevel(logging.ERROR)\n    # logging.getLogger(\"raspiCamSrv.camCfg\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.camera_pi\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.home\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.images\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.webcam\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.trigger\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.photoseriesCfg\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.photoseries\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.sun\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.motionDetector\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.motionAlgoIB\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.triggerHandler\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.settings\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.console\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.api\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.stereoCam\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.info\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.config\").setLevel(logging.DEBUG)\n    # logging.getLogger(\"raspiCamSrv.gpioDevices\").setLevel(logging.DEBUG)\n\n    # >>>>> Set log level for picamera2 (DEBUG, INFO, WARNING, ERROR)\n    Picamera2.set_logging(logging.ERROR)\n    # >>>>> Uncomment the following line to let Picamera2 log to the log file\n    # logging.getLogger(\"picamera2\").addHandler(filehandler)\n\n    # >>>>> Set log level for libcamera (0:DEBUG, 1:INFO, 2:WARNING, 3:ERROR, 4:FATAL)\n    os.environ[\"LIBCAMERA_LOG_LEVELS\"] = \"*:3\"\n\n    # Configure the logger for generation of program code\n    # This logger generates an executable Picamera2 Python application program\n    # including the entire interaction with Picamera2 during a server run\n    prgOutPath = os.path.dirname(app.instance_path) + \"/output\"\n    os.makedirs(prgOutPath, exist_ok=True)\n    prgLogger = logging.getLogger(\"pc2_prg\")\n    prgLogPath = os.path.dirname(app.instance_path) + \"/logs\"\n    prgLogTime = datetime.datetime.now()\n    prgLogFilename = \"prgLog_\" + prgLogTime.strftime(\"%Y%m%d_%H%M%S\") + \".log\"\n    prgLogFile = prgLogPath+ \"/\" + prgLogFilename\n    # >>>>> Uncomment the following 5 lines when code generation is activated (see below)\n    # Path(prgLogFile).touch(exist_ok=True)\n    # prgFilehandler = logging.FileHandler(prgLogFile)\n    # prgFormatter = logging.Formatter('%(message)s')\n    # prgFilehandler.setFormatter(prgFormatter)\n    # prgLogger.addHandler(prgFilehandler)\n    # >>>>> To activate Python code generation, set level to DEBUG\n    # prgLogger.setLevel(logging.DEBUG)\n\n    if test_config is None:\n        # load the instance config, if it exists, when not testing\n        app.config.from_pyfile(\"config.py\", silent=True)\n    else:\n        # load the test config if passed in\n        app.config.from_mapping(test_config)\n\n    # Make database available in the application context\n    from . import db\n    db.init_app(app)\n\n    # Configure Config\n    from . import camCfg\n    from . import settings\n    cfg = camCfg.CameraCfg()\n    sc = cfg.serverConfig\n    sc.photoRoot = app.static_folder\n    sc.prgOutputPath = prgOutPath\n    sc.checkEnvironment()\n    # Wait for system time syncronization\n    sc.wait_for_time_sync()\n    serverStartTime = sc.serverStartTime\n    if sc.supportsExtMotionDetection == False:\n        cfg.triggerConfig.motionDetectAlgos = [\"Mean Square Diff\",]\n    cfgPath = app.static_folder + \"/config\"\n    sc.cfgPath = cfgPath\n    if settings.getLoadConfigOnStart(cfgPath):\n        cfg.loadConfig(cfgPath)\n    cfg = camCfg.CameraCfg()\n    sc = cfg.serverConfig\n    sc.serverStartTime = serverStartTime\n    sc.cfgPath = cfgPath\n    sc.cfgBackupPath = os.path.dirname(app.instance_path) + \"/backups\"\n    sc.checkEnvironment()\n    sc.database = os.path.join(app.instance_path, \"raspiCamSrv.sqlite\")\n    stc = cfg.stereoCfg\n    stc.calibPhotosPath = app.static_folder + \"/calib_photos/\"\n    stc.calibPhotosSubPath = \"calib_photos/\"\n    stc.calibDataSubPath = \"calib_data/\"\n    stc.calibDataFile = \"calib_params.xml\"\n    cam = Camera()\n    cfg.setSupportedCameras()\n    cfg.setPiCameras()\n    # For testiing multi-camera features:\n    # sc.piCameras.pop(1)\n\n    # Check display photo and buffer\n    sc.displayBufferCheck()\n\n    # Check Latest version\n    sc.getLatestVersion(now=True)\n    \n    # Configure Triggered Capture\n    tcActionPath = app.static_folder + \"/events\"\n    os.makedirs(tcActionPath, exist_ok=True)\n    tc = cfg.triggerConfig\n    tc.actionPath = tcActionPath\n    Path(tc.logFilePath).touch(exist_ok=True)\n\n    # Configure Photoseries\n    from . import photoseriesCfg\n    tlRootPath = app.static_folder + \"/photoseries\"\n    os.makedirs(tlRootPath, exist_ok=True)\n    tlCfg = photoseriesCfg.PhotoSeriesCfg()\n    tlCfg.rootPath = tlRootPath\n    tlCfg.initFromTlFolder()\n    tlCfg = photoseriesCfg.PhotoSeriesCfg()\n\n    # Restart an active series if requested\n    if tlCfg.hasCurSeries:\n        sr = tlCfg.curSeries\n        if sr.status == \"ACTIVE\":\n            if sr.isExposureSeries == False \\\n            and sr.isFocusStackingSeries == False:\n                if sr.continueOnServerStart == True:\n                    sr.nextStatus(\"pause\")\n                    # Start live stream in order to load lowres config for later live stream compatibility\n                    Camera().startLiveStream()\n                    time.sleep(2)\n                    Camera().startPhotoSeries(sr)\n                    time.sleep(2)\n                    if sc.error is None and sr.error is None:\n                        sr.nextStatus(\"start\")\n                else:\n                    sr.nextStatus(\"pause\")\n            else:\n                sr.nextStatus(\"pause\")\n\n    # Autostart triggered capture, if configured\n    if tc.operationAutoStart == True:\n        if tc.triggeredByMotion == True:\n            MotionDetector().startMotionDetection()\n            sc.isTriggerRecording = True\n        if tc.triggeredByEvents == True:\n            TriggerHandler().start()\n            sc.isEventhandling = True\n\n    # Register required blueprints\n    from . import auth\n    app.register_blueprint(auth.bp)\n\n    from . import home\n    app.register_blueprint(home.bp)\n    app.add_url_rule(\"/\", endpoint=\"index\")\n\n    from . import config\n    app.register_blueprint(config.bp)\n\n    from . import images\n    app.register_blueprint(images.bp)\n\n    from . import info\n    app.register_blueprint(info.bp)\n\n    from . import settings\n    app.register_blueprint(settings.bp)\n\n    from . import photoseries\n    app.register_blueprint(photoseries.bp)\n\n    from . import trigger\n    app.register_blueprint(trigger.bp)\n\n    from . import webcam\n    app.register_blueprint(webcam.bp)\n\n    from . import console\n    app.register_blueprint(console.bp)\n\n    if sc.useAPI == True:\n        from . import api\n        app.register_blueprint(api.bp)\n\n        from flask_jwt_extended import JWTManager    \n\n        if sc.jwtAuthenticationActive == False:\n            sc.API_active = False\n        else:\n            sc.API_active = True\n            app.config[\"JWT_SECRET_KEY\"] = cfg.secrets.jwtSecretKey\n            if sc.jwtAccessTokenExpirationMin > 0:\n                app.config[\"JWT_ACCESS_TOKEN_EXPIRES\"] = datetime.timedelta(minutes=sc.jwtAccessTokenExpirationMin)\n            if sc.jwtRefreshTokenExpirationDays > 0:\n                app.config[\"JWT_REFRESH_TOKEN_EXPIRES\"] = datetime.timedelta(days=sc.jwtRefreshTokenExpirationDays)\n            jwt = JWTManager(app)\n\n    return app\n"
  },
  {
    "path": "raspiCamSrv/api.py",
    "content": "from flask import Blueprint, request, jsonify\nfrom werkzeug.security import check_password_hash\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.db import get_db\n\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg, TuningConfig\nfrom raspiCamSrv.photoseriesCfg import PhotoSeriesCfg\nfrom _thread import get_ident\nimport datetime\nimport time\nfrom raspiCamSrv.motionDetector import MotionDetector\nfrom raspiCamSrv.triggerHandler import TriggerHandler\nfrom raspiCamSrv.version import version\nfrom raspiCamSrv.home import generateHistogram\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\n# Try to import flask_jwt_extended to avoid errors when upgrading to V2.11 from earlier versions\ntry:\n    from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity\nexcept ImportError:\n    pass\n\n\nbp = Blueprint(\"api\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route('/api/login', methods=['POST'])\ndef login():\n    data = request.get_json()\n    username = data.get(\"username\")\n    password = data.get(\"password\")\n\n    db = get_db()\n    error = None\n    user = db.execute(\n        \"SELECT * FROM user WHERE username = ?\", (username,)\n    ).fetchone()\n\n    if user is None:\n        error = \"Invalid username or password\"\n    elif not check_password_hash(user[\"password\"], password):\n        error = \"Invalid username or password\"\n\n    if error is None:\n        if len(user) == 5:\n            if user[\"isinitial\"] == 1:\n                error = \"Password change required. Please log in through UI!\"\n\n    if error is None:\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n\n        access_token = create_access_token(identity=username)\n        if sc.jwtAccessTokenExpirationMin == 0:\n            return jsonify(access_token=access_token)\n        else:\n            refresh_token = create_refresh_token(identity=username)\n            return jsonify(access_token=access_token, refresh_token=refresh_token)\n    return jsonify({\"error\": error}), 401\n\n@bp.route('/api/refresh', methods=['POST'])\n@jwt_required(refresh=True) \ndef refresh():\n    current_user = get_jwt_identity()\n    new_access_token = create_access_token(identity=current_user)\n    return jsonify(access_token=new_access_token)\n\n@bp.route('/api/protected', methods=['GET'])\n@jwt_required()\ndef protected():\n    current_user = get_jwt_identity()\n    return jsonify(message=f\"Hello, {current_user}! You accessed a protected route.\")\n\n@bp.route(\"/api/take_photo\", methods=[\"GET\"])\n@jwt_required()\ndef take_photo():\n    logger.debug(\"Thread %s: In /api/take_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    timeImg = datetime.datetime.now()\n    filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n    logger.debug(\"Saving image %s\", filename)\n    fp = Camera().takeImage(filename)\n    if not sc.error:\n        logger.debug(\"take_photo - success\")\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n        msg=f\"Photo taken: {fp}\"\n        return jsonify(message=msg)\n    else:\n        msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/take_photo2\", methods=[\"GET\"])\n@jwt_required()\ndef take_photo2():\n    logger.debug(\"Thread %s: In /api/take_photo2\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image %s\", filename)\n        fp = Camera().takeImage2(filename)\n        if not sc.errorc2:\n            logger.debug(\"take_photo2 - success\")\n            msg=f\"Photo taken: {fp}\"\n            return jsonify(message=msg)\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"take_photo2 - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/take_photo_both\", methods=[\"GET\"])\n@jwt_required()\ndef take_photo_both():\n    logger.debug(\"Thread %s: In /api/take_photo_both\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image2 %s\", filename)\n        fp1 = Camera().takeImage(filename)\n        fp2 = Camera().takeImage2(filename)\n        msg = {}\n        err = False\n        if not sc.error:\n            logger.debug(\"takeImage - success\")\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg[\"Photo1\"] = fp1\n        else:\n            msg[\"Photo1\"] = \"Error in \" + sc.errorcSource + \": \" + sc.errorc\n            err = True\n        if not sc.errorc2:\n            logger.debug(\"takeImage2 - success\")\n            msg[\"Photo2\"] = fp2\n        else:\n            msg[\"Photo2\"] = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            err = True\n        if not err:\n            return jsonify(message=msg)\n        else:\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"take_photo_both - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/take_raw_photo\", methods=[\"GET\"])\n@jwt_required()\ndef take_raw_photo():\n    logger.debug(\"Thread %s: In /api/take_raw_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    timeImg = datetime.datetime.now()\n    filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n    filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n    logger.debug(\"Saving raw image %s\", filenameRaw)\n    fp = Camera().takeRawImage(filenameRaw, filename)\n    if not sc.error:\n        logger.debug(\"take_raw_photo - success\")\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n        msg = f\"Raw photo taken: {fp}\"\n        return jsonify(message=msg)\n    else:\n        msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/take_raw_photo2\", methods=[\"GET\"])\n@jwt_required()\ndef take_raw_photo2():\n    logger.debug(\"Thread %s: In /api/take_raw_photo2\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n        logger.debug(\"Saving raw image %s\", filenameRaw)\n        fp = Camera().takeRawImage2(filenameRaw, filename)\n        if not sc.error:\n            logger.debug(\"take_raw_photo2 - success\")\n            msg=f\"Raw photo taken: {fp}\"\n            return jsonify(message=msg)\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"take_raw_photo2 - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/take_raw_photo_both\", methods=[\"GET\"])\n@jwt_required()\ndef take_raw_photo_both():\n    logger.debug(\"Thread %s: In /api/take_raw_photo_both\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n        logger.debug(\"Saving raw images %s\", filenameRaw)\n        fp1 = Camera().takeRawImage(filenameRaw, filename)\n        fp2 = Camera().takeRawImage2(filenameRaw, filename)\n        msg = {}\n        err = False\n        if not sc.error:\n            logger.debug(\"takeRawImage - success\")\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg[\"Photo1\"] = fp1\n        else:\n            msg[\"Photo1\"] = \"Error in \" + sc.errorcSource + \": \" + sc.errorc\n            err = True\n        if not sc.errorc2:\n            logger.debug(\"takeRawImage2 - success\")\n            msg[\"Photo2\"] = fp2\n        else:\n            msg[\"Photo2\"] = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            err = True\n        if not err:\n            return jsonify(message=msg)\n        else:\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"take_raw_photo_both - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/start_triggered_capture\", methods=[\"GET\"])\n@jwt_required()\ndef start_triggered_capture():\n    logger.debug(\"In /api/start_triggered_capture\")\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.triggeredByMotion \\\n    or tc.triggeredByEvents:\n        if tc.triggeredByMotion:\n            MotionDetector().setAlgorithm()\n            MotionDetector().startMotionDetection()\n        if tc.triggeredByEvents:\n            TriggerHandler().start()\n        if sc.error:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            return jsonify(message=msg), 500\n        elif tc.error:\n            msg = \"Error in \" + tc.errorSource + \": \" + tc.error\n            return jsonify(message=msg), 500\n        else:\n            if tc.triggeredByMotion:\n                sc.isTriggerRecording = True\n                msg = \"Motion detection started\"\n            if tc.triggeredByEvents:\n                sc.isEventhandling = True\n                msg = \"Event handling started\"\n            if tc.triggeredByMotion \\\n            and tc.triggeredByEvents:\n                msg = \"Motion detection and event handlinfg started\"\n            return jsonify(message=msg)\n    else:\n        msg = \"There is no trigger activated\"\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/stop_triggered_capture\", methods=[\"GET\"])\n@jwt_required()\ndef stop_triggered_capture():\n    logger.debug(\"In /api/stop_triggered_capture\")\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.triggeredByMotion \\\n    or tc.triggeredByEvents:\n        if sc.isTriggerRecording:\n            MotionDetector().stopMotionDetection()\n            sc.isTriggerRecording = False\n            msg = \"Motion detection stopped\"\n        if sc.isEventhandling:\n            TriggerHandler().stop()\n            sc.isEventhandling = False\n            msg = \"Event handling stopped\"\n        if sc.isTriggerRecording \\\n        and sc.isEventhandling:\n            msg = \"Motion detection and event handling stopped\"\n        return jsonify(message=msg)\n    else:\n        msg = \"There is no trigger activated\"\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/info\", methods=[\"GET\"])\n@jwt_required()\ndef info():\n    logger.debug(\"In /api/info\")\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    \n    info = {}\n    info[\"version\"] = \"raspiCamSrv \" + version\n    info[\"server\"] = request.host\n    info[\"active_camera\"] = sc.activeCameraInfo\n    infoCams = []\n    cams = cfg.cameras\n    logger.debug(\"/api/info - cams: %s\", cams)\n    for cam in cams:\n        infoCam = {}\n        infoCam[\"num\"] = cam.num\n        infoCam[\"model\"] = cam.model\n        infoCam[\"is_usb\"] = cam.isUsb\n        if cam.num == sc.activeCamera:\n            infoCam[\"active\"] = True\n        else:\n            infoCam[\"active\"] = False\n        infoCam[\"status\"] = Camera.cameraStatus(cam.num)\n        infoCams.append(infoCam)\n    info[\"cameras\"] = infoCams\n    infoStatus = {}\n    infoStatus[\"livestream_active\"] = sc.isLiveStream\n    if len(cams) > 1:\n        infoStatus[\"livestream2_active\"] = sc.isLiveStream2\n    infoStatus[\"photoseries_recording\"] = sc.isPhotoSeriesRecording\n    infoStatus[\"motion_capturing\"] = sc.isTriggerRecording == True and tc.triggeredByMotion == True\n    infoStatus[\"event_handling\"] = sc.isEventhandling == True and sc.isEventsWaiting == False\n    infoStatus[\"video_recording\"] = sc.isVideoRecording\n    infoStatus[\"audio_recording\"] = sc.isAudioRecording\n    if len(cams) > 1:\n        infoStatus[\"video_recording2\"] = sc.isVideoRecording2\n    info[\"operation_status\"] = infoStatus\n    \n    return jsonify(message=info)\n\n@bp.route(\"/api/switch_cameras\", methods=[\"GET\"])\n@jwt_required()\ndef switch_cameras():\n    logger.debug(\"In /api/switch_cameras\")\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    msg = None\n    cs = cfg.cameras\n    activeCam = sc.activeCamera\n    newCam = activeCam\n    for cm in cs:\n        if cm.isUsb == False:\n            if activeCam != cm.num:\n                newCam = cm.num\n                newCamInfo = \"Camera \" + str(cm.num) + \" (\" + cm.model + \")\"\n                newCamModel = cm.model\n                break\n    if newCam != sc.activeCamera:\n        if sc.isTriggerRecording:\n            msg = \"Cameras cannot be switched because triggered capturing is active\"\n        if sc.isVideoRecording == True:\n            msg = \"Cameras cannot be switched because trigvideorecording is active\"\n        if sc.isPhotoSeriesRecording:\n            msg = \"Cameras cannot be switched because photo series recording is active\"\n        if not msg:\n            sc.activeCameraInfo = newCamInfo\n            sc.activeCameraModel = newCamModel\n            cfg.liveViewConfig.stream_size = None\n            cfg.photoConfig.stream_size = None\n            cfg.rawConfig.stream_size = None\n            cfg.videoConfig.stream_size = None\n            sc.activeCamera = newCam\n            strCfg = cfg.streamingCfg\n            newCamStr = str(newCam)\n            if newCamStr in strCfg:\n                ncfg = strCfg[newCamStr]\n                if \"tuningconfig\" in ncfg:\n                    cfg.tuningConfig = ncfg[\"tuningconfig\"]\n                else:\n                    cfg.tuningConfig = TuningConfig()\n            else:\n                cfg.tuningConfig = TuningConfig()\n            Camera.switchCamera()\n            if sc.isLiveStream2:\n                str2 = cfg.streamingCfg[str(Camera().camNum2)]\n            logger.debug(\"/api/switch_cameras - active camera set to %s\", sc.activeCamera)\n    else:\n        msg = \"No other camera available\"\n    if msg:\n        return jsonify(message=msg), 500\n    else:\n        msg = \"Camera switch successful\"\n        return jsonify(message=msg)\n\n@bp.route(\"/api/record_video\", methods=[\"GET\"])\n@jwt_required()\ndef record_video():\n    logger.debug(\"Thread %s: In /api/record_video\", get_ident())\n    data = request.get_json()\n    duration = 0\n    if \"duration\" in data:\n        duration = data.get(\"duration\")\n    logger.debug(\"Thread %s: /api/record_video - requested duration: %s\", get_ident(), duration)\n\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    timeImg = datetime.datetime.now()\n    filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n    filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n    logger.debug(\"Recording a video %s\", filenameVid)\n    fp = Camera().recordVideo(filenameVid, filename, duration)\n    time.sleep(4)\n    if not sc.error:\n        # Check whether video is being recorded\n        if Camera.isVideoRecording():\n            logger.debug(\"Video recording started\")\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            sc.isVideoRecording = True\n            if sc.recordAudio:\n                sc.isAudioRecording = True\n            msg=\"Video recorded to \" + fp\n            return jsonify(message=msg)\n        else:\n            logger.debug(\"Video recording did not start\")\n            sc.isVideoRecording = False\n            sc.isAudioRecording = False\n            msg=\"Video recording failed. Requested resolution too high\"\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/stop_video\", methods=[\"GET\"])\n@jwt_required()\ndef stop_video():\n    logger.debug(\"Thread %s: In /api/stop_video\", get_ident())\n\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    if sc.isVideoRecording == True:\n        fp = Camera().videoOutput\n        Camera().stopVideoRecording()\n        time.sleep(1)\n        msg = {\"Video\": fp, \"Status\": \"Stopped\"}\n        sc.isVideoRecording = False\n        return jsonify(message=msg)\n    else:\n        msg = \"No video recording in progress\"\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/record_video2\", methods=[\"GET\"])\n@jwt_required()\ndef record_video2():\n    logger.debug(\"Thread %s: In /api/record_video2\", get_ident())\n    if Camera().isCamera2Available():\n        data = request.get_json()\n        duration = 0\n        if \"duration\" in data:\n            duration = data.get(\"duration\")\n        logger.debug(\"Thread %s: /api/record_video2 - requested duration: %s\", get_ident(), duration)\n\n        cfg = CameraCfg()\n        cc = cfg.controls\n        sc = cfg.serverConfig\n        cp = cfg.cameraProperties\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording a video %s\", filenameVid)\n        fp = Camera().recordVideo2(filenameVid, filename, duration)\n        time.sleep(4)\n        if not sc.errorc2:\n            # Check whether video is being recorded\n            if Camera.isVideoRecording2():\n                logger.debug(\"Video recording 2 started\")\n                sc.isVideoRecording2 = True\n                msg = \"Video recorded to \" + fp\n                return jsonify(message=msg)\n            else:\n                logger.debug(\"Video recording 2 did not start\")\n                sc.isVideoRecording2 = False\n                msg = \"Video recording failed. Requested resolution too high\"\n                return jsonify(message=msg), 500\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"record_video2 - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/stop_video2\", methods=[\"GET\"])\n@jwt_required()\ndef stop_video2():\n    logger.debug(\"Thread %s: In /api/stop_video2\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        if sc.isVideoRecording2 == True:\n            fp = Camera().videoOutput2\n            Camera().stopVideoRecording2()\n            time.sleep(1)\n            msg = {\"Video\": fp, \"Status\": \"Stopped\"}\n            sc.isVideoRecording2 = False\n            return jsonify(message=msg)\n        else:\n            msg = \"No video recording in progress\"\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"stop_video2 - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/record_video_both\", methods=[\"GET\"])\n@jwt_required()\ndef record_video_both():\n    logger.debug(\"Thread %s: In /api/record_video_both\", get_ident())\n    if Camera().isCamera2Available():\n        data = request.get_json()\n        duration = 0\n        if \"duration\" in data:\n            duration = data.get(\"duration\")\n        logger.debug(\"Thread %s: /api/record_video_both - requested duration: %s\", get_ident(), duration)\n\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording 2 videos %s\", filenameVid)\n        fp1 = Camera().recordVideo(filenameVid, filename, duration)\n        fp2 = Camera().recordVideo2(filenameVid, filename, duration)\n        time.sleep(4)\n        msg = {}\n        err = False\n        if not sc.error:\n            # Check whether video is being recorded\n            if Camera.isVideoRecording():\n                logger.debug(\"Video recording 1 started\")\n                if sc.displayContent == \"hist\":\n                    if sc.displayHistogram is None:\n                        if sc.displayPhoto:\n                            generateHistogram(sc)\n                sc.isVideoRecording = True\n                msg[\"Video 1\"] = fp1\n            else:\n                logger.debug(\"Video recording 1 did not start\")\n                sc.isVideoRecording = False\n                msg[\"Video 1\"] = \"Video recording failed\"\n                err = True\n        else:\n            err = True\n            msg[\"Video 1\"] = \"Error in \" + sc.errorSource + \": \" + sc.error\n        if not sc.errorc2:\n            # Check whether video is being recorded\n            if Camera.isVideoRecording2():\n                logger.debug(\"Video recording 2 started\")\n                sc.isVideoRecording2 = True\n                msg[\"Video 2\"] = fp2\n            else:\n                logger.debug(\"Video recording 2 did not start\")\n                sc.isVideoRecording2 = False\n                msg[\"Video 2\"] = \"Video recording failed\"\n                err = True\n        else:\n            msg[\"Video 2\"] = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            err = True\n        if err == False:\n            return jsonify(message=msg)\n        else:\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"record_video_both - %s\", msg)\n        return jsonify(message=msg), 500\n\n@bp.route(\"/api/stop_video_both\", methods=[\"GET\"])\n@jwt_required()\ndef stop_video_both():\n    logger.debug(\"Thread %s: In /api/stop_video_both\", get_ident())\n    if Camera().isCamera2Available():\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n\n        msg = {}\n        err = False\n        if sc.isVideoRecording == False:\n            msg[\"Video 1\"] = \"No video recording in progress for camera 1\"\n            err = True\n        else:\n            fp1 = Camera().videoOutput\n            Camera().stopVideoRecording()\n            sc.isVideoRecording = False\n            msg[\"Video 1\"] = {\"Video\": fp1, \"Status\": \"Stopped\"}\n\n        if sc.isVideoRecording2 == False:\n            msg[\"Video 2\"] = \"No video recording in progress for camera 2\"\n            err = True\n        else:\n            fp2 = Camera().videoOutput2\n            Camera().stopVideoRecording2()\n            sc.isVideoRecording2 = False\n            msg[\"Video 2\"] = {\"Video\": fp2, \"Status\": \"Stopped\"}\n\n        time.sleep(1)\n\n        if err == False:\n            return jsonify(message=msg)\n        else:\n            return jsonify(message=msg), 500\n    else:\n        msg = \"Second camera is not available\"\n        logger.error(\"stop_video_both - %s\", msg)\n        return jsonify(message=msg), 500\n\ndef propGen(property):\n    \"\"\"Generator to yield properties of a property separated by dot.\"\"\"\n    while len(property) > 0:\n        p = property.find(\".\")\n        if p >= 0:\n            if p == 0:\n                method = \"\"\n            else:\n                method = property[:p]\n            property = property[p + 1 :]\n        else:\n            method = property\n            property = \"\"\n        params = []\n        ps = method.find(\"(\")\n        if ps >= 0:\n            pe = method.find(\")\", ps)\n            if pe < 0:\n                raise ValueError(\"Missing closing parenthesis in method: \" + method)\n            else:\n                pars = method[ps + 1 : pe]\n                if len(pars) > 0:\n                    params = [p.strip() for p in pars.split(\",\")]\n            method = method[:ps]\n        yield (method, params, len(property) == 0)\n\ndef probeTerm(property):\n    \"\"\"Evaluate a property.\"\"\"\n    logger.debug(\"Thread %s: In probeTerm - property=%s\", get_ident(), property)\n    res = None\n    obj = None\n    for prop, params, last in propGen(property):\n        logger.debug(\"Thread %s: In probeTerm - prop=%s, params=%s, last=%s\", get_ident(), prop, params, last)\n        if obj is None:\n            if len(params) > 0:\n                obj = globals()[prop](**params)\n            else:\n                obj = globals()[prop]()\n            if last == True:\n                res = obj\n            logger.debug(\"Thread %s: In probeTerm - Instantiated %s(%s)\", get_ident(), prop, params)\n        else:\n            if hasattr(obj, prop):\n                method = getattr(obj, prop)\n                if callable(method):\n                    logger.debug(\"Thread %s: In probeTerm - Calling method %s with params %s\", get_ident(), prop, params)\n                    if last == True:\n                        if len(params) > 0:\n                            res = method(*params)\n                        else:\n                            res = method()\n                    else:\n                        if len(params) > 0:\n                            obj = method(*params)\n                        else:\n                            obj = method()\n                else:\n                    logger.debug(\"Thread %s: In probeTerm - Accessing property %s\", get_ident(), prop)\n                    if last == True:\n                        res = method\n                    else:\n                        obj = method\n                if obj is None:\n                    logger.debug(\"Thread %s: In probeTerm - obj is None after accessing %s\", get_ident(), prop)\n                    break\n            else:\n                raise AttributeError(f\"Object {obj} has no attribute {prop}\")\n    try:\n        result = jsonify(res)\n    except TypeError as e:\n        if hasattr(res, \"toDict\"):\n            res = res.toDict()\n        elif hasattr(res, \"__dict__\"):\n            res = res.__dict__\n        else:\n            logger.error(\"Error in probeTerm - jsonify(res), error: %s\", str(e))\n            res = \"Error : \" + str(e)\n    except Exception as e:\n        logger.error(\"Error in probeTerm - jsonify(res), error: %s\", str(e))\n        res = \"Error : \" + str(e)\n    return res\n\n@bp.route(\"/api/probe\", methods=[\"GET\"])\n@jwt_required()\ndef probe():\n    logger.debug(\"Thread %s: In /api/probe\", get_ident())\n    if CameraCfg().serverConfig.useStereo == True:\n        from raspiCamSrv.stereoCam import StereoCam\n    result = {}\n    data = request.get_json()\n    if \"properties\" in data:\n        properties = data.get(\"properties\")\n    else:\n        result[\"error\"] = \"No properties provided\"\n        return jsonify(result), 400\n\n    if len(properties) == 0:\n        result[\"error\"] = \"properties must not be empty\"\n        return jsonify(result), 400\n\n    results = []\n    result[\"results\"] = results\n    for t in properties:\n        property = t[\"property\"]\n        logger.debug(\"Thread %s: In api/probe - property:%2s\", get_ident(), property)\n        res = {}\n        try:\n            res[property] = probeTerm(property)\n        except Exception as e:\n            logger.error(\"Error in api/probe - property: %s, error: %s\", property, str(e))\n            res[\"property\"] = \"ERROR:\" + str(e)\n        results.append(res)\n\n    return jsonify(result)\n"
  },
  {
    "path": "raspiCamSrv/auth.py",
    "content": "import functools\nfrom raspiCamSrv.version import version\n\nfrom flask import (\n    Blueprint,\n    flash,\n    g,\n    redirect,\n    render_template,\n    request,\n    session,\n    url_for,\n)\nfrom werkzeug.security import check_password_hash, generate_password_hash\nfrom raspiCamSrv.camCfg import CameraCfg\n\nfrom raspiCamSrv.db import get_db\nfrom raspiCamSrv.auth_su import superuser_required\nimport logging\n\nbp = Blueprint(\"auth\", __name__, url_prefix=\"/auth\")\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/register\", methods=(\"GET\", \"POST\"))\n@superuser_required\ndef register():\n    logger.debug(\"In register\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.curMenu = \"register\"\n    if request.method == \"POST\":\n        username = request.form[\"username\"]\n        password = request.form[\"password\"]\n        db = get_db()\n        \n        # Get number of registered users\n        nrUsers = 0\n        try:\n            nrUsers = db.execute(\"SELECT COUNT(*) from user\").fetchone()[0]\n        except db.Error as e:\n            logger.error(\"Database error: %s\", e)\n            nrUsers = 0\n        logger.debug(\"Found %s users\", nrUsers)\n            \n        error = None\n        if not username:\n            error = \"Username is required.\"\n        elif not password:\n            error = \"Password is required.\"\n\n        if error is None:\n            if nrUsers == 0:\n                isSuperUser = 1\n                isInitial = 0\n            else:\n                isSuperUser = 0\n                isInitial = 1\n\n            schemaOK = True\n            try:\n                db.execute(\n                    \"INSERT INTO user (username, password, issuperuser, isinitial) VALUES (?, ?, ?, ?)\",\n                    (username, generate_password_hash(password), isSuperUser, isInitial),\n                )\n                db.commit()\n                logger.debug(\"Insert with new schema OK\")\n            except db.IntegrityError:\n                error = f\"User {username} is already registered.\"\n            except db.OperationalError:\n                logger.debug(\"Got OperationalError\")\n                schemaOK = False\n                \n            if not schemaOK:\n                # Try with old db schema\n                logger.debug(\"Traying with old schema\")\n                try:\n                    db.execute(\n                        \"INSERT INTO user (username, password) VALUES (?, ?)\",\n                        (username, generate_password_hash(password)),\n                    )\n                    db.commit()\n                    logger.debug(\"Insert with old schema OK\")\n                except db.IntegrityError:\n                    error = f\"User {username} is already registered.\"\n        if error is None:\n            logger.debug(\"g.user: %s\", g.user)\n            if g.user:\n                return redirect(url_for(\"settings.main\"))\n            else:\n                return redirect(url_for(\"auth.login\"))\n\n        flash(error)\n\n    return render_template(\"auth/register.html\", sc=sc, cp=cp)\n\n\n@bp.route(\"/login\", methods=(\"GET\", \"POST\"))\ndef login():\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.curMenu = \"login\"\n    if request.method == \"POST\":\n        username = request.form[\"username\"]\n        password = request.form[\"password\"]\n        db = get_db()\n        error = None\n        user = db.execute(\n            \"SELECT * FROM user WHERE username = ?\", (username,)\n        ).fetchone()\n\n        if user is None:\n            error = \"Incorrect username.\"\n        elif not check_password_hash(user[\"password\"], password):\n            error = \"Incorrect password.\"\n\n        if error is None:\n            if len(user) == 5:\n                if user[\"isinitial\"] == 1:\n                    return redirect(url_for(\"auth.password\"))\n                else:\n                    session.clear()\n                    session[\"user_id\"] = user[\"id\"]\n                    return redirect(url_for(\"index\"))\n            else:\n                session.clear()\n                session[\"user_id\"] = user[\"id\"]\n                return redirect(url_for(\"index\"))\n            \n        flash(error)\n\n    return render_template(\"auth/login.html\", sc=sc, cp=cp)\n\n@bp.route(\"/password\", methods=(\"GET\", \"POST\"))\ndef password():\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.curMenu = \"password\"\n    if request.method == \"POST\":\n        username = request.form[\"username\"]\n        oldpassword = request.form[\"oldpassword\"]\n        newpassword = request.form[\"newpassword\"]\n        newpassword2 = request.form[\"newpassword2\"]\n        db = get_db()\n        error = None\n        user = db.execute(\n            \"SELECT * FROM user WHERE username = ?\", (username,)\n        ).fetchone()\n\n        if user is None:\n            error = \"Incorrect username.\"\n        elif not check_password_hash(user[\"password\"], oldpassword):\n            error = \"Old password is not correct.\"\n\n        if error is None:\n            if len(newpassword) <= 1:\n                error = \"New password too short. Must be at least 2 characters.\"\n            elif newpassword2 != newpassword:\n                error = \"New password repetition incorrect.\"\n        \n        if error is None:\n            schemaOK = True\n            isInitial = 0\n            try:\n                db.execute(\n                    \"UPDATE user SET password = ?, isinitial = ? WHERE id = ?\",\n                    (generate_password_hash(newpassword), isInitial, user[\"id\"]),\n                )\n                db.commit()\n                logger.debug(\"Update with new schema OK\")\n            except db.IntegrityError as e:\n                error = f\"Error {e} during update.\"\n            except db.OperationalError:\n                logger.debug(\"Got OperationalError\")\n                schemaOK = False\n                \n            if not schemaOK:\n                # Try with old db schema\n                logger.debug(\"Traying with old schema\")\n                try:\n                    db.execute(\n                        \"UPDATE user SET password = ? WHERE id = ?\",\n                        (generate_password_hash(newpassword), user[\"id\"]),\n                    )\n                    db.commit()\n                    logger.debug(\"Update with old schema OK\")\n                except db.IntegrityError as e:\n                    error = f\"Error {e} during update.\"\n        \n        if error is None:\n            return redirect(url_for(\"auth.login\"))\n        else:\n            flash(error)\n    return render_template(\"auth/password.html\", sc=sc, cp=cp)\n\n\n@bp.before_app_request\ndef load_logged_in_user():\n    logger.debug(\"In load_logged_in_user\")\n    user_id = session.get(\"user_id\")\n    logger.debug(\"user_id (session): %s \", user_id)\n\n    if user_id is None:\n        g.user = None\n    else:\n        userdb = (\n            get_db().execute(\"SELECT * FROM user WHERE id = ?\", (user_id,)).fetchone()\n        )\n        logger.debug(\"userdb: %s\", userdb)\n        if userdb == None:\n            g.user = None\n            session.clear()\n        else:\n            user = {}\n            user[\"id\"] = userdb[\"id\"]\n            user[\"username\"] = userdb[\"username\"]\n            if len(userdb) == 5:\n                user[\"issuperuser\"] = userdb[\"issuperuser\"]\n                user[\"isinitial\"] = userdb[\"isinitial\"]\n            else:\n                user[\"issuperuser\"] = 1\n                user[\"isinitial\"] = 0\n            g.user = user\n    logger.debug(\"Current user: %s\", g.user)\n        \n    g.nrUsers = get_db().execute(\"SELECT count(*) FROM user\").fetchone()[0]\n    logger.debug(\"Found %s users\", g.nrUsers)\n    usersdb = get_db().execute(\"SELECT * FROM user\").fetchall()\n    \n    users = []\n    for userdb in usersdb:\n        user = {}\n        user[\"id\"] = userdb[\"id\"]\n        user[\"username\"] = userdb[\"username\"]\n        if len(userdb) == 5:\n            user[\"issuperuser\"] = userdb[\"issuperuser\"]\n            user[\"isinitial\"] = userdb[\"isinitial\"]\n        users.append(user)\n    g.users = users\n    logger.debug(\"g.users: %s\", g.users)        \n\n@bp.route(\"/logout\")\ndef logout():\n    session.clear()\n    return redirect(url_for(\"index\"))\n\ndef login_required(view):\n    @functools.wraps(view)\n    def wrapped_view(**kwargs):\n        logging.getLogger(\"werkzeug\").setLevel(logging.ERROR)\n        if g.user is None:\n            db = get_db()\n            nrUsers = 0\n            try:\n                nrUsers = db.execute(\"SELECT COUNT(*) from user\").fetchone()[0]\n            except db.Error as e:\n                logger.error(\"Database error: %s\", e)\n                nrUsers = 0\n            if nrUsers == 0:\n                return redirect(url_for(\"auth.register\"))\n            else:\n                return redirect(url_for(\"auth.login\"))\n\n        return view(**kwargs)\n\n    return wrapped_view\n\ndef login_for_streaming(view):\n    @functools.wraps(view)\n    def wrapped_view(**kwargs):\n        logging.getLogger(\"werkzeug\").setLevel(logging.ERROR)\n        sc = CameraCfg().serverConfig\n        if sc.requireAuthForStreaming == True:\n            if g.user is None:\n                db = get_db()\n                nrUsers = 0\n                try:\n                    nrUsers = db.execute(\"SELECT COUNT(*) from user\").fetchone()[0]\n                except db.Error as e:\n                    logger.error(\"Database error: %s\", e)\n                    nrUsers = 0\n                if nrUsers == 0:\n                    return redirect(url_for(\"auth.register\"))\n                else:\n                    return redirect(url_for(\"auth.login\"))\n\n        return view(**kwargs)\n\n    return wrapped_view\n"
  },
  {
    "path": "raspiCamSrv/auth_su.py",
    "content": "import functools\n\nfrom flask import (\n    g,\n    redirect,\n    url_for,\n)\nfrom werkzeug.security import check_password_hash, generate_password_hash\nfrom raspiCamSrv.camCfg import CameraCfg\n\nfrom raspiCamSrv.db import get_db\nimport logging\n\nlogger = logging.getLogger(__name__)\n\ndef superuser_required(view):\n    @functools.wraps(view)\n    def wrapped_view(**kwargs):\n        logger.debug(\"superuser_required. g.user: %s\", g.user)\n        if g.user is None:\n            db = get_db()\n            nrUsers = 0\n            try:\n                nrUsers = db.execute(\"SELECT COUNT(*) from user\").fetchone()[0]\n            except db.Error as e:\n                logger.error(\"Database error: %s\", e)\n                nrUsers = 0\n            if nrUsers > 0:\n                logger.debug(\"found %s users. Redirecting to login\", nrUsers)\n                return redirect(url_for(\"auth.login\"))\n        else:\n            if g.user[\"issuperuser\"] == 0:\n                logger.debug(\"Logged-In user is not SuperUser. Redirecting to index\")\n                return redirect(url_for(\"index\"))\n        logger.debug(\"Allowing access\")\n        return view(**kwargs)\n\n    return wrapped_view\n"
  },
  {
    "path": "raspiCamSrv/camCfg.py",
    "content": "import subprocess\r\nimport importlib\r\nfrom subprocess import CalledProcessError\r\nimport json\r\nimport logging\r\nimport os\r\nimport re\r\nfrom ast import literal_eval\r\nfrom pathlib import Path\r\nfrom datetime import datetime\r\nfrom datetime import date\r\nfrom datetime import time\r\nfrom datetime import timedelta\r\nimport raspiCamSrv.dbx as dbx\r\nfrom raspiCamSrv.gpioDeviceTypes import gpioDeviceTypes\r\nfrom raspiCamSrv import versionDoc\r\nfrom raspiCamSrv.version import version as currentVersion\r\nimport smtplib\r\nimport zoneinfo\r\nfrom secrets import token_urlsafe\r\nimport threading\r\nfrom time import sleep\r\nimport importlib\r\nimport requests\r\nimport psutil\r\n\r\nlogger = logging.getLogger(__name__)\r\n\r\nclass GPIODevice():\r\n    def __init__(self):\r\n        self._id = \"\"\r\n        self._usage = \"\"\r\n        self._type = \"\"\r\n        self._params = {}\r\n        self._usedPins = \"\"\r\n        self._isOk = False\r\n        self._docUrl = \"\"\r\n        self._needsCalibration = False\r\n        self._isCalibrating = False\r\n        self._deviceStatePath = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        self._deviceStatePath = sc.cfgPath + \"/device_state\"\r\n        os.makedirs(self._deviceStatePath, exist_ok=True)\r\n        self._deviceStateFile = \"\"\r\n\r\n    @property\r\n    def id(self) -> str:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: str):\r\n        self._id = value\r\n        self._deviceStateFile = self._deviceStatePath + \"/\" + self._id + \".json\"\r\n\r\n    @property\r\n    def usage(self) -> str:\r\n        return self._usage\r\n\r\n    @usage.setter\r\n    def usage(self, value: str):\r\n        self._usage = value\r\n\r\n    @property\r\n    def type(self) -> str:\r\n        return self._type\r\n\r\n    @type.setter\r\n    def type(self, value: str):\r\n        self._type = value\r\n\r\n    @property\r\n    def params(self) -> dict:\r\n        return self._params\r\n\r\n    @params.setter\r\n    def params(self, value: dict):\r\n        self._params = value\r\n\r\n    @property\r\n    def usedPins(self) -> str:\r\n        return self._usedPins\r\n\r\n    @usedPins.setter\r\n    def usedPins(self, value: str):\r\n        self._usedPins = value\r\n\r\n    @property\r\n    def isOk(self) -> bool:\r\n        return self._isOk\r\n\r\n    @isOk.setter\r\n    def isOk(self, value: bool):\r\n        self._isOk = value\r\n\r\n    @property\r\n    def docUrl(self) -> str:\r\n        url = self._docUrl\r\n        if url.find(\"/latest/\") >= 0:\r\n            url = url.replace(\"/latest/\", f\"/{versionDoc.docversion}/\")\r\n        return url\r\n\r\n    @docUrl.setter\r\n    def docUrl(self, value: str):\r\n        self._docUrl = value\r\n\r\n    @property\r\n    def isCalibrating(self) -> bool:\r\n        return self._isCalibrating\r\n\r\n    @isCalibrating.setter\r\n    def isCalibrating(self, value: bool):\r\n        self._isCalibrating = value\r\n\r\n    @property\r\n    def needsCalibration(self) -> bool:\r\n        return self._needsCalibration\r\n\r\n    @needsCalibration.setter\r\n    def needsCalibration(self, value: bool):\r\n        self._needsCalibration = value\r\n        \r\n    def trackState(self, devObject:object) ->bool:\r\n        \"\"\" Track the state of a GPIO device for which calibration is required\r\n\r\n            The device object is expected to have the following attributes:\r\n            - value\r\n            The state is persisted in file\r\n\r\n        Args:\r\n            devObject (object): device object to track\r\n        Returns:\r\n            bool: True if the state is tracked successfully, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"GPIODevice.trackState - entry\")\r\n        res = False\r\n        state = {}\r\n        if self._needsCalibration:\r\n            if hasattr(devObject, \"value\"):\r\n                try:\r\n                    value = getattr(devObject, \"value\")\r\n                    state[\"value\"] = value\r\n                    logger.debug(\"GPIODevice.trackState - tracking value %s in file %s\", value, self._deviceStateFile)\r\n                    with open(self._deviceStateFile, \"w\") as f:\r\n                        json.dump(state, f)\r\n                    res = True\r\n                except Exception as e:\r\n                    logger.error(\"GPIODevice.trackState: Error %s tracking device state: %s\", type(e), e)\r\n        return res\r\n        \r\n    def setState(self, devObject:object) ->bool:\r\n        \"\"\" Set the state of a GPIO device for which calibration is required\r\n\r\n            The device object is expected to have the following attributes:\r\n            - value\r\n            The state is read from file\r\n\r\n        Args:\r\n            devObject (object): device object to track\r\n        Returns:\r\n            bool: True if the state is trasetcked successfully, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"GPIODevice.setState - entry\")\r\n        res = False\r\n        state = {}\r\n        if self._needsCalibration:\r\n            try:\r\n                with open(self._deviceStateFile, \"r\") as f:\r\n                    state = json.load(f)\r\n                    logger.debug(\"GPIODevice.trackState - read from file %s : %s\",self._deviceStateFile, state)\r\n                    if \"value\" in state:\r\n                        setattr(devObject, \"value\", state[\"value\"])\r\n                        res = True\r\n            except FileNotFoundError:\r\n                # If state has not yet been persisted, keep default state\r\n                pass\r\n            except Exception as e:\r\n                logger.error(\"GPIODevice.setState: Error %s setting device state: %s\", type(e), e)  \r\n        return res\r\n        \r\n    def getState(self) -> dict:\r\n        \"\"\" Get the state of a GPIO device for which calibration is required\r\n\r\n            The device object is expected to have the following attributes:\r\n            - value\r\n            The state is read from file\r\n\r\n        Returns:\r\n            dict: The state of the device\r\n        \"\"\"\r\n        logger.debug(\"GPIODevice.getState - entry\")\r\n        state = {}\r\n        if self._needsCalibration:\r\n            try:\r\n                with open(self._deviceStateFile, \"r\") as f:\r\n                    state = json.load(f)\r\n                    logger.debug(\"GPIODevice.getState - read from file %s : %s\",self._deviceStateFile, state)\r\n            except FileNotFoundError:\r\n                # If state has not yet been persisted, keep default state\r\n                pass\r\n            except Exception as e:\r\n                logger.error(\"GPIODevice.getState: Error %s getting device state: %s\", type(e), e)  \r\n        return state\r\n        \r\n    def getUncalibratedState(self) -> dict:\r\n        \"\"\" Get the uncalibrated state of a GPIO device for which calibration is required\r\n\r\n            The device object is expected to have the following attributes:\r\n            - value\r\n            The calibrated state is read from file\r\n            This is then adjusted depending on the calibration type:\r\n            - For devices having anternal state (e.g. a servo), the calibration velue of the device is added\r\n            - For devices without internal state (e.g. a stepper motor), the current state is returned\r\n\r\n        Returns:\r\n            dict: The uncalibrated state of the device\r\n        \"\"\"\r\n        logger.debug(\"GPIODevice.getUncalibratedState - entry\")\r\n        state = {}\r\n        if self._needsCalibration:\r\n            try:\r\n                with open(self._deviceStateFile, \"r\") as f:\r\n                    state = json.load(f)\r\n                    logger.debug(\"GPIODevice.getUncalibratedState - read from file %s : %s\",self._deviceStateFile, state)\r\n            except FileNotFoundError:\r\n                # If state has not yet been persisted, keep default state\r\n                pass\r\n            except Exception as e:\r\n                logger.error(\"GPIODevice.getUncalibratedState: Error %s getting device state: %s\", type(e), e)\r\n\r\n            if \"calibration\" in self.params:\r\n                calibration = self.params[\"calibration\"]\r\n                logger.debug(\"GPIODevice.getUncalibratedState - calibration: %s\", calibration)\r\n                if \"value\" in state:\r\n                    state[\"value\"] += calibration\r\n                    logger.debug(\"GPIODevice.getUncalibratedState - uncalibrated state: %s\", state)\r\n        return state\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        dev = GPIODevice()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(dev, key, value)\r\n            else:\r\n                if key == \"_params\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                setattr(dev, key, value)\r\n        return dev\r\n\r\nclass Trigger():\r\n    def __init__(self):\r\n        self._id = \"\"\r\n        self._source = \"\"\r\n        self._device = \"\"\r\n        self._event = \"\"\r\n        self._params = {}\r\n        self._control = {}\r\n        self._isActive = False\r\n        self._actions = {}\r\n\r\n    @property\r\n    def id(self) -> str:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: str):\r\n        self._id = value\r\n\r\n    @property\r\n    def source(self) -> str:\r\n        return self._source\r\n\r\n    @source.setter\r\n    def source(self, value: str):\r\n        self._source = value\r\n\r\n    @property\r\n    def device(self) -> str:\r\n        return self._device\r\n\r\n    @device.setter\r\n    def device(self, value: str):\r\n        self._device = value\r\n\r\n    @property\r\n    def event(self) -> str:\r\n        return self._event\r\n\r\n    @event.setter\r\n    def event(self, value: str):\r\n        self._event = value\r\n\r\n    @property\r\n    def params(self) -> dict:\r\n        return self._params\r\n\r\n    @params.setter\r\n    def params(self, value: dict):\r\n        self._params = value\r\n\r\n    @property\r\n    def control(self) -> dict:\r\n        return self._control\r\n\r\n    @control.setter\r\n    def control(self, value: dict):\r\n        self._control = value\r\n\r\n    @property\r\n    def isActive(self) -> bool:\r\n        return self._isActive\r\n\r\n    @isActive.setter\r\n    def isActive(self, value: bool):\r\n        self._isActive = value\r\n\r\n    @property\r\n    def actions(self) -> dict:\r\n        return self._actions\r\n\r\n    @actions.setter\r\n    def actions(self, value: dict):\r\n        self._actions = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        trg = Trigger()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(trg, key, value)\r\n            else:\r\n                if key == \"_device\":\r\n                    val = value\r\n                    if value == \"Active Camera\":\r\n                        val = \"CAM-1\"\r\n                    if value == \"Second Camera\":\r\n                        val = \"CAM-2\"\r\n                    value = val\r\n                if key == \"_params\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                if key == \"_control\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                elif key == \"_actions\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                setattr(trg, key, value)\r\n        return trg\r\n\r\nclass Action():\r\n    def __init__(self):\r\n        self._id = \"\"\r\n        self._isActive = False\r\n        self._source = \"\"\r\n        self._device = \"\"\r\n        self._method = \"\"\r\n        self._params = {}\r\n        self._control = {}\r\n\r\n    @property\r\n    def id(self) -> str:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: str):\r\n        self._id = value\r\n\r\n    @property\r\n    def isActive(self) -> bool:\r\n        return self._isActive\r\n\r\n    @isActive.setter\r\n    def isActive(self, value: bool):\r\n        self._isActive = value\r\n\r\n    @property\r\n    def source(self) -> str:\r\n        return self._source\r\n\r\n    @source.setter\r\n    def source(self, value: str):\r\n        self._source = value\r\n\r\n    @property\r\n    def device(self) -> str:\r\n        return self._device\r\n\r\n    @device.setter\r\n    def device(self, value: str):\r\n        self._device = value\r\n\r\n    @property\r\n    def method(self) -> str:\r\n        return self._method\r\n\r\n    @method.setter\r\n    def method(self, value: str):\r\n        self._method = value\r\n\r\n    @property\r\n    def params(self) -> dict:\r\n        return self._params\r\n\r\n    @params.setter\r\n    def params(self, value: dict):\r\n        self._params = value\r\n\r\n    @property\r\n    def control(self) -> dict:\r\n        return self._control\r\n\r\n    @control.setter\r\n    def control(self, value: dict):\r\n        self._control = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        act = Action()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(act, key, value)\r\n            else:\r\n                if key == \"_params\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                elif key == \"_control\":\r\n                    newval = {}\r\n                    for pkey, pvalue in value.items():\r\n                        if type(pvalue) is list:\r\n                            newval[pkey] = tuple(pvalue)\r\n                        else:\r\n                            newval[pkey] = pvalue\r\n                    value = newval\r\n                setattr(act, key, value)\r\n        return act\r\n\r\nclass TriggerConfig():\r\n    motionDetectAlgos = [\"Mean Square Diff\", \"Frame Differencing\", \"Optical Flow\", \"Background Subtraction\"]\r\n    videoRecorders = [\"Normal\", \"Circular\"]\r\n    backgroundSubtractionModels = [\"MOG2\", \"KNN\"]\r\n    def __init__(self):\r\n        self._triggeredByMotion = True\r\n        self._triggeredBySound = False\r\n        self._triggeredByEvents = False\r\n        self._actionVideo = True\r\n        self._actionPhoto = True\r\n        self._actionNotify = False\r\n        self._operationStartMinute: int = 0\r\n        self._operationEndMinute: int = 1439\r\n        self._operationWeekdays = {\"1\":True, \"2\":True, \"3\":True, \"4\":True, \"5\":True, \"6\":True, \"7\":True}\r\n        self._operationAutoStart = False\r\n        self._detectionDelaySec = 0\r\n        self._detectionPauseSec = 20\r\n        self._motionDetectAlgo = 1\r\n        self._motionRefTit = \"\"\r\n        self._motionRefURL = \"\"\r\n        self._msdThreshold = 10\r\n        self._bboxThreshold = 400\r\n        self._nmsThreshold = 0.001\r\n        self._motionThreshold = 1\r\n        self._useRoI = False\r\n        self._regionOfNoInterest = ()\r\n        self._regionOfInterest = ()\r\n        self._backSubModel = \"MOG2\"\r\n        self._videoBboxes = True\r\n        self._photoRois = False\r\n        self._motionTestFrame1Title = \"\"\r\n        self._motionTestFrame2Title = \"\"\r\n        self._motionTestFrame3Title = \"\"\r\n        self._motionTestFrame4Title = \"\"\r\n        self._motionTestFramerate = 0\r\n        self._actionVR = 1\r\n        self._actionCircSize = 5\r\n        self._actionPath = \"\"\r\n        self._actionVideoDuration = 10\r\n        self._actionPhotoBurst = 1\r\n        self._actionPhotoBurstDelaySec = 2\r\n        self._notifyHost = \"\"\r\n        self._notifyPort = 0\r\n        self._notifyUseSSL = False\r\n        self._notifyAuthenticate = True\r\n        self._notifyConOK = False\r\n        self._notifyPause = 0\r\n        self._notifyIncludeVideo = False\r\n        self._notifyIncludePhoto = False\r\n        self._notifySavePwd = False\r\n        self._notifyPwdPath = \"\"\r\n        self._notifyFrom = \"\"\r\n        self._notifyTo = \"\"\r\n        self._notifySubject = \"\"\r\n        self._retentionPeriod = 3\r\n        self._evStart = None\r\n        self._evIncludePhoto = False\r\n        self._evIncludeVideo = True\r\n        self._evAutoRefresh = False\r\n        self._calStart = None\r\n        self._error = None\r\n        self._error2 = None\r\n        self._errorSource = None\r\n        self._triggers = []\r\n        self._actions = []\r\n        self._noCamera = False\r\n\r\n    @property\r\n    def logFileName(self) -> str:\r\n        return \"_events.log\"\r\n        \r\n    @property\r\n    def logFilePath(self) -> str:\r\n        return self._actionPath + \"/\" + self.logFileName\r\n\r\n    @property\r\n    def operationStartMinute(self) -> int:\r\n        return self._operationStartMinute\r\n\r\n    @operationStartMinute.setter\r\n    def operationStartMinute(self, value: int):\r\n        self._operationStartMinute = value\r\n\r\n    @property\r\n    def operationStartStr(self) -> str:\r\n        h = self._operationStartMinute // 60\r\n        m = self._operationStartMinute % 60\r\n        return str(h).zfill(2) + \":\" + str(m).zfill(2)\r\n\r\n    @operationStartStr.setter\r\n    def operationStartStr(self, value: str):\r\n        h = 0\r\n        m = 0\r\n        if value:\r\n            if len(value) == 5:\r\n                h = int(value[:2])\r\n                m = int(value[3:])\r\n        self._operationStartMinute = 60 * h + m\r\n\r\n    @property\r\n    def operationEndMinute(self) -> int:\r\n        return self._operationEndMinute\r\n\r\n    @operationEndMinute.setter\r\n    def operationEndMinute(self, value: int):\r\n        self._operationEndMinute = value\r\n\r\n    @property\r\n    def operationEndStr(self) -> str:\r\n        h = self._operationEndMinute // 60\r\n        m = self._operationEndMinute % 60\r\n        return str(h).zfill(2) + \":\" + str(m).zfill(2)\r\n\r\n    @operationEndStr.setter\r\n    def operationEndStr(self, value: str):\r\n        h = 0\r\n        m = 0\r\n        if value:\r\n            if len(value) == 5:\r\n                h = int(value[:2])\r\n                m = int(value[3:])\r\n        self._operationEndMinute = 60 * h + m\r\n\r\n    @property\r\n    def operationWeekdays(self) -> dict:\r\n        return self._operationWeekdays\r\n\r\n    @operationWeekdays.setter\r\n    def operationWeekdays(self, value: dict):\r\n        self._operationWeekdays = value\r\n\r\n    @property\r\n    def operationAutoStart(self) -> bool:\r\n        return self._operationAutoStart\r\n\r\n    @operationAutoStart.setter\r\n    def operationAutoStart(self, value: bool):\r\n        self._operationAutoStart = value\r\n\r\n    @property\r\n    def detectionDelaySec(self) -> int:\r\n        return self._detectionDelaySec\r\n\r\n    @detectionDelaySec.setter\r\n    def detectionDelaySec(self, value: int):\r\n        self._detectionDelaySec = value\r\n\r\n    @property\r\n    def detectionPauseSec(self) -> int:\r\n        return self._detectionPauseSec\r\n\r\n    @detectionPauseSec.setter\r\n    def detectionPauseSec(self, value: int):\r\n        self._detectionPauseSec = value\r\n\r\n    @property\r\n    def triggeredByMotion(self) -> bool:\r\n        return self._triggeredByMotion\r\n\r\n    @triggeredByMotion.setter\r\n    def triggeredByMotion(self, value: bool):\r\n        self._triggeredByMotion = value\r\n\r\n    @property\r\n    def triggeredBySound(self) -> bool:\r\n        return self._triggeredBySound\r\n\r\n    @triggeredBySound.setter\r\n    def triggeredBySound(self, value: bool):\r\n        self._triggeredBySound = value\r\n\r\n    @property\r\n    def triggeredByEvents(self) -> bool:\r\n        return self._triggeredByEvents\r\n\r\n    @triggeredByEvents.setter\r\n    def triggeredByEvents(self, value: bool):\r\n        self._triggeredByEvents = value\r\n\r\n    @property\r\n    def motionDetectAlgo(self) -> int:\r\n        return self._motionDetectAlgo\r\n\r\n    @motionDetectAlgo.setter\r\n    def motionDetectAlgo(self, value: int):\r\n        self._motionDetectAlgo = value\r\n\r\n    @property\r\n    def motionRefTit(self) -> str:\r\n        return self._motionRefTit\r\n\r\n    @motionRefTit.setter\r\n    def motionRefTit(self, value: str):\r\n        self._motionRefTit = value\r\n\r\n    @property\r\n    def motionRefURL(self) -> str:\r\n        return self._motionRefURL\r\n\r\n    @motionRefURL.setter\r\n    def motionRefURL(self, value: str):\r\n        self._motionRefURL = value\r\n\r\n    @property\r\n    def actionVideo(self) -> bool:\r\n        return self._actionVideo\r\n\r\n    @actionVideo.setter\r\n    def actionVideo(self, value: bool):\r\n        self._actionVideo = value\r\n\r\n    @property\r\n    def actionPhoto(self) -> bool:\r\n        return self._actionPhoto\r\n\r\n    @actionPhoto.setter\r\n    def actionPhoto(self, value: bool):\r\n        self._actionPhoto = value\r\n\r\n    @property\r\n    def actionNotify(self) -> bool:\r\n        return self._actionNotify\r\n\r\n    @actionNotify.setter\r\n    def actionNotify(self, value: bool):\r\n        self._actionNotify = value\r\n\r\n    @property\r\n    def msdThreshold(self) -> int:\r\n        return self._msdThreshold\r\n\r\n    @msdThreshold.setter\r\n    def msdThreshold(self, value: int):\r\n        self._msdThreshold = value\r\n\r\n    @property\r\n    def bboxThreshold(self) -> int:\r\n        return self._bboxThreshold\r\n\r\n    @bboxThreshold.setter\r\n    def bboxThreshold(self, value: int):\r\n        self._bboxThreshold = value\r\n\r\n    # TODO: int->float\r\n    @property\r\n    def nmsThreshold(self) -> int:\r\n        return self._nmsThreshold\r\n\r\n    @nmsThreshold.setter\r\n    def nmsThreshold(self, value: int):\r\n        self._nmsThreshold = value\r\n\r\n    @property\r\n    def motionThreshold(self) -> int:\r\n        return self._motionThreshold\r\n\r\n    @motionThreshold.setter\r\n    def motionThreshold(self, value: int):\r\n        self._motionThreshold = value\r\n\r\n    @property\r\n    def useRoI(self) -> bool:\r\n        return self._useRoI\r\n\r\n    @useRoI.setter\r\n    def useRoI(self, value: bool):\r\n        self._useRoI = value\r\n\r\n    @property\r\n    def regionOfNoInterest(self) -> tuple:\r\n        return self._regionOfNoInterest\r\n\r\n    @regionOfNoInterest.setter\r\n    def regionOfNoInterest(self, value: tuple):\r\n        self._regionOfNoInterest = value\r\n        \r\n    @property\r\n    def regionOfNoInterestStr(self) -> str:\r\n        res = \"(\"\r\n        for win in self.regionOfNoInterest:\r\n            if len(res) > 1:\r\n                res = res + \",\"\r\n            res = res + \"(\" + str(win[0]) + \",\" + str(win[1]) + \",\" + str(win[2]) + \",\" + str(win[3]) + \")\"\r\n        res = res + \")\"\r\n        return res\r\n\r\n    @regionOfNoInterestStr.setter\r\n    def regionOfNoInterestStr(self, value: str):\r\n        \"\"\"Parse the string representation for regionOfNoInterest\r\n        \"\"\"\r\n        self._regionOfNoInterest = ()\r\n        # Get the list of windows\r\n        winlist = TriggerConfig._parseWindows(value)\r\n        for win in winlist:\r\n            awin = TriggerConfig._parseRectTuple(win)\r\n            # Add window from list to _regionOfNoInterest tuple\r\n            awin = (awin,)\r\n            self._regionOfNoInterest += awin\r\n\r\n    @property\r\n    def regionOfInterest(self) -> tuple:\r\n        return self._regionOfInterest\r\n\r\n    @regionOfInterest.setter\r\n    def regionOfInterest(self, value: tuple):\r\n        self._regionOfInterest = value\r\n        \r\n    @property\r\n    def regionOfInterestStr(self) -> str:\r\n        res = \"(\"\r\n        for win in self.regionOfInterest:\r\n            if len(res) > 1:\r\n                res = res + \",\"\r\n            res = res + \"(\" + str(win[0]) + \",\" + str(win[1]) + \",\" + str(win[2]) + \",\" + str(win[3]) + \")\"\r\n        res = res + \")\"\r\n        return res\r\n\r\n    @regionOfInterestStr.setter\r\n    def regionOfInterestStr(self, value: str):\r\n        \"\"\"Parse the string representation for regionOfInterest\r\n        \"\"\"\r\n        self._regionOfInterest = ()\r\n        # Get the list of windows\r\n        winlist = TriggerConfig._parseWindows(value)\r\n        for win in winlist:\r\n            awin = TriggerConfig._parseRectTuple(win)\r\n            # Add window from list to _regionOfInterest tuple\r\n            awin = (awin,)\r\n            self._regionOfInterest += awin\r\n\r\n    def _getRectangleFromCrop(self, crop: tuple) -> tuple:\r\n        \"\"\" Get rectangle (x1, y1, x2, y2) from crop (x, y, w, h)\r\n\r\n        Args:\r\n            crop (tuple): Cropping rectangle defined as (x, y, w, h)\r\n        Returns:\r\n            tuple: (x1, y1, x2, y2)\r\n        \"\"\"\r\n        x1 = crop[0]\r\n        y1 = crop[1]\r\n        x2 = x1 + crop[2]\r\n        y2 = y1 + crop[3]\r\n\r\n        if x2 < x1:\r\n            xx = x1\r\n            x1 = x2\r\n            x2 = xx\r\n\r\n        if y2 < y1:\r\n            yy = y1\r\n            y1 = y2\r\n            y2 = yy\r\n\r\n        return (x1, y1, x2, y2)\r\n\r\n    def checkRoisAgainstScalerCropLiveView(self, scalerCrop:tuple) -> bool:\r\n        \"\"\" Check that the RoIs are within the scaler crop used for live view\r\n\r\n        Args:\r\n            scalerCrop (tuple): (x, y, w, h) of scaler crop\r\n        Returns:\r\n            bool: True if RiIs/RoNIs are unchanged, False if they have been adjusted\r\n        \"\"\"\r\n        unchanged = True\r\n        (scX1, scY1, scX2, scY2) = self._getRectangleFromCrop(scalerCrop)\r\n        newRois = ()\r\n        for roi in self.regionOfInterest:\r\n            (rX1, rY1, rX2, rY2) = self._getRectangleFromCrop(roi)\r\n            if rX1 < scX1:\r\n                rX1 = scX1\r\n            if rY1 < scY1:\r\n                rY1 = scY1\r\n            if rX2 > scX2:\r\n                rX2 = scX2\r\n            if rY2 > scY2:\r\n                rY2 = scY2\r\n            if rX2 > rX1 and rY2 > rY1:\r\n                newRois += ((rX1, rY1, rX2 - rX1, rY2 - rY1),)\r\n        if self._regionOfInterest != newRois:\r\n            unchanged = False\r\n            self._regionOfInterest = newRois\r\n\r\n        newRonis = ()\r\n        for roni in self.regionOfNoInterest:\r\n            (rX1, rY1, rX2, rY2) = self._getRectangleFromCrop(roni)\r\n            if rX1 < scX1:\r\n                rX1 = scX1\r\n            if rY1 < scY1:\r\n                rY1 = scY1\r\n            if rX2 > scX2:\r\n                rX2 = scX2\r\n            if rY2 > scY2:\r\n                rY2 = scY2\r\n            if rX2 > rX1 and rY2 > rY1:\r\n                newRonis += ((rX1, rY1, rX2 - rX1, rY2 - rY1),)\r\n        if self._regionOfNoInterest != newRonis:\r\n            unchanged = False\r\n            self._regionOfNoInterest = newRonis\r\n        return unchanged\r\n\r\n    @property\r\n    def backSubModel(self) -> str:\r\n        return self._backSubModel\r\n\r\n    @backSubModel.setter\r\n    def backSubModel(self, value: str):\r\n        self._backSubModel = value\r\n\r\n    @property\r\n    def videoBboxes(self) -> bool:\r\n        return self._videoBboxes\r\n\r\n    @videoBboxes.setter\r\n    def videoBboxes(self, value: bool):\r\n        self._videoBboxes = value\r\n\r\n    @property\r\n    def photoRois(self) -> bool:\r\n        return self._photoRois\r\n\r\n    @photoRois.setter\r\n    def photoRois(self, value: bool):\r\n        self._photoRois = value\r\n\r\n    @property\r\n    def motionTestFrame1Title(self) -> str:\r\n        return self._motionTestFrame1Title\r\n\r\n    @motionTestFrame1Title.setter\r\n    def motionTestFrame1Title(self, value: str):\r\n        self._motionTestFrame1Title = value\r\n\r\n    @property\r\n    def motionTestFrame2Title(self) -> str:\r\n        return self._motionTestFrame2Title\r\n\r\n    @motionTestFrame2Title.setter\r\n    def motionTestFrame2Title(self, value: str):\r\n        self._motionTestFrame2Title = value\r\n\r\n    @property\r\n    def motionTestFrame3Title(self) -> str:\r\n        return self._motionTestFrame3Title\r\n\r\n    @motionTestFrame3Title.setter\r\n    def motionTestFrame3Title(self, value: str):\r\n        self._motionTestFrame3Title = value\r\n\r\n    @property\r\n    def motionTestFrame4Title(self) -> str:\r\n        return self._motionTestFrame4Title\r\n\r\n    @motionTestFrame4Title.setter\r\n    def motionTestFrame4Title(self, value: str):\r\n        self._motionTestFrame4Title = value\r\n\r\n    @property\r\n    def motionTestFramerate(self) -> float:\r\n        return self._motionTestFramerate\r\n\r\n    @motionTestFramerate.setter\r\n    def motionTestFramerate(self, value: str):\r\n        self._motionTestFramerate = value\r\n        \r\n    @property\r\n    def actionVR(self) -> int:\r\n        return self._actionVR\r\n\r\n    @actionVR.setter\r\n    def actionVR(self, value: int):\r\n        self._actionVR = value\r\n\r\n    @property\r\n    def actionCircSize(self) -> int:\r\n        return self._actionCircSize\r\n\r\n    @actionCircSize.setter\r\n    def actionCircSize(self, value: int):\r\n        self._actionCircSize = value\r\n\r\n    @property\r\n    def actionPath(self) -> str:\r\n        return self._actionPath\r\n\r\n    @actionPath.setter\r\n    def actionPath(self, value: str):\r\n        self._actionPath = value\r\n\r\n    @property\r\n    def actionVideoDuration(self) -> int:\r\n        return self._actionVideoDuration\r\n\r\n    @actionVideoDuration.setter\r\n    def actionVideoDuration(self, value: int):\r\n        self._actionVideoDuration = value\r\n\r\n    @property\r\n    def actionPhotoBurst(self) -> int:\r\n        return self._actionPhotoBurst\r\n\r\n    @actionPhotoBurst.setter\r\n    def actionPhotoBurst(self, value: int):\r\n        self._actionPhotoBurst = value\r\n\r\n    @property\r\n    def actionPhotoBurstDelaySec(self) -> int:\r\n        return self._actionPhotoBurstDelaySec\r\n\r\n    @actionPhotoBurstDelaySec.setter\r\n    def actionPhotoBurstDelaySec(self, value: int):\r\n        self._actionPhotoBurstDelaySec = value\r\n\r\n    @property\r\n    def notifyHost(self) -> str:\r\n        return self._notifyHost\r\n\r\n    @notifyHost.setter\r\n    def notifyHost(self, value: str):\r\n        self._notifyHost = value\r\n\r\n    @property\r\n    def notifyPort(self) -> int:\r\n        return self._notifyPort\r\n\r\n    @notifyPort.setter\r\n    def notifyPort(self, value: int):\r\n        self._notifyPort = value\r\n\r\n    @property\r\n    def notifyUseSSL(self) -> bool:\r\n        return self._notifyUseSSL\r\n\r\n    @notifyUseSSL.setter\r\n    def notifyUseSSL(self, value: bool):\r\n        self._notifyUseSSL = value\r\n\r\n    @property\r\n    def notifyAuthenticate(self) -> bool:\r\n        return self._notifyAuthenticate\r\n\r\n    @notifyAuthenticate.setter\r\n    def notifyAuthenticate(self, value: bool):\r\n        self._notifyAuthenticate = value\r\n\r\n    @property\r\n    def notifyConOK(self) -> bool:\r\n        return self._notifyConOK\r\n\r\n    @notifyConOK.setter\r\n    def notifyConOK(self, value: bool):\r\n        self._notifyConOK = value\r\n\r\n    @property\r\n    def notifyPause(self) -> int:\r\n        return self._notifyPause\r\n\r\n    @notifyPause.setter\r\n    def notifyPause(self, value: int):\r\n        self._notifyPause = value\r\n\r\n    @property\r\n    def notifyIncludeVideo(self) -> bool:\r\n        return self._notifyIncludeVideo\r\n\r\n    @notifyIncludeVideo.setter\r\n    def notifyIncludeVideo(self, value: bool):\r\n        self._notifyIncludeVideo = value\r\n\r\n    @property\r\n    def notifyIncludePhoto(self) -> bool:\r\n        return self._notifyIncludePhoto\r\n\r\n    @notifyIncludePhoto.setter\r\n    def notifyIncludePhoto(self, value: bool):\r\n        self._notifyIncludePhoto = value\r\n\r\n    @property\r\n    def notifySavePwd(self) -> bool:\r\n        return self._notifySavePwd\r\n\r\n    @notifySavePwd.setter\r\n    def notifySavePwd(self, value: bool):\r\n        self._notifySavePwd = value\r\n\r\n    @property\r\n    def notifyPwdPath(self) -> str:\r\n        return self._notifyPwdPath\r\n\r\n    @notifyPwdPath.setter\r\n    def notifyPwdPath(self, value: str):\r\n        self._notifyPwdPath = value\r\n\r\n    @property\r\n    def notifyFrom(self) -> str:\r\n        return self._notifyFrom\r\n\r\n    @notifyFrom.setter\r\n    def notifyFrom(self, value: str):\r\n        self._notifyFrom = value\r\n\r\n    @property\r\n    def notifyTo(self) -> str:\r\n        return self._notifyTo\r\n\r\n    @notifyTo.setter\r\n    def notifyTo(self, value: str):\r\n        self._notifyTo = value\r\n\r\n    @property\r\n    def notifySubject(self) -> str:\r\n        return self._notifySubject\r\n\r\n    @notifySubject.setter\r\n    def notifySubject(self, value: str):\r\n        self._notifySubject = value\r\n\r\n    @property\r\n    def retentionPeriod(self) -> int:\r\n        return self._retentionPeriod\r\n\r\n    @retentionPeriod.setter\r\n    def retentionPeriod(self, value: int):\r\n        self._retentionPeriod = value\r\n\r\n    @property\r\n    def retentionPeriodStr(self) -> str:\r\n        return str(self._retentionPeriod)\r\n\r\n    @property\r\n    def evStart(self) -> datetime:\r\n        return self._evStart\r\n\r\n    @evStart.setter\r\n    def evStart(self, value: datetime):\r\n        if value is None:\r\n            val = None\r\n        else:\r\n            val = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\r\n        self._evStart = val\r\n\r\n    @property\r\n    def evStartDateStr(self) -> str:\r\n        return self._evStart.isoformat()[:10]\r\n\r\n    @evStartDateStr.setter\r\n    def evStartDateStr(self, value: str):\r\n        try:\r\n            d = date.fromisoformat(value)\r\n        except ValueError:\r\n            d = datetime.now()\r\n        v = datetime(year=d.year, month=d.month, day=d.day, hour=self._evStart.hour, minute=self._evStart.minute)        \r\n        self._evStart = v\r\n\r\n    @property\r\n    def evStartTimeStr(self) -> str:\r\n        return self._evStart.isoformat()[11:16]\r\n\r\n    @evStartTimeStr.setter\r\n    def evStartTimeStr(self, value: str):\r\n        try:\r\n            d = time.fromisoformat(value)\r\n        except ValueError:\r\n            d = datetime.now()\r\n        v = datetime(year=self._evStart.year, month=self._evStart.month, day=self._evStart.day, hour=d.hour, minute=d.minute)        \r\n        self._evStart = v\r\n    \r\n    @property\r\n    def evStartIso(self) -> str:\r\n        return self._evStart.isoformat()\r\n    \r\n    def evStartMidnight(self):\r\n        self._evStart = datetime(year=self._evStart.year, month=self._evStart.month, day=self._evStart.day, hour=0, minute=0)\r\n\r\n    @property\r\n    def evIncludePhoto(self) -> bool:\r\n        return self._evIncludePhoto\r\n\r\n    @evIncludePhoto.setter\r\n    def evIncludePhoto(self, value: bool):\r\n        self._evIncludePhoto = value\r\n\r\n    @property\r\n    def evIncludeVideo(self) -> bool:\r\n        return self._evIncludeVideo\r\n\r\n    @evIncludeVideo.setter\r\n    def evIncludeVideo(self, value: bool):\r\n        self._evIncludeVideo = value\r\n\r\n    @property\r\n    def evAutoRefresh(self) -> bool:\r\n        return self._evAutoRefresh\r\n\r\n    @evAutoRefresh.setter\r\n    def evAutoRefresh(self, value: bool):\r\n        self._evAutoRefresh = value\r\n\r\n    @property\r\n    def calStart(self) -> datetime:\r\n        return self._calStart\r\n\r\n    @calStart.setter\r\n    def calStart(self, value: datetime):\r\n        if value == None:\r\n            val = None\r\n        else:\r\n            val = datetime(year=value.year, month=value.month, day=1, hour=0, minute=0, second=0)\r\n        self._calStart = val\r\n\r\n    @property\r\n    def calStartDateStr(self) -> str:\r\n        return self._calStart.isoformat()[:10]\r\n\r\n    @calStartDateStr.setter\r\n    def calStartDateStr(self, value: str):\r\n        try:\r\n            d = date.fromisoformat(value)\r\n        except ValueError:\r\n            d = datetime.now()\r\n        v = datetime(year=d.year, month=d.month, day=1, hour=0, minute=0)        \r\n        self._evStart = v\r\n\r\n    @property\r\n    def error(self) -> str:\r\n        return self._error\r\n\r\n    @error.setter\r\n    def error(self, value: str):\r\n        self._error = value\r\n        if value is None:\r\n            self._errorSource = None\r\n            self._error2 = None\r\n\r\n    @property\r\n    def error2(self) -> str:\r\n        return self._error2\r\n\r\n    @error2.setter\r\n    def error2(self, value: str):\r\n        self._error2 = value\r\n\r\n    @property\r\n    def errorSource(self) -> str:\r\n        return self._errorSource\r\n\r\n    @errorSource.setter\r\n    def errorSource(self, value: str):\r\n        self._errorSource = value\r\n\r\n    @property\r\n    def triggers(self) -> list[Trigger]:\r\n        return self._triggers\r\n\r\n    @triggers.setter\r\n    def triggers(self, value: list[Trigger]):\r\n        self._triggers = value\r\n\r\n    @property\r\n    def actions(self) -> list:\r\n        return self._actions\r\n\r\n    @actions.setter\r\n    def actions(self, value: list):\r\n        self._actions = value\r\n        \r\n    @property\r\n    def eventList(self) -> list:\r\n        return self.getEventList()\r\n        \r\n    def getEventList(self) -> list:\r\n        db = dbx.get_dbx()\r\n        events = []\r\n        seldate = self.evStartDateStr\r\n        seltime = self.evStartTimeStr\r\n        eventsdb = db.execute(\"SELECT * FROM events WHERE date = ? AND minute >= ?\",\r\n                              (seldate, seltime)\r\n        ).fetchall()\r\n        \r\n        for eventdb in eventsdb:\r\n            eventContainer = {}\r\n            event = {}\r\n            event[\"timestamp\"] = eventdb[\"timestamp\"]\r\n            event[\"date\"] = eventdb[\"date\"]\r\n            event[\"time\"] = eventdb[\"time\"]\r\n            event[\"type\"] = eventdb[\"type\"]\r\n            event[\"trigger\"] = eventdb[\"trigger\"]\r\n            event[\"triggertype\"] = eventdb[\"triggertype\"]\r\n            tps = eventdb[\"triggerparam\"]\r\n            # Handle DB entries from previous releases where params were just strings and no dict\r\n            handleAsStr = True\r\n            tpd = {}\r\n            try:\r\n                tpdt = literal_eval(tps)\r\n                if isinstance(tpdt, dict):\r\n                    tpd = tpdt\r\n                    handleAsStr = False                   \r\n            except Exception:\r\n                pass\r\n            if handleAsStr == True:\r\n                if tps[:5] == \"msd: \":\r\n                    tpd[\"msd\"] = tps[5:]\r\n                else:\r\n                    tpd[\"par\"] = tps\r\n            event[\"triggerparam\"] = tpd\r\n            eventContainer[\"event\"] = event\r\n            events.append(eventContainer)\r\n            \r\n        if self.evIncludeVideo:\r\n            for ev in events:\r\n                event = ev[\"event\"]\r\n                ts = event[\"timestamp\"]\r\n                eventactions = db.execute(\"SELECT * FROM eventactions WHERE event = ? AND actiontype = ?\",\r\n                                    (ts, \"Video\")\r\n                ).fetchone()\r\n                if eventactions is None:\r\n                    eventVideo = {}\r\n                else:\r\n                    eventVideo = {}\r\n                    eventVideo[\"timestamp\"] = eventactions[\"timestamp\"]\r\n                    eventVideo[\"date\"] = eventactions[\"date\"]\r\n                    eventVideo[\"time\"] = eventactions[\"time\"]\r\n                    eventVideo[\"duration\"] = round(eventactions[\"actionduration\"], 0)\r\n                    eventVideo[\"filename\"] = eventactions[\"filename\"]\r\n                    videophoto = db.execute(\"SELECT * FROM eventactions WHERE event = ? AND actiontype = ? AND timestamp = ?\",\r\n                                        (ts, \"Photo\", eventactions[\"timestamp\"])\r\n                    ).fetchone()\r\n                    if videophoto is None:\r\n                        eventVideo[\"photo\"] = None\r\n                    else:\r\n                        eventVideo[\"photo\"] = videophoto[\"filename\"]\r\n                ev[\"video\"] = eventVideo\r\n\r\n        if self.evIncludePhoto:\r\n            for ev in events:\r\n                event = ev[\"event\"]\r\n                ts = event[\"timestamp\"]\r\n                eventactions = None\r\n                #if self.evIncludeVideo:\r\n                #    if len(ev[\"video\"]) > 0:\r\n                #        eventVideo = ev[\"video\"]\r\n                #        if not eventVideo[\"photo\"] is None:\r\n                #            videoTs = eventVideo[\"timestamp\"]\r\n                #            eventactions = db.execute(\"SELECT * FROM eventactions WHERE event = ? AND actiontype = ? AND timestamp != ? ORDER BY timestamp ASC\",\r\n                #                                (ts, \"Photo\", videoTs)\r\n                #            ).fetchall()\r\n                if eventactions is None:\r\n                    eventactions = db.execute(\"SELECT * FROM eventactions WHERE event = ? AND actiontype = ? ORDER BY timestamp ASC\",\r\n                                        (ts, \"Photo\")\r\n                    ).fetchall()\r\n                if eventactions is None:\r\n                    eventPhotos = []\r\n                else:\r\n                    eventPhotos = []\r\n                    for eventactiondb in eventactions:\r\n                        eventPhoto = {}\r\n                        eventPhoto[\"timestamp\"] = eventactiondb[\"timestamp\"]\r\n                        eventPhoto[\"date\"] = eventactiondb[\"date\"]\r\n                        eventPhoto[\"time\"] = eventactiondb[\"time\"]\r\n                        eventPhoto[\"duration\"] = round(eventactiondb[\"actionduration\"], 0)\r\n                        eventPhoto[\"filename\"] = eventactiondb[\"filename\"]\r\n                        eventPhotos.append(eventPhoto)\r\n                ev[\"photos\"] = eventPhotos\r\n        return events\r\n\r\n    @property \r\n    def calendar(self) -> list:\r\n        return self.getCalendar()\r\n\r\n    @property \r\n    def calendarMonthStr(self) -> str:\r\n        return self.calStart.strftime(\"%B\") + \" \" + str(self.calStart.year)\r\n\r\n    def getCalendar(self)-> list:\r\n        \"\"\" Setup calendar for the selected month with information on events\r\n        \"\"\"\r\n        db = dbx.get_dbx()\r\n        wd = self.calStart.isocalendar().weekday\r\n        month = self.calStart.month\r\n        wnrStart = self.calStart.isocalendar().week\r\n        dayStart = self.calStart - timedelta(hours = (wd - 1) * 24)\r\n        \r\n        calendar = []\r\n        dayIter = dayStart\r\n        for week in range(wnrStart, wnrStart + 6):\r\n            calWeek = {}\r\n            calWeek[\"week\"] = week\r\n            weekdays = []\r\n            for weekday in range(1, 8):\r\n                day = {}\r\n                day[\"day\"] = dayIter.day\r\n                day[\"weekday\"] = dayIter.isocalendar().weekday\r\n                day[\"week\"] = dayIter.isocalendar().week\r\n                dayIso = dayIter.isoformat()[:10]\r\n                day[\"date\"] = dayIso\r\n                data = {}\r\n                nrEvents = db.execute(\"SELECT count(*) FROM events WHERE date = ?\",\r\n                                    (dayIso,)\r\n                ).fetchone()[0]\r\n                data[\"nrevents\"] = nrEvents\r\n                day[\"data\"] = data\r\n                weekdays.append(day)\r\n                dayIter = dayIter + timedelta(hours=24)\r\n            calWeek[\"weekdays\"] = weekdays\r\n            calendar.append(calWeek)\r\n            if dayIter.month > month:\r\n                break\r\n        return calendar\r\n    \r\n    def cleanupEvents(self):\r\n        \"\"\" Remove all events older than retention period\r\n        \"\"\"\r\n        logger.debug(\"TriggerConfig.cleanupEvents\")\r\n        db = dbx.get_dbx()\r\n        dr = datetime.now() - timedelta(days=self.retentionPeriod)\r\n        #dr = dr - timedelta(hours=23)\r\n        dateRem = str(dr.isoformat()[:10])\r\n        logger.debug(\"TriggerConfig.cleanupEvents - Removing %s and earlier\", dateRem)\r\n        \r\n        # Cleanup events log\r\n        fpLog = self.logFilePath\r\n        fpLogOld = os.path.dirname(fpLog) + \"/_backup.log\"\r\n        if os.path.exists(fpLog):\r\n            if os.path.exists(fpLogOld):\r\n                os.remove(fpLogOld)\r\n            os.rename(fpLog, fpLogOld)\r\n        with open(fpLogOld, \"r\") as src:\r\n            oldLines = src.readlines()\r\n        with open(fpLog, \"w\") as tgt:\r\n            for line in oldLines:\r\n                if line[:10] > dateRem:\r\n                    tgt.write(line)\r\n        os.remove(fpLogOld)\r\n        logger.debug(\"Cleaned up %s\", fpLog)\r\n        \r\n        # Remove files\r\n        cnt = 0\r\n        evadb = db.execute(\"SELECT * FROM eventactions WHERE date <= ?\", (dateRem,)).fetchall()\r\n        if not evadb is None:\r\n            for eva in evadb:\r\n                fp = eva[\"fullpath\"]\r\n                if os.path.exists(fp):\r\n                    os.remove(fp)\r\n                    cnt += 1\r\n        logger.debug(\"Removed %s files\", cnt)\r\n\r\n        # Delete eventactions\r\n        db.execute(\"DELETE FROM eventactions WHERE date <= ?\", (dateRem,)).fetchall()\r\n        db.commit()\r\n        logger.debug(\"Removed old eventaction\")\r\n\r\n        # Delete events\r\n        db.execute(\"DELETE FROM events WHERE date <= ?\", (dateRem,)).fetchall()\r\n        db.commit()\r\n        logger.debug(\"Removed old events\")\r\n\r\n    @staticmethod    \r\n    def _parseWindows(wins: str) -> list:\r\n        \"\"\"  Parses the tuple-string of one or multiple rectangles\r\n            \"((x,x,x,x),(x,x,x,x))\"\r\n            and returns an array of rectangles as strings\r\n        \"\"\"\r\n        resa = []\r\n        if wins.startswith(\"(\"):\r\n            wns = wins[1:]\r\n            if wns.endswith(\")\"):\r\n                wns = wns[0: len(wns) - 1]\r\n                while len(wns) > 0:\r\n                    i = wns.find(\")\")\r\n                    if i > 0:\r\n                        wn = wns[0: i + 1]\r\n                        resa.append(wn)\r\n                        if i < len(wns):\r\n                            wns = wns[i + 2:].strip()\r\n                        else:\r\n                            wns = \"\"\r\n                    else:\r\n                        wns = \"\"\r\n        return resa\r\n\r\n    @staticmethod    \r\n    def _parseRectTuple(stuple: str) -> tuple:\r\n        \"\"\"  Parse a Python tuple string for a rectangle\r\n             \"(xOffset, yOffset, width, height)\"\r\n        \"\"\"\r\n        rest = (0, 0, 0, 0)\r\n        if stuple.startswith(\"(\"):\r\n            tpl = stuple[1:]\r\n            if tpl.endswith(\")\"):\r\n                tpl = tpl[0: len(tpl) - 1]\r\n                res = tpl.rsplit(\",\")\r\n                if len(res) == 4:\r\n                    rest = (int(res[0]), int(res[1]), int(res[2]), int(res[3]))\r\n        return rest\r\n        \r\n    def checkNotificationRecipient(self, user=None, pwd=None) -> tuple:\r\n        \"\"\" Check login to mail server using available credentials\r\n\r\n            Return (user, password, error message)\r\n        \"\"\"\r\n        logger.debug(\"TriggerConfig.checkNotificationRecipient\")\r\n        logger.debug(\"user: %s, password: %s\", user, pwd)\r\n        err = \"\"\r\n        secHost = \"\"\r\n        secPort = -1\r\n        secUseSSL = None\r\n        secAuthenticate = None\r\n        secUser = \"\"\r\n        secPwd = \"\"\r\n        secretsOK = False\r\n        # Try to get credentials from the file\r\n        if os.path.exists(self.notifyPwdPath):\r\n            with open(self.notifyPwdPath) as f:\r\n                try:\r\n                    secrets = json.load(f)\r\n                    notifySecrets = secrets[\"eventnotification\"]\r\n                    secHost = notifySecrets[\"host\"]\r\n                    secPort = notifySecrets[\"port\"]\r\n                    secUseSSL = notifySecrets[\"useSSL\"]\r\n                    if \"authentication\" in notifySecrets:\r\n                        secAuthenticate = notifySecrets[\"authentication\"]\r\n                    secUser = notifySecrets[\"user\"]\r\n                    secPwd = notifySecrets[\"password\"]\r\n                    secretsOK = True\r\n                    logger.debug(\"TriggerConfig.checkNotificationRecipient - read credentials from file\")\r\n                except Exception as e:\r\n                    pass\r\n        if secHost == \"\":\r\n            secHost = self.notifyHost\r\n        else:\r\n            if secHost != self.notifyHost:\r\n                secHost = self.notifyHost\r\n                secretsOK = False\r\n        if secPort == -1:\r\n            secPort = self.notifyPort\r\n        else:\r\n            if secPort != self.notifyPort:\r\n                secPort = self.notifyPort\r\n                secretsOK = False\r\n        if secUseSSL is None:\r\n            secUseSSL = self.notifyUseSSL\r\n        else:\r\n            if secUseSSL != self.notifyUseSSL:\r\n                secUseSSL = self.notifyUseSSL\r\n                secretsOK = False\r\n        if secAuthenticate is None:\r\n            secAuthenticate = self.notifyAuthenticate\r\n        else:\r\n            if secAuthenticate != self.notifyAuthenticate:\r\n                secAuthenticate = self.notifyAuthenticate\r\n                secretsOK = False\r\n        if secUser == \"\":\r\n            if not user is None:\r\n                secUser = user\r\n        else:\r\n            if not user is None:\r\n                if user != \"\":\r\n                    secUser = user\r\n                    secretsOK = False\r\n        if secPwd == \"\":\r\n            if not pwd is None:\r\n                secPwd = pwd\r\n        else:\r\n            if not pwd is None:\r\n                if pwd != \"\":\r\n                    secPwd = pwd\r\n                    secretsOK = False\r\n                    \r\n        # Test SSL\r\n     # TODO: Investigate why this test no longer works\r\n       #try:\r\n        #    with smtplib.SMTP_SSL(host=secHost, port=secPort) as smtp_ssl:\r\n        #        smtp_ssl.ehlo()\r\n        #        if secUseSSL == False:\r\n        #            err = \"Server requires SSL\"\r\n        #except (smtplib.SMTPConnectError, ConnectionRefusedError):\r\n        #    if secUseSSL == True:\r\n        #        err = \"Server does not require SSL\"\r\n        \r\n        # Test connection\r\n        if err == \"\":\r\n            try:\r\n                if secUseSSL == True:\r\n                    server = smtplib.SMTP_SSL(host=secHost, port=secPort)\r\n                else:\r\n                    server = smtplib.SMTP(host=secHost, port=secPort)\r\n                server.connect(secHost)\r\n                server.ehlo()\r\n                if secAuthenticate == True:\r\n                    logger.debug(\"Authentication with user/pwd\")\r\n                    server.login(secUser, secPwd)\r\n                else:\r\n                    if \"auth\" in server.esmtp_features:\r\n                        err = \"The server requires authentication. Please provide 'User' and 'Password'\"\r\n                    logger.debug(\"Authentication skipped\")\r\n                server.quit()\r\n                logger.debug(\"TriggerConfig.checkNotificationRecipient - connection test successful\")\r\n            except Exception as e:\r\n                logger.debug(\"TriggerConfig.checkNotificationRecipient - connection test failed\")\r\n                err = \"Connection error: \" + str(e)\r\n            \r\n        if err == \"\":\r\n            self.notifyConOK = True\r\n            if secretsOK == False:\r\n                if self.notifySavePwd == True:\r\n                    # Store credentials\r\n                    if self.notifyPwdPath == \"\":\r\n                        err = \"Please enter the file path for storage of credentials!\"\r\n                    else:\r\n                        if not os.path.exists(self.notifyPwdPath):\r\n                            fp = Path(self.notifyPwdPath)\r\n                            dir = fp.parent.absolute()\r\n                            fn = fp.name\r\n                            if not os.path.exists(dir):\r\n                                os.makedirs(dir, exist_ok=True)\r\n                            self.notifyPwdPath = str(dir) + \"/\" + fn\r\n                            Path(self.notifyPwdPath).touch(exist_ok=True)\r\n                        else:\r\n                            if os.path.isdir(self.notifyPwdPath):\r\n                                err = \"The 'Password File Path' must be a file and not a directory!\"\r\n                        secrets = {}\r\n                        if err == \"\":\r\n                            if os.stat(self.notifyPwdPath).st_size > 0:\r\n                                with open(self.notifyPwdPath, \"r\") as f:\r\n                                    try:\r\n                                        secrets = json.load(f)\r\n                                    except Exception as e:\r\n                                        err = \"The file specified as 'Password File Path' has content which is not in JSON format\"\r\n                        if err == \"\":\r\n                            if \"eventnotification\" in secrets:\r\n                                notifySecrets = secrets[\"eventnotification\"]\r\n                            else:\r\n                                notifySecrets = {}\r\n                                secrets[\"eventnotification\"] = notifySecrets\r\n                            notifySecrets[\"host\"] = self.notifyHost\r\n                            notifySecrets[\"port\"] = self.notifyPort\r\n                            notifySecrets[\"useSSL\"] = self.notifyUseSSL\r\n                            notifySecrets[\"authentication\"] = self.notifyAuthenticate\r\n                            notifySecrets[\"user\"] = secUser\r\n                            notifySecrets[\"password\"] = secPwd\r\n                            with open(self.notifyPwdPath, \"w\") as f:\r\n                                try:\r\n                                    json.dump(secrets,fp=f, indent=4)\r\n                                    logger.debug(\"TriggerConfig.checkNotificationRecipient - saved credentials to file %s\", self.notifyPwdPath)\r\n                                except Exception as e:\r\n                                    logger.err(\"TriggerConfig.checkNotificationRecipient - error while saving credentials to file %s: %s\", self.notifyPwdPath, e)\r\n                                    err = \"Error writing to \" + self.notifyPwdPath + \": \" + str(e)\r\n        else:\r\n            self.notifyConOK = False\r\n        return (secUser, secPwd, err)\r\n    \r\n    def triggerSources(self) -> list[str]:\r\n        \"\"\" Return a list of trigger sources\r\n\r\n            Trigger sources are:\r\n            - 'Camera'          : Camera-based triggers\r\n            - 'GPIO'            : for GPIO input devices\r\n            - 'MotionDetector'  : for motion detection using a camera\r\n        Returns:\r\n            list[str] : List of trigger sources\r\n        \"\"\"\r\n        if self._noCamera == False:\r\n            triggerSources = [\"Camera\", \"GPIO\", \"MotionDetector\"]\r\n        else:\r\n            triggerSources = [\"GPIO\",]\r\n        return triggerSources\r\n        \r\n    def actionSources(self) -> list[str]:\r\n        \"\"\" Return a list of action sources\r\n\r\n            Action sources are:\r\n            - 'Camera'  : for photo taking and video recording\r\n            - 'GPIO'    : for GPIO output devices\r\n\r\n        Returns:\r\n            list[str]: List of action sources\r\n        \"\"\"\r\n        if self._noCamera == False:\r\n            actionSources = [\"Camera\", \"GPIO\", \"SMTP\"]\r\n        else:\r\n            actionSources = [\"GPIO\", \"SMTP\"]\r\n        return actionSources\r\n    \r\n    def triggerDevices(self, source:str) -> list[str]:\r\n        \"\"\" Return a list of trigger devices for the given source\r\n\r\n            for source 'Camera':\r\n                - \"CAM-1\"\r\n                - \"CAM-2\"\r\n            for source 'GPIO':\r\n                - list of IDs of Input devices\r\n            for source 'MotionDetector':\r\n                - \"CAM-1\"\r\n        Args:\r\n            source (str): trigger source ('Camera' or 'GPIO')\r\n\r\n        Returns:\r\n            list[str]: list of devices\r\n        \"\"\"\r\n        logger.debug(\"TriggerConfig.triggerDevices\")\r\n        deviceList = []\r\n        if source == \"Camera\":\r\n            deviceList = [\"CAM-1\",]\r\n            if len(CameraCfg().cameras) > 1:\r\n                deviceList.append(\"CAM-2\")\r\n        elif source == \"MotionDetector\":\r\n            deviceList = [\"CAM-1\",]\r\n        elif source == \"GPIO\":\r\n            devices = CameraCfg().serverConfig.gpioDevices\r\n            for device in devices:\r\n                if device.usage == \"Input\" \\\r\n                and device.isOk == True:\r\n                    id = device.id\r\n                    deviceList.append(id)\r\n        return deviceList\r\n    \r\n    def actionDevices(self, source:str) -> list[str]:\r\n        \"\"\" Return a list of action devices for the given source\r\n\r\n            for source 'Camera':\r\n                - \"CAM-1\"\r\n            for source 'GPIO':\r\n                - list of IDs of Output devices\r\n            for source 'SMTP':\r\n                - The configured SMTP srver, if any\r\n        Args:\r\n            source (str): trigger source ('Camera' or 'GPIO')\r\n\r\n        Returns:\r\n            list[str]: list of devices\r\n        \"\"\"\r\n        deviceList = []\r\n        if source == \"Camera\":\r\n            deviceList = [\"CAM-1\",]\r\n        if source == \"SMTP\":\r\n            deviceList = [self._notifyHost,]\r\n        elif source == \"GPIO\":\r\n            devices = CameraCfg().serverConfig.gpioDevices\r\n            for device in devices:\r\n                if device.usage == \"Output\":\r\n                    id = device.id\r\n                    deviceList.append(id)\r\n        return deviceList\r\n    \r\n    def triggerEvents(self, source:str, device:str) -> tuple[list[str], dict, dict]:\r\n        \"\"\" Return lists of events and event settings for the given device\r\n        \r\n            The returned events are methods which allow assignment of callback routines\r\n        Args:\r\n            source (str): Source ('Camera' or 'GPIO')\r\n            device (str): Device\r\n\r\n        Returns:\r\n            tuple[list[str], dict]: \r\n                - list of events\r\n                - dict of event settings\r\n                - dict of control data\r\n        \"\"\"\r\n        events = []\r\n        eventSettings = {}\r\n        control = {}\r\n        if source == \"Camera\":\r\n            if device == \"CAM-1\":\r\n                events = [\r\n                    \"when_photo_taken\", \r\n                    \"when_series_photo_taken\", \r\n                    \"when_recording_starts\",\r\n                    \"when_recording_stops\",\r\n                    \"when_streaming_1_starts\",\r\n                    \"when_streaming_1_stops\",\r\n                ]\r\n                control = {\r\n                    \"event_log\": False\r\n                }\r\n            elif device == \"CAM-2\":\r\n                events = [\r\n                    \"when_streaming_2_starts\",\r\n                    \"when_streaming_2_stops\",\r\n                ]\r\n                control = {\r\n                    \"event_log\": False\r\n                }\r\n        elif source == \"MotionDetector\":\r\n            if device == \"CAM-1\":\r\n                events = [\r\n                    \"when_motion_detected\", \r\n                ]\r\n                control = {}\r\n        elif source == \"GPIO\":\r\n            gpioDev = CameraCfg().serverConfig.getDevice(device)\r\n            if gpioDev is not None:\r\n                devType = gpioDev.type\r\n                for typ in gpioDeviceTypes:\r\n                    if typ[\"type\"] == devType:\r\n                        if \"events\" in typ:\r\n                            events = typ[\"events\"]\r\n                        if \"eventSettings\" in typ:\r\n                            eventSettings = typ[\"eventSettings\"]\r\n                        if \"control\" in typ:\r\n                            control = typ[\"control\"]\r\n                        break\r\n        return events, eventSettings, control\r\n    \r\n    def actionTargets(self, source:str, device:str) -> list[str]:\r\n        \"\"\" Return lists of action targets for the given device\r\n        \r\n            The returned actions are methods or properties for the device type\r\n        Args:\r\n            source (str): Source ('Camera' or 'GPIO')\r\n            device (str): Device\r\n\r\n        Returns:\r\n            list[str]: list of action targets\r\n        \"\"\"\r\n        actionTargets = []\r\n        if source == \"Camera\":\r\n            actionTargets = []\r\n            actionTargets = [\r\n                {\r\n                    \"method\": \"take_photo\",\r\n                    \"params\": {\r\n                        \"type\": \"jpg\"\r\n                        },\r\n                    \"control\": {\r\n                        \"burst_count\": 1,\r\n                        \"burst_intvl\": 1.0\r\n                    }\r\n                },\r\n                {\r\n                    \"method\": \"record_video\",\r\n                    \"params\": {\r\n                        \"type\": \"mp4\"\r\n                    },\r\n                    \"control\": {\r\n                        \"duration\": 1\r\n                    }\r\n                },\r\n                {\r\n                    \"method\": \"start_video\",\r\n                    \"params\": {\r\n                        \"type\": \"mp4\"\r\n                    },\r\n                    \"control\": {\r\n                    }\r\n                },\r\n                {\r\n                    \"method\": \"stop_video\",\r\n                    \"params\": {},\r\n                    \"control\": {}\r\n                }\r\n            ]\r\n        elif source == \"SMTP\":\r\n            if self._noCamera == False:\r\n                actionTargets = [\r\n                    {\r\n                        \"method\": \"send_mail\",\r\n                        \"params\": {},\r\n                        \"control\": {\r\n                            \"attach_photo\": False,\r\n                            \"attach_video\": False\r\n                        }\r\n                    }\r\n                ]\r\n            else:\r\n                actionTargets = [\r\n                    {\r\n                        \"method\": \"send_mail\",\r\n                        \"params\": {},\r\n                        \"control\": {\r\n                        }\r\n                    }\r\n                ]\r\n        elif source == \"GPIO\":\r\n            gpioDev = CameraCfg().serverConfig.getDevice(device)\r\n            if gpioDev is not None:\r\n                devType = gpioDev.type\r\n                for typ in gpioDeviceTypes:\r\n                    if typ[\"type\"] == devType:\r\n                        if \"actionTargets\" in typ:\r\n                            actionTargets = typ[\"actionTargets\"]\r\n                        break\r\n        return actionTargets\r\n    \r\n    def getTrigger(self, id:str) -> Trigger:\r\n        \"\"\" Return a trigger with a specific ID\r\n\r\n        Args:\r\n            id (str): ID of trigger to be returned\r\n\r\n        Returns:\r\n            Trigger: Trigger with the given ID or None\r\n        \"\"\"\r\n        trigger = None\r\n        for trg in self.triggers:\r\n            if trg.id == id:\r\n                trigger = trg\r\n                break\r\n        return trigger\r\n    \r\n    def getAction(self, id:str) -> Action:\r\n        \"\"\" Return an action with a specific ID\r\n\r\n        Args:\r\n            id (str): ID of action to be returned\r\n\r\n        Returns:\r\n            Action: Action with the given ID or None\r\n        \"\"\"\r\n        action = None\r\n        for act in self.actions:\r\n            if act.id == id:\r\n                action = act\r\n                break\r\n        return action\r\n\r\n    @property\r\n    def cameraSettings(self) -> dict:\r\n        cs = {}\r\n        cs[\"actionPhoto\"] = self._actionPhoto\r\n        cs[\"actionVideo\"] = self._actionVideo\r\n        cs[\"motionDetectAlgo\"] = self._motionDetectAlgo\r\n        cs[\"msdThreshold\"] = self._msdThreshold\r\n        cs[\"bboxThreshold\"] = self._bboxThreshold\r\n        cs[\"nmsThreshold\"] = self._nmsThreshold\r\n        cs[\"motionThreshold\"] = self._motionThreshold\r\n        cs[\"useRoI\"] = self._useRoI\r\n        cs[\"regionOfNoInterest\"] = self._regionOfNoInterest\r\n        cs[\"regionOfInterest\"] = self._regionOfInterest\r\n        cs[\"backSubModel\"] = self._backSubModel\r\n        cs[\"videoBboxes\"] = self._videoBboxes\r\n        cs[\"photoRois\"] = self._photoRois\r\n        cs[\"actionVR\"] = self._actionVR\r\n        cs[\"actionCircSize\"] = self._actionCircSize\r\n        cs[\"actionVideoDuration\"] = self._actionVideoDuration\r\n        cs[\"actionPhotoBurst\"] = self._actionPhotoBurst\r\n        cs[\"actionPhotoBurstDelaySec\"] = self._actionPhotoBurstDelaySec\r\n        return cs\r\n\r\n    @cameraSettings.setter\r\n    def cameraSettings(self, value: dict):\r\n        if \"actionPhoto\" in value:\r\n            self._actionPhoto = value[\"actionPhoto\"]\r\n        if \"actionVideo\" in value:\r\n            self._actionVideo = value[\"actionVideo\"]\r\n        if \"motionDetectAlgo\" in value:\r\n            self._motionDetectAlgo = value[\"motionDetectAlgo\"]\r\n        if \"msdThreshold\" in value:\r\n            self._msdThreshold = value[\"msdThreshold\"]\r\n        if \"bboxThreshold\" in value:\r\n            self._bboxThreshold = value[\"bboxThreshold\"]\r\n        if \"nmsThreshold\" in value:\r\n            self._nmsThreshold = value[\"nmsThreshold\"]\r\n        if \"motionThreshold\" in value:\r\n            self._motionThreshold = value[\"motionThreshold\"]\r\n        if \"useRoI\" in value:\r\n            self._useRoI = value[\"useRoI\"]\r\n        if \"regionOfNoInterest\" in value:\r\n            self._regionOfNoInterest = value[\"regionOfNoInterest\"]\r\n        if \"regionOfInterest\" in value:\r\n            self._regionOfInterest = value[\"regionOfInterest\"]\r\n        if \"backSubModel\" in value:\r\n            self._backSubModel = value[\"backSubModel\"]\r\n        if \"videoBboxes\" in value:\r\n            self._videoBboxes = value[\"videoBboxes\"]\r\n        if \"photoRois\" in value:\r\n            self._photoRois = value[\"photoRois\"]\r\n        if \"actionVR\" in value:\r\n            self._actionVR = value[\"actionVR\"]\r\n        if \"actionCircSize\" in value:\r\n            self._actionCircSize = value[\"actionCircSize\"]\r\n        if \"actionVideoDuration\" in value:\r\n            self._actionVideoDuration = value[\"actionVideoDuration\"]\r\n        if \"actionPhotoBurst\" in value:\r\n            self._actionPhotoBurst = value[\"actionPhotoBurst\"]\r\n        if \"actionPhotoBurstDelaySec\" in value:\r\n            self._actionPhotoBurstDelaySec = value[\"actionPhotoBurstDelaySec\"]\r\n\r\n    def setCameraSettingsToDefault(self):\r\n        self._actionPhoto = True\r\n        self._actionVideo = True\r\n        self._motionDetectAlgo = 1\r\n        self._msdThreshold = 10\r\n        self._bboxThreshold = 400\r\n        self._nmsThreshold = 0.001\r\n        self._motionThreshold = 1\r\n        self._useRoI = False\r\n        self._regionOfNoInterest = ()\r\n        self._regionOfInterest = ()\r\n        self._backSubModel = \"MOG2\"\r\n        self._videoBboxes = True\r\n        self._photoRois = True\r\n        self._actionVR = 1\r\n        self._actionCircSize = 5\r\n        self._actionVideoDuration = 10\r\n        self._actionPhotoBurst = 1\r\n        self._actionPhotoBurstDelaySec = 2\r\n\r\n    @property\r\n    def cameraDefaultSettings(self) -> dict:\r\n        cs = {}\r\n        cs[\"actionPhoto\"] = True\r\n        cs[\"actionVideo\"] = True\r\n        cs[\"motionDetectAlgo\"] = 1\r\n        cs[\"msdThreshold\"] = 10\r\n        cs[\"bboxThreshold\"] = 400\r\n        cs[\"nmsThreshold\"] = 0.001\r\n        cs[\"motionThreshold\"] = 1\r\n        cs[\"useRoI\"] = False\r\n        cs[\"regionOfNoInterest\"] = ()\r\n        cs[\"regionOfInterest\"] = ()\r\n        cs[\"backSubModel\"] = \"MOG2\"\r\n        cs[\"videoBboxes\"] = True\r\n        cs[\"actionVR\"] = 1\r\n        cs[\"actionCircSize\"] = 5\r\n        cs[\"actionVideoDuration\"] = 10\r\n        cs[\"actionPhotoBurst\"] = 1\r\n        cs[\"actionPhotoBurstDelaySec\"] = 2\r\n        return cs\r\n        \r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cc = TriggerConfig()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cc, key, value)\r\n            elif key == \"_triggers\":\r\n                if value is None:\r\n                    setattr(cc, key, value)\r\n                else:\r\n                    triggers = []\r\n                    for trg in value:\r\n                        trigger = Trigger.initFromDict(trg)\r\n                        triggers.append(trigger)\r\n                    setattr(cc, key, triggers)\r\n            elif key == \"_actions\":\r\n                if value is None:\r\n                    setattr(cc, key, value)\r\n                else:\r\n                    actions = []\r\n                    for act in value:\r\n                        action = Action.initFromDict(act)\r\n                        actions.append(action)\r\n                    setattr(cc, key, actions)\r\n            else:\r\n                setattr(cc, key, value)\r\n        #Reset some default values for which imported values shall be ignored\r\n        cc.evStart = None\r\n        cc.calStart = None\r\n        cc.notifyConOK = False\r\n        #Reset error\r\n        cc._error = None\r\n        cc._error2 = None\r\n        cc._errorSource = None\r\n        return cc\r\n\r\nclass CameraInfo():\r\n    def __init__(self):\r\n        self._model = \"\"\r\n        self._isUsb = False\r\n        self._usbDev = \"\"\r\n        self._hasAi = False\r\n        self._location = 0\r\n        self._rotation = 0\r\n        self._id = \"\"\r\n        self._num = 0\r\n        self._status = \"\"\r\n\r\n    @property\r\n    def model(self) -> str:\r\n        return self._model\r\n\r\n    @model.setter\r\n    def model(self, value: str):\r\n        self._model = value\r\n\r\n    @property\r\n    def isUsb(self) -> bool:\r\n        return self._isUsb\r\n\r\n    @isUsb.setter\r\n    def isUsb(self, value: bool):\r\n        self._isUsb = value\r\n\r\n    @property\r\n    def hasAi(self) -> bool:\r\n        return self._hasAi\r\n\r\n    @hasAi.setter\r\n    def hasAi(self, value: bool):\r\n        self._hasAi = value\r\n\r\n    @property\r\n    def usbDev(self) -> str:\r\n        return self._usbDev\r\n\r\n    @usbDev.setter\r\n    def usbDev(self, value: str):\r\n        self._usbDev = value\r\n\r\n    @property\r\n    def location(self) -> int:\r\n        return self._location\r\n\r\n    @location.setter\r\n    def location(self, value: int):\r\n        self._location = value\r\n\r\n    @property\r\n    def rotation(self) -> int:\r\n        return self._rotation\r\n\r\n    @rotation.setter\r\n    def rotation(self, value: int):\r\n        self._rotation = value\r\n\r\n    @property\r\n    def id(self) -> str:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: str):\r\n        self._id = value\r\n\r\n    @property\r\n    def num(self) -> int:\r\n        return self._num\r\n\r\n    @num.setter\r\n    def num(self, value: int):\r\n        self._num = value\r\n\r\n    @property\r\n    def status(self) -> str:\r\n        return self._status\r\n\r\n    @status.setter\r\n    def status(self, value: str):\r\n        self._status = value\r\n\r\n    def setUsbDev(self):\r\n        \"\"\"Determine and set the device for a USB camera, based on camera model and USB port\r\n\r\n        The USB port is determined from the camera ID.\r\n        The ID is assumed to have the following structure (example):\r\n        /base/axi/pcie@1000120000/rp1/usb@200000-2:1.0-046d:085c\r\n        |_______________________||_____________| | |_| |__| |__|\r\n           USB root port path      USB root hub  |  |   |    |\r\n                                                 |  |   |    └ Product ID\r\n                                                 |  |   └ Vendor ID\r\n                                                 |  └ Interface\r\n                                                 └ USB port\r\n\r\n        Information from the ID is matched to information on video devices from 'v4l2-ctl --list-devices'\r\n        \"\"\"\r\n        logger.debug(\"CameraInfo.setUsbDev for num=%s model=%s\", self.num, self.model)\r\n        usbDev = \"\"\r\n        if self._isUsb:\r\n            usbDev = \"UNKNOWN\"\r\n            logger.debug(\"CameraInfo.setUsbDev - ID=%s\", self.id)\r\n            idParts = self._id.split(\"-\")\r\n            if len(idParts) >= 2:\r\n                usbPart = idParts[1]\r\n                productPart = idParts[2]\r\n                logger.debug(\"CameraInfo.setUsbDev - usbPart=%s\", usbPart)\r\n                logger.debug(\"CameraInfo.setUsbDev - productPart=%s\", productPart)\r\n                usbPort = usbPart.split(\":\")[0]\r\n                vidPid = productPart\r\n                model = self.model\r\n                logger.debug(\"CameraInfo.setUsbDev - usbPort=%s vidPid=%s model=%s\", usbPort, vidPid, model)\r\n\r\n                # Find which /dev/video node has the same USB port and same model name or VID:PID\r\n                try:\r\n                    result = subprocess.run(\r\n                        [\"v4l2-ctl\", \"--list-devices\"], capture_output=True, text=True\r\n                    ).stdout\r\n\r\n                    # For each camera block in v4l2 output\r\n                    for block in result.strip().split(\"\\n\\n\"):\r\n                        if vidPid in block or model in block:\r\n                            logger.debug(\"CameraInfo.setUsbDev - Found matching block in v4l2-ctl output\")\r\n                            lines = [l.strip() for l in block.splitlines() if \"/dev/video\" in l]\r\n                            device = lines[0]\r\n                            logger.debug(\"CameraInfo.setUsbDev - Found device: %s\", device)\r\n                            usbDev = device\r\n                            break\r\n                    if usbDev == \"UNKNOWN\":\r\n                        logger.debug(\"CameraInfo.setUsbDev - No matching device found in v4l2-ctl output\")\r\n\r\n                except CalledProcessError as e:\r\n                    logger.error(\"CameraInfo.setUsbDev - CalledProcessError: %s\", e)\r\n                    # In case v4l2-ctl cannot be run, ignore the exception\r\n                    pass\r\n                except Exception as e:\r\n                    logger.error(\"CameraInfo.setUsbDev - Exception: %s\", e)\r\n                    pass\r\n        self._usbDev = usbDev\r\n        logger.debug(\"CameraInfo.setUsbDev - Set usbDev=%s\", self._usbDev)\r\n\r\nclass CameraControls():\r\n    def __init__(self):\r\n        self._aeConstraintMode = 0\r\n        self.include_aeConstraintMode = False\r\n        self._aeEnable = True\r\n        self.include_aeEnable = False\r\n        self._aeExposureMode = 0\r\n        self.include_aeExposureMode = False\r\n        self._aeFlickerMode = 0\r\n        self.include_aeFlickerMode = False\r\n        self._aeFlickerPeriod = 10000\r\n        self.include_aeFlickerPeriod = False\r\n        self._aeMeteringMode = 0\r\n        self.include_aeMeteringMode = False\r\n        self._afMode = 0\r\n        self.include_afMode = False\r\n        self._lensPosition = 1.0\r\n        self.include_lensPosition = False\r\n        self._afMetering = 0\r\n        self.include_afMetering = False\r\n        self._afPause = 0\r\n        self.include_afPause = False\r\n        self._afRange = 0\r\n        self.include_afRange = False\r\n        self._afSpeed = 0\r\n        self.include_afSpeed = False\r\n        self._afTrigger = 0\r\n        self.include_afTrigger = False\r\n        self._afWindows = ()\r\n        self.include_afWindows = False\r\n        self._analogueGain = 1.0\r\n        self.include_analogueGain = False\r\n        self._awbEnable = True\r\n        self.include_awbEnable = False\r\n        self._awbMode = 0\r\n        self.include_awbMode = False\r\n        self._brightness = 0.0\r\n        self.include_brightness = False\r\n        self._colourGains = (0, 0)\r\n        self.include_colourGains = False\r\n        self._contrast = 1.0\r\n        self.include_contrast = False\r\n        self._exposureTime = 0\r\n        self.include_exposureTime = False\r\n        self._exposureValue = 0.0\r\n        self.include_exposureValue = False\r\n        self._frameDurationLimits = (0, 0)\r\n        self.include_frameDurationLimits = False\r\n        self._hdrMode = 0\r\n        self.include_hdrMode = False\r\n        self._noiseReductionMode = 0\r\n        self.include_noiseReductionMode = False\r\n        self._saturation = 1.0\r\n        self.include_saturation = False\r\n        self._scalerCrop = (0, 0, 4608, 2592)\r\n        self.include_scalerCrop = False\r\n        self._sharpness = 1.0\r\n        self.include_sharpness = False\r\n        self.usbCamControls = {}\r\n\r\n    def dict(self) -> dict:\r\n        dict={}\r\n        dict[\"AeConstraintMode\"] = [self.include_aeConstraintMode, self._aeConstraintMode]\r\n        dict[\"AeEnable\"] = [self.include_aeEnable, self._aeEnable ]\r\n        dict[\"AeExposureMode\"] = [self.include_aeExposureMode, self._aeExposureMode]\r\n        dict[\"AeFlickerMode\"] = [self.include_aeFlickerMode, self._aeFlickerMode]\r\n        dict[\"AeFlickerPeriod\"] = [self.include_aeFlickerPeriod, self._aeFlickerPeriod]\r\n        dict[\"AeMeteringMode\"] = [self.include_aeMeteringMode, self._aeMeteringMode]\r\n        dict[\"AfMode\"] = [self.include_afMode, self._afMode]\r\n        dict[\"LensPosition\"] = [self.include_lensPosition, self._lensPosition]\r\n        dict[\"AfMetering\"] = [self.include_afMetering, self._afMetering]\r\n        dict[\"AfPause\"] = [self.include_afPause, self._afPause]\r\n        dict[\"AfRange\"] = [self.include_afRange, self._afRange]\r\n        dict[\"AfSpeed\"] = [self.include_afSpeed, self._afSpeed]\r\n        dict[\"AfTrigger\"] = [self.include_afTrigger, self._afTrigger]\r\n        dict[\"AfWindows\"] = [self.include_afWindows, self._afWindows]\r\n        dict[\"AnalogueGain\"] = [self.include_analogueGain, self._analogueGain]\r\n        dict[\"AwbEnable\"] = [self.include_awbEnable, self._awbEnable]\r\n        dict[\"AwbMode\"] = [self.include_awbMode, self._awbMode]\r\n        dict[\"Brightness\"] = [self.include_brightness, self._brightness]\r\n        dict[\"ColourGains\"] = [self.include_colourGains, self._colourGains]\r\n        dict[\"Contrast\"] = [self.include_contrast, self._contrast]\r\n        dict[\"ExposureTime\"] = [self.include_exposureTime, self._exposureTime]\r\n        dict[\"ExposureValue\"] = [self.include_exposureValue, self._exposureValue]\r\n        dict[\"FrameDurationLimits\"] = [self.include_frameDurationLimits, self._frameDurationLimits]\r\n        dict[\"HdrMode\"] = [self.include_hdrMode, self._hdrMode]\r\n        dict[\"NoiseReductionMode\"] = [self.include_noiseReductionMode, self._noiseReductionMode]\r\n        dict[\"Saturation\"] = [self.include_saturation, self._saturation]\r\n        dict[\"ScalerCrop\"] = [self.include_scalerCrop, self._scalerCrop]\r\n        dict[\"Sharpness\"] = [self.include_sharpness, self._sharpness]\r\n        return dict\r\n        \r\n    @property\r\n    def aeConstraintMode(self) -> int:\r\n        return self._aeConstraintMode\r\n\r\n    @aeConstraintMode.setter\r\n    def aeConstraintMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2 \\\r\n        or value == 3:\r\n            self._aeConstraintMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for aeConstraintMode\")\r\n\r\n    @aeConstraintMode.deleter\r\n    def aeConstraintMode(self):\r\n        del self._aeConstraintMode\r\n\r\n    @property\r\n    def aeEnable(self) -> bool:\r\n        return self._aeEnable\r\n\r\n    @aeEnable.setter\r\n    def aeEnable(self, value: bool):\r\n        self._aeEnable = value\r\n\r\n    @aeEnable.deleter\r\n    def aeEnable(self):\r\n        del self._aeEnable\r\n        \r\n    @property\r\n    def aeExposureMode(self) -> int:\r\n        return self._aeExposureMode\r\n\r\n    @aeExposureMode.setter\r\n    def aeExposureMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2 \\\r\n        or value == 3:\r\n            self._aeExposureMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for aeExposureMode\")\r\n\r\n    @aeExposureMode.deleter\r\n    def aeExposureMode(self):\r\n        del self._aeExposureMode\r\n        \r\n    @property\r\n    def aeFlickerMode(self) -> int:\r\n        return self._aeFlickerMode\r\n\r\n    @aeFlickerMode.setter\r\n    def aeFlickerMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2:\r\n            self._aeFlickerMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for aeFlickerMode\")\r\n\r\n    @aeFlickerMode.deleter\r\n    def aeFlickerMode(self):\r\n        del self._aeFlickerMode\r\n        \r\n    @property\r\n    def aeFlickerPeriod(self) -> int:\r\n        return self._aeFlickerPeriod\r\n\r\n    @aeFlickerPeriod.setter\r\n    def aeFlickerPeriod(self, value: int):\r\n        if value > 0:\r\n            self._aeFlickerPeriod = value\r\n        else:\r\n            raise ValueError(\"Invalid value for aeFlickerPeriod\")\r\n\r\n    @aeFlickerPeriod.deleter\r\n    def aeFlickerPeriod(self):\r\n        del self._aeFlickerPeriod\r\n        \r\n    @property\r\n    def aeMeteringMode(self) -> int:\r\n        return self._aeMeteringMode\r\n\r\n    @aeMeteringMode.setter\r\n    def aeMeteringMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2 \\\r\n        or value == 3:\r\n            self._aeMeteringMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for aeMeteringMode\")\r\n\r\n    @aeMeteringMode.deleter\r\n    def aeMeteringMode(self):\r\n        del self._aeMeteringMode\r\n\r\n    @property\r\n    def afMode(self) -> int:\r\n        return self._afMode\r\n\r\n    @afMode.setter\r\n    def afMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2:\r\n            self._afMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afMode\")\r\n\r\n    @afMode.deleter\r\n    def afMode(self):\r\n        del self._afMode\r\n        \r\n    @property\r\n    def lensPosition(self) -> float:\r\n        return self._lensPosition\r\n    \r\n    @lensPosition.setter\r\n    def lensPosition(self, value: float):\r\n        self._lensPosition = value\r\n\r\n    @lensPosition.deleter\r\n    def lensPosition(self):\r\n        del self._lensPosition\r\n        \r\n    @property\r\n    def focalDistance(self) -> float:\r\n        if self._lensPosition == 0:\r\n            return 9999.9\r\n        else:\r\n            fd = 1.0 / self._lensPosition\r\n            fd = int(1000 * fd)/1000\r\n            return fd\r\n\r\n    @focalDistance.setter\r\n    def focalDistance(self, value: float):\r\n        if value > 0:\r\n            if value > 9999.9:\r\n                self._lensPosition = 0\r\n            else:\r\n                self._lensPosition = 1.0 / value\r\n        else:\r\n            self._lensPosition = 9999.9\r\n        \r\n    @property\r\n    def afMetering(self) -> int:\r\n        return self._afMetering\r\n\r\n    @afMetering.setter\r\n    def afMetering(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1:\r\n            self._afMetering = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afMetering\")\r\n\r\n    @afMetering.deleter\r\n    def afMetering(self):\r\n        del self._afMetering\r\n        \r\n    @property\r\n    def afPause(self) -> int:\r\n        return self._afPause\r\n\r\n    @afPause.setter\r\n    def afPause(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2:\r\n            self._afPause = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afPause\")\r\n\r\n    @afPause.deleter\r\n    def afPause(self):\r\n        del self._afPause\r\n        \r\n    @property\r\n    def afRange(self) -> int:\r\n        return self._afRange\r\n\r\n    @afRange.setter\r\n    def afRange(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2:\r\n            self._afRange = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afRange\")\r\n\r\n    @afRange.deleter\r\n    def afRange(self):\r\n        del self._afRange\r\n        \r\n    @property\r\n    def afSpeed(self) -> int:\r\n        return self._afSpeed\r\n\r\n    @afSpeed.setter\r\n    def afSpeed(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1:\r\n            self._afSpeed = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afSpeed\")\r\n\r\n    @afSpeed.deleter\r\n    def afSpeed(self):\r\n        del self._afSpeed\r\n        \r\n    @property\r\n    def scalerCrop(self) -> tuple:\r\n        return self._scalerCrop\r\n\r\n    @scalerCrop.setter\r\n    def scalerCrop(self, value: tuple):\r\n        self._scalerCrop = value\r\n\r\n    @scalerCrop.deleter\r\n    def scalerCrop(self):\r\n        del self._scalerCrop\r\n        \r\n    @property\r\n    def scalerCropStr(self) -> str:\r\n        return \"(\" + str(self._scalerCrop[0]) + \",\" + str(self._scalerCrop[1]) + \",\" + str(self._scalerCrop[2]) + \",\" + str(self._scalerCrop[3]) + \")\"\r\n\r\n    @scalerCropStr.setter\r\n    def scalerCropStr(self, value: str):\r\n        self._scalerCrop = CameraControls._parseRectTuple(value)\r\n\r\n    @property\r\n    def afTrigger(self) -> int:\r\n        return self._afTrigger\r\n\r\n    @afTrigger.setter\r\n    def afTrigger(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1:\r\n            self._afTrigger = value\r\n        else:\r\n            raise ValueError(\"Invalid value for afTrigger\")\r\n\r\n    @afTrigger.deleter\r\n    def afTrigger(self):\r\n        del self._afTrigger\r\n\r\n    @property\r\n    def afWindows(self) -> tuple:\r\n        return self._afWindows\r\n\r\n    @afWindows.setter\r\n    def afWindows(self, value: tuple):\r\n        self._afWindows = value\r\n\r\n    @afWindows.deleter\r\n    def afWindows(self):\r\n        del self._afWindows\r\n        \r\n    @property\r\n    def afWindowsStr(self) -> str:\r\n        res = \"(\"\r\n        for win in self.afWindows:\r\n            if len(res) > 1:\r\n                res = res + \",\"\r\n            res = res + \"(\" + str(win[0]) + \",\" + str(win[1]) + \",\" + str(win[2]) + \",\" + str(win[3]) + \")\"\r\n        res = res + \")\"\r\n        return res\r\n\r\n    @afWindowsStr.setter\r\n    def afWindowsStr(self, value: str):\r\n        \"\"\"Parse the string representation for afWindows\r\n        \"\"\"\r\n        self._afWindows = ()\r\n        # Get the list of windows\r\n        winlist = CameraControls._parseWindows(value)\r\n        for win in winlist:\r\n            awin = CameraControls._parseRectTuple(win)\r\n            # Add window from list to _afWindows tuple\r\n            awin = (awin,)\r\n            self._afWindows += awin\r\n\r\n    @property\r\n    def analogueGain(self) -> float:\r\n        return self._analogueGain\r\n\r\n    @analogueGain.setter\r\n    def analogueGain(self, value: float):\r\n        if value >= 1:\r\n            self._analogueGain = value\r\n        else:\r\n            raise ValueError(\"Invalid value for _analogueGain. Must be >= 1.\")\r\n\r\n    @analogueGain.deleter\r\n    def analogueGain(self):\r\n        del self._analogueGain\r\n\r\n    @property\r\n    def awbEnable(self) -> bool:\r\n        return self._awbEnable\r\n\r\n    @awbEnable.setter\r\n    def awbEnable(self, value: bool):\r\n        self._awbEnable = value\r\n\r\n    @awbEnable.deleter\r\n    def awbEnable(self):\r\n        del self._awbEnable\r\n\r\n    @property\r\n    def awbMode(self) -> int:\r\n        return self._awbMode\r\n\r\n    @awbMode.setter\r\n    def awbMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 2 \\\r\n        or value == 3 \\\r\n        or value == 4 \\\r\n        or value == 5 \\\r\n        or value == 6 \\\r\n        or value == 7:\r\n            self._awbMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for awbMode\")\r\n\r\n    @awbMode.deleter\r\n    def awbMode(self):\r\n        del self._awbMode\r\n\r\n    @property\r\n    def brightness(self) -> float:\r\n        return self._brightness\r\n\r\n    @brightness.setter\r\n    def brightness(self, value: float):\r\n        self._brightness = value\r\n\r\n    @brightness.deleter\r\n    def brightness(self):\r\n        del self._brightness\r\n\r\n    @property\r\n    def colourGains(self) -> tuple:\r\n        return self._colourGains\r\n\r\n    @colourGains.setter\r\n    def colourGains(self, value: tuple):\r\n        if len(value) == 2:\r\n            if value[0] >= 0.0 \\\r\n            and value[1] >= 0.0 \\\r\n            and value[0] <= 32.0 \\\r\n            and value[1] <= 32.0:\r\n                self._colourGains = value\r\n            else:\r\n                raise ValueError(\"Invalid value for colourGains. Values must be in range [0.0;32.0]\")\r\n        else:\r\n            raise ValueError(\"Invalid value for colourGains. Must be tuple of 2\")\r\n\r\n    @colourGains.deleter\r\n    def colourGains(self):\r\n        del self._colourGains\r\n\r\n    @property\r\n    def colourGainRed(self) -> float:\r\n        return self._colourGains[0]\r\n\r\n    @property\r\n    def colourGainBlue(self) -> float:\r\n        return self._colourGains[1]\r\n\r\n    @property\r\n    def contrast(self) -> float:\r\n        return self._contrast\r\n\r\n    @contrast.setter\r\n    def contrast(self, value: float):\r\n        self._contrast = value\r\n\r\n    @contrast.deleter\r\n    def contrast(self):\r\n        del self._contrast\r\n\r\n    @property\r\n    def exposureTime(self) -> int:\r\n        return self._exposureTime\r\n\r\n    @exposureTime.setter\r\n    def exposureTime(self, value: int):\r\n        if value >= 0:\r\n            self._exposureTime = value\r\n        else:\r\n            raise ValueError(\"Invalid value for exposureTime. Must be > 0\")\r\n\r\n    @exposureTime.deleter\r\n    def exposureTime(self):\r\n        del self._exposureTime\r\n\r\n    @property\r\n    def exposureTimeSec(self) -> float:\r\n        return float(self._exposureTime / 1000000)\r\n\r\n    @exposureTimeSec.setter\r\n    def exposureTimeSec(self, value: float):\r\n        if value >= 0:\r\n            self._exposureTime = int(value * 1000000)\r\n        else:\r\n            raise ValueError(\"Invalid value for exposureTime. Must be > 0\")\r\n\r\n    @property\r\n    def exposureValue(self) -> float:\r\n        return self._exposureValue\r\n\r\n    @exposureValue.setter\r\n    def exposureValue(self, value: float):\r\n        if value >= -8.0 \\\r\n        and value <= 8.0:\r\n            self._exposureValue = value\r\n        else:\r\n            raise ValueError(\"Invalid value for exposureValue. Must be in range [-8.0;8.0]\")\r\n\r\n    @exposureValue.deleter\r\n    def exposureValue(self):\r\n        del self._exposureValue\r\n\r\n    @property\r\n    def frameDurationLimits(self) -> tuple:\r\n        return self._frameDurationLimits\r\n\r\n    @frameDurationLimits.setter\r\n    def frameDurationLimits(self, value: tuple):\r\n        if value[0] >= 0 \\\r\n        and value[1] >= 0:\r\n            self._frameDurationLimits = value\r\n        else:\r\n            raise ValueError(\"Invalid value for frameDurationLimits\")\r\n\r\n    @frameDurationLimits.deleter\r\n    def frameDurationLimits(self):\r\n        del self._frameDurationLimits\r\n\r\n    @property\r\n    def frameDurationLimitMax(self) -> int:\r\n        return self._frameDurationLimits[0]\r\n\r\n    @property\r\n    def frameDurationLimitMin(self) -> int:\r\n        return self._frameDurationLimits[1]\r\n\r\n    @property\r\n    def hdrMode(self) -> int:\r\n        return self._hdrMode\r\n\r\n    @hdrMode.setter\r\n    def hdrMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2 \\\r\n        or value == 3 \\\r\n        or value == 4:\r\n            self._hdrMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for hdrMode\")\r\n\r\n    @hdrMode.deleter\r\n    def hdrMode(self):\r\n        del self._hdrMode\r\n\r\n    @property\r\n    def noiseReductionMode(self) -> int:\r\n        return self._noiseReductionMode\r\n\r\n    @noiseReductionMode.setter\r\n    def noiseReductionMode(self, value: int):\r\n        if value == 0 \\\r\n        or value == 1 \\\r\n        or value == 2:\r\n            self._noiseReductionMode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for noiseReductionMode\")\r\n\r\n    @noiseReductionMode.deleter\r\n    def noiseReductionMode(self):\r\n        del self._noiseReductionMode\r\n\r\n    @property\r\n    def saturation(self) -> float:\r\n        return self._saturation\r\n\r\n    @saturation.setter\r\n    def saturation(self, value: float):\r\n        self._saturation = value\r\n\r\n    @saturation.deleter\r\n    def saturation(self):\r\n        del self._saturation\r\n\r\n    @property\r\n    def sharpness(self) -> float:\r\n        return self._sharpness\r\n\r\n    @sharpness.setter\r\n    def sharpness(self, value: float):\r\n        self._sharpness = value\r\n\r\n    @sharpness.deleter\r\n    def sharpness(self):\r\n        del self._sharpness\r\n    \r\n    @property\r\n    def usbCamControls(self) -> dict:\r\n        return self._usbCamControls\r\n\r\n    @usbCamControls.setter\r\n    def usbCamControls(self, value: dict):\r\n        self._usbCamControls = value\r\n\r\n    @usbCamControls.deleter\r\n    def usbCamControls(self):\r\n        del self._usbCamControls\r\n\r\n    @staticmethod    \r\n    def _parseWindows(wins: str) -> list:\r\n        \"\"\"  Parses the tuple-string of one or multiple rectangles\r\n            \"((x,x,x,x),(x,x,x,x))\"\r\n            and returns an array of rectangles as strings\r\n        \"\"\"\r\n        resa = []\r\n        if wins.startswith(\"(\"):\r\n            wns = wins[1:]\r\n            if wns.endswith(\")\"):\r\n                wns = wns[0: len(wns) - 1]\r\n                while len(wns) > 0:\r\n                    i = wns.find(\")\")\r\n                    if i > 0:\r\n                        wn = wns[0: i + 1]\r\n                        resa.append(wn)\r\n                        if i < len(wns):\r\n                            wns = wns[i + 2:].strip()\r\n                        else:\r\n                            wns = \"\"\r\n                    else:\r\n                        wns = \"\"\r\n        return resa\r\n\r\n    @staticmethod    \r\n    def _parseRectTuple(stuple: str) -> tuple:\r\n        \"\"\"  Parse a Python tuple string for libcamera.Rectangle\r\n             \"(xOffset, yOffset, width, height)\"\r\n        \"\"\"\r\n        rest = (0, 0, 0, 0)\r\n        if stuple.startswith(\"(\"):\r\n            tpl = stuple[1:]\r\n            if tpl.endswith(\")\"):\r\n                tpl = tpl[0: len(tpl) - 1]\r\n                res = tpl.rsplit(\",\")\r\n                if len(res) == 4:\r\n                    rest = (int(res[0]), int(res[1]), int(res[2]), int(res[3]))\r\n        return rest\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cc = CameraControls()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cc, key, value)\r\n            else:\r\n                if key == \"_scalerCrop\":\r\n                    setattr(cc, key, tuple(value))\r\n                elif key == \"_frameDurationLimits\":\r\n                    setattr(cc, key, tuple(value))\r\n                elif key == \"_colourGains\":\r\n                    setattr(cc, key, tuple(value))\r\n                elif key == \"_afWindows\":\r\n                    afws = ()\r\n                    for el in value:\r\n                        afw = (tuple(el),)\r\n                        afws += afw\r\n                    setattr(cc, key, afws)\r\n                elif key == \"_usbCamControls\":\r\n                    setattr(cc, key, value)\r\n                else:\r\n                    setattr(cc, key, value)\r\n        return cc\r\n\r\nclass SensorMode():\r\n    \"\"\" The class represents a specific sensor mode of the camera\r\n    \"\"\"\r\n    def __init__(self):\r\n        self._id = None\r\n        self._format = None\r\n        self._unpacked = None\r\n        self._bit_depth = None\r\n        self._size = None\r\n        self._fps = None\r\n        self._crop_limits = None\r\n        self._exposure_limits = None\r\n\r\n    @property\r\n    def id(self) -> int:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: int):\r\n        self._id = value\r\n\r\n    @property\r\n    def format(self) -> str:\r\n        return self._format\r\n\r\n    @format.setter\r\n    def format(self, value: str):\r\n        self._format = value\r\n\r\n    @property\r\n    def unpacked(self) -> str:\r\n        return self._unpacked\r\n\r\n    @unpacked.setter\r\n    def unpacked(self, value: str):\r\n        self._unpacked = value\r\n\r\n    @property\r\n    def bit_depth(self) -> int:\r\n        return self._bit_depth\r\n\r\n    @bit_depth.setter\r\n    def bit_depth(self, value: int):\r\n        self._bit_depth = value\r\n\r\n    @property\r\n    def size(self) -> tuple[int, int]:\r\n        return self._size\r\n\r\n    @size.setter\r\n    def size(self, value: tuple[int, int]):\r\n        self._size = value\r\n\r\n    @property\r\n    def fps(self) -> float:\r\n        return self._fps\r\n\r\n    @fps.setter\r\n    def fps(self, value: float):\r\n        self._fps = value\r\n\r\n    @property\r\n    def crop_limits(self) -> tuple:\r\n        return self._crop_limits\r\n\r\n    @crop_limits.setter\r\n    def crop_limits(self, value: tuple):\r\n        self._crop_limits = value\r\n\r\n    @property\r\n    def exposure_limits(self) -> tuple:\r\n        return self._exposure_limits\r\n\r\n    @exposure_limits.setter\r\n    def exposure_limits(self, value: tuple):\r\n        self._exposure_limits = value\r\n\r\n    @property\r\n    def tabId(self) -> str:\r\n        return \"sensormode\" + str(self.id)\r\n\r\n    @property\r\n    def tabButtonId(self) -> str:\r\n        return \"sensormodetab\" + str(self.id)\r\n\r\n    @property\r\n    def tabTitle(self) -> str:\r\n        return \"Sensor Mode \" + str(self.id)\r\n\r\nclass TuningConfig():\r\n    def __init__(self):\r\n        self._loadTuningFile = False\r\n        self._tuningFolderDef = None\r\n        self._tuningFolder = None\r\n        self._tuningFile = \"\"\r\n\r\n    @property\r\n    def loadTuningFile(self) -> bool:\r\n        return self._loadTuningFile\r\n\r\n    @loadTuningFile.setter\r\n    def loadTuningFile(self, value: bool):\r\n        self._loadTuningFile = value\r\n\r\n    @property\r\n    def tuningFolderDef(self) -> str:\r\n        return self._tuningFolderDef\r\n\r\n    @property\r\n    def tuningFolder(self) -> str:\r\n        return self._tuningFolder\r\n\r\n    @tuningFolder.setter\r\n    def tuningFolder(self, value: str):\r\n        self._tuningFolder = value\r\n\r\n    @property\r\n    def tuningFile(self) -> str:\r\n        return self._tuningFile\r\n\r\n    @tuningFile.setter\r\n    def tuningFile(self, value: str):\r\n        self._tuningFile = value\r\n\r\n    @property\r\n    def tuningFilePath(self) -> str:\r\n        if self.tuningFolder is None:\r\n            return self._tuningFile\r\n        else:\r\n            return self.tuningFolder + \"/\" + self._tuningFile\r\n\r\n    @property\r\n    def isDefaultFolder(self) -> bool:\r\n        return self.tuningFolder == self.tuningFolderDef\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cc = TuningConfig()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cc, key, value)\r\n            else:\r\n                setattr(cc, key, value)\r\n        return cc\r\n\r\nclass AiConfig():\r\n    def __init__(self):\r\n        self._enable = False\r\n        self._modelFolderDef = \"/usr/share/imx500-models\"\r\n        self.tasks = [\"Classification\", \"Object Detection\", \"Pose Estimation\", \"Segmentation\"]\r\n        self._task = None\r\n        self._modelFolder = \"/usr/share/imx500-models\"\r\n        self._modelFiles = []\r\n        self._modelFile = \"\"\r\n        self._modelIntrinsics = None\r\n        self._drawOnLores = True\r\n        self._drawOnMain = False\r\n        self._topK = 3\r\n        self._detectionThreshold = 0.6\r\n        self._iouThreshold = 0.6\r\n        self._maxDetections = 10\r\n\r\n    @property\r\n    def enable(self) -> bool:\r\n        return self._enable\r\n\r\n    @enable.setter\r\n    def enable(self, value: bool):\r\n        self._enable = value\r\n\r\n    @property\r\n    def task(self) -> str:\r\n        return self._task\r\n\r\n    @task.setter\r\n    def task(self, value: str):\r\n        self._task = value\r\n\r\n    @property\r\n    def modelFolder(self) -> str:\r\n        return self._modelFolder\r\n\r\n    @modelFolder.setter\r\n    def modelFolder(self, value: str):\r\n        self._modelFolder = value\r\n\r\n    @property\r\n    def modelFiles(self) -> list[str]:\r\n        return self._modelFiles\r\n\r\n    @modelFiles.setter\r\n    def modelFiles(self, value: list[str]):\r\n        self._modelFiles = value\r\n\r\n    @property\r\n    def modelFile(self) -> str:\r\n        return self._modelFile\r\n\r\n    @modelFile.setter\r\n    def modelFile(self, value: str):\r\n        self._modelFile = value\r\n\r\n    @property\r\n    def modelIntrinsics(self) -> dict:\r\n        return self._modelIntrinsics\r\n\r\n    @modelIntrinsics.setter\r\n    def modelIntrinsics(self, value: dict):\r\n        self._modelIntrinsics = value\r\n\r\n    @property\r\n    def drawOnLores(self) -> bool:\r\n        return self._drawOnLores\r\n\r\n    @drawOnLores.setter\r\n    def drawOnLores(self, value: bool):\r\n        self._drawOnLores = value\r\n\r\n    @property\r\n    def drawOnMain(self) -> bool:\r\n        return self._drawOnMain\r\n\r\n    @drawOnMain.setter\r\n    def drawOnMain(self, value: bool):\r\n        self._drawOnMain = value\r\n\r\n    @property\r\n    def topK(self) -> int:\r\n        return self._topK\r\n\r\n    @topK.setter\r\n    def topK(self, value: int):\r\n        self._topK = value\r\n\r\n    @property\r\n    def detectionThreshold(self) -> float:\r\n        return self._detectionThreshold\r\n\r\n    @detectionThreshold.setter\r\n    def detectionThreshold(self, value: float):\r\n        self._detectionThreshold = value\r\n\r\n    @property\r\n    def iouThreshold(self) -> float:\r\n        return self._iouThreshold\r\n\r\n    @iouThreshold.setter\r\n    def iouThreshold(self, value: float):\r\n        self._iouThreshold = value\r\n\r\n    @property\r\n    def maxDetections(self) -> int:\r\n        return self._maxDetections\r\n\r\n    @maxDetections.setter\r\n    def maxDetections(self, value: int):\r\n        self._maxDetections = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cc = AiConfig()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cc, key, value)\r\n            else:\r\n                setattr(cc, key, value)\r\n        return cc\r\n\r\nclass CameraConfig():\r\n    def __init__(self):\r\n        self._id = \"\"\r\n        self._use_case = \"\"\r\n        self._transform_hflip = False\r\n        self._transform_vflip = False\r\n        self._colour_space = \"sYCC\"\r\n        self._buffer_count = 1\r\n        self._queue = False\r\n        self._display = None\r\n        self._encode = None\r\n        self._sensor_mode = \"0\"\r\n        self._stream = \"main\"\r\n        self._stream_size = None\r\n        self._stream_size_align = False\r\n        self._format = \"RGB888\"\r\n        self._controls = {}\r\n\r\n    @property\r\n    def id(self) -> str:\r\n        return self._id\r\n\r\n    @id.setter\r\n    def id(self, value: str):\r\n        self._id = value\r\n\r\n    @property\r\n    def use_case(self) -> str:\r\n        return self._use_case\r\n\r\n    @use_case.setter\r\n    def use_case(self, value: str):\r\n        self._use_case = value\r\n\r\n    @property\r\n    def transform_hflip(self) -> bool:\r\n        return self._transform_hflip\r\n\r\n    @transform_hflip.setter\r\n    def transform_hflip(self, value: bool):\r\n        self._transform_hflip = value\r\n\r\n    @property\r\n    def transform_vflip(self) -> bool:\r\n        return self._transform_vflip\r\n\r\n    @transform_vflip.setter\r\n    def transform_vflip(self, value: bool):\r\n        self._transform_vflip = value\r\n\r\n    @property\r\n    def colour_space(self) -> str:\r\n        return self._colour_space\r\n\r\n    @colour_space.setter\r\n    def colour_space(self, value: str):\r\n        self._colour_space = value\r\n        \r\n    @property\r\n    def buffer_count(self) -> int:\r\n        return self._buffer_count\r\n\r\n    @buffer_count.setter\r\n    def buffer_count(self, value: int):\r\n        self._buffer_count = value\r\n\r\n    @property\r\n    def queue(self) -> bool:\r\n        return self._queue\r\n\r\n    @queue.setter\r\n    def queue(self, value: bool):\r\n        self._queue = value\r\n\r\n    @property\r\n    def display(self) -> str:\r\n        return self._display\r\n\r\n    @display.setter\r\n    def display(self, value: str):\r\n        self._display = value\r\n\r\n    @property\r\n    def encode(self) -> str:\r\n        return self._encode\r\n\r\n    @encode.setter\r\n    def encode(self, value: str):\r\n        if value is None:\r\n            self._encode = value\r\n        else:\r\n            if value == \"main\" \\\r\n            or value == \"lores\" \\\r\n            or value == \"raw\":\r\n                self._encode = value\r\n            else:\r\n                raise ValueError(\"Invalid value for encode: %s\", value)\r\n\r\n    @property\r\n    def sensor_mode(self) -> str:\r\n        return self._sensor_mode\r\n\r\n    @sensor_mode.setter\r\n    def sensor_mode(self, value: str):\r\n        self._sensor_mode = value\r\n\r\n    @property\r\n    def stream(self) -> str:\r\n        return self._stream\r\n\r\n    @stream.setter\r\n    def stream(self, value: str):\r\n        if value == \"main\" \\\r\n        or value == \"lores\" \\\r\n        or value == \"raw\":\r\n            self._stream = value\r\n        else:\r\n            raise ValueError(\"Invalid value for stream: %s. Must be 'main', 'lores' or 'raw'\", value)\r\n\r\n    @property\r\n    def stream_size(self) -> tuple[int, int]:\r\n        return self._stream_size\r\n\r\n    @stream_size.setter\r\n    def stream_size(self, value: tuple[int, int]):\r\n        self._stream_size = value\r\n\r\n    @property\r\n    def stream_size_align(self) -> bool:\r\n        return self._stream_size_align\r\n\r\n    @stream_size_align.setter\r\n    def stream_size_align(self, value: bool):\r\n        self._stream_size_align = value\r\n\r\n    @property\r\n    def format(self) -> str:\r\n        return self._format\r\n\r\n    @format.setter\r\n    def format(self, value: str):\r\n        self._format = value\r\n\r\n    @property\r\n    def controls(self) -> dict:\r\n        return self._controls\r\n\r\n    @controls.setter\r\n    def controls(self, value: dict):\r\n        self._controls = value\r\n\r\n    @property\r\n    def tabId(self) -> str:\r\n        return \"cfg\" + self.id\r\n\r\n    @property\r\n    def tabButtonId(self) -> str:\r\n        return \"cfg\" + self.id + \"btn\"\r\n\r\n    @property\r\n    def tabTitle(self) -> str:\r\n        return \"Config \" + self.id\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cc = CameraConfig()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cc, key, value)\r\n            else:\r\n                if key == \"_stream_size\":\r\n                    setattr(cc, key, tuple(value))\r\n                elif key == \"_controls\":\r\n                    ctrlt = {}\r\n                    for ckey, cvalue in value.items():\r\n                        vt = cvalue\r\n                        if ckey == \"ScalerCrop\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"FrameDurationLimits\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"ColourGains\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"AfWindows\":\r\n                            afws = ()\r\n                            for el in cvalue:\r\n                                afw = (tuple(el),)\r\n                                afws += afw\r\n                            vt = afws\r\n                        else:\r\n                            vt = cvalue\r\n                        ctrlt[ckey] = vt\r\n                    setattr(cc, key, ctrlt)\r\n                else:\r\n                    setattr(cc, key, value)\r\n        return cc\r\n\r\nclass CameraProperties():\r\n    def __init__(self):\r\n        self._hasFocus = True\r\n        self._hasFlicker = True\r\n        self._hasHdr = True\r\n        self._model = None\r\n        self._unitCellSize = None\r\n        self._location = None\r\n        self._rotation = None\r\n        self._pixelArraySize = None\r\n        self._pixelArrayActiveAreas = None\r\n        self._colorFilterArrangement = None\r\n        self._scalerCropMaximum = None\r\n        self._systemDevices = None\r\n        self._sensorSensitivity = None\r\n        self._colorSpace = None\r\n\r\n    @property\r\n    def hasFocus(self) -> bool:\r\n        return self._hasFocus\r\n\r\n    @hasFocus.setter\r\n    def hasFocus(self, value: bool):\r\n        self._hasFocus = value\r\n\r\n    @hasFocus.deleter\r\n    def hasFocus(self):\r\n        del self._hasFocus\r\n\r\n    @property\r\n    def hasFlicker(self) -> bool:\r\n        return self._hasFlicker\r\n\r\n    @hasFlicker.setter\r\n    def hasFlicker(self, value: bool):\r\n        self._hasFlicker = value\r\n\r\n    @hasFlicker.deleter\r\n    def hasFlicker(self):\r\n        del self._hasFlicker\r\n\r\n    @property\r\n    def hasHdr(self) -> bool:\r\n        return self._hasHdr\r\n\r\n    @hasHdr.setter\r\n    def hasHdr(self, value: bool):\r\n        self._hasHdr = value\r\n\r\n    @hasHdr.deleter\r\n    def hasHdr(self):\r\n        del self._hasHdr\r\n\r\n    @property\r\n    def model(self):\r\n        return self._model\r\n\r\n    @model.setter\r\n    def model(self, value: str):\r\n        self._model = value\r\n\r\n    @model.deleter\r\n    def model(self):\r\n        del self._model\r\n\r\n    @property\r\n    def unitCellSize(self):\r\n        return self._unitCellSize\r\n\r\n    @unitCellSize.setter\r\n    def unitCellSize(self, value: str):\r\n        self._unitCellSize = value\r\n\r\n    @unitCellSize.deleter\r\n    def unitCellSize(self):\r\n        del self._unitCellSize\r\n\r\n    @property\r\n    def location(self):\r\n        return self._location\r\n\r\n    @location.setter\r\n    def location(self, value: str):\r\n        self._location = value\r\n\r\n    @location.deleter\r\n    def location(self):\r\n        del self._location\r\n\r\n    @property\r\n    def rotation(self):\r\n        return self._rotation\r\n\r\n    @rotation.setter\r\n    def rotation(self, value: str):\r\n        self._rotation = value\r\n\r\n    @rotation.deleter\r\n    def rotation(self):\r\n        del self._rotation\r\n\r\n    @property\r\n    def pixelArraySize(self):\r\n        return self._pixelArraySize\r\n\r\n    @pixelArraySize.setter\r\n    def pixelArraySize(self, value: str):\r\n        self._pixelArraySize = value\r\n\r\n    @pixelArraySize.deleter\r\n    def pixelArraySize(self):\r\n        del self._pixelArraySize\r\n\r\n    @property\r\n    def pixelArrayActiveAreas(self):\r\n        return self._pixelArrayActiveAreas\r\n\r\n    @pixelArrayActiveAreas.setter\r\n    def pixelArrayActiveAreas(self, value: str):\r\n        self._pixelArrayActiveAreas = value\r\n\r\n    @pixelArrayActiveAreas.deleter\r\n    def pixelArrayActiveAreas(self):\r\n        del self._pixelArrayActiveAreas\r\n\r\n    @property\r\n    def colorFilterArrangement(self):\r\n        return self._colorFilterArrangement\r\n\r\n    @colorFilterArrangement.setter\r\n    def colorFilterArrangement(self, value: str):\r\n        self._colorFilterArrangement = value\r\n\r\n    @colorFilterArrangement.deleter\r\n    def colorFilterArrangement(self):\r\n        del self._colorFilterArrangement\r\n\r\n    @property\r\n    def scalerCropMaximum(self):\r\n        return self._scalerCropMaximum\r\n\r\n    @scalerCropMaximum.setter\r\n    def scalerCropMaximum(self, value: str):\r\n        self._scalerCropMaximum = value\r\n\r\n    @scalerCropMaximum.deleter\r\n    def scalerCropMaximum(self):\r\n        del self._scalerCropMaximum\r\n\r\n    @property\r\n    def systemDevices(self):\r\n        return self._systemDevices\r\n\r\n    @systemDevices.setter\r\n    def systemDevices(self, value: str):\r\n        self._systemDevices = value\r\n\r\n    @systemDevices.deleter\r\n    def systemDevices(self):\r\n        del self._systemDevices\r\n\r\n    @property\r\n    def sensorSensitivity(self) -> float:\r\n        return self._sensorSensitivity\r\n\r\n    @sensorSensitivity.setter\r\n    def sensorSensitivity(self, value: float):\r\n        self._sensorSensitivity = value\r\n\r\n    @property\r\n    def colorSpace(self):\r\n        return self._colorSpace\r\n\r\n    @colorSpace.setter\r\n    def colorSpace(self, value: str):\r\n        self._colorSpace = value\r\n\r\n    @colorSpace.deleter\r\n    def colorSpace(self):\r\n        del self._colorSpace\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        cp = CameraProperties()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(cp, key, value)\r\n            else:\r\n                if key == \"_pixelArraySize\":\r\n                    setattr(cp, key, tuple(value))\r\n                elif key == \"_scalerCropMaximum\":\r\n                    setattr(cp, key, tuple(value))\r\n                elif key == \"_pixelArrayActiveAreas\":\r\n                    paas = ()\r\n                    for el in value:\r\n                        paa = (tuple(el),)\r\n                        paas += paa\r\n                    setattr(cp, key, paas)\r\n                else:\r\n                    setattr(cp, key, value)\r\n        return cp\r\n\r\nclass vButton():\r\n    \"\"\" Versatile button\r\n\r\n    \"\"\"\r\n    def __init__(self) -> None:\r\n        self._row = 0\r\n        self._col = 0\r\n        self._isVisible = False\r\n        self._needsConfirm = False\r\n        self._buttonColor = None\r\n        self._buttonShape = None\r\n        self._buttonText = \"\"\r\n        self._buttonExec = \"\"\r\n\r\n    @property\r\n    def row(self) -> int:\r\n        return self._row\r\n\r\n    @row.setter\r\n    def row(self, value: int):\r\n        self._row = value\r\n\r\n    @property\r\n    def col(self) -> int:\r\n        return self._col\r\n\r\n    @col.setter\r\n    def col(self, value: int):\r\n        self._col = value\r\n\r\n    @property\r\n    def isVisible(self) -> bool:\r\n        return self._isVisible\r\n\r\n    @isVisible.setter\r\n    def isVisible(self, value: bool):\r\n        self._isVisible = value\r\n\r\n    @property\r\n    def needsConfirm(self) -> bool:\r\n        return self._needsConfirm\r\n\r\n    @needsConfirm.setter\r\n    def needsConfirm(self, value: bool):\r\n        self._needsConfirm = value\r\n        \r\n    @property\r\n    def buttonColor(self) -> str:\r\n        return self._buttonColor\r\n\r\n    @buttonColor.setter\r\n    def buttonColor(self, value: str):\r\n        self._buttonColor = value\r\n        \r\n    @property\r\n    def buttonShape(self) -> str:\r\n        return self._buttonShape\r\n\r\n    @buttonShape.setter\r\n    def buttonShape(self, value: str):\r\n        self._buttonShape = value\r\n\r\n    @property\r\n    def buttonText(self) -> str:\r\n        return self._buttonText\r\n\r\n    @buttonText.setter\r\n    def buttonText(self, value: str):\r\n        self._buttonText = value\r\n\r\n    @property\r\n    def buttonExec(self) -> str:\r\n        return self._buttonExec\r\n\r\n    @buttonExec.setter\r\n    def buttonExec(self, value: str):\r\n        self._buttonExec = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        vb = vButton()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(vb, key, value)\r\n            else:\r\n                setattr(vb, key, value)\r\n        return vb\r\n\r\nclass ActionButton():\r\n    \"\"\" Action button\r\n\r\n    \"\"\"\r\n    def __init__(self) -> None:\r\n        self._row = 0\r\n        self._col = 0\r\n        self._isVisible = False\r\n        self._needsConfirm = False\r\n        self._buttonColor = None\r\n        self._buttonShape = None\r\n        self._buttonText = \"\"\r\n        self._buttonAction = \"\"\r\n\r\n    @property\r\n    def row(self) -> int:\r\n        return self._row\r\n\r\n    @row.setter\r\n    def row(self, value: int):\r\n        self._row = value\r\n\r\n    @property\r\n    def col(self) -> int:\r\n        return self._col\r\n\r\n    @col.setter\r\n    def col(self, value: int):\r\n        self._col = value\r\n\r\n    @property\r\n    def isVisible(self) -> bool:\r\n        return self._isVisible\r\n\r\n    @isVisible.setter\r\n    def isVisible(self, value: bool):\r\n        self._isVisible = value\r\n\r\n    @property\r\n    def needsConfirm(self) -> bool:\r\n        return self._needsConfirm\r\n\r\n    @needsConfirm.setter\r\n    def needsConfirm(self, value: bool):\r\n        self._needsConfirm = value\r\n        \r\n    @property\r\n    def buttonColor(self) -> str:\r\n        return self._buttonColor\r\n\r\n    @buttonColor.setter\r\n    def buttonColor(self, value: str):\r\n        self._buttonColor = value\r\n        \r\n    @property\r\n    def buttonShape(self) -> str:\r\n        return self._buttonShape\r\n\r\n    @buttonShape.setter\r\n    def buttonShape(self, value: str):\r\n        self._buttonShape = value\r\n\r\n    @property\r\n    def buttonText(self) -> str:\r\n        return self._buttonText\r\n\r\n    @buttonText.setter\r\n    def buttonText(self, value: str):\r\n        self._buttonText = value\r\n\r\n    @property\r\n    def buttonAction(self) -> str:\r\n        return self._buttonAction\r\n\r\n    @buttonAction.setter\r\n    def buttonAction(self, value: str):\r\n        self._buttonAction = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        ab = ActionButton()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(ab, key, value)\r\n            else:\r\n                setattr(ab, key, value)\r\n        return ab\r\n\r\nclass LiveButton():\r\n    \"\"\" Live button\r\n\r\n    \"\"\"\r\n    def __init__(self) -> None:\r\n        self._row = 0\r\n        self._col = 0\r\n        self._isVisible = False\r\n        self._needsConfirm = False\r\n        self._buttonColor = None\r\n        self._buttonShape = None\r\n        self._buttonText = \"\"\r\n        self._isAction = False\r\n        self._buttonAction = \"\"\r\n        self._buttonExec = \"\"\r\n\r\n    @property\r\n    def row(self) -> int:\r\n        return self._row\r\n\r\n    @row.setter\r\n    def row(self, value: int):\r\n        self._row = value\r\n\r\n    @property\r\n    def col(self) -> int:\r\n        return self._col\r\n\r\n    @col.setter\r\n    def col(self, value: int):\r\n        self._col = value\r\n\r\n    @property\r\n    def isVisible(self) -> bool:\r\n        return self._isVisible\r\n\r\n    @isVisible.setter\r\n    def isVisible(self, value: bool):\r\n        self._isVisible = value\r\n\r\n    @property\r\n    def needsConfirm(self) -> bool:\r\n        return self._needsConfirm\r\n\r\n    @needsConfirm.setter\r\n    def needsConfirm(self, value: bool):\r\n        self._needsConfirm = value\r\n        \r\n    @property\r\n    def buttonColor(self) -> str:\r\n        return self._buttonColor\r\n\r\n    @buttonColor.setter\r\n    def buttonColor(self, value: str):\r\n        self._buttonColor = value\r\n        \r\n    @property\r\n    def buttonShape(self) -> str:\r\n        return self._buttonShape\r\n\r\n    @buttonShape.setter\r\n    def buttonShape(self, value: str):\r\n        self._buttonShape = value\r\n\r\n    @property\r\n    def buttonText(self) -> str:\r\n        return self._buttonText\r\n\r\n    @buttonText.setter\r\n    def buttonText(self, value: str):\r\n        self._buttonText = value\r\n\r\n    @property\r\n    def isAction(self) -> bool:\r\n        return self._isAction\r\n\r\n    @isAction.setter\r\n    def isAction(self, value: bool):\r\n        self._isAction = value\r\n\r\n    @property\r\n    def buttonAction(self) -> str:\r\n        return self._buttonAction\r\n\r\n    @buttonAction.setter\r\n    def buttonAction(self, value: str):\r\n        self._buttonAction = value\r\n\r\n    @property\r\n    def buttonExec(self) -> str:\r\n        return self._buttonExec\r\n\r\n    @buttonExec.setter\r\n    def buttonExec(self, value: str):\r\n        self._buttonExec = value\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        lb = LiveButton()\r\n        for key, value in dict.items():\r\n            if value is None:\r\n                setattr(lb, key, value)\r\n            else:\r\n                setattr(lb, key, value)\r\n        return lb\r\n\r\nclass StereoConfig():\r\n    intents = [\"DepthMap\", \"3DVideo\",]\r\n    intentNames = [\"Depth Map\", \"3D Video\",]\r\n    intentAlgos = [[\"StereoBM\", \"StereoSGBM\",],[]]\r\n    intentAlgoNames = [[\"Block Matching\", \"Semi-Global Matching\",],[]]\r\n    intentAlgoLinks = [\r\n        [\r\n            \"https://docs.opencv.org/4.6.0/d9/dba/classcv_1_1StereoBM.html\",\r\n            \"https://docs.opencv.org/4.6.0/d2/d85/classcv_1_1StereoSGBM.html\",\r\n        ],\r\n        [],\r\n    ]\r\n    calibrationPatterns = [\"Chessboard\",]\r\n    calibrationPatternRefs = [\"https://github.com/opencv/opencv/blob/4.x/doc/pattern.png\",]\r\n    def __init__(self):\r\n        self._calibPhotosOK = {}\r\n        self._calibShowCorners = False\r\n        self._calibPhotos = {}\r\n        self._calibPhotosCrn = {}\r\n        self._calibPhotosCount = {}\r\n        self._calibPhotosPath = \"\"\r\n        self._calibPhotosSubPath = \"\"\r\n        self._calibPhotosIdx = {}\r\n        self._calibCameraOK = {}\r\n        self._calibRmsReproError = {}\r\n        self._calibStereoOK = False\r\n        self._rectifyScale = 1\r\n        self._stereoRectifyOK = False\r\n        self._calibDataSubPath = \"\"\r\n        self._calibDataFile = \"\"\r\n        self._calibDate = None\r\n        self._calibDataOK = False\r\n        self._calibPatternIdx = 0\r\n        self._calibPatternSize = (9, 6)  # Default chessboard size\r\n        self._calibPhotosTarget = 20  # Default number of calibration photos\r\n        self._calibPhotoRecording = False\r\n        self._calibPhotoRecordingMsg = \"\"\r\n        self._applyCalibRectify = False\r\n        self._intentIdx = 0\r\n        self._intentAlgoIdx = 0\r\n        self._bm_numDisparitiesFactor = 1\r\n        self._bm_blockSize = 21\r\n        self._sgbm_minDisparity = 0\r\n        self._sgbm_numDisparitiesFactor = 1\r\n        self._sgbm_blockSize = 3\r\n        self._sgbm_P1 = 0\r\n        self._sgbm_P2 = 0\r\n        self._sgbm_disp12MaxDiff = 0\r\n        self._sgbm_preFilterCap = 0\r\n        self._sgbm_uniquenessRatio = 0\r\n        self._sgbm_speckleWindowSize = 0\r\n        self._sgbm_speckleRange = 0\r\n        self._sgbm_mode = 0\r\n\r\n    @property\r\n    def calibPhotosOK(self) -> dict:\r\n        return self._calibPhotosOK\r\n\r\n    @calibPhotosOK.setter\r\n    def calibPhotosOK(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibPhotosOK = value\r\n        else:\r\n            raise ValueError(\"calibPhotosOK must be a dictionary\")\r\n\r\n    @property\r\n    def calibShowCorners(self) -> bool:\r\n        return self._calibShowCorners\r\n\r\n    @calibShowCorners.setter\r\n    def calibShowCorners(self, value: bool):\r\n        self._calibShowCorners = value\r\n\r\n    def isCalibPhotosOK(self, camL: str, camR: str) -> bool:\r\n        \"\"\" Check if calibration photos are OK for the given camera IDs \"\"\"\r\n        res = True\r\n        if camL in self._calibPhotosOK:\r\n            res = res and self._calibPhotosOK[camL]\r\n        else:\r\n            res = False\r\n        if not camR is None:\r\n            if camR in self._calibPhotosOK:\r\n                res = res and self._calibPhotosOK[camR]\r\n            else:\r\n                res = False\r\n        return res\r\n\r\n    def isCalibCamerasOK(self, camL: str, camR: str) -> bool:\r\n        \"\"\" Check if camera calibration is OK for the given camera IDs \"\"\"\r\n        res = True\r\n        if camL in self._calibCameraOK:\r\n            res = res and self._calibCameraOK[camL]\r\n        else:\r\n            res = False\r\n        if not camR is None:\r\n            if camR in self._calibCameraOK:\r\n                res = res and self._calibCameraOK[camR]\r\n            else:\r\n                res = False\r\n        return res\r\n\r\n    @property\r\n    def calibPhotos(self) -> dict:\r\n        return self._calibPhotos\r\n\r\n    @calibPhotos.setter\r\n    def calibPhotos(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibPhotos = value\r\n        else:\r\n            raise ValueError(\"calibPhotos must be a dictionary\")\r\n\r\n    @property\r\n    def calibPhotosCrn(self) -> dict:\r\n        return self._calibPhotosCrn\r\n\r\n    @calibPhotosCrn.setter\r\n    def calibPhotosCrn(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibPhotosCrn = value\r\n        else:\r\n            raise ValueError(\"calibPhotosCrn must be a dictionary\")\r\n\r\n    @property\r\n    def calibPhotosCount(self) -> dict:\r\n        return self._calibPhotosCount\r\n\r\n    @calibPhotosCount.setter\r\n    def calibPhotosCount(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibPhotosCount = value\r\n        else:\r\n            raise ValueError(\"calibPhotosCount must be a dictionary\")\r\n\r\n    def getCalibPhotosCount(self, cam: str) -> int:\r\n        \"\"\" Get the number of calibration photos for the given camera ID \"\"\"\r\n        if cam is None:\r\n            return 0\r\n        if cam in self._calibPhotosCount:\r\n            return self._calibPhotosCount[cam]\r\n        else:\r\n            return 0\r\n\r\n    def hasCalibPhotos(self, camL: str, camR: str) -> bool:\r\n        \"\"\" Check if calibration photos have been taken for the given camera IDs \"\"\"\r\n        res = True\r\n        if camL in self._calibPhotosCount:\r\n            res = res and self._calibPhotosCount[camL] > 0\r\n        else:\r\n            res = False\r\n        if camR in self._calibPhotosCount:\r\n            res = res and self._calibPhotosCount[camR] > 0\r\n        else:\r\n            res = False\r\n        return res\r\n\r\n    @property\r\n    def calibPhotosPath(self) -> str:\r\n        return self._calibPhotosPath\r\n\r\n    @calibPhotosPath.setter\r\n    def calibPhotosPath(self, value: str):\r\n        if isinstance(value, str):\r\n            self._calibPhotosPath = value\r\n        else:\r\n            raise ValueError(\"calibPhotosPath must be a string\")\r\n\r\n    @property\r\n    def calibPhotosSubPath(self) -> str:\r\n        return self._calibPhotosSubPath\r\n\r\n    @calibPhotosSubPath.setter\r\n    def calibPhotosSubPath(self, value: str):\r\n        if isinstance(value, str):\r\n            self._calibPhotosSubPath = value\r\n        else:\r\n            raise ValueError(\"calibPhotosSubPath must be a string\")\r\n\r\n    @property\r\n    def calibPhotosIdx(self) -> dict:\r\n        return self._calibPhotosIdx\r\n\r\n    @calibPhotosIdx.setter\r\n    def calibPhotosIdx(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibPhotosIdx = value\r\n        else:\r\n            raise ValueError(\"calibPhotosIdx must be a dictionary\")\r\n\r\n    def getCalibPhotosIdx(self, cam: str) -> int:\r\n        \"\"\" Get the index of the calibration photos for the given camera ID \"\"\"\r\n        res = 0\r\n        if not cam is None:\r\n            if cam in self._calibPhotosIdx:\r\n                res = self._calibPhotosIdx[cam]\r\n        return res\r\n\r\n    @property\r\n    def calibCameraOK(self) -> dict:\r\n        return self._calibCameraOK\r\n\r\n    @calibCameraOK.setter\r\n    def calibCameraOK(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibCameraOK = value\r\n        else:\r\n            raise ValueError(\"calibCameraOK must be a dictionary\")\r\n\r\n    @property\r\n    def calibRmsReproError(self) -> dict:\r\n        return self._calibRmsReproError\r\n\r\n    @calibRmsReproError.setter\r\n    def calibRmsReproError(self, value: dict):\r\n        if isinstance(value, dict):\r\n            self._calibRmsReproError = value\r\n        else:\r\n            raise ValueError(\"calibRmsReproError must be a dictionary\")\r\n\r\n    @property\r\n    def calibStereoOK(self) -> bool:\r\n        return self._calibStereoOK\r\n\r\n    @calibStereoOK.setter\r\n    def calibStereoOK(self, value: bool):   \r\n        if isinstance(value, bool):\r\n            self._calibStereoOK = value\r\n        else:\r\n            raise ValueError(\"calibStereoOK must be a boolean\")\r\n\r\n    @property\r\n    def rectifyScale(self) -> int:\r\n        return self._rectifyScale\r\n\r\n    @rectifyScale.setter\r\n    def rectifyScale(self, value: int):\r\n        if isinstance(value, int):\r\n            self._rectifyScale = value\r\n        else:\r\n            raise ValueError(\"rectifyScale must be an integer\")\r\n\r\n    @property\r\n    def stereoRectifyOK(self) -> bool:\r\n        return self._stereoRectifyOK \r\n\r\n    @stereoRectifyOK.setter\r\n    def stereoRectifyOK(self, value: bool):\r\n        if isinstance(value, bool):\r\n            self._stereoRectifyOK = value\r\n        else:\r\n            raise ValueError(\"stereoRectifyOK must be a boolean\")\r\n\r\n    @property\r\n    def calibDataSubPath(self) -> str:\r\n        return self._calibDataSubPath\r\n\r\n    @calibDataSubPath.setter\r\n    def calibDataSubPath(self, value: str):\r\n        if isinstance(value, str):\r\n            self._calibDataSubPath = value\r\n        else:\r\n            raise ValueError(\"calibDataSubPath must be a string\")\r\n\r\n    @property\r\n    def calibDataFile(self) -> str:\r\n        return self._calibDataFile\r\n\r\n    @calibDataFile.setter\r\n    def calibDataFile(self, value: str):\r\n        if isinstance(value, str):\r\n            self._calibDataFile = value\r\n        else:\r\n            raise ValueError(\"calibDataFile must be a string\")\r\n\r\n    @property\r\n    def calibDate(self) -> str:\r\n        return self._calibDate\r\n\r\n    @calibDate.setter\r\n    def calibDate(self, value: datetime):\r\n        if value is None:\r\n            val = None\r\n        else:\r\n            val = datetime(\r\n                year=value.year,\r\n                month=value.month,\r\n                day=value.day,\r\n                hour=value.hour,\r\n                minute=value.minute,\r\n            )\r\n        self._calibDate = val\r\n\r\n    @property\r\n    def calibDataOK(self) -> bool:\r\n        return self._calibDataOK\r\n\r\n    @calibDataOK.setter\r\n    def calibDataOK(self, value: bool):\r\n        if isinstance(value, bool):\r\n            self._calibDataOK = value\r\n        else:\r\n            raise ValueError(\"calibDataOK must be a boolean\")\r\n\r\n    @property\r\n    def calibPatternIdx(self) -> int:\r\n        return self._calibPatternIdx\r\n\r\n    @calibPatternIdx.setter\r\n    def calibPatternIdx(self, value: int):\r\n        if value >= 0 and value < len(StereoConfig.calibrationPatterns):\r\n            self._calibPatternIdx = value\r\n        else:\r\n            raise ValueError(\"Invalid calibration pattern index\")\r\n\r\n    @property\r\n    def calibPattern(self) -> str:\r\n        return StereoConfig.calibrationPatterns[self._calibPatternIdx]\r\n\r\n    @property\r\n    def calibPatternRef(self) -> str:\r\n        if self._calibPatternIdx < len(StereoConfig.calibrationPatternRefs):\r\n            return StereoConfig.calibrationPatternRefs[self._calibPatternIdx]\r\n        else:\r\n            return \"\"\r\n\r\n    @property\r\n    def calibPatternSize(self) -> tuple:\r\n        return self._calibPatternSize\r\n\r\n    @calibPatternSize.setter\r\n    def calibPatternSize(self, value: tuple):\r\n        if isinstance(value, tuple) and len(value) == 2:\r\n            if value[0] > 0 and value[1] > 0:\r\n                self._calibPatternSize = value\r\n            else:\r\n                raise ValueError(\"Invalid calibration pattern size. Must be positive integers\")\r\n        else:\r\n            raise ValueError(\"calibPatternSize must be a tuple of two integers\")\r\n\r\n    @property\r\n    def calibPhotosTarget(self) -> int:\r\n        return self._calibPhotosTarget\r\n\r\n    @calibPhotosTarget.setter\r\n    def calibPhotosTarget(self, value: int):\r\n        if isinstance(value, int) and value > 0:\r\n            self._calibPhotosTarget = value\r\n        else:\r\n            raise ValueError(\"calibPhotosTarget must be a positive integer\")\r\n\r\n    @property\r\n    def calibPhotoRecording(self) -> bool:\r\n        return self._calibPhotoRecording\r\n\r\n    @calibPhotoRecording.setter\r\n    def calibPhotoRecording(self, value: bool):\r\n        if isinstance(value, bool):\r\n            self._calibPhotoRecording = value\r\n        else:\r\n            raise ValueError(\"calibPhotoRecording must be a boolean\")\r\n\r\n    @property\r\n    def calibPhotoRecordingMsg(self) -> str:\r\n        return self._calibPhotoRecordingMsg\r\n\r\n    @calibPhotoRecordingMsg.setter\r\n    def calibPhotoRecordingMsg(self, value: str):\r\n        if isinstance(value, str):\r\n            self._calibPhotoRecordingMsg = value\r\n        else:\r\n            raise ValueError(\"calibPhotoRecordingMsg must be a string\")\r\n\r\n    @property\r\n    def applyCalibRectify(self) -> bool:\r\n        return self._applyCalibRectify\r\n\r\n    @applyCalibRectify.setter\r\n    def applyCalibRectify(self, value: bool):\r\n        if isinstance(value, bool):\r\n            self._applyCalibRectify = value\r\n        else:\r\n            raise ValueError(\"applyCalibRectify must be a boolean\")\r\n\r\n    @property\r\n    def intentIdx(self) -> int:\r\n        return self._intentIdx\r\n\r\n    @intentIdx.setter\r\n    def intentIdx(self, value: int):\r\n        if value >= 0 and value < len(StereoConfig.intents):\r\n            self._intentIdx = value\r\n        else:\r\n            raise ValueError(\"Invalid intent index\")\r\n\r\n    @property\r\n    def intent(self) -> str:\r\n        return StereoConfig.intents[self._intentIdx]\r\n\r\n    @property\r\n    def intentName(self) -> str:\r\n        return StereoConfig.intentNames[self._intentIdx]\r\n\r\n    @property\r\n    def intentAlgoIdx(self) -> int:\r\n        return self._intentAlgoIdx\r\n\r\n    @intentAlgoIdx.setter\r\n    def intentAlgoIdx(self, value: int):\r\n        if value >= 0 and value < len(StereoConfig.intentAlgos[self._intentIdx]):\r\n            self._intentAlgoIdx = value\r\n        else:\r\n            raise ValueError(\"Invalid intent algorithm index\")\r\n\r\n    @property\r\n    def intentAlgo(self) -> str:\r\n        return StereoConfig.intentAlgos[self._intentIdx][self._intentAlgoIdx]\r\n\r\n    @property\r\n    def intentAlgoName(self) -> str:\r\n        return StereoConfig.intentAlgoNames[self._intentIdx][self._intentAlgoIdx]\r\n\r\n    @property\r\n    def bm_numDisparitiesFactor(self) -> int:\r\n        return self._bm_numDisparitiesFactor\r\n\r\n    @bm_numDisparitiesFactor.setter\r\n    def bm_numDisparitiesFactor(self, value: int):\r\n        if value >= 0:\r\n            self._bm_numDisparitiesFactor = value\r\n        else:\r\n            raise ValueError(\"Invalid value for bm_numDisparitiesFactor. Must be >= 0\")\r\n\r\n    @property\r\n    def bm_blockSize(self) -> int:\r\n        return self._bm_blockSize\r\n\r\n    @bm_blockSize.setter\r\n    def bm_blockSize(self, value: int):\r\n        if value > 1 and value <= 255 and value % 2 == 1:\r\n            self._bm_blockSize = value\r\n        else:\r\n            raise ValueError(\"Invalid value for bm_blockSize. Must be odd and in range [3;255]\")\r\n\r\n    @property\r\n    def sgbm_minDisparity(self) -> int:\r\n        return self._sgbm_minDisparity\r\n\r\n    @sgbm_minDisparity.setter\r\n    def sgbm_minDisparity(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_minDisparity = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_minDisparity. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_numDisparitiesFactor(self) -> int:\r\n        return self._sgbm_numDisparitiesFactor\r\n\r\n    @sgbm_numDisparitiesFactor.setter\r\n    def sgbm_numDisparitiesFactor(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_numDisparitiesFactor = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_numDisparitiesFactor. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_blockSize(self) -> int:\r\n        return self._sgbm_blockSize\r\n\r\n    @sgbm_blockSize.setter\r\n    def sgbm_blockSize(self, value: int):\r\n        if value > 1 and value <= 255 and value % 2 == 1:\r\n            self._sgbm_blockSize = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_blockSize. Must be odd and in range [3;255]\")\r\n\r\n    @property\r\n    def sgbm_P1(self) -> int:\r\n        return self._sgbm_P1\r\n\r\n    @sgbm_P1.setter\r\n    def sgbm_P1(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_P1 = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_P1. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_P2(self) -> int:\r\n        return self._sgbm_P2\r\n\r\n    @sgbm_P2.setter\r\n    def sgbm_P2(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_P2 = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_P2. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_disp12MaxDiff(self) -> int:\r\n        return self._sgbm_disp12MaxDiff\r\n\r\n    @sgbm_disp12MaxDiff.setter\r\n    def sgbm_disp12MaxDiff(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_disp12MaxDiff = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_disp12MaxDiff. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_preFilterCap(self) -> int:\r\n        return self._sgbm_preFilterCap\r\n\r\n    @sgbm_preFilterCap.setter\r\n    def sgbm_preFilterCap(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_preFilterCap = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_preFilterCap. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_uniquenessRatio(self) -> int:\r\n        return self._sgbm_uniquenessRatio\r\n\r\n    @sgbm_uniquenessRatio.setter\r\n    def sgbm_uniquenessRatio(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_uniquenessRatio = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_uniquenessRatio. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_speckleWindowSize(self) -> int:\r\n        return self._sgbm_speckleWindowSize\r\n\r\n    @sgbm_speckleWindowSize.setter\r\n    def sgbm_speckleWindowSize(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_speckleWindowSize = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_speckleWindowSize. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_speckleRange(self) -> int:\r\n        return self._sgbm_speckleRange\r\n\r\n    @sgbm_speckleRange.setter\r\n    def sgbm_speckleRange(self, value: int):\r\n        if value >= 0:\r\n            self._sgbm_speckleRange = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_speckleRange. Must be >= 0\")\r\n\r\n    @property\r\n    def sgbm_mode(self) -> int:\r\n        return self._sgbm_mode\r\n\r\n    @sgbm_mode.setter\r\n    def sgbm_mode(self, value: int):\r\n        if value >= 0 and value <= 3:\r\n            self._sgbm_mode = value\r\n        else:\r\n            raise ValueError(\"Invalid value for sgbm_mode. Must be 0 (default), 1 (SGBM), 2 (H-H), or 3 (H-H with subpixel refinement)\")\r\n\r\n    def getNextPhotoIdx(self) -> int:\r\n        \"\"\" Return the next available index for fotos\r\n        \"\"\"\r\n        logger.debug(\"StereoConfig.getNextPhotoIdx\")\r\n        index = -1\r\n        if len(self.calibPhotos) > 0:\r\n            for cam in self._calibPhotos:\r\n                logger.debug(\"StereoConfig.getNextPhotoIdx - cam: %s - len(calibPhotos[cam])=%s\", cam, len(self._calibPhotos[cam]))\r\n                if len(self._calibPhotos[cam]) == 0:\r\n                    break\r\n                else:\r\n                    for idx in range(0, len(self._calibPhotos[cam])):\r\n                        sp = self._calibPhotos[cam][idx]\r\n                        logger.debug(\"StereoConfig.getNextPhotoIdx - calibPhotos[%s][%s]=%s\", cam, idx, sp)\r\n                        p = sp.find(\"/img\")\r\n                        if p >= 0:\r\n                            inds = sp[p+4:p+7]\r\n                            if inds.isdigit():\r\n                                ind = int(inds)\r\n                            else:\r\n                                raise ValueError(f\"Invalid entry in calibration photos list: {sp}\")\r\n                        else:\r\n                            raise ValueError(f\"Invalid entry in calibration photos list: {sp}\")\r\n                        logger.debug(\"StereoConfig.getNextPhotoIdx inds=%s, ind=%s\", inds, ind)\r\n                        if ind > idx + 1:\r\n                            index = idx\r\n                            logger.debug(\"StereoConfig.getNextPhotoIdx - Found valid index: %s\", index)\r\n                            break\r\n                    if index == -1:\r\n                        index = len(self._calibPhotos[cam])\r\n                        logger.debug(\"StereoConfig.getNextPhotoIdx - No gap fond. index: %s\", index)\r\n                        break\r\n                if index >= 0:\r\n                    break\r\n            if index == -1:\r\n                index = 1\r\n        return index\r\n\r\n    @classmethod\r\n    def initFromDict(cls, dict:dict):\r\n        sc = StereoConfig()\r\n        for key, value in dict.items():\r\n            if key == \"_calibPatternSize\":\r\n                setattr(sc, key, tuple(value))\r\n            else:\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    setattr(sc, key, value)\r\n        return sc\r\n\r\nclass ServerConfig():\r\n    def __init__(self):\r\n        self._serverStartTime = None\r\n        self._unsavedChanges = False\r\n        self._error = None\r\n        self._error2 = None\r\n        self._errorSource = None\r\n        self._errorc2 = None\r\n        self._errorc22 = None\r\n        self._errorc2Source = None\r\n        self._database = None\r\n        self._raspiModelFull = \"\"\r\n        self._raspiModelLower5 = False\r\n        self._boardRevision = \"\"\r\n        self._kernelVersion = \"\"\r\n        self._debianVersion = \"\"\r\n        self._noCamera = False\r\n        self._supportedCameras = []\r\n        self._usbCamAvailable = False\r\n        self._aiCamAvailable = False\r\n        self._piCameras = []\r\n        self._activeCamera = 0\r\n        self._activeCameraIsUsb = False\r\n        self._activeCameraHasAi = False\r\n        self._activeCameraUsbDev = \"\"\r\n        self._activeCameraInfo = \"\"\r\n        self._activeCameraModel = \"\"\r\n        self._secondCamera = None\r\n        self._secondCameraIsUsb = False\r\n        self._secondCameraHasAi = False\r\n        self._secondCameraUsbDev = \"\"\r\n        self._secondCameraInfo = \"\"\r\n        self._secondCameraModel = \"\"\r\n        self._hasMicrophone = False\r\n        self._defaultMic = \"\"\r\n        self._isMicMuted = False\r\n        self._recordAudio = False\r\n        self._audioSync = 0.3\r\n        self._photoRoot = \".\"\r\n        self._cameraPhotoSubPath = \".\"\r\n        self._prgOutputPath = \".\"\r\n        self._photoType = \"jpg\"\r\n        self._rawPhotoType = \"dng\"\r\n        self._videoType = \"mp4\"\r\n        self._isZoomModeDraw = False\r\n        self._zoomFactor = 100\r\n        self._zoomFactorStep = 10\r\n        self._scalerCropLiveView = (0, 0, 4608, 2592)\r\n        self._scalerCropMin = (0, 0, 4608, 2592)\r\n        self._scalerCropMax = (0, 0, 4608, 2592)\r\n        self._scalerCropDef = (0, 0, 4608, 2592)\r\n        self._syncAspectRatio = True\r\n        self._curMenu = \"live\"\r\n        self._lastLiveTab = \"focus\"\r\n        self._lastConfigTab = \"cfglive\"\r\n        self._lastInfoTab = \"camprops\"\r\n        self._lastPhotoSeriesTab = \"series\"\r\n        self._lastTriggerTab = \"trgcontrol\"\r\n        self._lastCamTab = \"webcam\"\r\n        self._lastConsoleTab = \"versbuttons\"\r\n        self._lastSettingsTab = \"settingsparams\"\r\n        self._isLiveStream = False\r\n        self._isLiveStream2 = None\r\n        self._isVideoRecording = False\r\n        self._isVideoRecording2 = False\r\n        self._isStereoCamActive = False\r\n        self._isStereoCamRecording = False\r\n        self._isAudioRecording = False\r\n        self._isPhotoSeriesRecording = False\r\n        self._isTriggerRecording = False\r\n        self._isTriggerWaiting = False\r\n        self._isTriggerTesting = False\r\n        self._isEventhandling = False\r\n        self._isEventsWaiting = False\r\n        self._isDisplayHidden = True\r\n        self._displayPhoto = None\r\n        self._displayFile = None\r\n        self._displayMeta = None\r\n        self._displayMetaFirst = 0\r\n        self._displayMetaLast = 999\r\n        self._displayHistogram = None\r\n        self._displayContent = \"meta\"\r\n        self._displayBuffer = {}\r\n        self._cv2Available = False\r\n        self._numpyAvailable = False\r\n        self._matplotlibAvailable = False\r\n        self._flaskJwtLibAvailable = False\r\n        self._imx500Available = False\r\n        self._munkresAvailable = False\r\n        self._useUsbCameras = True\r\n        self._useStereo = False\r\n        self._useHistograms = False\r\n        self._useCameraAi = False\r\n        self._requireAuthForStreaming = False\r\n        self._locLongitude = 0.0\r\n        self._locLatitude = 0.0\r\n        self._locElevation = 0.0\r\n        self._locTzKey = \"localtime\"\r\n        self._pvCamera = None\r\n        self._pvFrom = None\r\n        self._pvTo = None\r\n        self._pvList = []\r\n        self._useAPI = False\r\n        self._API_active = False\r\n        self._jwtAuthenticationActive = False\r\n        self._jwtKeyStore = \"\"\r\n        self._jwtAccessTokenExpirationMin = 60\r\n        self._jwtRefreshTokenExpirationDays = 0\r\n        self._streamingClients = []\r\n        self._vButtonsRows = 0\r\n        self._vButtonsCols = 0\r\n        self._vButtons = []\r\n        self._vButtonCommand = None\r\n        self._vButtonArgs = None\r\n        self._vButtonReturncode = None\r\n        self._vButtonStdout = None\r\n        self._vButtonStderr = None\r\n        self._vButtonHasCommandLine = False\r\n        self._aButtonsRows = 0\r\n        self._aButtonsCols = 0\r\n        self._aButtons = []\r\n        self._aButtonAction = None\r\n        self._lButtonsRows = 0\r\n        self._lButtonsCols = 0\r\n        self._lButtons = []\r\n        self._curDeviceId = \"\"\r\n        self._curDevice = None\r\n        self._curDeviceType = None\r\n        self._gpioDevices = []\r\n        self._cfgPath = None\r\n        self._cfgBackupPath = None\r\n        self._changeLog = []\r\n        self._versionCurrent = \"\"\r\n        self._versionLatest = \"\"\r\n        self._versionCheckTime = None\r\n        self._versionCheckIntervalHours = 24\r\n        self._versionCheckEnabled = True\r\n        self._versionCheckFrom = \"\"\r\n        self._updateDone = False\r\n        self._webCamActiveCamPhotoCfg = \"LIVE\"\r\n        self._webCamSecondCamPhotoCfg = \"LIVE\"\r\n\r\n        # Check access of microphone\r\n        self.checkMicrophone()\r\n\r\n        # Get Raspi Info\r\n        model = self.getPiModel()\r\n        self._raspiModelFull = model\r\n        if model.startswith(\"Raspberry Pi 5\"):\r\n            self._raspiModelLower5 = False\r\n        elif model.startswith(\"Raspberry Pi 4\"):\r\n            self._raspiModelLower5 = True\r\n        elif model.startswith(\"Raspberry Pi 3\"):\r\n            self._raspiModelLower5 = True\r\n        elif model.startswith(\"Raspberry Pi 2\"):\r\n            self._raspiModelLower5 = True\r\n        elif model.startswith(\"Raspberry Pi 1\"):\r\n            self._raspiModelLower5 = True\r\n        elif model.startswith(\"Raspberry Pi Zero W\"):\r\n            self._raspiModelLower5 = True\r\n        elif model.startswith(\"Raspberry Pi Zero 2 W\"):\r\n            self._raspiModelLower5 = True\r\n        else:\r\n            self._raspiModelLower5 = False\r\n\r\n        boardRev = self.getBoardRevision()\r\n        self._boardRevision = boardRev\r\n\r\n        debianVers = self.getDebianVersion()\r\n        self._debianVersion = debianVers\r\n\r\n        kernelVers = self.getKernelVersion()\r\n        self._kernelVersion = kernelVers\r\n\r\n    @property\r\n    def serverStartTime(self) -> datetime:\r\n        return self._serverStartTime\r\n\r\n    @serverStartTime.setter\r\n    def serverStartTime(self, value: datetime):\r\n        self._serverStartTime = value\r\n\r\n    @property\r\n    def serverStartTimeStr(self) -> str:\r\n        if self._serverStartTime is None:\r\n            return \"System time not synced at raspiCamSrv start\"\r\n        else:\r\n            return self._serverStartTime.isoformat()\r\n\r\n    @property\r\n    def unsavedChanges(self) -> bool:\r\n        return self._unsavedChanges\r\n\r\n    @unsavedChanges.setter\r\n    def unsavedChanges(self, value: bool):\r\n        self._unsavedChanges = value\r\n\r\n    @property\r\n    def changeLog(self) -> list[dict]:\r\n        return self._changeLog\r\n\r\n    @changeLog.setter\r\n    def changeLog(self, value: list):\r\n        self._changeLog = value\r\n\r\n    def addChangeLogEntry(self, entry: str):\r\n        \"\"\" Adds a new entry to the change log\r\n        \"\"\"\r\n        entry = {\r\n            \"time\": datetime.now(),\r\n            \"entry\": entry}\r\n        self._changeLog.append(entry)\r\n\r\n    def clearChangeLog(self):\r\n        \"\"\" Clears the change log\r\n        \"\"\"\r\n        self._changeLog = []\r\n\r\n    @property\r\n    def error(self) -> str:\r\n        return self._error\r\n\r\n    @error.setter\r\n    def error(self, value: str):\r\n        self._error = value\r\n        if value is None:\r\n            self._errorSource = None\r\n            self._error2 = None\r\n\r\n    @property\r\n    def error2(self) -> str:\r\n        return self._error2\r\n\r\n    @error2.setter\r\n    def error2(self, value: str):\r\n        self._error2 = value\r\n\r\n    @property\r\n    def errorSource(self) -> str:\r\n        return self._errorSource\r\n\r\n    @errorSource.setter\r\n    def errorSource(self, value: str):\r\n        self._errorSource = value\r\n\r\n    @property\r\n    def errorc2(self) -> str:\r\n        return self._errorc2\r\n\r\n    @errorc2.setter\r\n    def errorc2(self, value: str):\r\n        self._errorc2 = value\r\n        if value is None:\r\n            self._errorc2Source = None\r\n            self._errorc22 = None\r\n\r\n    @property\r\n    def errorc22(self) -> str:\r\n        return self._errorc22\r\n\r\n    @errorc22.setter\r\n    def errorc22(self, value: str):\r\n        self._errorc22 = value\r\n\r\n    @property\r\n    def errorc2Source(self) -> str:\r\n        return self._errorc2Source\r\n\r\n    @errorc2Source.setter\r\n    def errorc2Source(self, value: str):\r\n        self._errorc2Source = value\r\n\r\n    @property\r\n    def database(self) -> str:\r\n        return self._database\r\n\r\n    @database.setter\r\n    def database(self, value: str):\r\n        self._database = value\r\n\r\n    @property\r\n    def raspiModelFull(self) -> str:\r\n        return self._raspiModelFull\r\n\r\n    @raspiModelFull.setter\r\n    def raspiModelFull(self, value: str):\r\n        self._raspiModelFull = value\r\n\r\n    @property\r\n    def raspiModelLower5(self) -> bool:\r\n        return self._raspiModelLower5\r\n\r\n    @raspiModelLower5.setter\r\n    def raspiModelLower5(self, value: bool):\r\n        self._raspiModelLower5 = value\r\n\r\n    @property\r\n    def boardRevision(self) -> str:\r\n        return self._boardRevision\r\n\r\n    @boardRevision.setter\r\n    def boardRevision(self, value: str):\r\n        self._boardRevision = value\r\n\r\n    @property\r\n    def kernelVersion(self) -> str:\r\n        return self._kernelVersion\r\n\r\n    @kernelVersion.setter\r\n    def kernelVersion(self, value: str):\r\n        self._kernelVersion = value\r\n\r\n    @property\r\n    def debianVersion(self) -> str:\r\n        return self._debianVersion\r\n\r\n    @debianVersion.setter\r\n    def debianVersion(self, value: str):\r\n        self._debianVersion = value\r\n\r\n    @property\r\n    def noCamera(self) -> bool:\r\n        return self._noCamera\r\n\r\n    @noCamera.setter\r\n    def noCamera(self, value: bool):\r\n        self._noCamera = value\r\n\r\n    @property\r\n    def supportedCameras(self) -> list:\r\n        return self._supportedCameras\r\n\r\n    @supportedCameras.setter\r\n    def supportedCameras(self, value: list):\r\n        self._supportedCameras = value\r\n\r\n    @property\r\n    def usbCamAvailable(self) -> bool:\r\n        return self._usbCamAvailable\r\n\r\n    @usbCamAvailable.setter\r\n    def usbCamAvailable(self, value: bool):\r\n        self._usbCamAvailable = value\r\n\r\n    @property\r\n    def aiCamAvailable(self) -> bool:\r\n        return self._aiCamAvailable\r\n\r\n    @aiCamAvailable.setter\r\n    def aiCamAvailable(self, value: bool):\r\n        self._aiCamAvailable = value\r\n\r\n    @noCamera.setter\r\n    def noCamera(self, value: bool):\r\n        self._noCamera = value\r\n\r\n    @property\r\n    def piCameras(self) -> list:\r\n        return self._piCameras\r\n\r\n    @piCameras.setter\r\n    def piCameras(self, value: list):\r\n        self._piCameras = value\r\n\r\n    @property\r\n    def activeCamera(self) -> int:\r\n        return self._activeCamera\r\n\r\n    @activeCamera.setter\r\n    def activeCamera(self, value: int):\r\n        self._activeCamera = value\r\n\r\n    @property\r\n    def activeCameraIsUsb(self) -> bool:\r\n        return self._activeCameraIsUsb\r\n\r\n    @activeCameraIsUsb.setter\r\n    def activeCameraIsUsb(self, value: bool):\r\n        self._activeCameraIsUsb = value\r\n\r\n    @property\r\n    def activeCameraHasAi(self) -> bool:\r\n        return self._activeCameraHasAi\r\n\r\n    @activeCameraHasAi.setter\r\n    def activeCameraHasAi(self, value: bool):\r\n        self._activeCameraHasAi = value\r\n\r\n    @property\r\n    def activeCameraUsbDev(self) -> str:\r\n        return self._activeCameraUsbDev\r\n\r\n    @activeCameraUsbDev.setter\r\n    def activeCameraUsbDev(self, value: str):\r\n        self._activeCameraUsbDev = value\r\n\r\n    @property\r\n    def activeCameraInfo(self) -> str:\r\n        return self._activeCameraInfo\r\n\r\n    @activeCameraInfo.setter\r\n    def activeCameraInfo(self, value: str):\r\n        self._activeCameraInfo = value\r\n\r\n    @property\r\n    def activeCameraModel(self) -> str:\r\n        return self._activeCameraModel\r\n\r\n    @activeCameraModel.setter\r\n    def activeCameraModel(self, value: str):\r\n        self._activeCameraModel = value\r\n\r\n    @property\r\n    def secondCamera(self) -> int:\r\n        return self._secondCamera\r\n\r\n    @secondCamera.setter\r\n    def secondCamera(self, value: int):\r\n        self._secondCamera = value\r\n\r\n    @property\r\n    def secondCameraIsUsb(self) -> bool:\r\n        return self._secondCameraIsUsb\r\n\r\n    @secondCameraIsUsb.setter\r\n    def secondCameraIsUsb(self, value: bool):\r\n        self._secondCameraIsUsb = value\r\n\r\n    @property\r\n    def secondCameraHasAi(self) -> bool:\r\n        return self._secondCameraHasAi\r\n\r\n    @secondCameraHasAi.setter\r\n    def secondCameraHasAi(self, value: bool):\r\n        self._secondCameraHasAi = value\r\n\r\n    @property\r\n    def secondCameraUsbDev(self) -> str:\r\n        return self._secondCameraUsbDev\r\n\r\n    @secondCameraUsbDev.setter\r\n    def secondCameraUsbDev(self, value: str):\r\n        self._secondCameraUsbDev = value\r\n\r\n    @property\r\n    def secondCameraInfo(self) -> str:\r\n        return self._secondCameraInfo\r\n\r\n    @secondCameraInfo.setter\r\n    def secondCameraInfo(self, value: str):\r\n        self._secondCameraInfo = value\r\n\r\n    @property\r\n    def secondCameraModel(self) -> str:\r\n        return self._secondCameraModel\r\n\r\n    @secondCameraModel.setter\r\n    def secondCameraModel(self, value: str):\r\n        self._secondCameraModel = value\r\n\r\n    @property\r\n    def hasMicrophone(self) -> bool:\r\n        return self._hasMicrophone\r\n\r\n    @hasMicrophone.setter\r\n    def hasMicrophone(self, value: bool):\r\n        self._hasMicrophone = value\r\n\r\n    @property\r\n    def defaultMic(self) -> str:\r\n        return self._defaultMic\r\n\r\n    @defaultMic.setter\r\n    def defaultMic(self, value: str):\r\n        self._defaultMic = value\r\n\r\n    @property\r\n    def isMicMuted(self) -> bool:\r\n        return self._isMicMuted\r\n\r\n    @isMicMuted.setter\r\n    def isMicMuted(self, value: bool):\r\n        self._isMicMuted = value\r\n\r\n    @property\r\n    def recordAudio(self) -> bool:\r\n        return self._recordAudio\r\n\r\n    @recordAudio.setter\r\n    def recordAudio(self, value: bool):\r\n        self._recordAudio = value\r\n\r\n    @property\r\n    def audioSync(self) -> float:\r\n        return self._audioSync\r\n\r\n    @audioSync.setter\r\n    def audioSync(self, value: float):\r\n        self._audioSync = value\r\n\r\n    @property\r\n    def photoRoot(self):\r\n        return self._photoRoot\r\n\r\n    @photoRoot.setter\r\n    def photoRoot(self, value: str):\r\n        self._photoRoot = value\r\n\r\n    @property\r\n    def cameraPhotoSubPath(self):\r\n        return self._cameraPhotoSubPath\r\n\r\n    @cameraPhotoSubPath.setter\r\n    def cameraPhotoSubPath(self, value: str):\r\n        self._cameraPhotoSubPath = value\r\n\r\n    @property\r\n    def prgOutputPath(self):\r\n        return self._prgOutputPath\r\n\r\n    @prgOutputPath.setter\r\n    def prgOutputPath(self, value: str):\r\n        self._prgOutputPath = value\r\n\r\n    @property\r\n    def cameraHistogramSubPath(self):\r\n        return self._cameraPhotoSubPath + \"/hist\"\r\n\r\n    @property\r\n    def photoType(self) -> str:\r\n        return self._photoType\r\n\r\n    @photoType.setter\r\n    def photoType(self, value: str):\r\n        if value.lower() == \"jpg\" \\\r\n        or value.lower() == \"jpeg\" \\\r\n        or value.lower() == \"png\" \\\r\n        or value.lower() == \"gif\" \\\r\n        or value.lower() == \"bmp\":\r\n            self._photoType = value\r\n        else:\r\n            raise ValueError(\"Invalid photo format\")\r\n\r\n    @property\r\n    def rawPhotoType(self) -> str:\r\n        return self._rawPhotoType\r\n\r\n    @rawPhotoType.setter\r\n    def rawPhotoType(self, value: str):\r\n        if value.lower() == \"dng\":\r\n            self._rawPhotoType = value\r\n        else:\r\n            raise ValueError(\"Invalid raw photo format\")\r\n\r\n    @property\r\n    def videoType(self) -> str:\r\n        return self._videoType\r\n\r\n    @videoType.setter\r\n    def videoType(self, value: str):\r\n        if value.lower() == \"h264\" \\\r\n        or value.lower() == \"mp4\":\r\n            self._videoType = value\r\n        else:\r\n            raise ValueError(\"Invalid video format\")\r\n\r\n    @property\r\n    def isZoomModeDraw(self) -> bool:\r\n        return self._isZoomModeDraw\r\n\r\n    @isZoomModeDraw.setter\r\n    def isZoomModeDraw(self, value: bool):\r\n        self._isZoomModeDraw = value\r\n\r\n    @property\r\n    def zoomFactor(self):\r\n        return self._zoomFactor\r\n\r\n    @zoomFactor.setter\r\n    def zoomFactor(self, value: int):\r\n        if value > 100:\r\n            value = 100\r\n        if value < self.zoomFactorStep:\r\n            value = self.zoomFactorStep\r\n        self._zoomFactor = value\r\n\r\n    @property\r\n    def zoomFactorStep(self):\r\n        return self._zoomFactorStep\r\n\r\n    @zoomFactorStep.setter\r\n    def zoomFactorStep(self, value: int):\r\n        if value > 20:\r\n            value = 20\r\n        if value < 2:\r\n            value = 2\r\n        self._zoomFactorStep = value\r\n\r\n    @property\r\n    def scalerCropLiveView(self) -> tuple:\r\n        return self._scalerCropLiveView\r\n\r\n    @scalerCropLiveView.setter\r\n    def scalerCropLiveView(self, value: tuple):\r\n        self._scalerCropLiveView = value\r\n\r\n    @property\r\n    def scalerCropLiveViewStr(self) -> str:\r\n        return \"(\" + str(self._scalerCropLiveView[0]) + \",\" + str(self._scalerCropLiveView[1]) + \",\" + str(self._scalerCropLiveView[2]) + \",\" + str(self._scalerCropLiveView[3]) + \")\"\r\n\r\n    @scalerCropLiveViewStr.setter\r\n    def scalerCropLiveViewStr(self, value: str):\r\n        self._scalerCropLiveView = CameraControls._parseRectTuple(value)\r\n\r\n    @property\r\n    def scalerCropMin(self) -> tuple:\r\n        return self._scalerCropMin\r\n\r\n    @scalerCropMin.setter\r\n    def scalerCropMin(self, value: tuple):\r\n        self._scalerCropMin = value\r\n\r\n    @property\r\n    def scalerCropMax(self) -> tuple:\r\n        return self._scalerCropMax\r\n\r\n    @scalerCropMax.setter\r\n    def scalerCropMax(self, value: tuple):\r\n        self._scalerCropMax = value\r\n\r\n    @property\r\n    def scalerCropDef(self) -> tuple:\r\n        return self._scalerCropDef\r\n\r\n    @scalerCropDef.setter\r\n    def scalerCropDef(self, value: tuple):\r\n        self._scalerCropDef = value\r\n\r\n    @property\r\n    def syncAspectRatio(self) -> bool:\r\n        return self._syncAspectRatio\r\n\r\n    @syncAspectRatio.setter\r\n    def syncAspectRatio(self, value: bool):\r\n        self._syncAspectRatio = value\r\n\r\n    @property\r\n    def curMenu(self) -> str:\r\n        return self._curMenu\r\n\r\n    @curMenu.setter\r\n    def curMenu(self, value: str):\r\n        self._curMenu = value\r\n\r\n    @property\r\n    def lastLiveTab(self):\r\n        return self._lastLiveTab\r\n\r\n    @lastLiveTab.setter\r\n    def lastLiveTab(self, value: str):\r\n        self._lastLiveTab = value\r\n\r\n    @property\r\n    def lastConfigTab(self):\r\n        return self._lastConfigTab\r\n\r\n    @lastConfigTab.setter\r\n    def lastConfigTab(self, value: str):\r\n        self._lastConfigTab = value\r\n\r\n    @property\r\n    def lastInfoTab(self):\r\n        return self._lastInfoTab\r\n\r\n    @lastInfoTab.setter\r\n    def lastInfoTab(self, value: str):\r\n        self._lastInfoTab = value\r\n\r\n    @property\r\n    def lastPhotoSeriesTab(self):\r\n        return self._lastPhotoSeriesTab\r\n\r\n    @lastPhotoSeriesTab.setter\r\n    def lastPhotoSeriesTab(self, value: str):\r\n        self._lastPhotoSeriesTab = value\r\n\r\n    @property\r\n    def lastTriggerTab(self):\r\n        return self._lastTriggerTab\r\n\r\n    @lastTriggerTab.setter\r\n    def lastTriggerTab(self, value: str):\r\n        self._lastTriggerTab = value\r\n\r\n    @property\r\n    def lastCamTab(self):\r\n        return self._lastCamTab\r\n\r\n    @lastCamTab.setter\r\n    def lastCamTab(self, value: str):\r\n        self._lastCamTab = value\r\n\r\n    @property\r\n    def lastConsoleTab(self):\r\n        return self._lastConsoleTab\r\n\r\n    @lastConsoleTab.setter\r\n    def lastConsoleTab(self, value: str):\r\n        self._lastConsoleTab = value\r\n\r\n    @property\r\n    def lastSettingsTab(self):\r\n        return self._lastSettingsTab\r\n\r\n    @lastSettingsTab.setter\r\n    def lastSettingsTab(self, value: str):\r\n        self._lastSettingsTab = value\r\n\r\n    @property\r\n    def isDisplayHidden(self) -> bool:\r\n        return self._isDisplayHidden\r\n\r\n    @isDisplayHidden.setter\r\n    def isDisplayHidden(self, value: bool):\r\n        self._isDisplayHidden = value\r\n\r\n    @property\r\n    def isLiveStream(self) -> bool:\r\n        return self._isLiveStream\r\n\r\n    @isLiveStream.setter\r\n    def isLiveStream(self, value: bool):\r\n        self._isLiveStream = value\r\n\r\n    @property\r\n    def isLiveStream2(self) -> bool:\r\n        return self._isLiveStream2\r\n\r\n    @isLiveStream2.setter\r\n    def isLiveStream2(self, value: bool):\r\n        self._isLiveStream2 = value\r\n\r\n    @property\r\n    def isVideoRecording(self) -> bool:\r\n        return self._isVideoRecording\r\n\r\n    @isVideoRecording.setter\r\n    def isVideoRecording(self, value: bool):\r\n        self._isVideoRecording = value\r\n\r\n    @property\r\n    def isVideoRecording2(self) -> bool:\r\n        return self._isVideoRecording2\r\n\r\n    @isVideoRecording2.setter\r\n    def isVideoRecording2(self, value: bool):\r\n        self._isVideoRecording2 = value\r\n\r\n    @property\r\n    def isStereoCamActive(self) -> bool:\r\n        return self._isStereoCamActive\r\n\r\n    @isStereoCamActive.setter\r\n    def isStereoCamActive(self, value: bool):\r\n        self._isStereoCamActive = value\r\n\r\n    @property\r\n    def isStereoCamRecording(self) -> bool:\r\n        return self._isStereoCamRecording\r\n\r\n    @isStereoCamRecording.setter\r\n    def isStereoCamRecording(self, value: bool):\r\n        self._isStereoCamRecording = value\r\n\r\n    @property\r\n    def isAudioRecording(self) -> bool:\r\n        return self._isAudioRecording\r\n\r\n    @isAudioRecording.setter\r\n    def isAudioRecording(self, value: bool):\r\n        self._isAudioRecording = value\r\n\r\n    @property\r\n    def isPhotoSeriesRecording(self) -> bool:\r\n        return self._isPhotoSeriesRecording\r\n\r\n    @isPhotoSeriesRecording.setter\r\n    def isPhotoSeriesRecording(self, value: bool):\r\n        self._isPhotoSeriesRecording = value\r\n\r\n    @property\r\n    def isTriggerRecording(self) -> bool:\r\n        return self._isTriggerRecording\r\n\r\n    @isTriggerRecording.setter\r\n    def isTriggerRecording(self, value: bool):\r\n        self._isTriggerRecording = value\r\n\r\n    @property\r\n    def isTriggerWaiting(self) -> bool:\r\n        return self._isTriggerWaiting\r\n\r\n    @isTriggerWaiting.setter\r\n    def isTriggerWaiting(self, value: bool):\r\n        self._isTriggerWaiting = value\r\n\r\n    @property\r\n    def isTriggerTesting(self) -> bool:\r\n        return self._isTriggerTesting\r\n\r\n    @isTriggerTesting.setter\r\n    def isTriggerTesting(self, value: bool):\r\n        self._isTriggerTesting = value\r\n\r\n    @property\r\n    def isEventhandling(self) -> bool:\r\n        return self._isEventhandling\r\n\r\n    @isEventhandling.setter\r\n    def isEventhandling(self, value: bool):\r\n        self._isEventhandling = value\r\n\r\n    @property\r\n    def isEventsWaiting(self) -> bool:\r\n        return self._isEventsWaiting\r\n\r\n    @isEventsWaiting.setter\r\n    def isEventsWaiting(self, value: bool):\r\n        self._isEventsWaiting = value\r\n\r\n    @property\r\n    def buttonClear(self) -> str:\r\n        return \"Clr(\" + str(self.displayBufferCount) + \")\"\r\n\r\n    @property\r\n    def displayPhoto(self):\r\n        return self._displayPhoto\r\n\r\n    @displayPhoto.setter\r\n    def displayPhoto(self, value: str):\r\n        self._displayPhoto = value\r\n\r\n    @property\r\n    def displayFile(self):\r\n        return self._displayFile\r\n\r\n    @displayFile.setter\r\n    def displayFile(self, value: str):\r\n        self._displayFile = value\r\n\r\n    @property\r\n    def displayMeta(self):\r\n        return self._displayMeta\r\n\r\n    @displayMeta.setter\r\n    def displayMeta(self, value: str):\r\n        self._displayMeta = value\r\n\r\n    @property\r\n    def displayMetaFirst(self):\r\n        return self._displayMetaFirst\r\n\r\n    @displayMetaFirst.setter\r\n    def displayMetaFirst(self, value: int):\r\n        self._displayMetaFirst = value\r\n\r\n    @property\r\n    def displayMetaLast(self):\r\n        return self._displayMetaLast\r\n\r\n    @displayMetaLast.setter\r\n    def displayMetaLast(self, value: int):\r\n        self._displayMetaLast = value\r\n\r\n    @property\r\n    def displayHistogram(self) -> str:\r\n        return self._displayHistogram\r\n\r\n    @displayHistogram.setter\r\n    def displayHistogram(self, value: str):\r\n        self._displayHistogram = value\r\n\r\n    @property\r\n    def displayContent(self) -> str:\r\n        return self._displayContent\r\n\r\n    @displayContent.setter\r\n    def displayContent(self, value: str):\r\n        if value == \"meta\" \\\r\n        or value == \"hist\":\r\n            self._displayContent = value\r\n        else:\r\n            self._displayContent = \"meta\"\r\n\r\n    @property\r\n    def cv2Available(self) -> bool:\r\n        return self._cv2Available\r\n\r\n    @cv2Available.setter\r\n    def cv2Available(self, value: bool):\r\n        self._cv2Available = value\r\n\r\n    @property\r\n    def numpyAvailable(self) -> bool:\r\n        return self._numpyAvailable\r\n\r\n    @numpyAvailable.setter\r\n    def numpyAvailable(self, value: bool):\r\n        self._numpyAvailable = value\r\n\r\n    @property\r\n    def matplotlibAvailable(self) -> bool:\r\n        return self._matplotlibAvailable\r\n\r\n    @matplotlibAvailable.setter\r\n    def matplotlibAvailable(self, value: bool):\r\n        self._matplotlibAvailable = value\r\n\r\n    @property\r\n    def flaskJwtLibAvailable(self) -> bool:\r\n        return self._flaskJwtLibAvailable\r\n\r\n    @flaskJwtLibAvailable.setter\r\n    def flaskJwtLibAvailable(self, value: bool):\r\n        self._flaskJwtLibAvailable = value\r\n\r\n    @property\r\n    def imx500Available(self) -> bool:\r\n        return self._imx500Available\r\n\r\n    @imx500Available.setter\r\n    def imx500Available(self, value: bool):\r\n        self._imx500Available = value\r\n\r\n    @property\r\n    def munkresAvailable(self) -> bool:\r\n        return self._munkresAvailable\r\n\r\n    @munkresAvailable.setter\r\n    def munkresAvailable(self, value: bool):\r\n        self._munkresAvailable = value\r\n\r\n    @property\r\n    def useUsbCameras(self) -> bool:\r\n        if self.supportsUsbCamera == False:\r\n            self._useUsbCameras = False\r\n        return self._useUsbCameras\r\n\r\n    @useUsbCameras.setter\r\n    def useUsbCameras(self, value: bool):\r\n        self._useUsbCameras = value\r\n\r\n    @property\r\n    def useStereo(self) -> bool:\r\n        if self.supportsStereo == False:\r\n            self._useStereo = False\r\n        return self._useStereo\r\n\r\n    @useStereo.setter\r\n    def useStereo(self, value: bool):\r\n        self._useStereo = value\r\n\r\n    @property\r\n    def useHistograms(self) -> bool:\r\n        return self._useHistograms\r\n\r\n    @useHistograms.setter\r\n    def useHistograms(self, value: bool):\r\n        self._useHistograms = value\r\n\r\n    @property\r\n    def useCameraAi(self) -> bool:\r\n        return self._useCameraAi\r\n\r\n    @useCameraAi.setter\r\n    def useCameraAi(self, value: bool):\r\n        self._useCameraAi = value\r\n\r\n    @property\r\n    def supportsExtMotionDetection(self) -> bool:\r\n        sup = self.cv2Available \\\r\n          and self.matplotlibAvailable \\\r\n          and self.numpyAvailable\r\n        return sup\r\n\r\n    @property\r\n    def supportsHistograms(self) -> bool:\r\n        sup = self.cv2Available \\\r\n          and self.matplotlibAvailable \\\r\n          and self.numpyAvailable\r\n        return sup\r\n\r\n    @property\r\n    def supportsStereo(self) -> bool:\r\n        sup = self.cv2Available \\\r\n          and self.numpyAvailable \\\r\n          and self.activeCameraModel == self.secondCameraModel\r\n        return sup\r\n\r\n    @property\r\n    def supportsAPI(self) -> bool:\r\n        sup = self.flaskJwtLibAvailable == True\r\n        return sup\r\n\r\n    @property\r\n    def supportsUsbCamera(self) -> bool:\r\n        sup = self.cv2Available == True\r\n        return sup\r\n\r\n    @property\r\n    def whyNotSupportsHistograms(self) -> str:\r\n        why = \"\"\r\n        if not self.supportsHistograms:\r\n            why = \"Histograms are not supported because\"\r\n            if not self.cv2Available:\r\n                why = why + \"<br>module cv2 is not available\"\r\n            if not self.matplotlibAvailable:\r\n                why = why + \"<br>module matplotlib is not available\"\r\n            if not self.numpyAvailable:\r\n                why = why + \"<br>module numpy is not available\"\r\n        return why\r\n\r\n    @property\r\n    def whyNotsupportsExtMotionDetection(self) -> str:\r\n        why = \"\"\r\n        if not self.supportsExtMotionDetection:\r\n            why = \"Extended motion detection is not supported because\"\r\n            if not self.cv2Available:\r\n                why = why + \"<br>module cv2 is not available\"\r\n            if not self.matplotlibAvailable:\r\n                why = why + \"<br>module matplotlib is not available\"\r\n            if not self.numpyAvailable:\r\n                why = why + \"<br>module numpy is not available\"\r\n        return why\r\n\r\n    @property\r\n    def whyNotSupportsStereo(self) -> str:\r\n        why = \"\"\r\n        if not self.supportsStereo:\r\n            why = \"Stereo Vision is not supported because\"\r\n            if not self.cv2Available:\r\n                why = why + \"<br>module cv2 is not available\"\r\n            if not self.numpyAvailable:\r\n                why = why + \"<br>module numpy is not available\"\r\n            if self.secondCamera is None:\r\n                why = why + \"<br>at least two cameras are required\"\r\n            else:\r\n                if self.activeCameraModel != self.secondCameraModel:\r\n                    why = why + \"<br>active and second camera are of different model\"\r\n        return why\r\n\r\n    @property\r\n    def whyNotSupportsAPI(self) -> str:\r\n        why = \"\"\r\n        if not self.supportsAPI:\r\n            why = \"The raspiCamSrv API is not supported because\"\r\n            if not self.flaskJwtLibAvailable:\r\n                why = why + \"<br>module flask_jwt_extended is not available\"\r\n        return why\r\n\r\n    @property\r\n    def whyNotSupportsUsbCamera(self) -> str:\r\n        why = \"\"\r\n        if not self.supportsUsbCamera:\r\n            why = \"USB Camera support is not available because\"\r\n            if not self.cv2Available:\r\n                why = why + \"<br>module cv2 is not available\"\r\n        return why\r\n\r\n    @property\r\n    def whyNotSupportsAiCamera(self) -> str:\r\n        why = \"\"\r\n        if self.aiCamAvailable == False \\\r\n        or self.imx500Available == False \\\r\n        or self.cv2Available == False:\r\n            why = \"Camera AI features are not available because\"\r\n        else:\r\n            if self.munkresAvailable == False:\r\n                why = \"Some Camera AI features are not available because\"\r\n                why = why + \"<br>module munkres is not available\"\r\n                why = why + \"<br>Install in venv with 'pip install --break-system-packages munkres'\"\r\n\r\n        if self.aiCamAvailable == False:\r\n            why = why + \"<br>No AI camera (imx500) is currently connected\"\r\n        if self.imx500Available == False:\r\n            why = why + \"<br>Package imx500-all is not installed.\"\r\n            why = why + \"<br>Install with: 'sudo apt install imx500-all'\"\r\n        if self.cv2Available == False:\r\n            why = why + \"<br>Module cv2 is not available\"\r\n        return why\r\n\r\n    @property\r\n    def requireAuthForStreaming(self) -> bool:\r\n        return self._requireAuthForStreaming\r\n\r\n    @requireAuthForStreaming.setter\r\n    def requireAuthForStreaming(self, value: bool):\r\n        self._requireAuthForStreaming = value\r\n\r\n    @property\r\n    def locLongitude(self) -> float:\r\n        return self._locLongitude\r\n\r\n    @locLongitude.setter\r\n    def locLongitude(self, value: float):\r\n        self._locLongitude = value\r\n\r\n    @property\r\n    def locLatitude(self) -> float:\r\n        return self._locLatitude\r\n\r\n    @locLatitude.setter\r\n    def locLatitude(self, value: float):\r\n        self._locLatitude = value\r\n\r\n    @property\r\n    def locElevation(self) -> float:\r\n        return self._locElevation\r\n\r\n    @locElevation.setter\r\n    def locElevation(self, value: float):\r\n        self._locElevation = value\r\n\r\n    @property\r\n    def locTzKey(self) -> str:\r\n        return self._locTzKey\r\n\r\n    @locTzKey.setter\r\n    def locTzKey(self, value: str):\r\n        self._locTzKey = value\r\n\r\n    def timeZoneKeys(self) -> list:\r\n        tzl = []\r\n        tzs = zoneinfo.available_timezones()\r\n        for tz in tzs:\r\n            tzl.append(tz)\r\n        tzl.sort()\r\n        return tzl\r\n\r\n    @property\r\n    def pvCamera(self) -> int:\r\n        return self._pvCamera\r\n\r\n    @pvCamera.setter\r\n    def pvCamera(self, value: int):\r\n        self._pvCamera = value\r\n\r\n    @property\r\n    def pvFrom(self) -> date:\r\n        return self._pvFrom\r\n\r\n    @pvFrom.setter\r\n    def pvFrom(self, value: date):\r\n        self._pvFrom = value\r\n\r\n    @property\r\n    def pvFromStr(self) -> str:\r\n        return self._pvFrom.isoformat()[:10]\r\n\r\n    @pvFromStr.setter\r\n    def pvFromStr(self, value: str):\r\n        try:\r\n            d = date.fromisoformat(value)\r\n        except ValueError:\r\n            d = datetime.now()\r\n        v = datetime(year=d.year, month=d.month, day=d.day, hour=0, minute=0)     \r\n        self._pvFrom = v\r\n\r\n    @property\r\n    def pvTo(self) -> date:\r\n        return self._pvTo\r\n\r\n    @pvTo.setter\r\n    def pvTo(self, value: date):\r\n        self._pvTo = value\r\n\r\n    @property\r\n    def pvToStr(self) -> str:\r\n        return self._pvTo.isoformat()[:10]\r\n\r\n    @pvToStr.setter\r\n    def pvToStr(self, value: str):\r\n        try:\r\n            d = date.fromisoformat(value)\r\n        except ValueError:\r\n            d = datetime.now()\r\n        v = datetime(year=d.year, month=d.month, day=d.day, hour=23, minute=59, second=59)        \r\n        self._pvTo = v\r\n\r\n    @property\r\n    def pvList(self) -> list:\r\n        return self._pvList\r\n\r\n    @pvList.setter\r\n    def pvList(self, value: list):\r\n        self._pvList = value\r\n\r\n    @property\r\n    def jwtAuthenticationActive(self) -> bool:\r\n        return self._jwtAuthenticationActive\r\n\r\n    @jwtAuthenticationActive.setter\r\n    def jwtAuthenticationActive(self, value: bool):\r\n        self._jwtAuthenticationActive = value\r\n\r\n    @property\r\n    def jwtKeyStore(self) -> str:\r\n        return self._jwtKeyStore\r\n\r\n    @jwtKeyStore.setter\r\n    def jwtKeyStore(self, value: str):\r\n        self._jwtKeyStore = value\r\n\r\n    @property\r\n    def jwtAccessTokenExpirationMin(self) -> int:\r\n        return self._jwtAccessTokenExpirationMin\r\n\r\n    @jwtAccessTokenExpirationMin.setter\r\n    def jwtAccessTokenExpirationMin(self, value: int):\r\n        self._jwtAccessTokenExpirationMin = value\r\n\r\n    @property\r\n    def jwtRefreshTokenExpirationDays(self) -> int:\r\n        return self._jwtRefreshTokenExpirationDays\r\n\r\n    @jwtRefreshTokenExpirationDays.setter\r\n    def jwtRefreshTokenExpirationDays(self, value: int):\r\n        self._jwtRefreshTokenExpirationDays = value\r\n\r\n    @property\r\n    def streamingClients(self) -> list:\r\n        return self._streamingClients\r\n\r\n    @streamingClients.setter\r\n    def streamingClients(self, value: list):\r\n        self._streamingClients = value\r\n\r\n    def registerStreamingClient(self, ipaddr: str, stream: str, thread: int):\r\n        cl = None\r\n        for scl in self.streamingClients:\r\n            if scl[\"ipaddr\"] == ipaddr:\r\n                cl = scl\r\n                break\r\n        if cl is None:\r\n            cl = {}\r\n            cl[\"ipaddr\"] = ipaddr\r\n            streams = []\r\n            s = {}\r\n            s[\"stream\"] = stream\r\n            s[\"thread\"] = thread\r\n            streams.append(s)\r\n            cl[\"streams\"] = streams\r\n            self.streamingClients.append(cl)\r\n        else:\r\n            streams = cl[\"streams\"]\r\n            append = True\r\n            if len(streams) > 0:\r\n                for s in streams:\r\n                    if s[\"thread\"] == thread and s[\"stream\"] == stream:\r\n                        append = False\r\n                        break\r\n            if append == True:\r\n                s = {}\r\n                s[\"stream\"] = stream\r\n                s[\"thread\"] = thread\r\n                streams.append(s)\r\n\r\n    def unregisterStreamingClient(self, ipaddr: str, stream: str, thread: int):\r\n        remcl = -1\r\n        idxcl = 0\r\n        for scl in self.streamingClients:\r\n            if scl[\"ipaddr\"] == ipaddr:\r\n                streams = scl[\"streams\"]\r\n                rems = -1\r\n                idxs = 0\r\n                for s in streams:\r\n                    if s[\"thread\"] == thread and s[\"stream\"] == stream:\r\n                        rems = idxs\r\n                    idxs += 1\r\n                if rems >= 0:\r\n                    streams.pop(rems)\r\n                if len(streams) == 0:\r\n                    remcl = idxcl\r\n            idxcl += 1\r\n        if remcl >= 0:\r\n            self.streamingClients.pop(remcl)\r\n\r\n    def streamingClientStreams(self, ipaddr: str) -> str:\r\n        res = \"\"\r\n        for scl in self.streamingClients:\r\n            if scl[\"ipaddr\"] == ipaddr:\r\n                streams = scl[\"streams\"]\r\n                for s in streams:\r\n                    stream = s[\"stream\"]\r\n                    if len(res) == 0:\r\n                        res = stream\r\n                    else:\r\n                        res = res + \", \" + stream\r\n        return res\r\n\r\n    def updateStreamingClients(self):\r\n        for cl in self.streamingClients:\r\n            ip = cl[\"ipaddr\"]\r\n            streams = cl[\"streams\"]\r\n            for s in streams:\r\n                thread = s[\"thread\"]\r\n                is_alive = any([th for th in threading.enumerate() if th.ident == thread])\r\n                if is_alive == False:\r\n                    self.unregisterStreamingClient(ip,s[\"stream\"], thread)\r\n\r\n    @property\r\n    def vButtonsRows(self) -> int:\r\n        return self._vButtonsRows\r\n\r\n    @vButtonsRows.setter\r\n    def vButtonsRows(self, value: int):\r\n        self._vButtonsRows = value\r\n\r\n    @property\r\n    def vButtonsCols(self) -> int:\r\n        return self._vButtonsCols\r\n\r\n    @vButtonsCols.setter\r\n    def vButtonsCols(self, value: int):\r\n        self._vButtonsCols = value\r\n\r\n    @property\r\n    def vButtons(self) -> list[list[vButton]]:\r\n        return self._vButtons\r\n\r\n    @vButtons.setter\r\n    def vButtons(self, value: list):\r\n        self._vButtons = value\r\n\r\n    @property\r\n    def vButtonCommand(self) -> str:\r\n        return self._vButtonCommand\r\n\r\n    @vButtonCommand.setter\r\n    def vButtonCommand(self, value: str):\r\n        self._vButtonCommand = value\r\n\r\n    @property\r\n    def vButtonArgs(self) -> list:\r\n        return self._vButtonArgs\r\n\r\n    @vButtonArgs.setter\r\n    def vButtonArgs(self, value: list):\r\n        self._vButtonArgs = value\r\n\r\n    @property\r\n    def vButtonReturncode(self) -> int:\r\n        return self._vButtonReturncode\r\n\r\n    @vButtonReturncode.setter\r\n    def vButtonReturncode(self, value: int):\r\n        self._vButtonReturncode = value\r\n\r\n    @property\r\n    def vButtonStdout(self) -> str:\r\n        return self._vButtonStdout\r\n\r\n    @vButtonStdout.setter\r\n    def vButtonStdout(self, value: str):\r\n        self._vButtonStdout = value\r\n\r\n    @property\r\n    def vButtonStderr(self) -> str:\r\n        return self._vButtonStderr\r\n\r\n    @vButtonStderr.setter\r\n    def vButtonStderr(self, value: str):\r\n        self._vButtonStderr = value\r\n\r\n    @property\r\n    def vButtonHasCommandLine(self) -> bool:\r\n        return self._vButtonHasCommandLine\r\n\r\n    @vButtonHasCommandLine.setter\r\n    def vButtonHasCommandLine(self, value: bool):\r\n        self._vButtonHasCommandLine = value\r\n\r\n    @property\r\n    def aButtonsRows(self) -> int:\r\n        return self._aButtonsRows\r\n\r\n    @aButtonsRows.setter\r\n    def aButtonsRows(self, value: int):\r\n        self._aButtonsRows = value\r\n\r\n    @property\r\n    def aButtonsCols(self) -> int:\r\n        return self._aButtonsCols\r\n\r\n    @aButtonsCols.setter\r\n    def aButtonsCols(self, value: int):\r\n        self._aButtonsCols = value\r\n\r\n    @property\r\n    def aButtons(self) -> list[list[ActionButton]]:\r\n        return self._aButtons\r\n\r\n    @aButtons.setter\r\n    def aButtons(self, value: list):\r\n        self._aButtons = value\r\n\r\n    @property\r\n    def aButtonAction(self) -> str:\r\n        return self._aButtonAction\r\n\r\n    @aButtonAction.setter\r\n    def aButtonAction(self, value: str):\r\n        self.aButtonAction = value\r\n\r\n    @property\r\n    def lButtonsRows(self) -> int:\r\n        return self._lButtonsRows\r\n\r\n    @lButtonsRows.setter\r\n    def lButtonsRows(self, value: int):\r\n        self._lButtonsRows = value\r\n\r\n    @property\r\n    def lButtonsCols(self) -> int:\r\n        return self._lButtonsCols\r\n\r\n    @lButtonsCols.setter\r\n    def lButtonsCols(self, value: int):\r\n        self._lButtonsCols = value\r\n\r\n    @property\r\n    def lButtons(self) -> list[list[LiveButton]]:\r\n        return self._lButtons\r\n\r\n    @lButtons.setter\r\n    def lButtons(self, value: list):\r\n        self._lButtons = value\r\n\r\n    @property\r\n    def curDeviceId(self) -> str:\r\n        return self._curDeviceId\r\n\r\n    @curDeviceId.setter\r\n    def curDeviceId(self, value: str):\r\n        self._curDeviceId = value\r\n\r\n    @property\r\n    def curDevice(self) -> GPIODevice:\r\n        return self._curDevice\r\n\r\n    @curDevice.setter\r\n    def curDevice(self, value: GPIODevice):\r\n        self._curDevice = value\r\n\r\n    @property\r\n    def curDeviceType(self) -> dict:\r\n        return self._curDeviceType\r\n\r\n    @curDeviceType.setter\r\n    def curDeviceType(self, value: dict):\r\n        self._curDeviceType = value\r\n\r\n    @property\r\n    def gpioDevices(self) ->list[GPIODevice]:\r\n        return self._gpioDevices\r\n\r\n    @gpioDevices.setter\r\n    def gpioDevices(self, value: list[GPIODevice]):\r\n        self._gpioDevices = value\r\n\r\n    @property\r\n    def cfgPath(self) -> str:\r\n        return self._cfgPath\r\n\r\n    @cfgPath.setter\r\n    def cfgPath(self, value: str):\r\n        self._cfgPath = value\r\n\r\n    @property\r\n    def cfgBackupPath(self) -> str:\r\n        return self._cfgBackupPath\r\n\r\n    @cfgBackupPath.setter\r\n    def cfgBackupPath(self, value: str):\r\n        self._cfgBackupPath = value\r\n\r\n    @property\r\n    def versionCurrent(self) -> str:\r\n        return currentVersion\r\n\r\n    @property\r\n    def versionLatest(self) -> str:\r\n        if self._versionLatest == \"\":\r\n            self._versionLatest = currentVersion\r\n        return self._versionLatest\r\n    \r\n    @versionLatest.setter\r\n    def versionLatest(self, value: str):\r\n        self._versionLatest = value\r\n\r\n    @property\r\n    def versionCheckTime(self) -> datetime:\r\n        return self._versionCheckTime\r\n    \r\n    @versionCheckTime.setter\r\n    def versionCheckTime(self, value: datetime):\r\n        dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\r\n        self._versionCheckTime = dt\r\n\r\n    @property\r\n    def versionCheckTimeIso(self) -> str:\r\n        if self.versionCheckTime is None:\r\n            return None\r\n        else:\r\n            return self.versionCheckTime.isoformat()\r\n\r\n    @property\r\n    def versionCheckIntervalHours(self) -> int:\r\n        return self._versionCheckIntervalHours\r\n\r\n    @versionCheckIntervalHours.setter\r\n    def versionCheckIntervalHours(self, value: int):\r\n        self._versionCheckIntervalHours = value\r\n\r\n    @property\r\n    def versionCheckEnabled(self) -> bool:\r\n        return self._versionCheckEnabled\r\n\r\n    @versionCheckEnabled.setter\r\n    def versionCheckEnabled(self, value: bool):\r\n        self._versionCheckEnabled = value\r\n\r\n    @property\r\n    def versionCheckFrom(self) -> str:\r\n        if self._versionCheckFrom == \"\":\r\n            self._versionCheckFrom = self.versionCurrent\r\n        if self.isLaterVersion(self.versionCurrent, self._versionCheckFrom):\r\n            self._versionCheckFrom = self.versionCurrent\r\n        return self._versionCheckFrom\r\n\r\n    @versionCheckFrom.setter\r\n    def versionCheckFrom(self, value: str):\r\n        self._versionCheckFrom = value\r\n\r\n    def getLatestVersion(self, now: bool = False) -> str:\r\n        \"\"\" Get the latest version from GitHub releases\r\n\r\n        Args:\r\n            now (bool, optional): If True, the version is fetched from GitHub even\r\n            if the last check was recent. Defaults to False.\r\n        Returns:\r\n            str: the latest version string\r\n        \"\"\"\r\n        version = currentVersion\r\n        if self.versionCheckEnabled == False:\r\n            return version\r\n\r\n        version = self.versionLatest\r\n\r\n        url = \"https://raw.githubusercontent.com/signag/raspi-cam-srv/main/docs/ReleaseNotes.md\"\r\n\r\n        try:\r\n            if now == False:\r\n                if self.versionCheckTime is not None:\r\n                    delta = datetime.now() - self.versionCheckTime\r\n                    hours = delta.total_seconds() / 3600\r\n                    if hours < self.versionCheckIntervalHours:\r\n                        return self.versionLatest\r\n                else:\r\n                    return self.versionLatest\r\n\r\n            response = requests.get(url)\r\n            response.raise_for_status()  # Raise error if request failed\r\n            content = response.text\r\n            lines = content.splitlines()\r\n            for line in lines:\r\n                if line.startswith(\"## V\"):\r\n                    version = line[3:].strip()\r\n                    self.versionLatest = version\r\n                    break\r\n                    \r\n            self.versionCheckTime = datetime.now()\r\n\r\n        except Exception as e:\r\n            logger.error(f\"Error getting latest version from GitHub: {e}\")\r\n            version = self.versionLatest\r\n        return version\r\n\r\n    @property\r\n    def canUpdate(self) -> bool:\r\n        \"\"\" Check whether installed version can be updated\r\n        \"\"\"\r\n        if self.versionCheckEnabled == False:\r\n            return False\r\n\r\n        verIgnore = self.versionCheckFrom[1:].split(\".\")\r\n        for i in range(len(verIgnore)):\r\n            verIgnore[i] = int(verIgnore[i])\r\n\r\n        verLatest = self.versionLatest[1:].split(\".\")\r\n        for i in range(len(verLatest)):\r\n            verLatest[i] = int(verLatest[i])\r\n        \r\n        if len(verIgnore) != len(verLatest):\r\n            return True\r\n        if len(verIgnore) != 3:\r\n                return True\r\n        if verLatest[0] > verIgnore[0]:\r\n            return True\r\n        if verLatest[0] == verIgnore[0]:\r\n            if verLatest[1] > verIgnore[1]:\r\n                return True\r\n            if verLatest[1] == verIgnore[1]:\r\n                if verLatest[2] > verIgnore[2]:\r\n                    return True\r\n        return False\r\n\r\n    def isLaterVersion(self, v1: str, v2: str) -> bool:\r\n        \"\"\" Check whether version v1 is later than version v2\r\n        \"\"\"\r\n        ver1 = v1[1:].split(\".\")\r\n        for i in range(len(ver1)):\r\n            ver1[i] = int(ver1[i])\r\n\r\n        ver2 = v2[1:].split(\".\")\r\n        for i in range(len(ver2)):\r\n            ver2[i] = int(ver2[i])\r\n\r\n        if len(ver1) != len(ver2):\r\n            return False\r\n        if len(ver1) != 3:\r\n                return False\r\n        if ver1[0] > ver2[0]:\r\n            return True\r\n        if ver1[0] == ver2[0]:\r\n            if ver1[1] > ver2[1]:\r\n                return True\r\n            if ver1[1] == ver2[1]:\r\n                if ver1[2] > ver2[2]:\r\n                    return True\r\n        return False\r\n\r\n    @property\r\n    def updateDone(self) -> bool:\r\n        return self._updateDone\r\n\r\n    @updateDone.setter\r\n    def updateDone(self, value: bool):\r\n        self._updateDone = value\r\n\r\n    @property\r\n    def webCamActiveCamPhotoCfg(self) -> str:\r\n        return self._webCamActiveCamPhotoCfg\r\n    \r\n    @webCamActiveCamPhotoCfg.setter\r\n    def webCamActiveCamPhotoCfg(self, value: str):\r\n        self._webCamActiveCamPhotoCfg = value\r\n\r\n    @property\r\n    def webCamSecondCamPhotoCfg(self) -> str:\r\n        return self._webCamSecondCamPhotoCfg\r\n\r\n    @webCamSecondCamPhotoCfg.setter\r\n    def webCamSecondCamPhotoCfg(self, value: str):\r\n        self._webCamSecondCamPhotoCfg = value\r\n\r\n    @property\r\n    def API_active(self) -> bool:\r\n        return self._API_active\r\n\r\n    @API_active.setter\r\n    def API_active(self, value: bool):\r\n        self._API_active = value\r\n\r\n    @property\r\n    def useAPI(self) -> bool:\r\n        return self._useAPI\r\n\r\n    @useAPI.setter\r\n    def useAPI(self, value: bool):\r\n        self._useAPI = value\r\n\r\n    @property\r\n    def processInfo(self) -> str:\r\n        pi = self._countThreads(\"raspiCamSrv\")\r\n        # This subprocess runs in an own thread,\r\n        # So we need to reduce prcNlwp to get the real number of threads\r\n        threadCount = pi[2] - 1\r\n        return f\"PID:{pi[0]} Start:{pi[1]} #Threads:{threadCount} CPU Process:{pi[3]} Threads:{pi[4]}\"\r\n\r\n    @property\r\n    def ffmpegProcessInfo(self) -> str:\r\n        pi = self._countThreads(\"ffmpeg\")\r\n        if pi[2] == 0:\r\n            return f\"No ffmpeg process active\"\r\n        else:\r\n            return f\"PID:{pi[0]} Start:{pi[1]} #Threads:{pi[2]} CPU Process:{pi[3]} Threads:{pi[4]}\"\r\n\r\n    @property\r\n    def deviceTypes(self) -> list:\r\n        return gpioDeviceTypes\r\n\r\n    def getDevice(self, id: str) -> GPIODevice:\r\n        device = None\r\n        for dev in self.gpioDevices:\r\n            if dev.id == id:\r\n                device = dev\r\n                break\r\n        return device\r\n\r\n    def getDeviceType(self, id: str) -> dict:\r\n        deviceType = None\r\n        for typ in self.deviceTypes:\r\n            if typ[\"type\"] == id:\r\n                deviceType = typ\r\n                break\r\n        return deviceType\r\n\r\n    @property\r\n    def freeGpioPins(self) -> list[int]:\r\n        \"\"\" Return a list with the numbers of free GPIO pins\r\n\r\n        Returns:\r\n            list[int]: the free GPIO pins\r\n        \"\"\"\r\n        pins = []\r\n        for pin in range(0, 28):\r\n            pins.append(pin)\r\n        logger.debug(\"freeGpioPins\")\r\n        for device in self.gpioDevices:\r\n            typ = device.type\r\n            deviceParams = device.params\r\n            devType = self.getDeviceType(typ)\r\n            for param, value in devType[\"params\"].items():\r\n                if \"isPin\" in value:\r\n                    if value[\"isPin\"] == True:\r\n                        pin = deviceParams[param]\r\n                        if type(pin) is int:\r\n                            if pin in pins:\r\n                                pins.remove(pin)\r\n        return pins\r\n        \r\n    @property\r\n    def pythonInfo(self) -> str:\r\n        \"\"\"Get Python version and location\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import sys\r\n            version = f\"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\"\r\n            ex = sys.executable\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n        return info\r\n\r\n    @property\r\n    def flaskInfo(self) -> str:\r\n        \"\"\"Get version and location of Flask module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import flask\r\n            version = importlib.metadata.version(\"flask\")\r\n            ex = os.path.dirname(flask.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def libcameraInfo(self) -> str:\r\n        try:\r\n            result = subprocess.run(\r\n                [\"dpkg\", \"-l\"],\r\n                capture_output=True,\r\n                text=True,\r\n                check=False\r\n            )\r\n        except Exception:\r\n            return \"Unknown\"\r\n\r\n        vers = \"\"\r\n        for line in result.stdout.splitlines():\r\n            if \"libcamera\" in line and line.startswith(\"ii\"):\r\n                parts = line.split()\r\n                if len(parts) >= 3:\r\n                    pkg_name = parts[1]\r\n                    version = parts[2]\r\n\r\n                    # Prefer the main runtime package\r\n                    if pkg_name.startswith(\"libcamera0\"):\r\n                        vers = version\r\n\r\n        if vers == \"\":\r\n            # Fallback: return first libcamera-related package version\r\n            for line in result.stdout.splitlines():\r\n                if \"libcamera\" in line and line.startswith(\"ii\"):\r\n                    parts = line.split()\r\n                    if len(parts) >= 3:\r\n                        vers = parts[2]\r\n\r\n        if vers == \"\":\r\n            info = \"Unknown\"\r\n        else:\r\n            info = f\"Ver: {vers}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def picamera2Info(self) -> str:\r\n        \"\"\"Get version and location of picamera2 module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import picamera2\r\n            version = importlib.metadata.version(\"picamera2\")\r\n            ex = os.path.dirname(picamera2.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def openCvInfo(self) -> str:\r\n        \"\"\"Get version and location of openCV module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import cv2\r\n            version = cv2.__version__\r\n            path = os.path.dirname(cv2.__file__)\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def numpyInfo(self) -> str:\r\n        \"\"\"Get version and location of numpy module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import numpy\r\n            version = numpy.__version__\r\n            ex = os.path.dirname(numpy.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def matplotlibInfo(self) -> str:\r\n        \"\"\"Get version and location of matplotlib module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import matplotlib\r\n            version = matplotlib.__version__\r\n            ex = os.path.dirname(matplotlib.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def flask_jwt_extended(self) -> str:\r\n        \"\"\"Get version and location of flask_jwt_extended module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import flask_jwt_extended\r\n            version = importlib.metadata.version(\"flask_jwt_extended\")\r\n            ex = os.path.dirname(flask_jwt_extended.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def imx500Info(self) -> str:\r\n        \"\"\"Get version and location of imx500-all package\r\n        \"\"\"\r\n        info = \"\"\r\n        res = self._get_dpkg_info(\"imx500-all\")\r\n\r\n        if res[\"installed\"] == False:\r\n            info = \"Package not installed\"\r\n        else:\r\n            version = res[\"version\"]\r\n            files = res[\"files\"]\r\n            path = \"N/A\"\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def munkresInfo(self) -> str:\r\n        \"\"\"Get version and location of munkres module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import munkres\r\n            version = importlib.metadata.version(\"munkres\")\r\n            ex = os.path.dirname(munkres.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def gunicornInfo(self) -> str:\r\n        \"\"\"Get version and location of gunicorn module\r\n        \"\"\"\r\n        info = \"\"\r\n        try:\r\n            import gunicorn\r\n            version = importlib.metadata.version(\"gunicorn\")\r\n            ex = os.path.dirname(gunicorn.__file__)\r\n            pex = Path(ex)\r\n            path = pex.parent\r\n            info = f\"Ver: {version} - Loc: {path}\"\r\n\r\n        except ModuleNotFoundError as e:\r\n            info = \"Module not found\"\r\n        except Exception as e:\r\n            info = f\"Error: {e}\"\r\n\r\n        return info\r\n\r\n    @property\r\n    def wsgiInfo(self) -> str:\r\n        \"\"\"Get WSGI server\r\n        \"\"\"\r\n        from flask import request\r\n\r\n        software = request.environ.get(\"SERVER_SOFTWARE\", \"\").lower()\r\n\r\n        if \"gunicorn\" in software:\r\n            threads = os.getenv(\"GUNICORN_THREADS\", \"?\")\r\n            return f\"gunicorn (worker with {threads} threads)\"\r\n        elif \"werkzeug\" in software:\r\n            return \"werkzeug (Flask built-in development server)\"\r\n        elif \"waitress\" in software:\r\n            return \"waitress\"\r\n        elif \"uwsgi\" in software:\r\n            return \"uwsgi\"\r\n        else:\r\n            return \"unknown\"\r\n\r\n    @property\r\n    def startupInfo(self) -> str:\r\n        \"\"\"Return info how server is started\r\n        \"\"\"\r\n        info = \"Unknown\"\r\n        if self.detect_startup_source() == 1:\r\n            info = \"Server started via systemd system service\"\r\n        if self.detect_startup_source() == 2:\r\n            info = \"Server started via systemd user service\"\r\n        if self.detect_startup_source() == 3:\r\n            info = \"Server started via command line\"\r\n        return info\r\n\r\n    @property\r\n    def environmentInfo(self) -> str:\r\n        \"\"\"Return info whether or not server is running in a container\r\n        \"\"\"\r\n        info = \"Unknown\"\r\n        if self.runningInContainer():\r\n            info = \"Docker Container\"\r\n        else:\r\n            info = \"Host System\"\r\n        return info\r\n\r\n    def _checkModule(self, moduleName: str):\r\n        logger.debug(\"_checkModule for module: %s\", moduleName)\r\n        module = None\r\n        try:\r\n            module = importlib.import_module(moduleName)\r\n        except ModuleNotFoundError as e:\r\n            logger.debug(\"_checkModule for module: %s - ModuleNotFoundError: %s\", moduleName, e)\r\n            module = None\r\n        except ImportError as e:\r\n            logger.debug(\"_checkModule for module: %s - ImportError: %s\", moduleName, e)\r\n            module = None\r\n        except Exception as e:\r\n            logger.debug(\"_checkModule for module: %s - Exception: %s\", moduleName, e)\r\n            module = None\r\n        except:\r\n            logger.debug(\"_checkModule for module: %s - Other exception\", moduleName)\r\n            module = None\r\n        return module\r\n\r\n    def checkEnvironment(self):\r\n        \"\"\" Check the availability of specific modules \r\n            which might be required for specific tasks.\r\n            - cv2\r\n            - numpy\r\n            - matplotlib\r\n            - flask_jwt_extended\r\n            - imx500 (for Sony IMX500/IMX501 camera support)\r\n            - munkres (for Hungarian Algorithm support used in pose estimation)\r\n        \"\"\"\r\n        logger.debug(\"checkEnvironment\")\r\n        self.cv2Available = self._checkModule(\"cv2\") is not None\r\n        self.numpyAvailable = self._checkModule(\"numpy\") is not None\r\n        self.matplotlibAvailable = self._checkModule(\"matplotlib\") is not None\r\n        self.flaskJwtLibAvailable = self._checkModule(\"flask_jwt_extended\") is not None\r\n        self.imx500Available = self._get_dpkg_info(\"imx500-all\")[\"installed\"]\r\n        self.munkresAvailable = self._checkModule(\"munkres\") is not None\r\n        if self.supportsHistograms:\r\n            self.useHistograms = True\r\n        else:\r\n            self.useHistograms = False\r\n        if self.supportsAPI:\r\n            self.useAPI = True\r\n        else:\r\n            self.useAPI = False\r\n        logger.debug(\"cv2Available: %s numpyAvailable: %s matplotlibAvailable: %s flaskJwtLibAvailable: %s\", self. cv2Available, self.numpyAvailable, self.matplotlibAvailable, self.flaskJwtLibAvailable)\r\n\r\n\r\n    def _get_dpkg_info(self, package: str) -> dict:\r\n        \"\"\" Get information about a Debian package using dpkg-query\"\"\"\r\n        # Check whether the package is installed\r\n        check = subprocess.run(\r\n            [\"dpkg-query\", \"-W\", \"-f=${Status}\", package],\r\n            capture_output=True,\r\n            text=True\r\n        )\r\n\r\n        if check.returncode != 0 or \"installed\" not in check.stdout:\r\n            return {\r\n                \"installed\": False,\r\n                \"version\": None,\r\n                \"files\": []\r\n            }\r\n\r\n        # Package is installed → get version\r\n        version = subprocess.run(\r\n            [\"dpkg-query\", \"-W\", \"-f=${Version}\", package],\r\n            capture_output=True,\r\n            text=True\r\n        ).stdout.strip()\r\n\r\n        # Get installed file list\r\n        files = subprocess.run(\r\n            [\"dpkg\", \"-L\", package],\r\n            capture_output=True,\r\n            text=True\r\n        ).stdout.splitlines()\r\n\r\n        return {\r\n            \"installed\": True,\r\n            \"version\": version,\r\n            \"files\": files\r\n        }\r\n\r\n    def is_time_synchronized(self) -> tuple[bool, bool]:\r\n        \"\"\" Check if the system time is synchronized with NTP server\r\n        \r\n        \"\"\"\r\n        err = False\r\n        sync = False\r\n        if self.runningInContainer():\r\n            logger.debug(\"Running in container - assuming time is synchronized\")\r\n            return (err, True)\r\n        try:\r\n            output = subprocess.check_output([\"timedatectl\"], text=True)\r\n            for line in output.splitlines():\r\n                if \"System clock synchronized:\" in line:\r\n                    sync = \"yes\" in line.split(\":\")[1].strip().lower()\r\n                    return (err, sync)\r\n        except Exception as e:\r\n            logger.error(f\"Error checking time sync: {e}\")\r\n            err = True\r\n        return (err, sync)\r\n\r\n    def runningInContainer(self):\r\n        if os.path.exists(\"/.dockerenv\"):\r\n            return True\r\n        try:\r\n            with open(\"/proc/1/cgroup\", \"rt\") as f:\r\n                content = f.read()\r\n                return \"docker\" in content or \"containerd\" in content or \"kubepods\" in content\r\n        except FileNotFoundError:\r\n            return False\r\n\r\n    def wait_for_time_sync(self, timeout:int=60, interval:int=2) -> bool:\r\n        \"\"\" Wait for time synchronization with NTP server\r\n\r\n        Args:\r\n            timeout (int, optional): Timeout in seconds. Defaults to 60.\r\n            interval (int, optional): test cycle interval in seconds. Defaults to 2.\r\n\r\n        Returns:\r\n            bool: True if time is synchronized, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"ServerConfig.wait_for_time_sync\")\r\n        for _ in range(int(timeout / interval)):\r\n            (err, sync) = self.is_time_synchronized()\r\n            if err == True:\r\n                break\r\n            if sync == True:\r\n                logger.debug(\"System time is synchronized\")\r\n                self.serverStartTime = datetime.now()\r\n                return True\r\n            else:\r\n                logger.debug(\"Still waiting for time synchronization...\")\r\n            sleep(interval)\r\n        logger.debug(\"Timeout while waiting for system time synchronization\")\r\n        return False    \r\n\r\n    @property\r\n    def displayBufferCount(self) -> int:\r\n        \"\"\" Returns the number of elements in the display buffer\r\n        \"\"\"\r\n        return len(self._displayBuffer)\r\n\r\n    @property\r\n    def displayBufferIndex(self) -> str:\r\n        \"\"\" Returns the index of the active element in the form (x/y)\r\n        \"\"\"\r\n        res = \"\"\r\n        if self.isDisplayBufferIn():\r\n            for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                if key == self.displayFile:\r\n                    res = \"(\" + str(i + 1) + \"/\" + str(self.displayBufferCount) + \")\"\r\n                    break\r\n\r\n        return res\r\n\r\n    def isDisplayBufferIn(self) -> bool:\r\n        \"\"\"Determine whether the current display is in the buffer\"\"\"\r\n        res = False\r\n        if len(self._displayBuffer) > 0:\r\n            if self._displayFile in self._displayBuffer:\r\n                res = True\r\n        return res\r\n\r\n    def displayBufferAdd(self):\r\n        \"\"\" Adds the current display photo to the buffer\r\n            if it is not yet included\r\n        \"\"\"\r\n        if self.isDisplayBufferIn() == False:\r\n            el = {}\r\n            el[\"displayPhoto\"] = self._displayPhoto\r\n            el[\"displayFile\"]  = self._displayFile\r\n            el[\"displayMeta\"]  = self._displayMeta\r\n            el[\"displayHisto\"]  = self._displayHistogram\r\n            el[\"displayMetaFirst\"]  = self._displayMetaFirst\r\n            el[\"displayMetaLast\"]  = self._displayMetaLast\r\n            self._displayBuffer[self._displayFile] = el\r\n\r\n    def displayBufferRemove(self):\r\n        \"\"\" Removes the current display photo from the buffer\r\n            and set active display to next element\r\n        \"\"\"\r\n        if self.displayBufferCount > 0:\r\n            if self.displayBufferCount == 1:\r\n                # If the buffer contains just one element: clear it\r\n                self.displayBufferClear()\r\n            else:\r\n                # Buffer contains more than one element\r\n                if self.isDisplayBufferIn():\r\n                    # Active element is in buffer\r\n                    idel = -1\r\n                    if self.isDisplayBufferIn() == True:\r\n                        # If active element in buffer: find and delete it\r\n                        for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                            if key == self.displayFile:\r\n                                idel = i\r\n                                # idel is now the index of the element to activate (show)\r\n                                del self._displayBuffer[key]\r\n                                break\r\n                    if idel >= 0:\r\n                        # If the previouslay active element has been deleted,\r\n                        # activate another element\r\n                        # This will normally the next in buffer ...\r\n                        if idel >= self.displayBufferCount:\r\n                            # ... except when the last element has been deleted.\r\n                            # then activate the previous element\r\n                            idel = idel - 1\r\n                        for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                            if i == idel:\r\n                                self.displayFile = key\r\n                                self.displayPhoto = value[\"displayPhoto\"]\r\n                                self.displayMeta = value[\"displayMeta\"]\r\n                                self.displayHistogram = value[\"displayHisto\"]\r\n                                self.displayMetaFirst = value[\"displayMetaFirst\"]\r\n                                self.displayMetaLast = value[\"displayMetaLast\"]\r\n                                break\r\n                else:\r\n                    # Active element is not in buffer: Just clear active element\r\n                    self.displayFile = None\r\n                    self.displayPhoto = None\r\n                    self.displayMeta = None\r\n                    self.displayHistogram = None\r\n                    self.displayMetaFirst = 0\r\n                    self.displayMetaLast = 999\r\n        else:\r\n            # Buffer is empty: Just clear active element\r\n            self.displayFile = None\r\n            self.displayPhoto = None\r\n            self.displayMeta = None\r\n            self.displayHistogram = None\r\n            self.displayMetaFirst = 0\r\n            self.displayMetaLast = 999\r\n\r\n    def displayBufferClear(self):\r\n        \"\"\" Clears the display buffer as well as the current display\r\n        \"\"\"\r\n        self._displayBuffer.clear()\r\n        self.displayFile = None\r\n        self.displayPhoto = None\r\n        self.displayMeta = None\r\n        self.displayHistogram = None\r\n        self.displayMetaFirst = 0\r\n        self.displayMetaLast = 999\r\n\r\n    def displayBufferCheck(self):\r\n        \"\"\" Clear display info for entries for which files no longer exist\r\n        \"\"\"\r\n        if not self.displayPhoto is None:\r\n            done = False\r\n            while not done:\r\n                fp = self.photoRoot + \"/\" + self.displayPhoto\r\n                if os.path.isfile(fp):\r\n                    done = True\r\n                else:\r\n                    self.displayBufferRemove()\r\n                if self.displayPhoto is None:\r\n                    done = True\r\n        if self.displayBufferCount > 0:\r\n            keysToRemove = []\r\n            for key, value in self._displayBuffer.items():\r\n                fp = self.photoRoot + \"/\" + value[\"displayPhoto\"]\r\n                if not os.path.isfile(fp):\r\n                    keysToRemove.append(key)\r\n            for key in keysToRemove:\r\n                del self._displayBuffer[key]\r\n\r\n    def isDisplayBufferFirst(self) -> bool:\r\n        \"\"\"Determine whether the current display is the first element in the buffer\"\"\"\r\n        res = False\r\n        if self.isDisplayBufferIn():\r\n            for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                if i == 0:\r\n                    if key == self.displayFile:\r\n                        res = True\r\n                else:\r\n                    break\r\n        return res\r\n\r\n    def isDisplayBufferLast(self) -> bool:\r\n        \"\"\"Determine whether the current display is the last element in the buffer\"\"\"\r\n        res = False\r\n        l = len(self._displayBuffer) - 1\r\n        if self.isDisplayBufferIn():\r\n            for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                if i == l:\r\n                    if key == self.displayFile:\r\n                        res = True\r\n        return res\r\n\r\n    def displayBufferFirst(self):\r\n        \"\"\"Change the current display element to the first in buffer\"\"\"\r\n        firstKey = None\r\n        firstEl = None\r\n        if self.displayBufferCount > 0:\r\n            for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                if i == 0:\r\n                    firstKey = key\r\n                    firstEl = value\r\n                    break\r\n        if firstKey:\r\n            self.displayFile = firstKey\r\n            self.displayPhoto = firstEl[\"displayPhoto\"]\r\n            self.displayMeta = firstEl[\"displayMeta\"]\r\n            self.displayHistogram = firstEl[\"displayHisto\"]\r\n            self.displayMetaFirst = firstEl[\"displayMetaFirst\"]\r\n            self.displayMetaLast = firstEl[\"displayMetaLast\"]\r\n\r\n    def displayBufferNext(self):\r\n        \"\"\"Change the current display element to the next in buffer\"\"\"\r\n        nextKey = None\r\n        nextEl = None\r\n        if self.isDisplayBufferIn():\r\n            if not self.isDisplayBufferLast():\r\n                found = False\r\n                for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                    if key == self.displayFile:\r\n                        found = True\r\n                    else:\r\n                        if found:\r\n                            nextKey = key\r\n                            nextEl = value\r\n                            break\r\n        else:\r\n            self.displayBufferFirst()\r\n        if nextKey:\r\n            self.displayFile = nextKey\r\n            self.displayPhoto = nextEl[\"displayPhoto\"]\r\n            self.displayMeta = nextEl[\"displayMeta\"]\r\n            self.displayHistogram = nextEl[\"displayHisto\"]\r\n            self.displayMetaFirst = nextEl[\"displayMetaFirst\"]\r\n            self.displayMetaLast = nextEl[\"displayMetaLast\"]\r\n\r\n    def displayBufferPrev(self):\r\n        \"\"\"Change the current display element to the previous in buffer\"\"\"\r\n        prevKey = None\r\n        prevEl = None\r\n        if self.isDisplayBufferIn():\r\n            if not self.isDisplayBufferFirst():\r\n                for i, (key, value) in enumerate(self._displayBuffer.items()):\r\n                    if key == self.displayFile:\r\n                        break\r\n                    prevKey = key\r\n                    prevEl = value\r\n        if prevKey:\r\n            self.displayFile = prevKey\r\n            self.displayPhoto = prevEl[\"displayPhoto\"]\r\n            self.displayMeta = prevEl[\"displayMeta\"]\r\n            self.displayHistogram = prevEl[\"displayHisto\"]\r\n            self.displayMetaFirst = prevEl[\"displayMetaFirst\"]\r\n            self.displayMetaLast = prevEl[\"displayMetaLast\"]\r\n\r\n    def _lineGen(self, s):\r\n        \"\"\"Generator to yield lines of a text\r\n        \"\"\"\r\n        while len(s) > 0:\r\n            p = s.find(\"\\n\")\r\n            if p >= 0:\r\n                if p == 0:\r\n                    line = \"\"\r\n                else:\r\n                    line = s[:p]\r\n                s = s[p+1:]\r\n            else:\r\n                line = s\r\n                s = \"\"\r\n            yield line\r\n\r\n    def _checkMicrophoneNoJson(self):\r\n        \"\"\"Check connection of microphone for older PulseAudio versions where pactl has no -fjson option\r\n        \"\"\"\r\n        logger.debug(\"ServerConfig._checkMicrophoneNoJson\")\r\n        hasMic = False\r\n        defMic = \"\"\r\n        isMute = False\r\n        try:\r\n            result = subprocess.run([\"pactl\", \"list\", \"sources\"], capture_output=True, text=True, check=True).stdout\r\n            logger.debug(\"ServerConfig._checkMicrophoneNoJson - got result from 'pactl list sources: \\n%s'\", result)\r\n\r\n            sourceId = \"\"\r\n            desc = \"\"\r\n            getPorts = False\r\n            for line in self._lineGen(result):\r\n                if line.startswith(\"Source\"):\r\n                    # Start of a new source\r\n                    if sourceId == \"\":\r\n                        # First source\r\n                        sourceId = line[8:]\r\n                        desc = \"\"\r\n                        getPorts = False\r\n                    else:\r\n                        # Terminate last source (actually nothing specific)\r\n                        sourceId = line[8:]\r\n                        desc = \"\"\r\n                        getPorts = False\r\n                else:\r\n                    if line.startswith(\"\\t\"):\r\n                        line = line[1:]\r\n                        if line.startswith(\"Description:\"):\r\n                            desc = line[13:]\r\n                        if getPorts:\r\n                            if line.find(\"type: Mic\") > 0:\r\n                                # We stop if the first microphone has been found\r\n                                # This version of pactl does not allow to get the default mic.\r\n                                hasMic = True\r\n                                defMic = desc\r\n                                break\r\n                            getPorts = False\r\n                        else:\r\n                            if line.startswith(\"Ports:\"):\r\n                                getPorts = True\r\n\r\n        except CalledProcessError as e:\r\n            # In case pactl cannot be run, ignore the exception\r\n            # And assume that no microphone is connected\r\n            pass\r\n        except Exception as e:\r\n            pass\r\n\r\n        logger.debug(\"ServerConfig._checkMicrophoneNoJson - hasMic=%s, defMic=%s'\", hasMic, defMic)\r\n        return hasMic, defMic, isMute\r\n\r\n    def checkMicrophone(self):\r\n        \"\"\"Check whether a microphone is connected.\r\n           Update configuration with description of default configuration.\r\n           \r\n           This infomation is obtained by querying the PulseAudio server through pactl\r\n        \"\"\"\r\n        logger.debug(\"ServerConfig._checkMicrophone\")\r\n        hasMic = False\r\n        defMic = \"\"\r\n        isMute = True\r\n        try:\r\n            result = subprocess.run([\"pactl\", \"-fjson\", \"list\", \"sources\"], capture_output=True, text=True, check=True).stdout\r\n            logger.debug(\"ServerConfig._checkMicrophone - got result from 'pactl -fjson list sources'\")\r\n\r\n            sources=json.loads(result)\r\n\r\n            if len(sources) > 0:\r\n                definput  = subprocess.run([\"pactl\", \"get-default-source\"], capture_output=True, text=True, check=True).stdout\r\n                if definput.endswith(\"\\n\"):\r\n                    definput = definput[:len(definput) - 1]\r\n                for source in sources:\r\n                    if \"name\" in source:\r\n                        srcName = source[\"name\"]\r\n                        if srcName == definput:\r\n                            if \"ports\" in source:\r\n                                ports = source[\"ports\"]\r\n                                for port in ports:\r\n                                    if \"type\" in port:\r\n                                        type = port[\"type\"]\r\n                                        if type.casefold() == \"mic\":\r\n                                            hasMic = True\r\n                                            break\r\n                            if hasMic == True:\r\n                                if \"description\" in source:\r\n                                    defMic = source[\"description\"]\r\n                                if \"mute\" in source:\r\n                                    isMute = source[\"mute\"]\r\n                                else:\r\n                                    isMute = False\r\n        except CalledProcessError as e:\r\n            # In case pactl cannot be run successfully, assume an older PulseAudio version\r\n            # and try without -fjson option\r\n            hasMic, defMic, isMute = self._checkMicrophoneNoJson()\r\n        except Exception as e:\r\n            pass\r\n\r\n        if hasMic == True:\r\n            self.hasMicrophone = True\r\n            if len(defMic) > 0:\r\n                self.defaultMic = defMic\r\n            else:\r\n                self._defaultMic = \"Unknown description\"\r\n            self.isMicMuted = isMute\r\n        else:\r\n            self.hasMicrophone = False\r\n            self.defaultMic = \"No Microphone found\"\r\n            self.recordAudio = False\r\n            self.isMicMuted = False\r\n        logger.debug(\"ServerConfig._checkMicrophone - hasMicrophone=%s, defaultMic=%s\", self.hasMicrophone, self.defaultMic)\r\n\r\n    @staticmethod\r\n    def getPiModel() -> str:\r\n        \"\"\" Get the Raspberry Pi model\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getPiModel\")\r\n        model = \"\"\r\n        try:\r\n            with open('/proc/device-tree/model') as f:\r\n                model = f.read()\r\n                if model.endswith(\"\\x00\"):\r\n                    model = model[:len(model)-1]\r\n            logger.debug(\"CameraCfg.getPiModel - model: %s\", model)\r\n        except Exception as e:\r\n            pass\r\n        return model\r\n\r\n    @staticmethod\r\n    def getBoardRevision():\r\n        \"\"\" Get the revision of the Raspberry Pi board\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getBoardRevision\")\r\n        boardRev = \"0000\"\r\n        try:\r\n            with open('/proc/cpuinfo','r') as f:\r\n                for line in f:\r\n                    if line[0:8]=='Revision':\r\n                        length=len(line)\r\n                        boardRev = line[11:length-1]\r\n        except Exception as e:\r\n            logger.error(\"Error opening /proc/cpuinfo : %s\", e)\r\n            boardRev = \"0000\"\r\n\r\n        logger.debug(\"CameraCfg.getBoardRevision - boardRev = %s\", boardRev)\r\n        return boardRev\r\n\r\n    def getDebianVersion(self):\r\n        \"\"\" Get the Debian Version of the installed OS\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getDebianVersion\")\r\n        debianVers = \"\"\r\n        try:\r\n            with open('/etc/debian_version','r') as f:\r\n                for line in f:\r\n                    debianVers += line\r\n        except Exception as e:\r\n            logger.error(\"Error opening /etc/debian_version : %s\", e)\r\n            debianVers = \"\"\r\n\r\n        debianVers = self.getOsName() + \" - Version \" + debianVers + \" - \" + self.getOSArch()\r\n        logger.debug(\"CameraCfg.getDebianVersion - debianVers = %s\", debianVers)\r\n        return debianVers\r\n\r\n    def getOSArch(self):\r\n        \"\"\" Get the architecture (32/64) of the installed OS\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getOSArch\")\r\n        osArch = \"\"\r\n        arch = \"\"\r\n        try:\r\n            result = subprocess.run([\"dpkg-architecture\", \"--query\", \"DEB_HOST_ARCH\"], capture_output=True, text=True, check=True).stdout\r\n            for line in self._lineGen(result):\r\n                arch += line.strip()\r\n                if arch == \"arm64\" \\\r\n                or arch == \"aarch64\" \\\r\n                or arch.find(\"64\") >= 0:\r\n                    osArch = \"64-bit\"\r\n                else:\r\n                    osArch = \"32-bit\"\r\n        except Exception as e:\r\n            logger.error(\"Error executing dpkg-architecture --query DEB_HOST_ARCH : %s\", e)\r\n            osArch = \"error\"\r\n\r\n        logger.debug(\"CameraCfg.getOSArch - osArch = %s\", osArch)\r\n        return osArch\r\n\r\n    def getKernelVersion(self):\r\n        \"\"\" Get the Kernel Version of the installed OS\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getKernelVersion\")\r\n        kernelVers = \"\"\r\n        try:\r\n            result = subprocess.run([\"uname\", \"-r\"], capture_output=True, text=True, check=True).stdout\r\n            for line in self._lineGen(result):\r\n                kernelVers += line.strip()\r\n        except Exception as e:\r\n            logger.error(\"Error opening /etc/debian_version : %s\", e)\r\n            kernelVers = \"\"\r\n\r\n        logger.debug(\"CameraCfg.getKernelVersion - kernelVers = %s\", kernelVers)\r\n        return kernelVers\r\n\r\n    def getOsName(self):\r\n        \"\"\" Get the name of the installed OS\r\n        \r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getOsName\")\r\n        osName = \"\"\r\n\r\n        logger.debug(\"CameraCfg.getOsName - trying lsb_release\")\r\n        try:\r\n            result = subprocess.run([\"lsb_release\", \"-a\"], capture_output=True, text=True, check=True).stdout\r\n            for line in self._lineGen(result):\r\n                logger.debug(\"CameraCfg.getOsName - line:%s\", line)\r\n                if line[0:12] == \"Description:\":\r\n                    osName = line[13:].strip()\r\n                    break\r\n        except Exception as e:\r\n            osName = \"\"\r\n\r\n        if osName == \"\":\r\n            logger.debug(\"CameraCfg.getOsName - trying cat /etc/os-release\")\r\n            try:\r\n                result = subprocess.run([\"cat\", \"/etc/os-release\"], capture_output=True, text=True, check=True).stdout\r\n                for line in self._lineGen(result):\r\n                    logger.debug(\"CameraCfg.getOsName - line:%s\", line)\r\n                    if line[0:12] == \"PRETTY_NAME=\":\r\n                        osName = line[13:].strip()\r\n                        osName = osName.strip('\"')\r\n                        break\r\n            except Exception as e:\r\n                osName = \"\"\r\n\r\n        logger.debug(\"CameraCfg.getOsName - osName = %s\", osName)\r\n        return osName\r\n\r\n    def checkJwtSettings(self) -> tuple:\r\n        \"\"\" Get secret key for JSON Wob Tokens JWT\r\n\r\n            The secret key is expected in the JWT secrets file\r\n            If a secret key is found, JWT authentication for the API is enabled\r\n        \"\"\"\r\n        logger.debug(\"ServerConfig.checkJwtSettings\")\r\n        self.jwtAuthenticationActive = False\r\n        # Try to get secret key from the file\r\n        err = None\r\n        msg = \"\"\r\n        jwtSecretKey = None\r\n        if self.jwtKeyStore != \"\":\r\n            logger.debug(\"ServerConfig.checkJwtSettings - jwtKeyStore = %s\", self.jwtKeyStore)\r\n            if not os.path.exists(self.jwtKeyStore):\r\n                fp = Path(self.jwtKeyStore)\r\n                dir = fp.parent.absolute()\r\n                fn = fp.name\r\n                if not os.path.exists(dir):\r\n                    os.makedirs(dir, exist_ok=True)\r\n                    logger.debug(\"ServerConfig.checkJwtSettings - dir created: %s\", dir)\r\n                self.jwtKeyStore = str(dir) + \"/\" + fn\r\n                Path(self.jwtKeyStore).touch(exist_ok=True)\r\n                logger.debug(\"ServerConfig.checkJwtSettings - file created: %s\", self.jwtKeyStore)\r\n            else:\r\n                logger.debug(\"ServerConfig.checkJwtSettings - path exists: %s\", self.jwtKeyStore)\r\n                if os.path.isdir(self.jwtKeyStore):\r\n                    err = \"The 'Password File Path' must be a file and not a directory!\"\r\n            secrets = {}\r\n            if err is None:\r\n                if os.stat(self.jwtKeyStore).st_size > 0:\r\n                    with open(self.jwtKeyStore, \"r\") as f:\r\n                        try:\r\n                            secrets = json.load(f)\r\n                        except Exception as e:\r\n                            err = \"The file specified as 'JWT Secret Key File Path' has content which is not in JSON format\"\r\n            if err is None:\r\n                jwtSecretKey = \"\"\r\n                if \"jwtSecrets\" in secrets:\r\n                    jwtSecrets = secrets[\"jwtSecrets\"]\r\n                    if \"jwtSecretKey\" in jwtSecrets:\r\n                        jwtSecretKey = jwtSecrets[\"jwtSecretKey\"]\r\n                        logger.debug(\"ServerConfig.checkJwtSettings - JWT secret key read from file\")\r\n                        msg = \"JWT secret key read from Secret Key Store\"\r\n                else:\r\n                    jwtSecrets = {}\r\n                if jwtSecretKey == \"\":\r\n                    jwtSecretKey = token_urlsafe()\r\n                    logger.debug(\"ServerConfig.checkJwtSettings - jwtSecretKey generated: %s\", jwtSecretKey)\r\n                    msg = \"New JWT secret key generated\"\r\n                    secrets[\"jwtSecrets\"] = jwtSecrets\r\n                    jwtSecrets[\"jwtSecretKey\"] = jwtSecretKey\r\n                    with open(self.jwtKeyStore, \"w\") as f:\r\n                        try:\r\n                            json.dump(secrets,fp=f, indent=4)\r\n                            logger.debug(\"ServerConfig.checkJwtSettings -  - saved secrets to file %s\", self.jwtKeyStore)\r\n                        except Exception as e:\r\n                            logger.err(\"ServerConfig.checkJwtSettings -  - error while saving secrets to file %s: %s\", self.jwtKeyStore, e)\r\n                            err = \"Error writing to \" + self.jwtKeyStore + \": \" + str(e)\r\n        else:\r\n            logger.debug(\"ServerConfig.checkJwtSettings - jwtKeyStore not set\")\r\n            msg = \"API inactive - No JWT Secret Key Store specified\"\r\n\r\n        if jwtSecretKey is None:\r\n            self.jwtAuthenticationActive = False\r\n        else:\r\n            self.jwtAuthenticationActive = True\r\n        logger.debug(\"ServerConfig.checkJwtSettings - jwtAuthenticationActive = %s\", self.jwtAuthenticationActive)\r\n        return (jwtSecretKey, err, msg)\r\n\r\n    def detect_startup_source(self) -> int:\r\n        \"\"\"Detect the source from which the application was started.\r\n\r\n        Returns:\r\n            int: Type of the startup source.\r\n                1: systemd system unit\r\n                2: systemd user unit\r\n                3: command line\r\n                0: unknown\r\n        \"\"\"\r\n        logger.debug(\"ServerConfig.detect_startup_source\")\r\n        ret = 0\r\n\r\n        # Check parent\r\n        parent = psutil.Process(os.getpid()).parent().name()\r\n\r\n        # Check cgroup\r\n        cgroup = Path(\"/proc/self/cgroup\").read_text()\r\n\r\n        # systemd user or system unit\r\n        if \"system.slice\" in cgroup:\r\n            ret = 1\r\n        if \"user.slice\" in cgroup and \".service\" in cgroup:\r\n            ret = 2\r\n\r\n        # Command line terminal\r\n        if parent in (\"bash\", \"zsh\", \"fish\") or \"session-\" in cgroup:\r\n            ret = 3\r\n\r\n        logger.debug(\"ServerConfig.detect_startup_source - ret=%s\", ret)\r\n        return ret\r\n\r\n    @staticmethod\r\n    def _lineGen(s):\r\n        \"\"\"Generator to yield lines of a text\r\n        \"\"\"\r\n        while len(s) > 0:\r\n            p = s.find(\"\\n\")\r\n            if p >= 0:\r\n                if p == 0:\r\n                    line = \"\"\r\n                else:\r\n                    line = s[:p]\r\n                s = s[p+1:]\r\n            else:\r\n                line = s\r\n                s = \"\"\r\n            yield line\r\n\r\n    def _countThreads(self, process: str=None):\r\n        \"\"\"Count number of threads for a given process\r\n        \r\n        \"\"\"\r\n        cntAll = -1\r\n        cntReq = 0\r\n        prcPid = 0\r\n        prcPids = \"\"\r\n        prcStime = \"\"\r\n        prcNlwp = 0\r\n        prcTime = \"\"\r\n        thrTime = \"\"\r\n        thrTimed = timedelta(0)\r\n        prcCnt = 1\r\n        prcIdx = 0\r\n\r\n        try:\r\n            result = subprocess.run([\"ps\", \"-e\", \"-L\", \"-f\"], capture_output=True, text=True, check=True).stdout\r\n            for line in self._lineGen(result):\r\n                cntAll += 1\r\n                if cntAll > 0:\r\n                    uid = line[sUID:eUID].strip()\r\n                    pid = int(line[sPID:ePID].strip())\r\n                    ppid = int(line[sPPID:ePPID].strip())\r\n                    lwp = int(line[sLWP:eLWP].strip())\r\n                    c = int(line[sC:eC].strip())\r\n                    nlwp = int(line[sNLWP:eNLWP].strip())\r\n                    stime = line[sSTIME:eSTIME].strip()\r\n                    tty = line[sTTY:eTTY].strip()\r\n                    time = line[sTIME:eTIME].strip()\r\n                    cmd = line[sCMD:].strip()\r\n                    if not process is None:\r\n                        if cmd.find(process) >= 0:\r\n                            if cmd.find(\"gunicorn\") >= 0:\r\n                                prcCnt = 2\r\n                            if pid == lwp:\r\n                                cntReq += 1\r\n                                prcIdx += 1\r\n                                if prcCnt > 1:\r\n                                    if prcPids == \"\":\r\n                                        prcPids = f\"{pid}\"\r\n                                    else:\r\n                                        prcPids += f\", {pid}\"\r\n                                prcPid = pid\r\n                                prcStime = stime\r\n                                prcNlwp += nlwp\r\n                                t = datetime.strptime(time, \"%H:%M:%S\")\r\n                                td = timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)\r\n                                if prcTime == \"\":\r\n                                    prcTime = td\r\n                                else:\r\n                                    prcTime += td\r\n                            else:\r\n                                if pid == prcPid:\r\n                                    cntReq += 1\r\n                                    t = datetime.strptime(time, \"%H:%M:%S\")\r\n                                    td = timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)\r\n                                    thrTimed += td\r\n                            if cntReq >= prcNlwp:\r\n                                if prcIdx >= prcCnt:\r\n                                    break\r\n                else:\r\n                    p = 0\r\n                    p = line.find(\"UID\", p)\r\n                    sUID = p\r\n                    p = line.find(\"PID\", p + 3)\r\n                    ePID = p + 3\r\n                    sPID = ePID - 6\r\n                    eUID = sPID\r\n                    p = line.find(\"PPID\", p + 3)\r\n                    ePPID = p + 4\r\n                    sPPID = ePID\r\n                    p = line.find(\"LWP\", p + 4)\r\n                    eLWP = p + 3\r\n                    sLWP = ePPID\r\n                    p = line.find(\"C\", p + 3)\r\n                    eC = p + 1\r\n                    sC = eLWP\r\n                    p = line.find(\"NLWP\", p + 1)\r\n                    eNLWP = p + 4\r\n                    sNLWP = eC\r\n                    p = line.find(\"STIME\", p + 4)\r\n                    eSTIME = p + 5\r\n                    sSTIME = eNLWP\r\n                    p = line.find(\"TTY\", p + 5)\r\n                    sTTY = p - 1\r\n                    p = line.find(\"TIME\", p + 3)\r\n                    eTTY = p - 5\r\n                    eTIME = p + 4\r\n                    sTIME = eTTY\r\n                    p = line.find(\"CMD\", p + 4)\r\n                    sCMD = p\r\n\r\n        except CalledProcessError as e:\r\n            pass\r\n        except Exception as e:\r\n            pass\r\n\r\n        if process is None:\r\n            return (cntAll,)\r\n        else:\r\n            thrTime = str(thrTimed)\r\n            if prcCnt > 1:\r\n                prcTime = str(prcTime)\r\n                prcPid = prcPids\r\n            return (prcPid, prcStime, prcNlwp, prcTime, thrTime)\r\n    \r\n    def getBaseHelpUrl(self) -> str:\r\n        \"\"\" Get the base URL for help pages\r\n        \r\n        \"\"\"\r\n        logger.debug(\"ServerConfig.getBaseHelpUrl\")\r\n        baseHelpUrl = \"https://signag.github.io/raspi-cam-srv/\" + versionDoc.docversion\r\n        logger.debug(\"ServerConfig.getBaseHelpUrl - baseHelpUrl = %s\", baseHelpUrl)\r\n        return baseHelpUrl\r\n\r\n    @classmethod                \r\n    def initFromDict(cls, dict:dict):\r\n        sc = ServerConfig()\r\n        for key, value in dict.items():\r\n            # logger.debug(\"serverConfig.initFromDict - processing key %s\", key)\r\n            if key == \"_scalerCropLiveView\":\r\n                setattr(sc, key, tuple(value))\r\n            elif key == \"_scalerCropMin\":\r\n                setattr(sc, key, tuple(value))\r\n            elif key == \"_scalerCropMax\":\r\n                setattr(sc, key, tuple(value))\r\n            elif key == \"_scalerCropDef\":\r\n                setattr(sc, key, tuple(value))\r\n            elif key == \"_displayMeta\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    metat = {}\r\n                    for ckey, cvalue in value.items():\r\n                        vt = cvalue\r\n                        if ckey == \"ScalerCrop\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"FrameDurationLimits\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"ColourGains\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"ColourCorrectionMatrix\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"SensorBlackLevels\":\r\n                            vt = tuple(cvalue)\r\n                        elif ckey == \"AfWindows\":\r\n                            afws = ()\r\n                            for el in cvalue:\r\n                                afw = (tuple(el),)\r\n                                afws += afw\r\n                            vt = afws\r\n                        else:\r\n                            vt = cvalue\r\n                        metat[ckey] = vt\r\n                    setattr(sc, key, metat)\r\n            elif key == \"_displayBuffer\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    dbt = {}\r\n                    for bkey, bvalue in value.items():\r\n                        belt = {}\r\n                        for belmetakey, belmetavalue in bvalue.items():\r\n                            if belmetakey == \"displayMeta\":\r\n                                metat = {}\r\n                                for ckey, cvalue in belmetavalue.items():\r\n                                    vt = cvalue\r\n                                    if ckey == \"ScalerCrop\":\r\n                                        vt = tuple(cvalue)\r\n                                    elif ckey == \"FrameDurationLimits\":\r\n                                        vt = tuple(cvalue)\r\n                                    elif ckey == \"ColourGains\":\r\n                                        vt = tuple(cvalue)\r\n                                    elif ckey == \"ColourCorrectionMatrix\":\r\n                                        vt = tuple(cvalue)\r\n                                    elif ckey == \"SensorBlackLevels\":\r\n                                        vt = tuple(cvalue)\r\n                                    elif ckey == \"AfWindows\":\r\n                                        afws = ()\r\n                                        for el in cvalue:\r\n                                            afw = (tuple(el),)\r\n                                            afws += afw\r\n                                        vt = afws\r\n                                    else:\r\n                                        vt = cvalue\r\n                                    metat[ckey] = vt\r\n                                belt[belmetakey] = metat\r\n                            else:\r\n                                belt[belmetakey] = belmetavalue\r\n                        dbt[bkey] = belt\r\n                    setattr(sc, key, dbt)\r\n            elif key == \"_noCamera\":\r\n                setattr(sc, key, False)\r\n            elif key == \"_pvList\":\r\n                # Photo viewer list shall not be imported\r\n                # It will be filled on demand\r\n                setattr(sc, key, [])\r\n            elif key == \"_streamingClients\":\r\n                # Streaming clients shall not be imported\r\n                # They will be populated during server runtime when clients start/stop streaming\r\n                setattr(sc, key, [])\r\n            elif key == \"_pvCamera\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_pvFrom\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_pvTo\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_vButtons\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    vButtons = []\r\n                    for row in value:\r\n                        vButtonRow = []\r\n                        for btn in row:\r\n                            button = vButton.initFromDict(btn)\r\n                            vButtonRow.append(button)\r\n                        vButtons.append(vButtonRow)\r\n                    setattr(sc, key, vButtons)\r\n            # Initialize last vButton execution result\r\n            elif key == \"_vButtonCommand\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_vButtonArgs\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_vButtonReturncode\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_vButtonStdout\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_vButtonStderr\":\r\n                setattr(sc, key, None)\r\n            elif key == \"_aButtons\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    aButtons = []\r\n                    for row in value:\r\n                        aButtonRow = []\r\n                        for btn in row:\r\n                            button = ActionButton.initFromDict(btn)\r\n                            aButtonRow.append(button)\r\n                        aButtons.append(aButtonRow)\r\n                    setattr(sc, key, aButtons)\r\n            elif key == \"_lButtons\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    lButtons = []\r\n                    for row in value:\r\n                        lButtonRow = []\r\n                        for btn in row:\r\n                            button = LiveButton.initFromDict(btn)\r\n                            lButtonRow.append(button)\r\n                        lButtons.append(lButtonRow)\r\n                    setattr(sc, key, lButtons)\r\n            elif key == \"_gpioDevices\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    gpioDevices = []\r\n                    for device in value:\r\n                        gpioDevice = GPIODevice.initFromDict(device)\r\n                        gpioDevices.append(gpioDevice)\r\n                    setattr(sc, key, gpioDevices)\r\n            elif key == \"_curDevice\":\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    curDevice = GPIODevice.initFromDict(value)\r\n                    setattr(sc, key, curDevice)\r\n            elif key == \"_curDeviceType\":\r\n                # Take the current device type from a fresh declaration rather than from stored data\r\n                # This will allow later modifications of gpioDeviceTypes being immediately effective\r\n                if value is None:\r\n                    setattr(sc, key, value)\r\n                else:\r\n                    type = value[\"type\"]\r\n                    for typ in gpioDeviceTypes:\r\n                        if typ[\"type\"] == type:\r\n                            setattr(sc, key, typ)\r\n                            break\r\n            elif key == \"_unsavedChanges\":\r\n                setattr(sc, key, False)\r\n            elif key == \"_isTriggerTesting\":\r\n                # Never start with trigger testing active\r\n                setattr(sc, key, False)\r\n            elif key == \"_debianVersion\":\r\n                # Do not overwrite the Debian version from stored configuration\r\n                # It has been set when the ServerConfig singleton has been instantiated\r\n                pass\r\n            elif key == \"_kernelVersion\":\r\n                # Do not overwrite the kernel version from stored configuration\r\n                # It has been set when the ServerConfig singleton has been instantiated\r\n                pass\r\n            elif key == \"_serverStartTime\":\r\n                # Do not overwrite the server start time\r\n                # It has been set when the ServerConfig singleton has been instantiated\r\n                pass\r\n            elif key == \"_versionCurrent\":\r\n                setattr(sc, key, \"\")\r\n            elif key == \"_versionLatest\":\r\n                setattr(sc, key, \"\")\r\n            elif key == \"_updateDone\":\r\n                setattr(sc, key, False)\r\n            else:\r\n                setattr(sc, key, value)\r\n        # Reset process status variables\r\n        sc.isLiveStream = False\r\n        sc.isLiveStream2 = False\r\n        sc.isStereoCamActive = False\r\n        sc.isAudioRecording = False\r\n        sc.isPhotoSeriesRecording = False\r\n        sc.isTriggerRecording = False\r\n        sc.isVideoRecording = False\r\n        sc.isEventhandling = False\r\n        sc.isStereoCamActive = False\r\n        sc.isStereoCamRecording = False\r\n        sc.changeLog = []\r\n\r\n        # Set the sc.curDevice attribute to the corresponding object from sc.gpioDevices\r\n        # After import fom the JSON file sc.curDevice is an own object and not the one\r\n        # from the sc.gpioDevices list.\r\n        for device in sc.gpioDevices:\r\n            if device.id == sc.curDeviceId:\r\n                sc.curDevice = device\r\n                break\r\n\r\n        return sc\r\n    \r\n    def sliderPosToCtrlVal(self, min:float, max:float, default:float, pos:float) -> float:\r\n        \"\"\"Convert slider position (-1 ... 1) to control value (min ... max; default)\r\n\r\n           Function: \r\n              pos <  0: value = default + (default - min) * pos^3\r\n              pos >= 0: value = default + (max - default) * pos^3\r\n\r\n              -1 -> min\r\n               0 -> default\r\n               1 -> max\r\n        \"\"\"\r\n        if pos < 0:\r\n            val = default + (default - min) * (pos ** 3)\r\n        else:\r\n            val = default + (max - default) * (pos ** 3)\r\n        val = round(val, 3)\r\n        return val\r\n\r\n    def ctrlValToSliderPos(self, min:float, max:float, default:float, val:float) -> float:\r\n        \"\"\"Convert control value (min ... max; default) to slider position (-1 ... 1)\r\n\r\n           Function: \r\n              val <  default: pos = - ((default - val) / (default - min))^(1/3)\r\n              val >= default: pos =   ((val - default) / (max - default))^(1/3)\r\n\r\n              min -> -1\r\n              default -> 0\r\n              max -> 1\r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.ctrlValToSliderPos - min: %f, max: %f, default: %f, val: %f\", min, max, default, val)\r\n        if default <= min:\r\n            pos =   ((val - default) / (max - default)) ** (1/3)\r\n        elif default >= max:\r\n            pos = - ((default - val) / (default - min)) ** (1/3)\r\n        else:\r\n            if val < default:\r\n                pos = - ((default - val) / (default - min)) ** (1/3)\r\n            else:\r\n                pos =   ((val - default) / (max - default)) ** (1/3)\r\n        pos = round(pos, 3)\r\n        logger.debug(\"CameraCfg.ctrlValToSliderPos - pos: %f\", pos)\r\n        return pos\r\n\r\nclass Secrets():\r\n    \"\"\" Class for secrets which are never persisted\r\n    \"\"\"\r\n    def __init__(self) -> None:\r\n        self._notifyUser = \"\"\r\n        self._notifyPwd = \"\"\r\n        self._jwtSecretKey = \"\"\r\n\r\n    @property\r\n    def notifyUser(self) -> str:\r\n        return self._notifyUser\r\n\r\n    @notifyUser.setter\r\n    def notifyUser(self, value: str):\r\n        self._notifyUser = value\r\n\r\n    @property\r\n    def notifyPwd(self) -> str:\r\n        return self._notifyPwd\r\n\r\n    @notifyPwd.setter\r\n    def notifyPwd(self, value: str):\r\n        self._notifyPwd = value\r\n\r\n    @property\r\n    def jwtSecretKey(self) -> str:\r\n        return self._jwtSecretKey\r\n\r\n    @jwtSecretKey.setter\r\n    def jwtSecretKey(self, value: str):\r\n        self._jwtSecretKey = value\r\n\r\nclass CameraCfg():\r\n    _instance = None\r\n    def __new__(cls):\r\n        if cls._instance is None:\r\n            cls._instance = super(CameraCfg, cls).__new__(cls)\r\n            cls._cameras = []\r\n            cls._sensorModes = []\r\n            cls._rawFormats = []\r\n            cls._tuningConfig = TuningConfig()\r\n            cls._aiConfig = AiConfig()\r\n            cls._controls = CameraControls()\r\n            cls._controlsBackup: CameraControls = None\r\n            cls._cameraProperties = CameraProperties()\r\n            cls._liveViewConfig = CameraConfig()\r\n            cls._liveViewConfig.id = \"LIVE\"\r\n            cls._liveViewConfig.use_case = \"Live view\"\r\n            cls._liveViewConfig.stream = \"lores\"\r\n            cls._liveViewConfig.buffer_count = 6\r\n            cls._liveViewConfig.encode = \"main\"\r\n            cls._liveViewConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n            cls._photoConfig = CameraConfig()\r\n            cls._photoConfig.id = \"FOTO\"\r\n            cls._photoConfig.use_case = \"Photo\"\r\n            cls._photoConfig.buffer_count = 1\r\n            cls._photoConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n            cls._rawConfig = CameraConfig()\r\n            cls._rawConfig.id = \"PRAW\"\r\n            cls._rawConfig.use_case = \"Raw Photo\"\r\n            cls._rawConfig.buffer_count = 1\r\n            cls._rawConfig.stream = \"raw\"\r\n            cls._rawConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n            cls._videoConfig = CameraConfig()\r\n            cls._videoConfig.buffer_count = 6\r\n            cls._videoConfig.id = \"VIDO\"\r\n            cls._videoConfig.use_case = \"Video\"\r\n            cls._videoConfig.buffer_count = 6\r\n            cls._videoConfig.encode = \"main\"\r\n            cls._videoConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n            cls._cameraConfigs = []\r\n            cls._triggerConfig = TriggerConfig()\r\n            cls._serverConfig = ServerConfig()\r\n            # For Raspi models < 5 the lowres format must be YUV\r\n            # See Picamera2 manual ch. 4.2, p. 16\r\n            if cls._serverConfig.raspiModelLower5:\r\n                cls._liveViewConfig.format = \"YUV420\"\r\n            if cls._serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\") \\\r\n            or cls._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\") \\\r\n            or cls._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\") \\\r\n            or cls._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\") \\\r\n            or cls._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\"):\r\n                # For Pi Zero and 4 reduce buffer_count defaults for live view and video\r\n                cls._liveViewConfig.buffer_count = 2\r\n                cls._videoConfig.buffer_count = 2\r\n            cls._streamingCfg = {}\r\n            cls._streamingCfgInvalid = False\r\n            cls._stereoCfg = StereoConfig()\r\n            cls._secrets = Secrets()\r\n        return cls._instance\r\n\r\n    @property\r\n    def cameras(self) -> list:\r\n        return self._cameras\r\n\r\n    @cameras.setter\r\n    def cameras(self, value: list):\r\n        self._cameras = value\r\n\r\n    @property\r\n    def controls(self) -> CameraControls:\r\n        return self._controls\r\n\r\n    @controls.setter\r\n    def controls(self, value: CameraControls):\r\n        self._controls = value\r\n\r\n    @property\r\n    def tuningConfig(self) -> TuningConfig:\r\n        return self._tuningConfig\r\n\r\n    @tuningConfig.setter\r\n    def tuningConfig(self, value: TuningConfig):\r\n        self._tuningConfig = value\r\n\r\n    @property\r\n    def aiConfig(self) -> AiConfig:\r\n        return self._aiConfig\r\n\r\n    @aiConfig.setter\r\n    def aiConfig(self, value: AiConfig):\r\n        self._aiConfig = value\r\n\r\n    @property\r\n    def controlsBackup(self) -> CameraControls:\r\n        return self._controlsBackup\r\n\r\n    @controlsBackup.setter\r\n    def controlsBackup(self, value: CameraControls):\r\n        self._controlsBackup = value\r\n\r\n    @property\r\n    def cameraProperties(self) -> CameraProperties:\r\n        return self._cameraProperties\r\n\r\n    @cameraProperties.setter\r\n    def cameraProperties(self, value: CameraProperties):\r\n        self._cameraProperties = value\r\n\r\n    @property\r\n    def sensorModes(self) -> list:\r\n        return self._sensorModes\r\n\r\n    @sensorModes.setter\r\n    def sensorModes(self, value: list):\r\n        self._sensorModes = value\r\n\r\n    @property\r\n    def rawFormats(self) -> list:\r\n        return self._rawFormats\r\n\r\n    @rawFormats.setter\r\n    def rawFormats(self, value: list):\r\n        self._rawFormats = value\r\n\r\n    @property\r\n    def nrSensorModes(self) -> int:\r\n        return len(self._sensorModes)\r\n\r\n    @property\r\n    def liveViewConfig(self) -> CameraConfig:\r\n        return self._liveViewConfig\r\n\r\n    @liveViewConfig.setter\r\n    def liveViewConfig(self, value: CameraConfig):\r\n        self._liveViewConfig = value\r\n\r\n    @property\r\n    def photoConfig(self) -> CameraConfig:\r\n        return self._photoConfig\r\n\r\n    @photoConfig.setter\r\n    def photoConfig(self, value: CameraConfig):\r\n        self._photoConfig = value\r\n\r\n    @property\r\n    def rawConfig(self) -> CameraConfig:\r\n        return self._rawConfig\r\n\r\n    @rawConfig.setter\r\n    def rawConfig(self, value: CameraConfig):\r\n        self._rawConfig = value\r\n\r\n    @property\r\n    def videoConfig(self) -> CameraConfig:\r\n        return self._videoConfig\r\n\r\n    @videoConfig.setter\r\n    def videoConfig(self, value: CameraConfig):\r\n        self._videoConfig = value\r\n\r\n    @property\r\n    def cameraConfigs(self) -> list:\r\n        return self._cameraConfigs\r\n\r\n    @cameraConfigs.setter\r\n    def cameraConfigs(self, value: list):\r\n        self._cameraConfigs = value\r\n\r\n    @property\r\n    def triggerConfig(self) -> TriggerConfig:\r\n        return self._triggerConfig\r\n\r\n    @triggerConfig.setter\r\n    def triggerConfig(self, value: TriggerConfig):\r\n        self._triggerConfig = value\r\n\r\n    @property\r\n    def serverConfig(self) -> ServerConfig:\r\n        return self._serverConfig\r\n\r\n    @serverConfig.setter\r\n    def serverConfig(self, value: ServerConfig):\r\n        self._serverConfig = value\r\n\r\n    @property\r\n    def streamingCfg(self) -> dict:\r\n        return self._streamingCfg\r\n\r\n    @streamingCfg.setter\r\n    def streamingCfg(self, value: dict):\r\n        self._streamingCfg = value\r\n\r\n    @property\r\n    def streamingCfgInvalid(self) -> dict:\r\n        return self._streamingCfgInvalid\r\n\r\n    @streamingCfgInvalid.setter\r\n    def streamingCfgInvalid(self, value: dict):\r\n        self._streamingCfgInvalid = value\r\n\r\n    @property\r\n    def stereoCfg(self) -> StereoConfig:\r\n        return self._stereoCfg\r\n\r\n    @stereoCfg.setter\r\n    def stereoCfg(self, value: StereoConfig):\r\n        self._stereoCfg = value\r\n\r\n    @property\r\n    def secrets(self) -> Secrets:\r\n        return self._secrets\r\n\r\n    @secrets.setter\r\n    def secrets(self, value: Secrets):\r\n        self._secrets = value\r\n\r\n    def setSupportedCameras(self):\r\n        \"\"\" Set up the list of supported cameras\r\n        \"\"\"\r\n        self.serverConfig.usbCamAvailable = False\r\n        self.serverConfig.aiCamAvailable = False\r\n        supCams = []\r\n        for cam in self.cameras:\r\n            if cam.isUsb == False:\r\n                supCams.append(cam)\r\n                if cam.model == \"imx500\":\r\n                    self.serverConfig.aiCamAvailable = True\r\n            else:\r\n                if cam.usbDev != \"UNKNOWN\":\r\n                    self.serverConfig.usbCamAvailable = True\r\n                    if self.serverConfig.useUsbCameras == True:\r\n                        supCams.append(cam)\r\n        if len(self.cameras) == 0:\r\n            self.serverConfig.noCamera = True\r\n        else:\r\n            if len(supCams) == 0:\r\n                self.serverConfig.noCamera = True\r\n            else:\r\n                self.serverConfig.noCamera = False\r\n        if self.serverConfig.noCamera == True:\r\n            self.triggerConfig._noCamera = True\r\n        else:\r\n            self.triggerConfig._noCamera = False\r\n        self.serverConfig.supportedCameras = supCams\r\n\r\n    def setPiCameras(self):\r\n        \"\"\" Set up the list of Raspberry Pi cameras\r\n        \"\"\"\r\n        piCams = []\r\n        for cam in self.cameras:\r\n            if cam.isUsb == False:\r\n                piCams.append(cam)\r\n        self.serverConfig.piCameras = piCams\r\n\r\n    def resetActiveCameraSettings(self):\r\n        \"\"\" Reset configuration and controls for the active camera\r\n        \"\"\"\r\n        # self._tuningConfig = TuningConfig()\r\n        # self._aiConfig = AiConfig()\r\n        self._controls = CameraControls()\r\n        self._controlsBackup: CameraControls = None\r\n        self._cameraProperties = CameraProperties()\r\n        self._liveViewConfig = CameraConfig()\r\n        self._liveViewConfig.id = \"LIVE\"\r\n        self._liveViewConfig.use_case = \"Live view\"\r\n        self._liveViewConfig.stream = \"lores\"\r\n        self._liveViewConfig.buffer_count = 6\r\n        self._liveViewConfig.encode = \"main\"\r\n        self._liveViewConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n        self._photoConfig = CameraConfig()\r\n        self._photoConfig.id = \"FOTO\"\r\n        self._photoConfig.use_case = \"Photo\"\r\n        self._photoConfig.buffer_count = 1\r\n        self._photoConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n        self._rawConfig = CameraConfig()\r\n        self._rawConfig.id = \"PRAW\"\r\n        self._rawConfig.use_case = \"Raw Photo\"\r\n        self._rawConfig.buffer_count = 1\r\n        self._rawConfig.stream = \"raw\"\r\n        self._rawConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n        self._videoConfig = CameraConfig()\r\n        self._videoConfig.buffer_count = 6\r\n        self._videoConfig.id = \"VIDO\"\r\n        self._videoConfig.use_case = \"Video\"\r\n        self._videoConfig.buffer_count = 6\r\n        self._videoConfig.encode = \"main\"\r\n        self._videoConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n        # For Raspi models < 5 the lowres format must be YUV\r\n        # See Picamera2 manual ch. 4.2, p. 16\r\n        if self._serverConfig.raspiModelLower5:\r\n            self._liveViewConfig.format = \"YUV420\"\r\n        if self._serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\") \\\r\n        or self._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\") \\\r\n        or self._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\") \\\r\n        or self._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\") \\\r\n        or self._serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\"):\r\n            # For Pi Zero and 4 reduce buffer_count defaults for live view and video\r\n            self._liveViewConfig.buffer_count = 2\r\n            self._videoConfig.buffer_count = 2\r\n\r\n    def _persistCl(self, cl, fn: str, cfgPath: str):\r\n        \"\"\" Store class dictionary for class cl in the config file fn\r\n        \"\"\"\r\n        fp = cfgPath + \"/\" + fn\r\n        Path(fp).touch()\r\n        f = open(fp, \"w\")\r\n        cj = self._toJson(cl)\r\n        f.write(str(cj))\r\n        f.close()\r\n\r\n    def persist(self, cfgPath: str):\r\n        \"\"\" Store class dictionary in the config file\r\n        \"\"\"\r\n        if cfgPath:\r\n            if not os.path.exists(cfgPath):\r\n                os.makedirs(cfgPath, exist_ok=True)\r\n            self._persistCl(self.cameras, \"cameras.json\", cfgPath)\r\n            self._persistCl(self.tuningConfig, \"tuningConfig.json\", cfgPath)\r\n            self._persistCl(self.aiConfig, \"aiConfig.json\", cfgPath)\r\n            self._persistCl(self.sensorModes, \"sensorModes.json\", cfgPath)\r\n            self._persistCl(self.rawFormats, \"rawFormats.json\", cfgPath)\r\n            self._persistCl(self.cameraProperties, \"cameraProperties.json\", cfgPath)\r\n            self._persistCl(self.cameraConfigs, \"cameraConfigs.json\", cfgPath)\r\n            self._persistCl(self.liveViewConfig, \"liveViewConfig.json\", cfgPath)\r\n            self._persistCl(self.photoConfig, \"photoConfig.json\", cfgPath)\r\n            self._persistCl(self.rawConfig, \"rawConfig.json\", cfgPath)\r\n            self._persistCl(self.videoConfig, \"videoConfig.json\", cfgPath)\r\n            self._persistCl(self.controls, \"controls.json\", cfgPath)\r\n            self._persistCl(self.serverConfig, \"serverConfig.json\", cfgPath)\r\n            self._persistCl(self.triggerConfig, \"triggerConfig.json\", cfgPath)\r\n            self._persistCl(self.streamingCfg, \"streamingCfg.json\", cfgPath)\r\n            self._persistCl(self.stereoCfg, \"stereoCfg.json\", cfgPath)\r\n\r\n    def _toJson(self, cl):\r\n        return json.dumps(cl, default=lambda o: getattr(o, '__dict__', str(o)), indent=4)\r\n\r\n    def _loadConfigCl(self, cl, fn: str, cfgPath: str):\r\n        \"\"\" Load configuration from files, except camera-specific configs\r\n        \"\"\"\r\n        fp = cfgPath + \"/\" + fn\r\n        obj = cl()\r\n        if os.path.exists(fp):\r\n            with open(fp) as f:\r\n                try:\r\n                    cldict = json.load(f)\r\n                    obj = cl.initFromDict(cldict)\r\n                except Exception as e:\r\n                    logger.error(\"Error loading from %s: %s\", fp, e)\r\n                    obj = cl()\r\n        return obj\r\n\r\n    def _initStreamingConfigFromDisc(self, fn: str, cfgPath: str) -> dict:\r\n        \"\"\" Load streaming configuration\r\n        \"\"\"\r\n        sc = {}\r\n        scdict = {}\r\n        fp = cfgPath + \"/\" + fn\r\n        if os.path.exists(fp):\r\n            with open(fp) as f:\r\n                try:\r\n                    scdict = json.load(f)\r\n                except Exception as e:\r\n                    logger.error(\"Error loading StreamingConfig from %s: %s\", fp, e)\r\n                    scdict = {}\r\n        if len(scdict) > 0:\r\n            for camKey, camValue in scdict.items():\r\n                scfg = {}\r\n                for key, value in camValue.items():\r\n                    if key == \"liveconfig\":\r\n                        scfg[\"liveconfig\"] = CameraConfig.initFromDict(value)\r\n                    elif key == \"photoconfig\":\r\n                        scfg[\"photoconfig\"] = CameraConfig.initFromDict(value)\r\n                    elif key == \"rawconfig\":\r\n                        scfg[\"rawconfig\"] = CameraConfig.initFromDict(value)\r\n                    elif key == \"videoconfig\":\r\n                        scfg[\"videoconfig\"] = CameraConfig.initFromDict(value)\r\n                    elif key == \"controls\":\r\n                        scfg[\"controls\"] = CameraControls.initFromDict(value)\r\n                    elif key == \"tuningconfig\":\r\n                        scfg[\"tuningconfig\"] = TuningConfig.initFromDict(value)\r\n                    elif key == \"aiconfig\":\r\n                        scfg[\"aiconfig\"] = AiConfig.initFromDict(value)\r\n                    elif key == \"cameraproperties\":\r\n                        scfg[\"cameraproperties\"] = CameraProperties.initFromDict(value)\r\n                    else:\r\n                        scfg[key] = value\r\n                sc[camKey] = scfg\r\n        return sc\r\n\r\n    def _initGpioDevicesFromDisc(self, fn: str, cfgPath: str) -> list:\r\n        \"\"\" Load GPIO devices\r\n        \"\"\"\r\n        devs = []\r\n        fdevs = {}\r\n        fp = cfgPath + \"/\" + fn\r\n        if os.path.exists(fp):\r\n            with open(fp) as f:\r\n                try:\r\n                    fdevs = json.load(f)\r\n                except Exception as e:\r\n                    logger.error(\"Error loading GPIO devices from %s: %s\", fp, e)\r\n                    fdevs = []\r\n        if len(fdevs) > 0:\r\n            for dev in fdevs.items():\r\n                devo = GPIODevice.initFromDict(dev)\r\n                devs.append(devo)\r\n        return devs\r\n\r\n    def loadConfig(self, cfgPath):\r\n        \"\"\" Load configuration from files, except camera-specific configs\r\n        \"\"\"\r\n        if cfgPath:\r\n            if os.path.exists(cfgPath):\r\n                self.tuningConfig = self._loadConfigCl(TuningConfig, \"tuningConfig.json\", cfgPath)\r\n                self.aiConfig = self._loadConfigCl(AiConfig, \"aiConfig.json\", cfgPath)\r\n                self.serverConfig = self._loadConfigCl(ServerConfig, \"serverConfig.json\", cfgPath)\r\n                self.liveViewConfig = self._loadConfigCl(CameraConfig, \"liveViewConfig.json\", cfgPath)\r\n                self.photoConfig = self._loadConfigCl(CameraConfig, \"photoConfig.json\", cfgPath)\r\n                self.rawConfig = self._loadConfigCl(CameraConfig, \"rawConfig.json\", cfgPath)\r\n                self.videoConfig = self._loadConfigCl(CameraConfig, \"videoConfig.json\", cfgPath)\r\n                self.controls = self._loadConfigCl(CameraControls, \"controls.json\", cfgPath)\r\n                self.triggerConfig = self._loadConfigCl(TriggerConfig, \"triggerConfig.json\", cfgPath)\r\n                self.streamingCfg = self._initStreamingConfigFromDisc(\"streamingCfg.json\", cfgPath)\r\n                self.stereoCfg = self._loadConfigCl(StereoConfig, \"stereoCfg.json\", cfgPath)\r\n                self.gpioDevices = self._initGpioDevicesFromDisc(\"gpioDevices.json\", cfgPath)\r\n                sc = self.secrets\r\n                tc = self.triggerConfig\r\n                (usr, pwd, err) = tc.checkNotificationRecipient()\r\n                if tc.notifyConOK == True:\r\n                    sc.notifyUser = usr\r\n                    sc.notifyPwd = pwd\r\n                srv = self.serverConfig\r\n                if srv.useAPI == True:\r\n                    (secretKey, err, msg) = srv.checkJwtSettings()\r\n                    if err is None:\r\n                        sc.jwtSecretKey = secretKey\r\n\r\n    @staticmethod\r\n    def _lineGen(s):\r\n        \"\"\"Generator to yield lines of a text\"\"\"\r\n        while len(s) > 0:\r\n            p = s.find(\"\\n\")\r\n            if p >= 0:\r\n                if p == 0:\r\n                    line = \"\"\r\n                else:\r\n                    line = s[:p]\r\n                s = s[p + 1 :]\r\n            else:\r\n                line = s\r\n                s = \"\"\r\n            yield line\r\n\r\n    def setUsbCameraProperties(self) -> bool:\r\n        \"\"\"Set properties of the active USB camera from v4l2-ctl output\r\n\r\n        Returns:\r\n            bool: True if properties have been found, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.setUsbCameraProperties\")\r\n        usbDev = self.serverConfig.activeCameraUsbDev\r\n\r\n        cfgProps = CameraProperties()\r\n        #cfgProps.hasFocus = False  # Assume no focus control for USB cameras\r\n        cfgProps.hasFlicker = False  # Assume no flicker control for USB cameras\r\n        cfgProps.hasHdr = False  # Assume no HDR control for USB cameras\r\n        cfgProps.unitCellSize = None\r\n        cfgProps.location = None\r\n        cfgProps.rotation = None\r\n        cfgProps.pixelArraySize = None\r\n        cfgProps.pixelArrayActiveAreas = None\r\n        cfgProps.colorFilterArrangement = None\r\n        cfgProps.scalerCropMaximum = None\r\n        cfgProps.systemDevices = None\r\n        cfgProps.colorSpace = None\r\n\r\n        found = False\r\n        try:\r\n            result = subprocess.run(\r\n                [\"v4l2-ctl\", f\"--device={usbDev}\", \"--all\"],\r\n                capture_output=True,\r\n                text=True,\r\n            ).stdout\r\n            for line in self._lineGen(result):\r\n                # Find model\r\n                fmtMatch = re.match(r\"Model\\s+:\\s+(.+)\", line.strip())                \r\n                if fmtMatch:\r\n                    found = True\r\n                    cfgProps.model = fmtMatch.group(1)\r\n                # Find color space\r\n                fmtMatch = re.match(r\"Colorspace\\s+:\\s+(.+)\", line.strip())\r\n                if fmtMatch:\r\n                    cfgProps.colorSpace = fmtMatch.group(1)\r\n\r\n        except CalledProcessError as e:\r\n            logger.error(\"CameraInfo.setUsbCameraProperties - CalledProcessError: %s\", e)\r\n            # In case v4l2-ctl cannot be run, ignore the exception\r\n            pass\r\n        except Exception as e:\r\n            logger.error(\"CameraInfo.setUsbCameraProperties - Exception: %s\", e)\r\n            pass\r\n\r\n        if found == False:\r\n            logger.debug(\"CameraCfg.setUsbCameraProperties - No USB camera found\")\r\n            return False\r\n\r\n        maxWidth, maxHeight = self.getUsbPixelArraySize()\r\n        cfgProps.pixelArraySize = (maxWidth, maxHeight)\r\n        activeAreas = []\r\n        activeArea = (0, 0, maxWidth, maxHeight)\r\n        activeAreas.append(activeArea)\r\n        cfgProps.pixelArrayActiveAreas = activeAreas\r\n        cfgProps.scalerCropMaximum = (0, 0, maxWidth, maxHeight)\r\n\r\n        self.cameraProperties = cfgProps\r\n        logger.debug(\"CameraCfg.setUsbCameraProperties - USB camera properties found\")\r\n        return True\r\n\r\n    def getUsbPixelArraySize(self) -> tuple:\r\n        \"\"\"Get the pixel array size of the active USB camera from v4l2-ctl output\r\n\r\n        Returns:\r\n            tuple: (maxWidth, maxHeight)\r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.getUsbPixelArraySize\")\r\n        maxWidth = 0\r\n        maxHeight = 0\r\n\r\n        usbDev = self.serverConfig.activeCameraUsbDev\r\n\r\n        try:\r\n            result = subprocess.run(\r\n                [\"v4l2-ctl\", f\"--device={usbDev}\", \"--list-formats-ext\"],\r\n                capture_output=True,\r\n                text=True,\r\n            ).stdout\r\n            for line in self._lineGen(result):\r\n                # Evaluate size block header\r\n                fmtMatch = re.match(r\"Size: Discrete (\\d+)x(\\d+)\", line.strip())\r\n                if fmtMatch:\r\n                    width = int(fmtMatch.group(1))\r\n                    height = int(fmtMatch.group(2))\r\n                    if width > maxWidth:\r\n                        maxWidth = width\r\n                    if height > maxHeight:\r\n                        maxHeight = height\r\n\r\n        except CalledProcessError as e:\r\n            logger.error(\"CameraInfo.getUsbPixelArraySize - CalledProcessError: %s\", e)\r\n            # In case v4l2-ctl cannot be run, ignore the exception\r\n            pass\r\n        except Exception as e:\r\n            logger.error(\"CameraInfo.getUsbPixelArraySize - Exception: %s\", e)\r\n            pass\r\n        return (maxWidth, maxHeight)\r\n\r\n    def setUsbSensorModes(self) -> bool:\r\n        \"\"\"Set the sensor modes of the active USB camera from v4l2-ctl output\r\n\r\n        Returns:\r\n            bool: True if sensor modes have been found, False otherwise\r\n\r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.setUsbSensorModes\")\r\n        cfgSensorModes = []\r\n        cfgRawFormats = []\r\n\r\n        usbDev = self.serverConfig.activeCameraUsbDev\r\n\r\n        found = False\r\n        try:\r\n            result = subprocess.run(\r\n                [\"v4l2-ctl\", f\"--device={usbDev}\", \"--list-formats-ext\"],\r\n                capture_output=True,\r\n                text=True,\r\n            ).stdout\r\n            for line in self._lineGen(result):\r\n                # Evaluate format block header\r\n                fmtMatch = re.match(r\"\\[(\\d+)\\]: '(\\w+)' \\((.+)\\)\", line.strip())\r\n                if fmtMatch:\r\n                    fmtId = int(fmtMatch.group(1))\r\n                    fmtCode = fmtMatch.group(2)\r\n                    fmtDesc = fmtMatch.group(3)\r\n                    if not fmtId in cfgRawFormats:\r\n                        cfgRawFormats.append(fmtCode)\r\n\r\n                # Evaluate size block header\r\n                fmtMatch = re.match(r\"Size: Discrete (\\d+)x(\\d+)\", line.strip())\r\n                if fmtMatch:\r\n                    width = int(fmtMatch.group(1))\r\n                    height = int(fmtMatch.group(2))\r\n                    cfgSensorMode = SensorMode()\r\n                    cfgSensorMode.id = str(len(cfgSensorModes))\r\n                    cfgSensorMode.format = fmtCode\r\n                    if fmtCode == \"YUYV\":\r\n                        cfgSensorMode.bit_depth = 16\r\n                    elif fmtCode == \"NV12\":\r\n                        cfgSensorMode.bit_depth = 12\r\n                    elif fmtCode == \"MJPG\":\r\n                        cfgSensorMode.bit_depth = 8\r\n                    else:\r\n                        cfgSensorMode.bit_depth = None\r\n                    cfgSensorMode.size = (width, height)\r\n                    cfgSensorModes.append(cfgSensorMode)\r\n                    found = True\r\n\r\n                # Evaluate Interval line\r\n                fmtMatch = re.match(\r\n                    r\"Interval: Discrete (\\d).(\\d+)s \\((\\d+).(\\d+) fps\\)\", line.strip()\r\n                )\r\n                if fmtMatch:\r\n                    fps = int(fmtMatch.group(3))\r\n                    if len(cfgSensorModes) > 0:\r\n                        if cfgSensorModes[-1].fps is None:\r\n                            cfgSensorModes[-1].fps = fps\r\n\r\n        except CalledProcessError as e:\r\n            logger.error(\"CameraInfo.setUsbSensorModes - CalledProcessError: %s\", e)\r\n            pass\r\n        except Exception as e:\r\n            logger.error(\"CameraInfo.setUsbSensorModes - Exception: %s\", e)\r\n            pass\r\n\r\n        if found == False:\r\n            logger.debug(\"CameraCfg.setUsbSensorModes - No sensor modes found\")\r\n            return False\r\n\r\n        self.sensorModes = cfgSensorModes\r\n        self.rawFormats = cfgRawFormats\r\n        logger.debug(\"CameraCfg.setUsbSensorModes - sensor modes found\")\r\n        return True\r\n\r\n    def setUsbCamControls(self):\r\n        \"\"\"Set the controls of the active USB camera from v4l2-ctl output\r\n        \"\"\"\r\n        logger.debug(\"CameraCfg.setUsbCamControls\")\r\n\r\n        try:\r\n            usbDev = self.serverConfig.activeCameraUsbDev\r\n            # Run v4l2-ctl\r\n            result = subprocess.run(\r\n                [\"v4l2-ctl\", \"-d\", usbDev, \"--list-ctrls\"],\r\n                capture_output=True, text=True\r\n            )\r\n\r\n            lines = result.stdout.strip().split(\"\\n\")\r\n            controls = {}\r\n\r\n            # Regex for the format:\r\n            # name HEX (type) : min=... max=... step=... default=...\r\n            regex = re.compile(\r\n                r\"^(?P<name>[\\w\\-]+)\\s+\"\r\n                r\"(?P<hex>0x[0-9a-fA-F]+)\\s+\"\r\n                r\"\\((?P<type>\\w+)\\)\\s*:\\s*\"\r\n                r\"(?:min=(?P<min>-?\\d+))?\\s*\"\r\n                r\"(?:max=(?P<max>-?\\d+))?\\s*\"\r\n                r\"(?:step=(?P<step>-?\\d+))?\\s*\"\r\n                r\"(?:default=(?P<default>-?\\d+))?\",\r\n                re.IGNORECASE\r\n            )\r\n\r\n            for line in lines:\r\n                line = line.strip()\r\n                match = regex.search(line)\r\n                if match:\r\n                    info = match.groupdict()\r\n\r\n                    # Convert numeric fields\r\n                    for key in (\"min\", \"max\", \"step\", \"default\"):\r\n                        if info[key] is not None:\r\n                            info[key] = int(info[key])\r\n\r\n                    name = info[\"name\"]\r\n                    info.pop(\"name\")\r\n                    info.pop(\"hex\")\r\n                    controls[name] = info\r\n\r\n            logger.debug(\"CameraCfg.setUsbCamControls - %s USB camera controls found\", len(controls))\r\n\r\n        except CalledProcessError as e:\r\n            logger.error(\"CameraCfg.setUsbCamControls - CalledProcessError: %s\", e)\r\n            pass\r\n        except Exception as e:\r\n            logger.error(\"CameraCfg.setUsbCamControls - Exception: %s\", e)\r\n            pass\r\n\r\n        # Map USB camera controls to standard controls\r\n        ctrl = self.controls\r\n        usbCC = ctrl.usbCamControls\r\n        if \"focus_automatic_continuous\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"focus_automatic_continuous\"\r\n            usbCtrl[\"type\"] = controls[\"focus_automatic_continuous\"][\"type\"]\r\n            mapping = {}\r\n            mapping[\"0\"] = 0 # Manual focus\r\n            mapping[\"1\"] = 1 # Auto focus\r\n            mapping[\"2\"] = 1 # Continuous auto focus\r\n            usbCtrl[\"mapping\"] = mapping\r\n            usbCC[\"AfMode\"] = usbCtrl\r\n            ctrl.afMode = 2  # Default to auto focus\r\n            logger.debug(\"CameraCfg.setUsbCamControls - Camera has focus control include_afMode = %s\", ctrl.include_afMode)\r\n            self.cameraProperties.hasFocus = True\r\n        if \"focus_absolute\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"focus_absolute\"\r\n            usbCtrl[\"type\"] = controls[\"focus_absolute\"][\"type\"]\r\n            usbCtrl[\"min\"] = controls[\"focus_absolute\"][\"min\"]\r\n            usbCtrl[\"max\"] = controls[\"focus_absolute\"][\"max\"]\r\n            usbCtrl[\"step\"] = controls[\"focus_absolute\"][\"step\"]\r\n            usbCtrl[\"default\"] = controls[\"focus_absolute\"][\"default\"]\r\n            if usbCtrl[\"default\"] < usbCtrl[\"min\"] or usbCtrl[\"default\"] > usbCtrl[\"max\"]:\r\n                usbCtrl[\"default\"] = int((usbCtrl[\"min\"] + usbCtrl[\"max\"]) / 2)\r\n            usbCC[\"LensPosition\"] = usbCtrl\r\n            if usbCtrl[\"default\"] != 0:\r\n                ctrl.lensPosition = 1.0 / usbCtrl[\"default\"]\r\n            else:\r\n                ctrl.lensPosition = 9999.0  # Set to max\r\n        if \"white_balance_automatic\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"white_balance_automatic\"\r\n            usbCtrl[\"type\"] = controls[\"white_balance_automatic\"][\"type\"]\r\n            mapping = {}\r\n            mapping[\"0\"] = 0  # Manual WB\r\n            mapping[\"1\"] = 1  # Auto WB\r\n            usbCtrl[\"mapping\"] = mapping\r\n            usbCC[\"AwbEnable\"] = usbCtrl\r\n            ctrl.awbEnable = 1  # Default to auto WB\r\n        if \"white_balance_temperature\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"white_balance_temperature\"\r\n            usbCtrl[\"type\"] = controls[\"white_balance_temperature\"][\"type\"]\r\n            mapping = {}\r\n            mapping[\"0\"] = 2000  # Tungsten\r\n            mapping[\"2\"] = 3000  # Tungsern\r\n            mapping[\"3\"] = 4000  # Fluorescent\r\n            mapping[\"4\"] = 3200  # Indoor\r\n            mapping[\"5\"] = 5300  # Daylight\r\n            mapping[\"6\"] = 6200  # Cloudy\r\n            mapping[\"7\"] = 6500  # Cloudy\r\n            usbCtrl[\"mapping\"] = mapping\r\n            usbCC[\"AwbMode\"] = usbCtrl\r\n            ctrl.awbMode = 5  # Default to daylight\r\n        if \"brightness\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"brightness\"\r\n            usbCtrl[\"type\"] = controls[\"brightness\"][\"type\"]\r\n            usbCtrl[\"min\"] = controls[\"brightness\"][\"min\"]\r\n            usbCtrl[\"max\"] = controls[\"brightness\"][\"max\"]\r\n            usbCtrl[\"step\"] = controls[\"brightness\"][\"step\"]\r\n            usbCtrl[\"default\"] = controls[\"brightness\"][\"default\"]\r\n            if usbCtrl[\"default\"] < usbCtrl[\"min\"] or usbCtrl[\"default\"] > usbCtrl[\"max\"]:\r\n                usbCtrl[\"default\"] = int((usbCtrl[\"min\"] + usbCtrl[\"max\"]) / 2)\r\n            usbCC[\"Brightness\"] = usbCtrl\r\n            ctrl.brightness = usbCtrl[\"default\"]\r\n        if \"contrast\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"contrast\"\r\n            usbCtrl[\"type\"] = controls[\"contrast\"][\"type\"]\r\n            usbCtrl[\"min\"] = controls[\"contrast\"][\"min\"]\r\n            usbCtrl[\"max\"] = controls[\"contrast\"][\"max\"]\r\n            usbCtrl[\"step\"] = controls[\"contrast\"][\"step\"]\r\n            usbCtrl[\"default\"] = controls[\"contrast\"][\"default\"]\r\n            if usbCtrl[\"default\"] < usbCtrl[\"min\"] or usbCtrl[\"default\"] > usbCtrl[\"max\"]:\r\n                usbCtrl[\"default\"] = int((usbCtrl[\"min\"] + usbCtrl[\"max\"]) / 2)\r\n            usbCC[\"Contrast\"] = usbCtrl\r\n            ctrl.contrast = usbCtrl[\"default\"]\r\n        if \"saturation\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"saturation\"\r\n            usbCtrl[\"type\"] = controls[\"saturation\"][\"type\"]\r\n            usbCtrl[\"min\"] = controls[\"saturation\"][\"min\"]\r\n            usbCtrl[\"max\"] = controls[\"saturation\"][\"max\"]\r\n            usbCtrl[\"step\"] = controls[\"saturation\"][\"step\"]\r\n            usbCtrl[\"default\"] = controls[\"saturation\"][\"default\"]\r\n            if usbCtrl[\"default\"] < usbCtrl[\"min\"] or usbCtrl[\"default\"] > usbCtrl[\"max\"]:\r\n                usbCtrl[\"default\"] = int((usbCtrl[\"min\"] + usbCtrl[\"max\"]) / 2)\r\n            usbCC[\"Saturation\"] = usbCtrl\r\n            ctrl.saturation = usbCtrl[\"default\"]\r\n        if \"sharpness\" in controls:\r\n            usbCtrl = {}\r\n            usbCtrl[\"ctrlName\"] = \"sharpness\"\r\n            usbCtrl[\"type\"] = controls[\"sharpness\"][\"type\"]\r\n            usbCtrl[\"min\"] = controls[\"sharpness\"][\"min\"]\r\n            usbCtrl[\"max\"] = controls[\"sharpness\"][\"max\"]\r\n            usbCtrl[\"step\"] = controls[\"sharpness\"][\"step\"]\r\n            usbCtrl[\"default\"] = controls[\"sharpness\"][\"default\"]\r\n            if usbCtrl[\"default\"] < usbCtrl[\"min\"] or usbCtrl[\"default\"] > usbCtrl[\"max\"]:\r\n                usbCtrl[\"default\"] = int((usbCtrl[\"min\"] + usbCtrl[\"max\"]) / 2)\r\n            usbCC[\"Sharpness\"] = usbCtrl\r\n            ctrl.sharpness = usbCtrl[\"default\"]\r\n"
  },
  {
    "path": "raspiCamSrv/camera_pi.py",
    "content": "import io\r\nimport time\r\nimport datetime\r\nimport threading\r\nfrom _thread import get_ident, allocate_lock\r\nfrom raspiCamSrv.camCfg import (\r\n    CameraInfo,\r\n    CameraCfg,\r\n    SensorMode,\r\n    CameraConfig,\r\n    TuningConfig,\r\n    AiConfig,\r\n)\r\nfrom typing import List\r\nfrom raspiCamSrv.photoseriesCfg import Series\r\nfrom picamera2 import Picamera2, CameraConfiguration, StreamConfiguration, Controls\r\nfrom picamera2 import CompletedRequest, MappedArray\r\nfrom libcamera import Transform, Size, ColorSpace, controls\r\nfrom libcamera import Rectangle\r\nfrom picamera2.encoders import JpegEncoder, MJPEGEncoder\r\nfrom picamera2.outputs import FileOutput, FfmpegOutput, CircularOutput\r\nfrom picamera2.encoders import H264Encoder\r\nfrom threading import Condition, Lock\r\nimport copy\r\nimport os\r\nfrom pathlib import Path\r\nimport logging\r\nimport gc\r\nimport math\r\nimport subprocess\r\nfrom subprocess import CalledProcessError\r\nfrom functools import lru_cache\r\nfrom typing import Dict\r\n\r\n\r\n# Try to import SensorConfiguration, which is missing in Bullseye Picamera2 distributions\r\ntry:\r\n    from picamera2.configuration import SensorConfiguration\r\n\r\n    useSensorConfiguration = True\r\nexcept ImportError:\r\n    useSensorConfiguration = False\r\n# Try to import cv2\r\ntry:\r\n    import cv2\r\n    cv2Available = True\r\nexcept ImportError:\r\n    cv2Available = False\r\n\r\n# Try to import numpy\r\ntry:\r\n    import numpy as np\r\n    numpyAvailable = True\r\nexcept ImportError:\r\n    numpyAvailable = False\r\n\r\n# Try to import imx500 modules\r\ntry:\r\n    from picamera2.devices.imx500.postprocess import softmax\r\n    from picamera2.devices.imx500.postprocess import COCODrawer\r\n    from picamera2.devices.imx500.postprocess_highernet import \\\r\n        postprocess_higherhrnet\r\n    from picamera2.devices.imx500 import (NetworkIntrinsics,\r\n                                        postprocess_nanodet_detection)\r\n    imx500Available = True\r\nexcept ImportError:\r\n    imx500Available = False\r\n\r\n\r\nlogger = logging.getLogger(__name__)\r\n\r\nprgLogger = logging.getLogger(\"pc2_prg\")\r\n\r\n\r\nclass CameraStopError(RuntimeError):\r\n    pass\r\n\r\nclass UsbCameraOpenError(RuntimeError):\r\n    \"\"\"Exception raised when a USB camera is unexpectedly not open\r\n    \r\n       The reason why the USB camera is found to be not open after it had been opened\r\n       are currently not yet clear.\r\n    \"\"\"\r\n    # TODO: Clarify under which conditions this exception is raised\r\n    pass\r\n\r\nclass UsbCameraNoFrameReceivedError(RuntimeError):\r\n    \"\"\"Exception raised when a USB camera does not deliver frames for 1 second after being opened\r\n    \r\n       The reason is currently not yet clear.\r\n    \"\"\"\r\n    # TODO: Clarify under which conditions this exception is raised\r\n    pass\r\n\r\nclass Classification:\r\n    def __init__(self, idx: int, score: float):\r\n        \"\"\"Create a Classification object, recording the idx and score.\"\"\"\r\n        self.idx = idx\r\n        self.score = score\r\n\r\n\r\nclass Detection:\r\n    def __init__(self, coords, category, conf, metadata):\r\n        \"\"\"Create a Detection object, recording the bounding box, category and confidence.\"\"\"\r\n        # logger.debug(\"Thread %s: Detection.__init__ - coords: %s category: %s conf: %s\", get_ident(), coords, category, conf)\r\n        self.category = category\r\n        self.conf = conf\r\n        coords = (coords[0][0], coords[1][0], coords[2][0], coords[3][0])\r\n        config = Camera.cam.camera_configuration()\r\n        # logger.debug(\"Thread %s: Detection.__init__ - camera_configuration: %s\", get_ident(), config)\r\n        if \"lores\" in config and config[\"lores\"] is not None:\r\n            # logger.debug(\"Thread %s: Detection.__init__ - converting coords for lores stream\", get_ident())\r\n            self.box = Camera.cam_imx500.convert_inference_coords(coords, metadata, Camera.cam, stream=\"lores\")\r\n        else:\r\n            self.box = None\r\n        if \"main\" in config and config[\"main\"] is not None:\r\n            # logger.debug(\"Thread %s: Detection.__init__ - converting coords for lores stream\", get_ident())\r\n            self.box_main = Camera.cam_imx500.convert_inference_coords(coords, metadata, Camera.cam, stream=\"main\")\r\n        else:\r\n            self.box_main = None\r\n        # logger.debug(\"Thread %s: Detection.__init__ - box: %s box_main: %s\", get_ident(), self.box, self.box_main)\r\n\r\nclass Cam2Detection:\r\n    def __init__(self, coords, category, conf, metadata):\r\n        \"\"\"Create a Detection object, recording the bounding box, category and confidence.\"\"\"\r\n        self.category = category\r\n        self.conf = conf\r\n        coords = (coords[0][0], coords[1][0], coords[2][0], coords[3][0])\r\n        config = Camera.cam2.camera_configuration()\r\n        if \"lores\" in config and config[\"lores\"] is not None:\r\n            self.box = Camera.cam2_imx500.convert_inference_coords(coords, metadata, Camera.cam2, stream=\"lores\")\r\n        else:\r\n            self.box = None\r\n        if \"main\" in config and config[\"main\"] is not None:\r\n            self.box_main = Camera.cam2_imx500.convert_inference_coords(coords, metadata, Camera.cam2, stream=\"main\")\r\n        else:\r\n            self.box_main = None\r\n\r\n\r\nclass StreamingOutput(io.BufferedIOBase):\r\n    def __init__(self):\r\n        # logger.debug(\"Thread %s: StreamingOutput.__init__\", get_ident())\r\n        self.frame = None\r\n        self.lock = Lock()\r\n        self.condition = Condition(self.lock)\r\n\r\n    def write(self, buf):\r\n        # logger.debug(\"Thread %s: StreamingOutput.write\", get_ident())\r\n        with self.condition:\r\n            self.frame = buf\r\n            # logger.debug(\"Thread %s: StreamingOutput.write - got buffer of length %s\", get_ident(), len(buf))\r\n            self.condition.notify_all()\r\n            # logger.debug(\"Thread %s: StreamingOutput.write - notification done\", get_ident())\r\n        # logger.debug(\"Thread %s: StreamingOutput.write - write done\", get_ident())\r\n\r\n\r\nclass CameraController:\r\n    \"\"\"The class controls status change actions for the camera\"\"\"\r\n\r\n    def __init__(self, isUsb: bool = False, usbDev: str = None, forActiveCamera: bool =True):\r\n        logger.debug(\r\n            \"Thread %s: CameraController.__init__ - isUsb: %s, usbDev: %s, forActiveCamera: %s\", get_ident(), isUsb, usbDev, forActiveCamera\r\n        )\r\n        if not useSensorConfiguration:\r\n            logger.info(\r\n                \"Could not import SensorConfiguration from picamera2.configuration. Bypassing sensor configuration\"\r\n            )\r\n        self._activeCfg: CameraConfiguration = None\r\n        self._requestedCfg: CameraConfiguration = CameraConfiguration()\r\n        self._activeEncoders = {}\r\n        self._isUsb = isUsb\r\n        self._usbDev = usbDev\r\n        self._forActiveCamera = forActiveCamera\r\n        logger.debug(\r\n            \"Thread %s: CameraController.__init__ - requestedCfg: %s\",\r\n            get_ident(),\r\n            self._requestedCfg,\r\n        )\r\n\r\n    @property\r\n    def configuration(self) -> CameraConfiguration:\r\n        return self._requestedCfg\r\n\r\n    @property\r\n    def isUsb(self) -> bool:\r\n        return self._isUsb\r\n\r\n    @property\r\n    def usbDev(self) -> str:\r\n        return self._usbDev\r\n\r\n    def requestCameraForConfig(\r\n        self,\r\n        cam: Picamera2,\r\n        camNum,\r\n        cfg: CameraConfig,\r\n        cfgPhoto: CameraConfig = None,\r\n        forLiveStream: bool = False,\r\n        forActiveCamera=True,\r\n        forceExclusive: bool = False,\r\n    ):\r\n        \"\"\"Request camera start for a specific configuration\r\n\r\n        Parameters:\r\n        cam      Camera\r\n        camNum   Camera number\r\n        isUsb    Whether the camera is a USB camera\r\n        cfg      Configuration for which camera is requested\r\n                 If None, request start for the active configuration\r\n        cfgPhoto Photo configuration. To be provided when cfg is a raw photo configuration\r\n        forLiveStream:  The request is for the Live Stream -> don't deactivate Live Stream\r\n        forActiveCamera: Whether the request is for the active camera\r\n        forceExclusive: Whether the request is for an exclusive camera start\r\n\r\n        Return:\r\n        True  if start is exclusive for the requested configuration\r\n        False if the active configuration is used\r\n        imx500 IMX500 device if used, else None\r\n        \"\"\"\r\n        if cfg:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig cfg:        %s\",\r\n                get_ident(),\r\n                cfg.__dict__,\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig cfg:        %s\",\r\n                get_ident(),\r\n                cfg,\r\n            )\r\n        if cfgPhoto:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig - cfgPhoto: %s\",\r\n                get_ident(),\r\n                cfgPhoto.__dict__,\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig - cfgPhoto: %s\",\r\n                get_ident(),\r\n                cfgPhoto,\r\n            )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestCameraForConfig - forLiveStream: %s\",\r\n            get_ident(),\r\n            forLiveStream,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestCameraForConfig - forActiveCamera: %s\",\r\n            get_ident(),\r\n            forActiveCamera,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestCameraForConfig - forceExclusive: %s\",\r\n            get_ident(),\r\n            forceExclusive,\r\n        )\r\n\r\n        exclusive = False\r\n        imx500 = None\r\n\r\n        if cfg:\r\n            self.requestConfig(cfg, cfgPhoto=cfgPhoto)\r\n        if forceExclusive == False:\r\n            cam, started, imx500 = self.requestStart(\r\n                cam, camNum, self.isUsb, self.usbDev, forActiveCamera\r\n            )\r\n        else:\r\n            started = False\r\n        if started:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig - camera started\",\r\n                get_ident(),\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig: Camara stop required\",\r\n                get_ident(),\r\n            )\r\n            if not forLiveStream:\r\n                if forActiveCamera == True:\r\n                    Camera.liveViewDeactivated = True\r\n                else:\r\n                    Camera.liveView2Deactivated = True\r\n                logger.debug(\r\n                    \"Thread %s: CameraController.requestCameraForConfig - Live stream deactivated\",\r\n                    get_ident(),\r\n                )\r\n            if forActiveCamera == True:\r\n                Camera.stopLiveStream()\r\n            else:\r\n                Camera.stopLiveStream2()\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestCameraForConfig: Live stream stopped\",\r\n                get_ident(),\r\n            )\r\n            cam, stopped = self.requestStop(cam)\r\n            if stopped:\r\n                if forActiveCamera == True:\r\n                    cam, started, imx500 = Camera.ctrl.requestStart(\r\n                        cam, camNum, self.isUsb, self.usbDev, forActiveCamera\r\n                    )\r\n                else:\r\n                    cam, started, imx500 = Camera.ctrl2.requestStart(\r\n                        cam, camNum, self.isUsb, self.usbDev, forActiveCamera\r\n                    )\r\n                if started:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestCameraForConfig - camera started\",\r\n                        get_ident(),\r\n                    )\r\n                else:\r\n                    logger.error(\r\n                        \"Thread %s: CameraController.requestCameraForConfig - camera could not be started\",\r\n                        get_ident(),\r\n                    )\r\n                    raise RuntimeError(\r\n                        \"CameraController.requestCameraForConfig - Camera could not be started\"\r\n                    )\r\n            else:\r\n                logger.error(\r\n                    \"Thread %s: CameraController.requestCameraForConfig - camera did not stop\",\r\n                    get_ident(),\r\n                )\r\n                raise RuntimeError(\r\n                    \"CameraController.requestCameraForConfig - Camera did not stop\"\r\n                )\r\n            exclusive = True\r\n        return cam, exclusive, imx500\r\n\r\n    def restoreLivestream(self, cam, exclusive: bool):\r\n        \"\"\"Restart the live stream after exclusive camera use by other task\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.restoreLivestream - exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n        if exclusive:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream - Need to stop camera and restart live stream\",\r\n                get_ident(),\r\n            )\r\n            cam, stopped = self.requestStop(cam)\r\n            if not stopped:\r\n                logger.error(\r\n                    \"Thread %s: CameraController.restoreLivestream - camera did not stop\",\r\n                    get_ident(),\r\n                )\r\n                raise RuntimeError(\r\n                    \"CameraController.restoreLivestream - Camera did not stop\"\r\n                )\r\n            Camera.liveViewDeactivated = False\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream - Live stream activated\",\r\n                get_ident(),\r\n            )\r\n            Camera.startLiveStream()\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream: Live stream started\",\r\n                get_ident(),\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream - Restart live stream not required\",\r\n                get_ident(),\r\n            )\r\n        return cam\r\n\r\n    def restoreLivestream2(self, cam, exclusive: bool):\r\n        \"\"\"Restart the live stream 2 after exclusive camera use by other task\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.restoreLivestream2 - exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n        if exclusive:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream2 - Need to stop camera and restart live stream\",\r\n                get_ident(),\r\n            )\r\n            cam, stopped = self.requestStop(cam)\r\n            if not stopped:\r\n                logger.error(\r\n                    \"Thread %s: CameraController.restoreLivestream2 - camera did not stop\",\r\n                    get_ident(),\r\n                )\r\n                raise RuntimeError(\r\n                    \"CameraController.restoreLivestream2 - Camera did not stop\"\r\n                )\r\n            Camera.liveView2Deactivated = False\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream2 - Live stream activated\",\r\n                get_ident(),\r\n            )\r\n            Camera.startLiveStream2()\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream2: Live stream started\",\r\n                get_ident(),\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.restoreLivestream2 - Restart live stream not required\",\r\n                get_ident(),\r\n            )\r\n        return cam\r\n\r\n    def requestStart(\r\n        self, cam, camNum, isUsb=False, camUsbDev=None, forActiveCamera=True\r\n    ):\r\n        \"\"\"Request to start the camera\r\n\r\n        If the camera is not yet started, it is configured and started\r\n\r\n        forActiveCamera: Whether the request is for the active camera\r\n        Return:\r\n        - True  if the camera was started\r\n                or if the camera had been started before with the same configuration\r\n        - False if the camera was already started or if an exception occurs during start\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestStart - camNum: %s isUsb: %s camUsbDev: %s\",\r\n            get_ident(),\r\n            camNum,\r\n            isUsb,\r\n            camUsbDev,\r\n        )\r\n        res = False\r\n        imx500 = None\r\n\r\n        if isUsb == False:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestStart - cam.started: %s\",\r\n                get_ident(),\r\n                cam.started,\r\n            )\r\n            if cam.started == False:\r\n                try:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - cam.is_open: %s\",\r\n                        get_ident(),\r\n                        cam.is_open,\r\n                    )\r\n                    if cam.is_open == False:\r\n                        cfg = CameraCfg()\r\n                        if forActiveCamera == True:\r\n                            tc = cfg.tuningConfig\r\n                            ai = cfg.aiConfig\r\n                        else:\r\n                            strc = cfg.streamingCfg\r\n                            camNumStr = str(camNum)\r\n                            if camNumStr in strc:\r\n                                scfg = strc[camNumStr]\r\n                                if \"tuningconfig\" in scfg:\r\n                                    tc = scfg[\"tuningconfig\"]\r\n                                else:\r\n                                    tc = TuningConfig()\r\n                                if \"aiconfig\" in scfg:\r\n                                    ai = scfg[\"aiconfig\"]\r\n                                else:\r\n                                    ai = AiConfig()\r\n                            else:\r\n                                tc = TuningConfig()\r\n                                ai = AiConfig()\r\n                        if tc.loadTuningFile == False:\r\n                            cam = Picamera2(camNum)\r\n                            prgLogger.debug(\"picam2 = Picamera2(%s)\", camNum)\r\n                        else:\r\n                            tuning = Picamera2.load_tuning_file(\r\n                                tc.tuningFile, tc.tuningFolder\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: CameraController.requestStart - Tuning file loaded: File=%s Folder=%s\",\r\n                                get_ident(),\r\n                                tc.tuningFile,\r\n                                tc.tuningFolder,\r\n                            )\r\n                            cam = Picamera2(camNum, tuning=tuning)\r\n                            logger.debug(\r\n                                \"Thread %s: CameraController.requestStart - Initialized camera %s with tuning\",\r\n                                get_ident(),\r\n                                camNum,\r\n                            )\r\n                            prgLogger.debug(\r\n                                \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                                tc.tuningFile,\r\n                                tc.tuningFolder,\r\n                            )\r\n                            prgLogger.debug(\r\n                                \"picam2 = Picamera2(%s, tuning=tuning)\", camNum\r\n                            )\r\n                        # Set model for AI Camera\r\n                        if ai.enable:\r\n                            # Try to import IMX500\r\n                            try:\r\n                                from picamera2.devices import IMX500\r\n                                logger.debug(\r\n                                    \"Thread %s: CameraController.requestStart - import IMX500 successful\",\r\n                                    get_ident(),\r\n                                )\r\n                            except ImportError:\r\n                                logger.error(\r\n                                    \"CameraController.requestStart - Could not import IMX500 from picamera2.devices\",\r\n                                )\r\n                                ai.enable = False\r\n                        if ai.enable:\r\n                            modelPath = os.path.join(ai.modelFolder, ai.modelFile)\r\n                            imx500 = IMX500(modelPath)\r\n                            logger.debug(\r\n                                \"Thread %s: CameraController.requestStart - IMX500 instantiated with model: %s\",\r\n                                get_ident(),\r\n                                modelPath,\r\n                            )\r\n                    else:\r\n                        logger.debug(\r\n                            \"Thread %s: CameraController.requestStart - Camera is already open\",\r\n                            get_ident(),\r\n                        )\r\n                        imx500 = Camera.cam_imx500\r\n\r\n                    self._activeCfg = self.copyConfig(self._requestedCfg)\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - activeCfg b: %s\",\r\n                        get_ident(),\r\n                        self._activeCfg,\r\n                    )\r\n                    wrkCfg = self.copyConfig(self._activeCfg)\r\n                    cam.configure(wrkCfg)\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - activeCfg a: %s\",\r\n                        get_ident(),\r\n                        self._activeCfg,\r\n                    )\r\n                    if self.isUsb == False:\r\n                        if prgLogger.level == logging.DEBUG:\r\n                            self.codeGenConfig(self._activeCfg)\r\n                            prgLogger.debug(\"picam2.configure(ccfg)\")\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - Camera configured\",\r\n                        get_ident(),\r\n                    )\r\n                    cam.start(show_preview=False)\r\n                    prgLogger.debug(\"picam2.start(show_preview=False)\")\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - Camera started\",\r\n                        get_ident(),\r\n                    )\r\n                    res = True\r\n                    # let camera warm up\r\n                    time.sleep(1.5)\r\n                    prgLogger.debug(\"time.sleep(1.5)\")\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: CameraController.requestStart - Error starting camera: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    cfg = CameraCfg()\r\n                    sc = cfg.serverConfig\r\n                    if not sc.error:\r\n                        sc.error = \"Error while starting camera: \" + str(e)\r\n                        sc.errorSource = \"CameraController.requestStart\"\r\n\r\n            else:\r\n                isIdentical, dif = self.compareConfig(\r\n                    self._requestedCfg, self._activeCfg\r\n                )\r\n                if isIdentical:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - Camera was already started with same configuration.\",\r\n                        get_ident(),\r\n                    )\r\n                    res = True\r\n                else:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - Camera was already started, but with different configuration. Difference is: %s\",\r\n                        get_ident(),\r\n                        dif,\r\n                    )\r\n                imx500 = Camera.cam_imx500\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestStart - cam.isOpened: %s\",\r\n                get_ident(),\r\n                cam.isOpened(),\r\n            )\r\n            # For USB cameras, just open the camera if not already opened\r\n            if cam.isOpened() == False:\r\n                cam = cv2.VideoCapture(camUsbDev, cv2.CAP_V4L2)\r\n                if (not cam) or (cam.isOpened() == False):\r\n                    logger.error(\r\n                        \"Thread %s: CameraController.requestStart - Error: USB camera not opened\",\r\n                        get_ident(),\r\n                    )\r\n                    cfg = CameraCfg()\r\n                    sc = cfg.serverConfig\r\n                    sc.error = \"Error while initializing camera: USB camera not opened\"\r\n                    sc.errorSource = \"CV2\"\r\n                else:\r\n                    res = True\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStart - USB camera started\",\r\n                        get_ident(),\r\n                    )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: CameraController.requestStart - USB Camera was already opened.\",\r\n                    get_ident(),\r\n                )\r\n                res = True\r\n            if cam.isOpened() == True:\r\n                # Apply configuration\r\n                self._activeCfg = self.copyConfig(self._requestedCfg)\r\n                wrkCfg = self.copyConfig(self._activeCfg)\r\n                fmt = wrkCfg.main.format\r\n                width = wrkCfg.main.size[0]\r\n                height = wrkCfg.main.size[1]\r\n                cam.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*fmt))\r\n                cam.set(cv2.CAP_PROP_FRAME_WIDTH, width)\r\n                cam.set(cv2.CAP_PROP_FRAME_HEIGHT, height)\r\n                logger.debug(\r\n                    \"Thread %s: CameraController.requestStart - USB Cam started with format: %s, size: %s x %s\",\r\n                    get_ident(),\r\n                    fmt,\r\n                    width,\r\n                    height,\r\n                )\r\n\r\n        logger.debug(\"Thread %s: CameraController.requestStart: %s\", get_ident(), res)\r\n        return cam, res, imx500\r\n\r\n    def requestStop(self, cam, close=False):\r\n        \"\"\"Request to stop the camera\r\n\r\n        If the camera is started,\r\n        - stop the active encoders, if any\r\n        - stop the camera\r\n        - if close: close the camera\r\n        Return:\r\n        - True  if the camera was stopped / closed\r\n                or if the camera was not started\r\n        - False if the camera could not be stopped\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: CameraController.requestStop\", get_ident())\r\n        res = False\r\n        if self.isUsb == False:\r\n            try:\r\n                if cam.started == True:\r\n                    # First stop encoders\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - Stopping %s encoders\",\r\n                        get_ident(),\r\n                        len(self._activeEncoders),\r\n                    )\r\n                    while len(self._activeEncoders) > 0:\r\n                        task, encoder = self._activeEncoders.popitem()\r\n                        cam.stop_encoder(encoder)\r\n                        encoder = None\r\n                        prgLogger.debug(\"picam2.stop_encoder(encoder)\")\r\n                        logger.debug(\r\n                            \"Thread %s: CameraController.requestStop - Stopped Encoder for %s\",\r\n                            get_ident(),\r\n                            task,\r\n                        )\r\n                    # Then stop the camera\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - Stopping camera\",\r\n                        get_ident(),\r\n                    )\r\n                    cam.stop()\r\n                    prgLogger.debug(\"picam2.stop()\")\r\n                    cnt = 0\r\n                    while cam.started == True:\r\n                        time.sleep(0.01)\r\n                        cnt += 1\r\n                        if cnt > 200:\r\n                            logger.error(\r\n                                \"Thread %s: CameraController.requestStop - Camera did not stop\",\r\n                                get_ident(),\r\n                            )\r\n                            raise TimeoutError(\r\n                                \"CameraController.requestStop: Camera did not stop within 2 sec\"\r\n                            )\r\n                    if cnt < 200:\r\n                        logger.debug(\r\n                            \"Thread %s: CameraController.requestStop - Camera stopped\",\r\n                            get_ident(),\r\n                        )\r\n                        res = True\r\n                else:\r\n                    res = True\r\n\r\n            except TimeoutError:\r\n                raise\r\n            except Exception as e:\r\n                logger.error(\r\n                    \"Thread %s: CameraController.requestStop - error: %s\",\r\n                    get_ident(),\r\n                    e,\r\n                )\r\n                raise\r\n\r\n            if close == True:\r\n                try:\r\n                    if cam.is_open == True:\r\n                        logger.debug(\r\n                            \"Thread %s: CameraController.requestStop - About to close camera\",\r\n                            get_ident(),\r\n                        )\r\n                        prgLogger.debug(\"picam2.close()\")\r\n                        cam.close()\r\n                        logger.debug(\r\n                            \"Thread %s: CameraController.requestStop - Camera closed\",\r\n                            get_ident(),\r\n                        )\r\n                except Exception as e:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - Ignoring error while closing camera: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                gc.collect()\r\n                prgLogger.debug(\"gc.collect()\")\r\n                logger.debug(\r\n                    \"Thread %s: CameraController.requestStop - Garbage collection completed\",\r\n                    get_ident(),\r\n                )\r\n        else:\r\n            # For USB cameras, just close the camera\r\n            try:\r\n                if cam.isOpened() == True:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - About to close USB camera\",\r\n                        get_ident(),\r\n                    )\r\n                    cam.release()\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - USB Camera closed\",\r\n                        get_ident(),\r\n                    )\r\n                else:\r\n                    logger.debug(\r\n                        \"Thread %s: CameraController.requestStop - USB Camera was not opened\",\r\n                        get_ident(),\r\n                    )\r\n                res = True\r\n            except Exception as e:\r\n                logger.debug(\r\n                    \"Thread %s: CameraController.requestStop - Ignoring error while closing USB camera: %s\",\r\n                    get_ident(),\r\n                    e,\r\n                )\r\n            gc.collect()\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestStop - Garbage collection completed\",\r\n                get_ident(),\r\n            )\r\n        logger.debug(\"Thread %s: CameraController.requestStop: %s\", get_ident(), res)\r\n\r\n        if self._forActiveCamera == True:\r\n            Camera.camWaitingForFirstFrame = True\r\n            Camera.camProgressCounter = 0\r\n        else:\r\n            Camera.cam2WaitingForFirstFrame = True\r\n            Camera.cam2ProgressCounter = 0\r\n        return cam, res\r\n\r\n    def requestConfig(\r\n        self, cfg: CameraConfig, test: bool = False, cfgPhoto: CameraConfig = None\r\n    ):\r\n        \"\"\"Register a new configuration\r\n\r\n        Parameters:\r\n        cfg:     configuration to register\r\n        test:    Run in test mode without modifying self._requestedCfg\r\n        cfgPhoto Configuration for Photo.\r\n                 Required only if cfg is a Raw Photo configuration.\r\n                 In this case, cfgPhoto is used to configure the main stream for placeholder jpg photos\r\n\r\n        Return:\r\n        configChange:       True/False if the requested configuration caused a change in configuration\r\n        configChangeReason: Reason for configuration change: list of discrepancies\r\n\r\n        If there are no configuration conflicts,\r\n        the requested configuration is merged into the active configuration.\r\n        Otherwise, the active configuration is replaced by the requested configuration\r\n        and configChange is set to True and configChangeReason is filled with detected conflicts\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestConfig - test: %s cfg     : %s\",\r\n            get_ident(),\r\n            test,\r\n            cfg.__dict__,\r\n        )\r\n        if cfgPhoto:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestConfig - test: %s cfgPhoto: %s\",\r\n                get_ident(),\r\n                test,\r\n                cfgPhoto.__dict__,\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: CameraController.requestConfig - test: %s cfgPhoto: %s\",\r\n                get_ident(),\r\n                test,\r\n                cfgPhoto,\r\n            )\r\n\r\n        cfgRef = self._requestedCfg\r\n\r\n        configChange = False\r\n        configChangeReason = \"\"\r\n\r\n        if not test:\r\n            if cfgRef.use_case:\r\n                if cfgRef.use_case.find(cfg.use_case) < 0:\r\n                    cfgRef.use_case += \",\" + cfg.use_case\r\n            else:\r\n                cfgRef.use_case = cfg.use_case\r\n\r\n        # Transform of new config must be identical to existing\r\n        if cfgRef.transform:\r\n            if (\r\n                cfgRef.transform.hflip != cfg.transform_hflip\r\n                or cfgRef.transform.vflip != cfg.transform_vflip\r\n            ):\r\n                configChange = True\r\n                configChangeReason += \"transform,\"\r\n        else:\r\n            if not test:\r\n                cfgRef.transform = Transform(\r\n                    vflip=cfg.transform_vflip, hflip=cfg.transform_hflip\r\n                )\r\n\r\n        # For buffer_count, always choose the larger one\r\n        if not test:\r\n            if cfgRef.buffer_count:\r\n                if cfg.buffer_count > cfgRef.buffer_count:\r\n                    cfgRef.buffer_count = cfg.buffer_count\r\n            else:\r\n                cfgRef.buffer_count = cfg.buffer_count\r\n\r\n        if self.isUsb == False:\r\n            # Colour space must be identical\r\n            cosp = cfg.colour_space\r\n            if cosp == \"sYCC\":\r\n                colourSpace = ColorSpace.Sycc()\r\n            elif cosp == \"Smpte170m\":\r\n                colourSpace = ColorSpace.Smpte170m()\r\n            elif cosp == \"Rec709\":\r\n                colourSpace = ColorSpace.Rec709()\r\n            else:\r\n                colourSpace = ColorSpace.Sycc()\r\n\r\n            if cfgRef.colour_space:\r\n                if cfgRef.colour_space != colourSpace:\r\n                    configChange = True\r\n                    configChangeReason += \"colourSpace,\"\r\n            else:\r\n                if not test:\r\n                    cfgRef.colour_space = colourSpace\r\n\r\n        # queue must be identical\r\n        if cfgRef.queue:\r\n            if cfgRef.queue != cfg.queue:\r\n                configChange = True\r\n                configChangeReason += \"queue,\"\r\n        else:\r\n            if not test:\r\n                cfgRef.queue = cfg.queue\r\n\r\n        # display must be identical\r\n        if cfgRef.display:\r\n            if cfgRef.display != cfg.display:\r\n                configChange = True\r\n                configChangeReason += \"display,\"\r\n        else:\r\n            if not test:\r\n                cfgRef.display = cfg.display\r\n\r\n        # encode is not used. Always set it to 'main'\r\n        if not test:\r\n            cfgRef.encode = \"main\"\r\n\r\n        # Sensor is not explicitely set in the configuration\r\n        # It will be selected and updated by picamera2 automaticallx\r\n        if useSensorConfiguration:\r\n            if not cfgRef.sensor:\r\n                if not test:\r\n                    sensor = SensorConfiguration()\r\n                    sensor.output_size = None\r\n                    sensor.bit_depth = None\r\n                    cfgRef.sensor = sensor\r\n\r\n        #'main' stream must be identical\r\n        if cfg.stream == \"main\":\r\n            if cfgRef.main:\r\n                if cfgRef.main.size != cfg.stream_size:\r\n                    configChange = True\r\n                    configChangeReason += \"main.size,\"\r\n                if cfgRef.main.format != cfg.format:\r\n                    configChange = True\r\n                    configChangeReason += \"main.format,\"\r\n            else:\r\n                if not test:\r\n                    mstream = StreamConfiguration()\r\n                    mstream.size = cfg.stream_size\r\n                    mstream.format = cfg.format\r\n                    mstream.stride = None\r\n                    mstream.framesize = None\r\n                    cfgRef.main = mstream\r\n\r\n        #'lores' stream must be identical\r\n        if cfg.stream == \"lores\":\r\n            if cfgRef.lores:\r\n                if cfgRef.lores.size != cfg.stream_size:\r\n                    configChange = True\r\n                    configChangeReason += \"lores.size,\"\r\n                if cfgRef.lores.format != cfg.format:\r\n                    configChange = True\r\n                    configChangeReason += \"lores.format,\"\r\n            else:\r\n                if not test:\r\n                    lstream = StreamConfiguration()\r\n                    lstream.size = cfg.stream_size\r\n                    lstream.format = cfg.format\r\n                    lstream.stride = None\r\n                    lstream.framesize = None\r\n                    cfgRef.lores = lstream\r\n\r\n        #'raw' stream must be identical\r\n        if cfg.stream == \"raw\":\r\n            if cfgRef.raw:\r\n                if cfgRef.raw.size:\r\n                    if cfgRef.raw.size != cfg.stream_size:\r\n                        configChange = True\r\n                        configChangeReason += \"raw.size,\"\r\n                else:\r\n                    if not test:\r\n                        cfgRef.raw.size = cfg.stream_size\r\n                if cfgRef.raw.format:\r\n                    if cfgRef.raw.format != cfg.format:\r\n                        configChange = True\r\n                        configChangeReason += \"raw.format,\"\r\n                else:\r\n                    if not test:\r\n                        cfgRef.raw.format = cfg.format\r\n            else:\r\n                if not test:\r\n                    rstream = StreamConfiguration()\r\n                    rstream.size = cfg.stream_size\r\n                    rstream.format = cfg.format\r\n                    rstream.stride = None\r\n                    rstream.framesize = None\r\n                    cfgRef.raw = rstream\r\n            if cfgPhoto:\r\n                if cfgRef.main:\r\n                    if cfgRef.main.size != cfgPhoto.stream_size:\r\n                        configChange = True\r\n                        configChangeReason += \"main.size,\"\r\n                    if cfgRef.main.format != cfgPhoto.format:\r\n                        configChange = True\r\n                        configChangeReason += \"main.format,\"\r\n                else:\r\n                    if not test:\r\n                        mstream = StreamConfiguration()\r\n                        mstream.size = cfgPhoto.stream_size\r\n                        mstream.format = cfgPhoto.format\r\n                        mstream.stride = None\r\n                        mstream.framesize = None\r\n                        cfgRef.main = mstream\r\n\r\n        if not test:\r\n            if cfgRef.controls:\r\n                for key, value in cfg.controls.items():\r\n                    if not key in cfgRef.controls:\r\n                        cfgRef.controls[key] = value\r\n            else:\r\n                ctrls = copy.deepcopy(cfg.controls)\r\n                cfgRef.controls = ctrls\r\n        if not test:\r\n            if configChange:\r\n                # If cofig change is detected, replace entire configuration\r\n                camCfg = CameraConfiguration()\r\n\r\n                camCfg.use_case = cfg.use_case\r\n                camCfg.transform = Transform(\r\n                    vflip=cfg.transform_vflip, hflip=cfg.transform_hflip\r\n                )\r\n                camCfg.buffer_count = cfg.buffer_count\r\n                cosp = cfg.colour_space\r\n                if self.isUsb == False:\r\n                    if cosp == \"sYCC\":\r\n                        colourSpace = ColorSpace.Sycc()\r\n                    elif cosp == \"Smpte170m\":\r\n                        colourSpace = ColorSpace.Smpte170m()\r\n                    elif cosp == \"Rec709\":\r\n                        colourSpace = ColorSpace.Rec709()\r\n                    else:\r\n                        colourSpace = ColorSpace.Sycc()\r\n                    camCfg.colour_space = colourSpace\r\n                else:\r\n                    camCfg.colour_space = cfgRef.colour_space\r\n                camCfg.queue = cfg.queue\r\n                camCfg.display = cfg.display\r\n                camCfg.encode = cfg.encode\r\n\r\n                stream = StreamConfiguration()\r\n                stream.size = cfg.stream_size\r\n                stream.format = cfg.format\r\n                if cfg.stream == \"main\":\r\n                    camCfg.main = stream\r\n                    camCfg.lores = None\r\n                    camCfg.raw = None\r\n                if cfg.stream == \"lores\":\r\n                    camCfg.main = stream\r\n                    camCfg.lores = stream\r\n                    camCfg.raw = None\r\n                if cfg.stream == \"raw\":\r\n                    if cfgPhoto:\r\n                        mstream = StreamConfiguration()\r\n                        mstream.size = cfgPhoto.stream_size\r\n                        mstream.format = cfgPhoto.format\r\n                        camCfg.main = mstream\r\n                    else:\r\n                        camCfg.main = stream\r\n                    camCfg.lores = None\r\n                    camCfg.raw = stream\r\n                ctrls = copy.deepcopy(cfg.controls)\r\n                if len(ctrls) == 0:\r\n                    raise ValueError(\r\n                        \"controls in camera configuration must not be empty\"\r\n                    )\r\n                else:\r\n                    camCfg.controls = ctrls\r\n                cfgRef = camCfg\r\n\r\n            # Automatically align the stream size, if selected\r\n            if cfg.stream_size_align and cfg.sensor_mode == \"custom\":\r\n                cfgRef.align()\r\n                if cfg.stream == \"main\":\r\n                    cfg.stream_size = cfgRef.main.size\r\n                if cfg.stream == \"lores\":\r\n                    cfg.stream_size = cfgRef.lores.size\r\n\r\n        self._requestedCfg = cfgRef\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestConfig - configChange: %s\",\r\n            get_ident(),\r\n            configChange,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestConfig - configChangeReason: %s\",\r\n            get_ident(),\r\n            configChangeReason,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.requestConfig - cfg: %s\",\r\n            get_ident(),\r\n            self._requestedCfg,\r\n        )\r\n        return configChange, configChangeReason\r\n\r\n    def codeGenConfig(self, cfg: CameraConfiguration):\r\n        \"\"\"Generate code for the given configuration\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.codeGenConfig cfg: %s\",\r\n            get_ident(),\r\n            cfg.__dict__,\r\n        )\r\n        prgLogger.debug(\"ccfg = CameraConfiguration()\")\r\n        prgLogger.debug('ccfg.use_case = \"%s\"', cfg.use_case)\r\n        if cfg.encode:\r\n            prgLogger.debug('ccfg.encode = \"%s\"', cfg.encode)\r\n        else:\r\n            prgLogger.debug(\"ccfg.encode = None\")\r\n        if cfg.display:\r\n            prgLogger.debug('ccfg.display = \"%s\"', cfg.display)\r\n        else:\r\n            prgLogger.debug(\"ccfg.display = None\")\r\n        prgLogger.debug(\"ccfg.buffer_count = %s\", cfg.buffer_count)\r\n        prgLogger.debug(\"ccfg.queue = %s\", cfg.queue)\r\n\r\n        if cfg.transform:\r\n            prgLogger.debug(\r\n                \"ccfg.transform = Transform(vflip=%s, hflip=%s)\",\r\n                cfg.transform.vflip,\r\n                cfg.transform.hflip,\r\n            )\r\n        else:\r\n            prgLogger.debug(\"ccfg.transform = None\")\r\n\r\n        if cfg.colour_space.__str__().find(\"sYCC\") >= 0:\r\n            prgLogger.debug(\"ccfg.colour_space = ColorSpace.Sycc()\")\r\n        if cfg.colour_space.__str__().find(\"SMPTE170M\") >= 0:\r\n            prgLogger.debug(\"ccfg.colour_space = ColorSpace.Smpte170m()\")\r\n        if cfg.colour_space.__str__().find(\"Rec709\") >= 0:\r\n            prgLogger.debug(\"ccfg.colour_space = ColorSpace.Rec709()\")\r\n\r\n        if cfg.controls:\r\n            prgLogger.debug(\"ccfg.controls = %s\", cfg.controls)\r\n        else:\r\n            prgLogger.debug(\"ccfg.controls = None\")\r\n\r\n        if useSensorConfiguration:\r\n            if cfg.sensor:\r\n                prgLogger.debug(\"ccfg.sensor = SensorConfiguration()\")\r\n                prgLogger.debug(\"ccfg.sensor.output_size = %s\", cfg.sensor.output_size)\r\n                prgLogger.debug(\"ccfg.sensor.bit_depth = %s\", cfg.sensor.bit_depth)\r\n            else:\r\n                prgLogger.debug(\"ccfg.sensor = None\")\r\n\r\n        if cfg.main:\r\n            prgLogger.debug(\"ccfg.main = StreamConfiguration()\")\r\n            prgLogger.debug(\"ccfg.main.size = %s\", cfg.main.size)\r\n            prgLogger.debug('ccfg.main.format = \"%s\"', cfg.main.format)\r\n            prgLogger.debug(\"ccfg.main.stride = %s\", cfg.main.stride)\r\n            prgLogger.debug(\"ccfg.main.framesize = %s\", cfg.main.framesize)\r\n        else:\r\n            prgLogger.debug(\"ccfg.main = None\")\r\n\r\n        if cfg.lores:\r\n            prgLogger.debug(\"ccfg.lores = StreamConfiguration()\")\r\n            prgLogger.debug(\"ccfg.lores.size = %s\", cfg.lores.size)\r\n            prgLogger.debug('ccfg.lores.format = \"%s\"', cfg.lores.format)\r\n            prgLogger.debug(\"ccfg.lores.stride = %s\", cfg.lores.stride)\r\n            prgLogger.debug(\"ccfg.lores.framesize = %s\", cfg.lores.framesize)\r\n        else:\r\n            prgLogger.debug(\"ccfg.lores = None\")\r\n\r\n        if cfg.raw:\r\n            prgLogger.debug(\"ccfg.raw = StreamConfiguration()\")\r\n            prgLogger.debug(\"ccfg.raw.size = %s\", cfg.raw.size)\r\n            prgLogger.debug('ccfg.raw.format = \"%s\"', cfg.raw.format)\r\n            prgLogger.debug(\"ccfg.raw.stride = %s\", cfg.raw.stride)\r\n            prgLogger.debug(\"ccfg.raw.framesize = %s\", cfg.raw.framesize)\r\n        else:\r\n            prgLogger.debug(\"ccfg.raw = None\")\r\n\r\n    def copyConfig(self, cfg: CameraConfiguration) -> CameraConfiguration:\r\n        \"\"\"Return a copy of the given configuration\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.copyConfig cfg(in) : %s\",\r\n            get_ident(),\r\n            cfg.__dict__,\r\n        )\r\n        ccfg = CameraConfiguration()\r\n        ccfg.use_case = cfg.use_case\r\n        ccfg.encode = cfg.encode\r\n        ccfg.display = cfg.display\r\n        ccfg.buffer_count = cfg.buffer_count\r\n        ccfg.queue = cfg.queue\r\n\r\n        if cfg.transform:\r\n            ccfg.transform = Transform(\r\n                vflip=cfg.transform.vflip, hflip=cfg.transform.hflip\r\n            )\r\n        else:\r\n            ccfg.transform = None\r\n\r\n        ccfg.colour_space = cfg.colour_space\r\n\r\n        if cfg.controls:\r\n            ccfg.controls = copy.copy(cfg.controls)\r\n        else:\r\n            ccfg.controls = None\r\n\r\n        if useSensorConfiguration:\r\n            if cfg.sensor:\r\n                ccfg.sensor = SensorConfiguration()\r\n                ccfg.sensor.output_size = copy.copy(cfg.sensor.output_size)\r\n                ccfg.sensor.bit_depth = cfg.sensor.bit_depth\r\n            else:\r\n                ccfg.sensor = None\r\n\r\n        if cfg.main:\r\n            ccfg.main = StreamConfiguration()\r\n            ccfg.main.size = copy.copy(cfg.main.size)\r\n            ccfg.main.format = cfg.main.format\r\n            ccfg.main.stride = cfg.main.stride\r\n            ccfg.main.framesize = cfg.main.framesize\r\n        else:\r\n            ccfg.main = None\r\n\r\n        if cfg.lores:\r\n            ccfg.lores = StreamConfiguration()\r\n            ccfg.lores.size = copy.copy(cfg.lores.size)\r\n            ccfg.lores.format = cfg.lores.format\r\n            ccfg.lores.stride = cfg.lores.stride\r\n            ccfg.lores.framesize = cfg.lores.framesize\r\n        else:\r\n            ccfg.lores = None\r\n\r\n        if cfg.raw:\r\n            ccfg.raw = StreamConfiguration()\r\n            ccfg.raw.size = copy.copy(cfg.raw.size)\r\n            ccfg.raw.format = cfg.raw.format\r\n            ccfg.raw.stride = cfg.raw.stride\r\n            ccfg.raw.framesize = cfg.raw.framesize\r\n        else:\r\n            ccfg.raw = None\r\n        logger.debug(\r\n            \"Thread %s: CameraController.copyConfig cfg(out): %s\",\r\n            get_ident(),\r\n            ccfg.__dict__,\r\n        )\r\n        return ccfg\r\n\r\n    def compareConfig(\r\n        self, cfg1: CameraConfiguration, cfg2: CameraConfiguration\r\n    ) -> bool:\r\n        \"\"\"Check equality of configurations\r\n\r\n        Return:\r\n        result (bool):\r\n            True  if configurations are identical\r\n            False if configuration differ\r\n        difference (str): List of differences\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.compareConfig cfg1: %s\",\r\n            get_ident(),\r\n            cfg1.__dict__,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: CameraController.compareConfig cfg2: %s\",\r\n            get_ident(),\r\n            cfg2.__dict__,\r\n        )\r\n        res = True\r\n        dif = \"\"\r\n        if cfg1.encode:\r\n            if cfg2.encode:\r\n                if cfg1.encode != cfg2.encode:\r\n                    res = False\r\n                    dif += \"encode,\"\r\n            else:\r\n                res = False\r\n                dif += \"encode,\"\r\n        else:\r\n            if cfg2.encode:\r\n                res = False\r\n                dif += \"encode,\"\r\n\r\n        if cfg1.display:\r\n            if cfg2.display:\r\n                if cfg1.display != cfg2.display:\r\n                    res = False\r\n                    dif += \"display,\"\r\n            else:\r\n                res = False\r\n                dif += \"display,\"\r\n        else:\r\n            if cfg2.display:\r\n                res = False\r\n                dif += \"display,\"\r\n\r\n        if cfg1.buffer_count:\r\n            if cfg2.buffer_count:\r\n                if cfg1.buffer_count != cfg2.buffer_count:\r\n                    res = False\r\n                    dif += \"buffer_count,\"\r\n            else:\r\n                res = False\r\n                dif += \"buffer_count,\"\r\n        else:\r\n            if cfg2.buffer_count:\r\n                res = False\r\n                dif += \"buffer_count,\"\r\n\r\n        if cfg1.transform:\r\n            if cfg2.transform:\r\n                if (\r\n                    cfg1.transform.hflip != cfg2.transform.hflip\r\n                    or cfg1.transform.vflip != cfg2.transform.vflip\r\n                ):\r\n                    res = False\r\n                    dif += \"transform,\"\r\n            else:\r\n                res = False\r\n                dif += \"transform,\"\r\n        else:\r\n            if cfg2.transform:\r\n                res = False\r\n                dif += \"transform,\"\r\n\r\n        if cfg1.colour_space:\r\n            if cfg2.colour_space:\r\n                if cfg1.colour_space != cfg2.colour_space:\r\n                    res = False\r\n                    dif += \"colour_space,\"\r\n            else:\r\n                res = False\r\n                dif += \"colour_space,\"\r\n        else:\r\n            if cfg2.colour_space:\r\n                res = False\r\n                dif += \"colour_space,\"\r\n\r\n        if cfg1.queue:\r\n            if cfg2.queue:\r\n                if cfg1.queue != cfg2.queue:\r\n                    res = False\r\n                    dif += \"queue,\"\r\n            else:\r\n                res = False\r\n                dif += \"queue,\"\r\n        else:\r\n            if cfg2.queue:\r\n                res = False\r\n                dif += \"queue,\"\r\n\r\n        if useSensorConfiguration:\r\n            if cfg1.sensor:\r\n                if cfg2.sensor:\r\n                    if cfg1.sensor.bit_depth != cfg2.sensor.bit_depth:\r\n                        res = False\r\n                        dif += \"sensor.bit_depth,\"\r\n                    if cfg1.sensor.output_size != cfg2.sensor.output_size:\r\n                        res = False\r\n                        dif += \"sensor.output_size,\"\r\n                else:\r\n                    res = False\r\n                    dif += \"sensor,\"\r\n            else:\r\n                if cfg2.sensor:\r\n                    res = False\r\n                    dif += \"sensor,\"\r\n\r\n        if cfg1.main:\r\n            if cfg2.main:\r\n                if cfg1.main.size != cfg2.main.size:\r\n                    res = False\r\n                    dif += \"main.size,\"\r\n                if cfg1.main.format != cfg2.main.format:\r\n                    res = False\r\n                    dif += \"main.format,\"\r\n            else:\r\n                res = False\r\n                dif += \"main,\"\r\n        else:\r\n            if cfg2.main:\r\n                res = False\r\n                dif += \"main,\"\r\n\r\n        if cfg1.lores:\r\n            if cfg2.lores:\r\n                if cfg1.lores.size != cfg2.lores.size:\r\n                    res = False\r\n                    dif += \"lores.size,\"\r\n                if cfg1.lores.format != cfg2.lores.format:\r\n                    res = False\r\n                    dif += \"lores.format,\"\r\n            else:\r\n                res = False\r\n                dif += \"lores,\"\r\n        else:\r\n            if cfg2.lores:\r\n                res = False\r\n                dif += \"lores,\"\r\n\r\n        if cfg1.raw:\r\n            if cfg2.raw:\r\n                if cfg1.raw.size != cfg2.raw.size:\r\n                    res = False\r\n                    dif += \"raw.size,\"\r\n                if cfg1.raw.format != cfg2.raw.format:\r\n                    res = False\r\n                    dif += \"raw.format,\"\r\n            else:\r\n                res = False\r\n                dif += \"raw,\"\r\n        else:\r\n            if cfg2.raw:\r\n                res = False\r\n                dif += \"raw,\"\r\n\r\n        if cfg1.controls:\r\n            resCtrls = True\r\n            if cfg2.controls:\r\n                for key, value in cfg1.controls.items():\r\n                    if key in cfg2.controls:\r\n                        if value != cfg2.controls[key]:\r\n                            resCtrls = False\r\n                if len(cfg1.controls) != len(cfg2.controls):\r\n                    resCtrls = False\r\n            else:\r\n                res = False\r\n                dif += \"controls,\"\r\n        else:\r\n            if cfg2.controls:\r\n                res = False\r\n                dif += \"controls,\"\r\n        if not resCtrls:\r\n            res = False\r\n            dif += \"controls,\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.compareConfig res: %s, dif: %s\",\r\n            get_ident(),\r\n            res,\r\n            dif,\r\n        )\r\n        return res, dif\r\n\r\n    def clearConfig(self):\r\n        \"\"\"Clear the configuration\"\"\"\r\n        logger.debug(\"Thread %s: CameraController.clearConfig\", get_ident())\r\n        self._requestedCfg = CameraConfiguration()\r\n\r\n    def registerEncoder(self, task: str, encoder):\r\n        \"\"\"Register an encoder which needs to be stopped when stopping the camera\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: CameraController.registerEncoder: %s\", get_ident(), encoder\r\n        )\r\n        self._activeEncoders[task] = encoder\r\n\r\n    def stopEncoder(self, cam, task: str):\r\n        \"\"\"Stop an encoder for a specific task\"\"\"\r\n        logger.debug(\"Thread %s: CameraController.stopEncoder: %s\", get_ident(), task)\r\n        if task in self._activeEncoders:\r\n            encoder = self._activeEncoders[task]\r\n            cam.stop_encoder(encoder)\r\n            prgLogger.debug(\"picam2.stop_encoder(encoder)\")\r\n            del self._activeEncoders[task]\r\n            logger.debug(\r\n                \"Thread %s: CameraController.stopEncoder - Encoder stopped\", get_ident()\r\n            )\r\n\r\n\r\nclass CameraEvent(object):\r\n    \"\"\"An Event-like class that signals all active clients when a new frame is\r\n    available.\r\n    \"\"\"\r\n\r\n    def __init__(self):\r\n        # logger.debug(\"Thread %s: CameraEvent.__init__\", get_ident())\r\n        self.events = {}\r\n\r\n    def wait(self):\r\n        \"\"\"Invoked from each client's thread to wait for the next frame.\"\"\"\r\n        # logger.debug(\"Thread %s: CameraEvent.wait\", get_ident())\r\n        ident = get_ident()\r\n        if ident not in self.events:\r\n            # this is a new client\r\n            # add an entry for it in the self.events dict\r\n            # each entry has two elements, a threading.Event() and a timestamp\r\n            self.events[ident] = [threading.Event(), time.time()]\r\n            # logger.debug(\"Thread %s: CameraEvent.wait - Event ident: %s added to events dict. time:%s\", get_ident(), ident, self.events[ident][1])\r\n        # for ident, event in self.events.items():\r\n        # logger.debug(\"Thread %s: CameraEvent.wait - Event ident: %s Flag: %s Time: %s (Flag False -> blocking)\", get_ident(), ident, self.events[ident][0].is_set(), event[1])\r\n\r\n        return self.events[ident][0].wait()\r\n\r\n    def set(self):\r\n        \"\"\"Invoked by the camera thread when a new frame is available.\"\"\"\r\n        # logger.debug(\"Thread %s: CameraEvent.set\", get_ident())\r\n        now = time.time()\r\n        remove = None\r\n        for ident, event in self.events.items():\r\n            if not event[0].isSet():\r\n                # if this client's event is not set, then set it\r\n                # also update the last set timestamp to now\r\n                event[0].set()\r\n                event[1] = now\r\n                # logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s Flag: False -> True (unblock/notify)\", get_ident(), ident)\r\n            else:\r\n                # if the client's event is already set, it means the client\r\n                # did not process a previous frame\r\n                # if the event stays set for more than 5 seconds, then assume\r\n                # the client is gone and remove it\r\n                # logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s Flag: True (Last image not processed).\", get_ident(), ident)\r\n                if now - event[1] > 5:\r\n                    # logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s  too old; marked for removal.\", get_ident(), ident)\r\n                    remove = ident\r\n        if remove:\r\n            del self.events[remove]\r\n            # logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s removed.\", get_ident(), ident)\r\n\r\n    def clear(self):\r\n        \"\"\"Invoked from each client's thread after a frame was processed.\"\"\"\r\n        ident = get_ident()\r\n        if ident in self.events:\r\n            self.events[get_ident()][0].clear()\r\n        # logger.debug(\"Thread %s: CameraEvent.clear - Flag set to False -> blocking.\", get_ident())\r\n\r\n    def toDict(self):\r\n        \"\"\"Convert the event to a dict representation.\"\"\"\r\n        return {\r\n            \"events\": {\r\n                ident: {\r\n                    \"flag\": event[0].is_set(),\r\n                    \"time\": event[1],\r\n                    \"timeHR\": datetime.datetime.fromtimestamp(event[1]).strftime(\r\n                        \"%Y-%m-%d %H:%M:%S.%f\"\r\n                    ),\r\n                }\r\n                for ident, event in self.events.items()\r\n            }\r\n        }\r\n\r\n\r\nclass Camera:\r\n    logger.debug(\"Thread %s: Camera - setting class variables\", get_ident())\r\n    _instance = None\r\n    ENCODER_LIVESTREAM = \"LIVESTREAM\"\r\n    ENCODER_VIDEO = \"VIDEO\"\r\n    ENCODER_PHOTOSERIES = \"PHOTOSERIES\"\r\n\r\n    cam = None\r\n    camIsUsb = False\r\n    camUsbDev = \"\"\r\n    camHasAi = False\r\n    camWaitingForFirstFrame = True\r\n    camProgressCounter = 0\r\n    cam_imx500 = None\r\n    cam_imx500_last_detections = []\r\n    cam_imx500_last_results = None\r\n    cam_imx500_labels = None\r\n    cam_imx500_last_boxes = None\r\n    cam_imx500_last_scores = None\r\n    cam_imx500_last_keypoints = None\r\n    cam_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n    cam_imx500_last_overlay = None\r\n    cam_drawer = None\r\n    camNum = -1\r\n    cam2 = None\r\n    cam2IsUsb = False\r\n    cam2UsbDev = \"\"\r\n    cam2HasAi = False\r\n    cam2WaitingForFirstFrame = True\r\n    cam2ProgressCounter = 0\r\n    cam2_imx500 = None\r\n    cam2_imx500_last_detections = []\r\n    cam2_imx500_last_results = None\r\n    cam2_imx500_labels = None\r\n    cam2_imx500_last_boxes = None\r\n    cam2_imx500_last_scores = None\r\n    cam2_imx500_last_keypoints = None\r\n    cam2_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n    cam2_imx500_last_overlay = None\r\n    cam2_drawer = None\r\n    camNum2 = -1\r\n    ctrl: CameraController = None\r\n    ctrl2: CameraController = None\r\n    videoOutput = None\r\n    videoOutput2 = None\r\n    prgVideoOutput = None\r\n    prgVideoOutput2 = None\r\n    photoSeries: Series = None\r\n\r\n    thread = None  # background thread that reads frames from camera\r\n    threadLock = allocate_lock()  # lock for stopping the camera thread\r\n    thread2 = None  # background thread for second camera\r\n    thread2Lock = allocate_lock()  # lock for stopping the second camera thread\r\n    threadUsbVideo = None  # background thread that records video from USB camera\r\n    threadUsbVideoLock = allocate_lock()  # lock for stopping the USB video thread\r\n    logUsbFrameApplyControls = False\r\n    logUsbFrame2ApplyControls = False\r\n    liveViewDeactivated = False\r\n    liveView2Deactivated = False\r\n    videoThread = None\r\n    videoThread2 = None\r\n    photoSeriesThread = None\r\n    frame = None  # current frame is stored here by background thread\r\n    frame2 = None  # current frame for second camera\r\n    frameRaw = None  # current raw frame is stored here by background thread\r\n    frame2Raw = None  # current raw frame for second camera\r\n    streamOutput = None  # output for MJPEG streaming for live stream\r\n    stream2Output = None  # output for MJPEG streaming for second camera\r\n    last_access = 0  # time of last client access to the camera\r\n    last_access2 = 0  # time of last client access for second camera\r\n    stopRequested = False  # Request to stop the background thread\r\n    stopRequested2 = False  # Request to stop the background thread for second camera\r\n    stopVideoRequested = False  # Request to stop the video thread\r\n    stopVideoRequested2 = False  # Request to stop the video thread\r\n    stopUsbVideoRequested = False  # Request to stop the video thread\r\n    videoDuration = 0  # Planned duration of video recording in sec\r\n    videoDuration2 = 0  # Planned duration of video recording in sec\r\n    stopPhotoSeriesRequested = False  # Request to stop the photoseries thread\r\n    resetScalerCropRequested = False\r\n    event = CameraEvent()\r\n    event2 = None\r\n\r\n    # Callbacks\r\n    when_photo_taken = None\r\n    when_photo_2_taken = None\r\n    when_series_photo_taken = None\r\n    when_recording_starts = None\r\n    when_recording_stops = None\r\n    when_recording_2_starts = None\r\n    when_recording_2_stops = None\r\n    when_streaming_1_starts = None\r\n    when_streaming_1_stops = None\r\n    when_streaming_2_starts = None\r\n    when_streaming_2_stops = None\r\n\r\n    COLOURS = np.array([ \\\r\n        [128.0, 0.0, 0.0, 255.0], \\\r\n        [0.0, 128.0, 0.0, 255.0], \\\r\n        [128.0, 128.0, 0.0, 255.0], \\\r\n        [0.0, 0.0, 128.0, 255.0], \\\r\n        [128.0, 0.0, 128.0, 255.0], \\\r\n        [0.0, 128.0, 128.0, 255.0], \\\r\n        [128.0, 128.0, 128.0, 255.0], \\\r\n        [64.0, 0.0, 0.0, 255.0], \\\r\n        [192.0, 0.0, 0.0, 255.0], \\\r\n        [64.0, 128.0, 0.0, 255.0], \\\r\n        [192.0, 128.0, 0.0, 255.0], \\\r\n        [64.0, 0.0, 128.0, 255.0], \\\r\n        [192.0, 0.0, 128.0, 255.0], \\\r\n        [64.0, 128.0, 128.0, 255.0], \\\r\n        [192.0, 128.0, 128.0, 255.0], \\\r\n        [0.0, 64.0, 0.0, 255.0], \\\r\n        [128.0, 64.0, 0.0, 255.0], \\\r\n        [0.0, 192.0, 0.0, 255.0], \\\r\n        [128.0, 192.0, 0.0, 255.0], \\\r\n        [0.0, 64.0, 128.0, 255.0], \\\r\n        [0.0, 0.0, 0.0, 255.0] \\\r\n    ])\r\n\r\n\r\n    def __new__(cls):\r\n        logger.debug(\"Thread %s: Camera.__new__\", get_ident())\r\n        if cls._instance is None:\r\n            logger.debug(\r\n                \"Thread %s: Camera.__new__ - Instantiating Camera Class\", get_ident()\r\n            )\r\n            cls._instance = super(Camera, cls).__new__(cls)\r\n            cls.cam = None\r\n            cls.camIsUsb = False\r\n            cls.camUsbDev = \"\"\r\n            cls.camHasAi = False\r\n            cls.camWaitingForFirstFrame = True\r\n            cls.camProgressCounter = 0\r\n            cls.cam_imx500 = None\r\n            cls.cam_imx500_last_detections = []\r\n            cls.cam_imx500_last_results = None\r\n            cls.cam_imx500_labels = None\r\n            cls.cam_imx500_last_boxes = None\r\n            cls.cam_imx500_last_scores = None\r\n            cls.cam_imx500_last_keypoints = None\r\n            cls.cam_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n            cls.cam_imx500_last_overlay = None\r\n            cls.cam_drawer = None\r\n            cls.camNum = -1\r\n            cls.cam2 = None\r\n            cls.cam2IsUsb = False\r\n            cls.cam2UsbDev = \"\"\r\n            cls.cam2HasAi = False\r\n            cls.cam2WaitingForFirstFrame = True\r\n            cls.cam2ProgressCounter = 0\r\n            cls.cam2_imx500 = None\r\n            cls.cam2_imx500_last_detections = []\r\n            cls.cam2_imx500_last_results = None\r\n            cls.cam2_imx500_labels = None\r\n            cls.cam2_imx500_last_boxes = None\r\n            cls.cam2_imx500_last_scores = None\r\n            cls.cam2_imx500_last_keypoints = None\r\n            cls.cam2_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n            cls.cam2_imx500_last_overlay = None\r\n            cls.cam2_drawer = None\r\n            cls.camNum2 = -1\r\n            cls.ctrl: CameraController = None\r\n            cls.ctrl2: CameraController = None\r\n            cls.videoOutput = None\r\n            cls.videoOutput2 = None\r\n            cls.prgVideoOutput = None\r\n            cls.prgVideoOutput2 = None\r\n            cls.photoSeries: Series = None\r\n            cls.thread = None\r\n            cls.threadLock = allocate_lock()\r\n            cls.thread2 = None\r\n            cls.thread2Lock = allocate_lock()\r\n            cls.threadUsbVideo = None\r\n            cls.threadUsbVideoLock = allocate_lock()\r\n            cls.liveViewDeactivated = False\r\n            cls.liveView2Deactivated = False\r\n            cls.videoThread = None\r\n            cls.videoThread2 = None\r\n            cls.photoSeriesThread = None\r\n            cls.frame = None\r\n            cls.frame2 = None\r\n            cls.frameRaw = None\r\n            cls.frame2Raw = None\r\n            cls.streamOutput = None\r\n            cls.stream2Output = None\r\n            cls.last_access = 0\r\n            cls.last_access2 = 0\r\n            cls.stopRequested = False\r\n            cls.stopRequested2 = False\r\n            cls.stopVideoRequested = False\r\n            cls.stopVideoRequested2 = False\r\n            cls.stopUsbVideoRequested = False\r\n            cls.videoDuration = 0\r\n            cls.videoDuration2 = 0\r\n            cls.stopPhotoSeriesRequested = False\r\n            cls.resetScalerCropRequested = False\r\n            cls.event = CameraEvent()\r\n            cls.event2 = None\r\n            cls.when_photo_taken = None\r\n            cls.when_photo_2_taken = None\r\n            cls.when_series_photo_taken = None\r\n            cls.when_recording_starts = None\r\n            cls.when_recording_stops = None\r\n            cls.when_recording_2_starts = None\r\n            cls.when_recording_2_stops = None\r\n            cls.when_streaming_1_starts = None\r\n            cls.when_streaming_1_stops = None\r\n            cls.when_streaming_2_starts = None\r\n            cls.when_streaming_2_stops = None\r\n\r\n            cls.initCamera()\r\n        else:\r\n            if CameraCfg().serverConfig.noCamera == False:\r\n                if cls.cam is None:\r\n                    cls.initCamera()\r\n                else:\r\n                    CameraCfg().serverConfig.error = None\r\n        return cls._instance\r\n\r\n    @classmethod\r\n    def isCamera2Available(cls) -> bool:\r\n        \"\"\"Check if the second camera is available\r\n        Returns True if the second camera is available, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.isCamera2Available\", get_ident())\r\n        if cls.cam2 is not None:\r\n            return True\r\n            logger.debug(\r\n                \"Thread %s: Camera.isCamera2Available - Second camera is available\",\r\n                get_ident(),\r\n            )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.isCamera2Available - Second camera not available\",\r\n                get_ident(),\r\n            )\r\n            return False\r\n\r\n    @classmethod\r\n    def initCamera(cls):\r\n        \"\"\"Instantiate the camera\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.initCamera - Instantiating Camera Class\", get_ident()\r\n        )\r\n\r\n        prgLogger.debug(\r\n            \"from picamera2 import Picamera2, CameraConfiguration, StreamConfiguration, Controls\"\r\n        )\r\n        prgLogger.debug(\"from libcamera import Transform, Size, ColorSpace, controls\")\r\n        prgLogger.debug(\"from picamera2.encoders import JpegEncoder, MJPEGEncoder\")\r\n        if useSensorConfiguration:\r\n            prgLogger.debug(\"from picamera2.configuration import SensorConfiguration\")\r\n        prgLogger.debug(\"from picamera2.outputs import FileOutput, FfmpegOutput\")\r\n        prgLogger.debug(\"from picamera2.encoders import H264Encoder\")\r\n        prgLogger.debug(\"import time\")\r\n        prgLogger.debug(\"import os\")\r\n        prgLogger.debug(\"import gc\")\r\n        prgLogger.debug(\"import logging\")\r\n        prgLogger.debug(\"Picamera2.set_logging(logging.ERROR)\")\r\n        prgLogger.debug('os.environ[\"LIBCAMERA_LOG_LEVELS\"] = \"*:3\"')\r\n        prgLogger.debug(\"videoDuration = 10\")\r\n\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        sc.error = None\r\n        # Before all, load the global camera info to get the installed cameras and the active cam\r\n        activeCam, activeCamIsUsb, activeCamUsbDev, activeCamHasAi = cls.getActiveCamera()\r\n        if sc.noCamera == True:\r\n            return\r\n\r\n        if cls.cam is None:\r\n            logger.debug(\r\n                \"Thread %s: Camera.initCamera: Active camera is None - Needing initialization\",\r\n                get_ident(),\r\n            )\r\n            if activeCamIsUsb == False:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.initCamera: Instantiating Pi camera %s\",\r\n                    get_ident(),\r\n                    activeCam,\r\n                )\r\n                cls.camIsUsb = False\r\n                cls.camUsbDev = activeCamUsbDev\r\n                cls.camHasAi = activeCamHasAi\r\n                try:\r\n                    tc = cfg.tuningConfig\r\n                    if tc.loadTuningFile == False:\r\n                        cls.cam = Picamera2(activeCam)\r\n                        prgLogger.debug(\"picam2 = Picamera2(%s)\", activeCam)\r\n                    else:\r\n                        tuning = Picamera2.load_tuning_file(\r\n                            tc.tuningFile, tc.tuningFolder\r\n                        )\r\n                        cls.cam = Picamera2(activeCam, tuning=tuning)\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.initCamera - Initialized camera %s with tuning file %s\",\r\n                            get_ident(),\r\n                            activeCam,\r\n                            tc.tuningFilePath,\r\n                        )\r\n                        prgLogger.debug(\r\n                            \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                            tc.tuningFile,\r\n                            tc.tuningFolder,\r\n                        )\r\n                        prgLogger.debug(\r\n                            \"picam2 = Picamera2(%s, tuning=tuning)\", activeCam\r\n                        )\r\n                    cls.camNum = activeCam\r\n                    cls.ctrl = CameraController(cls.camIsUsb, cls.camUsbDev)\r\n                except RuntimeError as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.initCamera - Error %s\", get_ident(), e\r\n                    )\r\n                    if not sc.error:\r\n                        sc.error = \"Error while initializing camera: \" + str(e)\r\n                        sc.error2 = \"Probably another process is using the camera.\"\r\n                        sc.errorSource = \"Picamera2\"\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.initCamera: Instantiating USB camera %s\",\r\n                    get_ident(),\r\n                    activeCam,\r\n                )\r\n                cls.camIsUsb = True\r\n                cls.camUsbDev = activeCamUsbDev\r\n                cls.camHasAi = activeCamHasAi\r\n                cls.cam = cv2.VideoCapture(cls.camUsbDev, cv2.CAP_V4L2)\r\n                if not cls.cam or not cls.cam.isOpened():\r\n                    logger.error(\r\n                        \"Thread %s: Camera.initCamera - Error: USB camera not opened\",\r\n                        get_ident(),\r\n                    )\r\n                    sc.error = \"Error while initializing camera: USB camera not opened\"\r\n                    sc.errorSource = \"CV2\"\r\n                else:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.initCamera - Initialized USB camera %s\",\r\n                        get_ident(),\r\n                        activeCam,\r\n                    )\r\n                    cls.camNum = activeCam\r\n                    cls.ctrl = CameraController(cls.camIsUsb, cls.camUsbDev)\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.initCamera: Active camera is already set for %s. Checking if switch is needed\",\r\n                get_ident(),\r\n                Camera.camNum,\r\n            )\r\n            if activeCam != Camera.camNum:\r\n                try:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.initCamera: About to switch camera from %s to %s\",\r\n                        get_ident(),\r\n                        Camera.camNum,\r\n                        activeCam,\r\n                    )\r\n                    cls.stopCameraSystem()\r\n                    if activeCamIsUsb == False:\r\n                        tc = cfg.tuningConfig\r\n                        if tc.loadTuningFile == False:\r\n                            cls.cam = Picamera2(activeCam)\r\n                            prgLogger.debug(\"picam2 = Picamera2(%s)\", activeCam)\r\n                        else:\r\n                            tuning = Picamera2.load_tuning_file(\r\n                                tc.tuningFile, tc.tuningFolder\r\n                            )\r\n                            cls.cam = Picamera2(activeCam, tuning=tuning)\r\n                            logger.debug(\r\n                                \"Thread %s: Camera.initCamera - Initialized camera %s with tuning file %s\",\r\n                                get_ident(),\r\n                                activeCam,\r\n                                tc.tuningFilePath,\r\n                            )\r\n                            prgLogger.debug(\r\n                                \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                                tc.tuningFile,\r\n                                tc.tuningFolder,\r\n                            )\r\n                            prgLogger.debug(\r\n                                \"picam2 = Picamera2(%s, tuning=tuning)\", activeCam\r\n                            )\r\n                        cls.camNum = activeCam\r\n                        cls.camIsUsb = False\r\n                        cls.camUsbDev = \"\"\r\n                        cls.camHasAi = activeCamHasAi\r\n                        cls.ctrl = CameraController(cls.camIsUsb, cls.camUsbDev)\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.initCamera: Switch camera to %s successful\",\r\n                            get_ident(),\r\n                            activeCam,\r\n                        )\r\n                        # Force refresh of camera properties\r\n                        cfg.cameraProperties.model = None\r\n                        cfg.sensorModes = []\r\n                        cfg.rawFormats = []\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.initCamera: Camera-specific configs were reset\",\r\n                            get_ident(),\r\n                        )\r\n                    else:\r\n                        cls.cam = cv2.VideoCapture(activeCamUsbDev, cv2.CAP_V4L2)\r\n                        if not cls.cam or not cls.cam.isOpened():\r\n                            raise RuntimeError(\"USB camera not opened\")\r\n                        cls.camNum = activeCam\r\n                        cls.camIsUsb = True\r\n                        cls.camUsbDev = activeCamUsbDev\r\n                        cls.camHasAi = activeCamHasAi\r\n                        cls.ctrl = CameraController(cls.camIsUsb, cls.camUsbDev)\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.initCamera: Switch camera to %s successful\",\r\n                            get_ident(),\r\n                            activeCam,\r\n                        )\r\n                        # Force refresh of camera properties\r\n                        cfg.cameraProperties.model = None\r\n                        cfg.sensorModes = []\r\n                        cfg.rawFormats = []\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.initCamera: Camera-specific configs were reset\",\r\n                            get_ident(),\r\n                        )\r\n                except RuntimeError as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.initCamera - Error %s\", get_ident(), e\r\n                    )\r\n                    if not sc.error:\r\n                        if activeCamIsUsb == False:\r\n                            sc.error = \"Error while initializing camera: \" + str(e)\r\n                            sc.error2 = \"Probably another process is using the camera.\"\r\n                            sc.errorSource = \"Picamera2\"\r\n                        else:\r\n                            sc.error = (\r\n                                \"Error while initializing camera: USB camera not opened\"\r\n                            )\r\n                            sc.errorSource = \"CV2\"\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.initCamera - Error %s\", get_ident(), e\r\n                    )\r\n                    if not sc.error:\r\n                        sc.error = \"Error while initializing camera: \" + str(e)\r\n                        sc.errorSource = \"Picamera2\"\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.initCamera: Camera was already instantiated\",\r\n                    get_ident(),\r\n                )\r\n        if not sc.error:\r\n            if cls.camIsUsb == False:\r\n                cls.loadCameraSpecifics()\r\n            else:\r\n                if cls.loadUsbCameraSpecifics() == False:\r\n                    sc.error = \"USB Camera not found. Apply Settings/Configuration/Reload Cameras\"\r\n                    sc.errorSource = \"V4L2\"\r\n        if not sc.error:\r\n            cls.setSecondCamera()\r\n\r\n        if (\r\n            sc.isPhotoSeriesRecording == False\r\n            and sc.isVideoRecording == False\r\n            and sc.isLiveStream == False\r\n        ):\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n        if sc.isLiveStream2 == False:\r\n            if Camera.cam2:\r\n                Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n\r\n    @staticmethod\r\n    def getActiveCamera() -> tuple:\r\n        \"\"\"Determine the active camera and return its number, whether it is USB, and USB device path\r\n\r\n        First load the global camera info, if not already done,\r\n        Which gives us the list of currently connected cameras.\r\n\r\n        Then check the active camera and return it.\r\n        If a stored configuration had an active camera, camera number (Num) and model are checked.\r\n\r\n        Returns:\r\n            tuple: (active camera number (int),\r\n                    is USB (bool),\r\n                    USB device path (str),\r\n                    has AI capabilities (bool)\r\n                    )\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.getActiveCamera\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        trc = cfg.triggerConfig\r\n        if (len(cfg.cameras) == 0) and (sc.noCamera == False):\r\n            cfgCams = []\r\n            cams = Picamera2.global_camera_info()\r\n            if len(cams) == 0:\r\n                sc.noCamera = True\r\n                logger.debug(\r\n                    \"Thread %s: Camera.getActiveCamera - no cameras found\", get_ident()\r\n                )\r\n                return 0, False, \"\", False\r\n            camNum = 0\r\n            for camera in cams:\r\n                cfgCam = CameraInfo()\r\n                if \"Model\" in camera:\r\n                    cfgCam.model = camera[\"Model\"]\r\n                    if cfgCam.model == \"imx500\":\r\n                        cfgCam.hasAi = True\r\n                if \"Location\" in camera:\r\n                    cfgCam.location = camera[\"Location\"]\r\n                if \"Rotation\" in camera:\r\n                    cfgCam.rotation = camera[\"Rotation\"]\r\n                if \"Id\" in camera:\r\n                    cfgCam.id = camera[\"Id\"]\r\n                    # Check for USB camera\r\n                    if cfgCam.id.find(\"/usb@\") > 0:\r\n                        cfgCam.isUsb = True\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.getActiveCamera - USB camera found:  %s\",\r\n                            get_ident(),\r\n                            cfgCam.id,\r\n                        )\r\n                    else:\r\n                        cfgCam.usbDev = \"\"\r\n                # On Bullseye systems, \"Num\" is not in the dict\r\n                if \"Num\" in camera:\r\n                    cfgCam.num = camera[\"Num\"]\r\n                else:\r\n                    cfgCam.num = camNum\r\n                    camNum += 1\r\n                cfgCam.setUsbDev()\r\n                cfgCams.append(cfgCam)\r\n            cfg.cameras = cfgCams\r\n            logger.debug(\r\n                \"Thread %s: Camera.getActiveCamera - %s cameras found\",\r\n                get_ident(),\r\n                len(cfg.cameras),\r\n            )\r\n            # Set the list of supported cameras\r\n            cfg.setSupportedCameras()\r\n            # Set the list of Pi cameras\r\n            cfg.setPiCameras()\r\n\r\n        # Check that active camera is within the list of cameras\r\n        logger.debug(\r\n            \"Thread %s: Camera.getActiveCamera - Checking active camera %s (model: %s, isUsb: %s, usbDev: %s, hasAi: %s) against %s found cameras\",\r\n            get_ident(),\r\n            sc.activeCamera,\r\n            sc.activeCameraModel,\r\n            sc.activeCameraIsUsb,\r\n            sc.activeCameraUsbDev,\r\n            sc.activeCameraHasAi,\r\n            len(cfg.cameras)\r\n        )\r\n        activeCamOK = False\r\n        if sc.activeCameraModel != \"\":\r\n            for cfgCam in sc.supportedCameras:\r\n                if (\r\n                    cfgCam.num == sc.activeCamera\r\n                    and cfgCam.model == sc.activeCameraModel\r\n                    and cfgCam.isUsb == sc.activeCameraIsUsb\r\n                    and cfgCam.usbDev == sc.activeCameraUsbDev\r\n                    and cfgCam.hasAi == sc.activeCameraHasAi\r\n                ):\r\n                    activeCamOK = True\r\n                    break\r\n            logger.debug(\r\n                \"Thread %s: Camera.getActiveCamera - Active camera:%s - activeCamOK:%s\",\r\n                get_ident(),\r\n                sc.activeCamera,\r\n                activeCamOK,\r\n            )\r\n        # If config for active camera is not in the list,\r\n        # set it to the first camera\r\n        if activeCamOK == False:\r\n            logger.debug(\r\n                \"Thread %s: Camera.getActiveCamera - Resetting active camera to first of %s supported cameras\",\r\n                get_ident(),\r\n                len(sc.supportedCameras),\r\n            )\r\n            for cfgCam in sc.supportedCameras:\r\n                sc.activeCamera = cfgCam.num\r\n                sc.activeCameraInfo = (\r\n                    \"Camera \" + str(cfgCam.num) + \" (\" + cfgCam.model + \")\"\r\n                )\r\n                sc.activeCameraModel = cfgCam.model\r\n                sc.activeCameraIsUsb = cfgCam.isUsb\r\n                sc.activeCameraHasAi = cfgCam.hasAi\r\n                sc.activeCameraUsbDev = cfgCam.usbDev\r\n                break\r\n            logger.debug(\r\n                \"Thread %s: Camera.getActiveCamera - active camera reset to %s\",\r\n                get_ident(),\r\n                sc.activeCamera,\r\n            )\r\n            # Reset the active camera configuration\r\n            cfg.resetActiveCameraSettings()\r\n            cfg.aiConfig=AiConfig()\r\n            trc.setCameraSettingsToDefault()\r\n            sc.unsavedChanges = True\r\n            sc.addChangeLogEntry(\r\n                f\"Camera settings for {sc.activeCameraInfo} were reset due to camera model change\"\r\n            )\r\n\r\n        # Make sure that folder for photos exists\r\n        sc.cameraPhotoSubPath = \"photos/\" + \"camera_\" + str(sc.activeCamera)\r\n        fp = sc.photoRoot + \"/\" + sc.cameraPhotoSubPath\r\n        if not os.path.exists(fp):\r\n            os.makedirs(fp)\r\n            logger.debug(\r\n                \"Thread %s: Camera.getActiveCamera - Photo directory created %s\",\r\n                get_ident(),\r\n                fp,\r\n            )\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.getActiveCamera - activeCamera: %s - isUsb: %s - usbDev: %s - hasAi: %s\",\r\n            get_ident(),\r\n            sc.activeCamera,\r\n            sc.activeCameraIsUsb,\r\n            sc.activeCameraUsbDev,\r\n            sc.activeCameraHasAi\r\n        )\r\n        return sc.activeCamera, sc.activeCameraIsUsb, sc.activeCameraUsbDev, sc.activeCameraHasAi\r\n\r\n    @classmethod\r\n    def switchCamera(cls):\r\n        \"\"\"Switch the camera\"\"\"\r\n        logger.debug(\"Thread %s: Camera.switchCamera\", get_ident())\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.switchCamera - stopping Live Stream\", get_ident()\r\n        )\r\n        cls.stopLiveStream()\r\n        logger.debug(\r\n            \"Thread %s: Camera.switchCamera - Live Stream stopped\", get_ident()\r\n        )\r\n        if cls.cam2:\r\n            cls.stopLiveStream2()\r\n            logger.debug(\r\n                \"Thread %s: Camera.switchCamera - Live Stream2 stopped\", get_ident()\r\n            )\r\n\r\n        time.sleep(1)\r\n\r\n        activeCam, activeCamIsUsb, activeCamUsbDev, activeCamHasAi = Camera.getActiveCamera()\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        trc = cfg.triggerConfig\r\n        if sc.noCamera == True:\r\n            return\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.switchCamera - cfg.aiConfig 1: %s\", get_ident(), cfg.aiConfig.__dict__\r\n        )\r\n\r\n        if Camera.cam is None:\r\n            if activeCamIsUsb == False:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera: Instantiating Pi camera %s\",\r\n                    get_ident(),\r\n                    activeCam,\r\n                )\r\n                cls.camIsUsb = False\r\n                cls.camUsbDev = \"\"\r\n                cls.camHasAi = activeCamHasAi\r\n                tc = cfg.tuningConfig\r\n                ai = cfg.aiConfig\r\n                if tc.loadTuningFile == False:\r\n                    cls.cam = Picamera2(activeCam)\r\n                    prgLogger.debug(\"picam2 = Picamera2(%s)\", activeCam)\r\n                else:\r\n                    tuning = Picamera2.load_tuning_file(tc.tuningFile, tc.tuningFolder)\r\n                    cls.cam = Picamera2(activeCam, tuning=tuning)\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera - Initialized camera %s with tuning file %s\",\r\n                        get_ident(),\r\n                        activeCam,\r\n                        tc.tuningFilePath,\r\n                    )\r\n                    prgLogger.debug(\r\n                        \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                        tc.tuningFile,\r\n                        tc.tuningFolder,\r\n                    )\r\n                    prgLogger.debug(\"picam2 = Picamera2(%s, tuning=tuning)\", activeCam)\r\n\r\n                # Set model for AI Camera\r\n                if ai.enable:\r\n                    # Try to import IMX500\r\n                    try:\r\n                        from picamera2.devices import IMX500\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.switchCamera - import IMX500 successful\",\r\n                            get_ident(),\r\n                        )\r\n                    except ImportError:\r\n                        logger.error(\r\n                            \"Camera.switchCamera - Could not import IMX500 from picamera2.devices\",\r\n                        )\r\n                        ai.enable = False\r\n                if ai.enable:\r\n                    modelPath = os.path.join(ai.modelFolder, ai.modelFile)\r\n                    Camera.cam_imx500 = IMX500(modelPath)\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera - IMX500 instantiated with model: %s\",\r\n                        get_ident(),\r\n                        modelPath,\r\n                    )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera: Instantiating USB camera %s\",\r\n                    get_ident(),\r\n                    activeCam,\r\n                )\r\n                cls.camIsUsb = True\r\n                cls.camUsbDev = activeCamUsbDev\r\n                cls.camHasAi = activeCamHasAi\r\n                cls.cam = cv2.VideoCapture(cls.camUsbDev, cv2.CAP_V4L2)\r\n                if not cls.cam or not cls.cam.isOpened():\r\n                    logger.error(\r\n                        \"Thread %s: Camera.initCamera - Error: USB camera not opened\",\r\n                        get_ident(),\r\n                    )\r\n            Camera.camNum = activeCam\r\n            Camera.camIsUsb = activeCamIsUsb\r\n            Camera.camUsbDev = activeCamUsbDev\r\n            Camera.camHasAi = activeCamHasAi\r\n            Camera.ctrl = CameraController(Camera.camIsUsb, Camera.camUsbDev)\r\n        else:\r\n            if activeCam != Camera.camNum:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera: About to switch camera from %s to %s\",\r\n                    get_ident(),\r\n                    Camera.camNum,\r\n                    activeCam,\r\n                )\r\n\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera - cfg.aiConfig 2: %s\", get_ident(), cfg.aiConfig.__dict__\r\n                )\r\n                Camera.stopCameraSystem()\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera - cfg.aiConfig 3: %s\", get_ident(), cfg.aiConfig.__dict__\r\n                )\r\n                if activeCamIsUsb == False:\r\n                    tc = cfg.tuningConfig\r\n                    ai = cfg.aiConfig\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: tc.loadTuningFile=%s\",\r\n                        get_ident(),\r\n                        tc.loadTuningFile,\r\n                    )\r\n                    if tc.loadTuningFile == False:\r\n                        cls.cam = Picamera2(activeCam)\r\n                        prgLogger.debug(\"picam2 = Picamera2(%s)\", activeCam)\r\n                    else:\r\n                        tuning = Picamera2.load_tuning_file(\r\n                            tc.tuningFile, tc.tuningFolder\r\n                        )\r\n                        cls.cam = Picamera2(activeCam, tuning=tuning)\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.switchCamera - Initialized camera %s with tuning file %s\",\r\n                            get_ident(),\r\n                            activeCam,\r\n                            tc.tuningFilePath,\r\n                        )\r\n                        prgLogger.debug(\r\n                            \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                            tc.tuningFile,\r\n                            tc.tuningFolder,\r\n                        )\r\n                        prgLogger.debug(\r\n                            \"picam2 = Picamera2(%s, tuning=tuning)\", activeCam\r\n                        )\r\n                    \r\n                    # Set model for AI Camera\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: ai.enable=%s\",\r\n                        get_ident(),\r\n                        ai.enable,\r\n                    )\r\n                    if ai.enable:\r\n                        # Try to import IMX500\r\n                        try:\r\n                            from picamera2.devices import IMX500\r\n                            logger.debug(\r\n                                \"Thread %s: Camera.switchCamera - import IMX500 successful\",\r\n                                get_ident(),\r\n                            )\r\n                        except ImportError:\r\n                            logger.error(\r\n                                \"Camera.switchCamera - Could not import IMX500 from picamera2.devices\",\r\n                            )\r\n                            ai.enable = False\r\n                    if ai.enable:\r\n                        modelPath = os.path.join(ai.modelFolder, ai.modelFile)\r\n                        Camera.cam_imx500 = IMX500(modelPath)\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.switchCamera - IMX500 instantiated with model: %s\",\r\n                            get_ident(),\r\n                            modelPath,\r\n                        )\r\n                    Camera.camNum = activeCam\r\n                    Camera.camIsUsb = False\r\n                    Camera.camUsbDev = \"\"\r\n                    Camera.camHasAi = activeCamHasAi\r\n                    Camera.ctrl = CameraController(Camera.camIsUsb, Camera.camUsbDev)\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: Switch camera to %s successful\",\r\n                        get_ident(),\r\n                        activeCam,\r\n                    )\r\n                    # Force refresh of camera properties\r\n                    CameraCfg().cameraProperties.model = None\r\n                    CameraCfg().sensorModes = []\r\n                    CameraCfg().rawFormats = []\r\n                    CameraCfg().resetActiveCameraSettings() \r\n                    trc.setCameraSettingsToDefault()\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: Camera-specific configs were reset\",\r\n                        get_ident(),\r\n                    )\r\n                else:\r\n                    cls.cam = cv2.VideoCapture(activeCamUsbDev, cv2.CAP_V4L2)\r\n                    if not cls.cam or not cls.cam.isOpened():\r\n                        logger.error(\r\n                            \"Thread %s: Camera.switchCamera - Error: USB camera not opened\",\r\n                            get_ident(),\r\n                        )\r\n                    Camera.camNum = activeCam\r\n                    Camera.camIsUsb = True\r\n                    Camera.camUsbDev = activeCamUsbDev\r\n                    Camera.camHasAi = activeCamHasAi\r\n                    Camera.ctrl = CameraController(Camera.camIsUsb, Camera.camUsbDev)\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: Switch camera to %s successful\",\r\n                        get_ident(),\r\n                        activeCam,\r\n                    )\r\n                    # Force refresh of camera properties\r\n                    CameraCfg().cameraProperties.model = None\r\n                    CameraCfg().sensorModes = []\r\n                    CameraCfg().rawFormats = []\r\n                    CameraCfg().resetActiveCameraSettings() \r\n                    trc.setCameraSettingsToDefault()\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.switchCamera: Camera-specific configs were reset\",\r\n                        get_ident(),\r\n                    )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.switchCamera: Camera was already instantiated\",\r\n                    get_ident(),\r\n                )\r\n\r\n        time.sleep(1)\r\n\r\n        if cls.camIsUsb == False:\r\n            cls.loadCameraSpecifics()\r\n            cls.setSecondCamera()\r\n        else:\r\n            if cls.loadUsbCameraSpecifics() == False:\r\n                sc.error = \"USB Camera not found. Apply Settings/Configuration/Reload Cameras\"\r\n                sc.errorSource = \"V4L2\"\r\n            else:\r\n                cls.setSecondCamera()\r\n\r\n        # Restore streaming config, if available\r\n        cls.restoreConfigFromStreamingConfig()\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.switchCamera - starting Live Stream\", get_ident()\r\n        )\r\n        cls.startLiveStream()\r\n        logger.debug(\r\n            \"Thread %s: Camera.switchCamera - Live Stream started\", get_ident()\r\n        )\r\n\r\n        logger.debug(\"Thread %s: Camera.switchCamera - second camera set\", get_ident())\r\n        if cls.cam2:\r\n            cls.startLiveStream2()\r\n            logger.debug(\r\n                \"Thread %s: Camera.switchCamera - Live Stream 2 started\", get_ident()\r\n            )\r\n\r\n    @classmethod\r\n    def startLiveStream(cls):\r\n        \"\"\"Start thread for live stream\"\"\"\r\n        logger.debug(\"Thread %s: Camera.startLiveStream\", get_ident())\r\n        if (not CameraCfg().serverConfig.error) and (not CameraCfg().serverConfig.noCamera):\r\n            if Camera.liveViewDeactivated:\r\n                logger.debug(\r\n                    \"Thread %s: Not starting Live View thread. Live View deactivated\",\r\n                    get_ident(),\r\n                )\r\n                CameraCfg().serverConfig.isLiveStream = False\r\n            else:\r\n                with Camera.threadLock:\r\n                    Camera.last_access = time.time()\r\n                logger.debug(\r\n                    \"Thread %s: Camera.startLiveStream - last_access set\", get_ident()\r\n                )\r\n                if Camera.thread is None:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.startLiveStream: Starting new thread\",\r\n                        get_ident(),\r\n                    )\r\n\r\n                    # start background frame thread\r\n                    Camera.thread = threading.Thread(target=cls._thread)\r\n                    Camera.thread.start()\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.startLiveStream - Thread started\",\r\n                        get_ident(),\r\n                    )\r\n\r\n                    # wait until first frame is available\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.startLiveStream - waiting for frame\",\r\n                        get_ident(),\r\n                    )\r\n                    # Waiting not necessary if camera-start animation is shown\r\n                    if cv2Available == False:\r\n                        Camera.event.wait()\r\n                    if not CameraCfg().serverConfig.error:\r\n                        CameraCfg().serverConfig.isLiveStream = True\r\n                else:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.startLiveStream - Thread exists\", get_ident()\r\n                    )\r\n                    if not Camera.thread.is_alive:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream - Thread is not alive\",\r\n                            get_ident(),\r\n                        )\r\n                        Camera.thread = threading.Thread(target=cls._thread)\r\n                        Camera.thread.start()\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream - Thread started\",\r\n                            get_ident(),\r\n                        )\r\n\r\n    @classmethod\r\n    def startLiveStream2(cls):\r\n        \"\"\"Start thread for live stream\"\"\"\r\n        logger.debug(\"Thread %s: Camera.startLiveStream2\", get_ident())\r\n        if not CameraCfg().serverConfig.errorc2:\r\n            if cls.cam2:\r\n                if Camera.liveView2Deactivated:\r\n                    logger.debug(\r\n                        \"Thread %s: Not starting Live View 2 thread. Live View 2 deactivated\",\r\n                        get_ident(),\r\n                    )\r\n                    CameraCfg().serverConfig.isLiveStream2 = False\r\n                else:\r\n                    # logger.debug(\"Thread %s: Camera.startLiveStream2 - About to acquire Lock: thread2Lock=%s.\", get_ident(), Camera.thread2Lock.locked())\r\n                    with Camera.thread2Lock:\r\n                        Camera.last_access2 = time.time()\r\n                    # logger.debug(\"Thread %s: Camera.startLiveStream2 - last_access2 set\", get_ident())\r\n                    if Camera.thread2 is None:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream2: Starting new thread\",\r\n                            get_ident(),\r\n                        )\r\n\r\n                        # start background frame thread\r\n                        Camera.thread2 = threading.Thread(target=cls._thread2)\r\n                        Camera.thread2.start()\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream2 - Thread started\",\r\n                            get_ident(),\r\n                        )\r\n\r\n                        # wait until first frame is available\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream2 - waiting for frame\",\r\n                            get_ident(),\r\n                        )\r\n                        # Waiting not necessary if camera-start animation is shown\r\n                        if cv2Available == False:\r\n                            Camera.event2.wait()\r\n                        if not CameraCfg().serverConfig.errorc2:\r\n                            CameraCfg().serverConfig.isLiveStream2 = True\r\n                    else:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.startLiveStream2 - Thread exists\",\r\n                            get_ident(),\r\n                        )\r\n                        if not Camera.thread2.is_alive:\r\n                            logger.debug(\r\n                                \"Thread %s: Camera.startLiveStream2 - Thread is not alive\",\r\n                                get_ident(),\r\n                            )\r\n                            Camera.thread2 = threading.Thread(target=cls._thread2)\r\n                            Camera.thread2.start()\r\n                            logger.debug(\r\n                                \"Thread %s: Camera.startLiveStream2 - Thread started\",\r\n                                get_ident(),\r\n                            )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.startLiveStream2 - Not starting Live View 2 thread. Second camera not available\",\r\n                    get_ident(),\r\n                )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.startLiveStream2 - Not starting Live View 2 thread. Error present: %s\",\r\n                get_ident(),\r\n                CameraCfg().serverConfig.errorc2,\r\n            )\r\n\r\n    @classmethod\r\n    def stopLiveStream(cls):\r\n        \"\"\"Stop thread for live stream\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopLiveStream\", get_ident())\r\n        if not Camera.thread is None:\r\n            logger.debug(\r\n                \"Thread %s: Camera.stopLiveStream - stopping live stream thread\",\r\n                get_ident(),\r\n            )\r\n            Camera.stopRequested = True\r\n            cnt = 0\r\n            while Camera.thread:\r\n                time.sleep(0.01)\r\n                cnt += 1\r\n                if cnt > 200:\r\n                    # Assume thread dead\r\n                    Camera.thread = None\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.stopLiveStream: Thread assumed dead\",\r\n                        get_ident(),\r\n                    )\r\n                    break\r\n                    # raise TimeoutError(\"Background thread did not stop within 2 sec\")\r\n            if cnt < 200:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopLiveStream: Thread has stopped\", get_ident()\r\n                )\r\n            Camera.ctrl.stopEncoder(Camera.cam, Camera.ENCODER_LIVESTREAM)\r\n            CameraCfg().serverConfig.isLiveStream = False\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.stopLiveStream: Thread was not started\", get_ident()\r\n            )\r\n            CameraCfg().serverConfig.isLiveStream = False\r\n\r\n    @classmethod\r\n    def stopLiveStream2(cls):\r\n        \"\"\"Stop thread for live stream 2\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopLiveStream2\", get_ident())\r\n        if Camera.cam2:\r\n            if not Camera.thread2 is None:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopLiveStream2 - stopping live stream thread\",\r\n                    get_ident(),\r\n                )\r\n                Camera.stopRequested2 = True\r\n                cnt = 0\r\n                while Camera.thread2:\r\n                    time.sleep(0.01)\r\n                    cnt += 1\r\n                    if cnt > 200:\r\n                        # Assume thread dead\r\n                        Camera.thread2 = None\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.stopLiveStream2: Thread assumed dead\",\r\n                            get_ident(),\r\n                        )\r\n                        break\r\n                        # raise TimeoutError(\"Background thread did not stop within 2 sec\")\r\n                if cnt < 200:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.stopLiveStream2: Thread has stopped\",\r\n                        get_ident(),\r\n                    )\r\n                Camera.ctrl2.stopEncoder(Camera.cam2, Camera.ENCODER_LIVESTREAM)\r\n                CameraCfg().serverConfig.isLiveStream2 = False\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopLiveStream2: Thread was not started\",\r\n                    get_ident(),\r\n                )\r\n                CameraCfg().serverConfig.isLiveStream2 = False\r\n\r\n    @staticmethod\r\n    def restartLiveStream():\r\n        logger.debug(\"Thread %s: Camera.restartLiveStream\", get_ident())\r\n        Camera.liveViewDeactivated = True\r\n        Camera.stopLiveStream()\r\n        time.sleep(0.5)\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream: Live stream stopped\", get_ident()\r\n        )\r\n        Camera.cam, done = Camera.ctrl.requestStop(Camera.cam)\r\n        logger.debug(\"Thread %s: Camera.restartLiveStream: Camera stopped\", get_ident())\r\n        time.sleep(0.5)\r\n        Camera.ctrl.clearConfig()\r\n        logger.debug(\"Thread %s: Camera.restartLiveStream: Config cleared\", get_ident())\r\n        Camera.liveViewDeactivated = False\r\n        Camera.startLiveStream()\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream: Live stream started\", get_ident()\r\n        )\r\n\r\n    @staticmethod\r\n    def restartLiveStream2():\r\n        logger.debug(\"Thread %s: Camera.restartLiveStream2\", get_ident())\r\n        Camera.liveView2Deactivated = True\r\n        Camera.stopLiveStream2()\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream2: Live stream stopped\", get_ident()\r\n        )\r\n        Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2)\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream2: Camera stopped\", get_ident()\r\n        )\r\n        Camera.ctrl2.clearConfig()\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream2: Config cleared\", get_ident()\r\n        )\r\n        Camera.liveView2Deactivated = False\r\n        Camera.startLiveStream2()\r\n        logger.debug(\r\n            \"Thread %s: Camera.restartLiveStream2: Live stream started\", get_ident()\r\n        )\r\n\r\n    def getLiveViewImageForMotionDetection(self):\r\n        \"\"\"Capture and return a buffer\"\"\"\r\n        cfg = CameraCfg()\r\n        if Camera.camIsUsb == False:\r\n            if cfg.triggerConfig.motionDetectAlgo == 0:\r\n                buf = Camera.cam.capture_buffer(cfg.liveViewConfig.stream)\r\n                (w, h) = cfg.liveViewConfig.stream_size\r\n                buf = buf[: w * h].reshape(h, w)\r\n                frameRaw = buf\r\n            else:\r\n                frameRaw = Camera.cam.capture_array(cfg.liveViewConfig.stream)\r\n                if cfg.liveViewConfig.format == \"YUV420\":\r\n                    if cv2Available == True:\r\n                        frameRaw = cv2.cvtColor(frameRaw, cv2.COLOR_YUV2BGR_I420)\r\n        else:\r\n            frame, frameRaw = self.get_frame()\r\n        return copy.copy(frameRaw)\r\n\r\n    def getLeftImageForStereo(self):\r\n        \"\"\"Capture and return a buffer\"\"\"\r\n        if Camera.camIsUsb == False:\r\n            return Camera.cam.capture_array(CameraCfg().liveViewConfig.stream)\r\n        else:\r\n            frame, frameRaw = self.get_frame()\r\n            return frameRaw\r\n\r\n    def getRightImageForStereo(self):\r\n        \"\"\"Capture and return a buffer\"\"\"\r\n        if Camera.camIsUsb == False:\r\n            return Camera.cam2.capture_array(\r\n                CameraCfg().streamingCfg[str(Camera.camNum2)][\"liveconfig\"].stream\r\n            )\r\n        else:\r\n            frame, frameRaw = self.get_frame2()\r\n            return frameRaw\r\n\r\n    def startAnimation(self):\r\n        \"\"\"Create animation while camera is starting\"\"\"\r\n        canvas = np.ones((480, 640, 3), dtype=\"uint8\") * 255 \r\n\r\n        cv2.putText(\r\n            canvas,\r\n            \"Camera Starting\",\r\n            (65, 300),\r\n            cv2.FONT_HERSHEY_SIMPLEX,\r\n            2,\r\n            (0, 0, 0),\r\n            3,\r\n            cv2.LINE_AA,\r\n        )\r\n        if Camera.camHasAi:\r\n            cv2.putText(\r\n                canvas,\r\n                \"imx500 loading model\",\r\n                (140, 350),\r\n                cv2.FONT_HERSHEY_SIMPLEX,\r\n                1,\r\n                (0, 0, 0),\r\n                1,\r\n                cv2.LINE_AA,\r\n            )\r\n            cv2.putText(\r\n                canvas,\r\n                \"This may take a while\",\r\n                (150, 400),\r\n                cv2.FONT_HERSHEY_SIMPLEX,\r\n                1,\r\n                (0, 0, 0),\r\n                1,\r\n                cv2.LINE_AA,\r\n            )\r\n\r\n            angle = Camera.camProgressCounter * math.pi / 60.0\r\n            x = int(320 + 60 * math.cos(angle))\r\n            y = int(140 + 60 * math.sin(angle))\r\n            cv2.circle(canvas, (x, y), 15, (0, 0, 0), -1)\r\n            Camera.camProgressCounter += 1\r\n            time.sleep(0.05)\r\n        return canvas\r\n\r\n    def startAnimation2(self):\r\n        \"\"\"Create animation while camera 2 is starting\"\"\"\r\n        canvas = np.ones((480, 640, 3), dtype=\"uint8\") * 255 \r\n\r\n        cv2.putText(\r\n            canvas,\r\n            \"Camera Starting\",\r\n            (65, 300),\r\n            cv2.FONT_HERSHEY_SIMPLEX,\r\n            2,\r\n            (0, 0, 0),\r\n            3,\r\n            cv2.LINE_AA,\r\n        )\r\n        if Camera.cam2HasAi:\r\n            cv2.putText(\r\n                canvas,\r\n                \"imx500 loading model\",\r\n                (140, 350),\r\n                cv2.FONT_HERSHEY_SIMPLEX,\r\n                1,\r\n                (0, 0, 0),\r\n                1,\r\n                cv2.LINE_AA,\r\n            )\r\n            cv2.putText(\r\n                canvas,\r\n                \"This may take a while\",\r\n                (150, 400),\r\n                cv2.FONT_HERSHEY_SIMPLEX,\r\n                1,\r\n                (0, 0, 0),\r\n                1,\r\n                cv2.LINE_AA,\r\n            )\r\n\r\n            angle = Camera.cam2ProgressCounter * math.pi / 60.0\r\n            x = int(320 + 60 * math.cos(angle))\r\n            y = int(140 + 60 * math.sin(angle))\r\n            cv2.circle(canvas, (x, y), 15, (0, 0, 0), -1)\r\n            Camera.cam2ProgressCounter += 1\r\n            time.sleep(0.05)\r\n        return canvas\r\n\r\n    def get_frame(self):\r\n        \"\"\"Return the current camera frame.\"\"\"\r\n        # logger.debug(\"Thread %s: Camera.get_frame\", get_ident())\r\n        \r\n        with Camera.threadLock:\r\n            Camera.last_access = time.time()\r\n        \r\n        if cv2Available == True:\r\n            if Camera.camWaitingForFirstFrame == True:\r\n                frame = self.startAnimation()\r\n                stat, frame_jpg = cv2.imencode(\".jpg\", frame)\r\n                if stat:\r\n                    return frame_jpg.tobytes(), None\r\n                return None, None\r\n\r\n        # wait for a signal from the camera thread\r\n        # logger.debug(\"Thread %s: Camera.get_frame - waiting for frame\", get_ident())\r\n        Camera.event.wait()\r\n        # logger.debug(\"Thread %s: Camera.get_frame - continue\", get_ident())\r\n        Camera.event.clear()\r\n\r\n        # logger.debug(\"Thread %s: Returning frame\", get_ident())\r\n        return Camera.frame, Camera.frameRaw\r\n\r\n    def get_frame2(self):\r\n        \"\"\"Return the current camera 2 frame.\"\"\"\r\n        # logger.debug(\"Thread %s: Camera.get_frame2\", get_ident())\r\n        if Camera.cam2:\r\n            with Camera.thread2Lock:\r\n                Camera.last_access2 = time.time()\r\n\r\n            if cv2Available == True:\r\n                if Camera.cam2WaitingForFirstFrame == True:\r\n                    frame = self.startAnimation2()\r\n                    stat, frame_jpg = cv2.imencode(\".jpg\", frame)\r\n                    if stat:\r\n                        return frame_jpg.tobytes(), None\r\n                    return None, None\r\n\r\n            # wait for a signal from the camera thread\r\n            # logger.debug(\"Thread %s: Camera.get_frame2 - waiting for frame\", get_ident())\r\n            Camera.event2.wait()\r\n            # logger.debug(\"Thread %s: Camera.get_frame2 - continue\", get_ident())\r\n            Camera.event2.clear()\r\n\r\n            # logger.debug(\"Thread %s: Returning frame2\", get_ident())\r\n            return Camera.frame2, Camera.frame2Raw\r\n        else:\r\n            return None, None\r\n\r\n    def get_photoFrame(self):\r\n        \"\"\"Return the current camera frame.\"\"\"\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame\", get_ident())\r\n        with Camera.threadLock:\r\n            Camera.last_access = time.time()\r\n\r\n        # wait for a signal from the camera thread\r\n        logger.debug(\r\n            \"Thread %s: Camera.get_photoFrame - waiting for frame\", get_ident()\r\n        )\r\n        Camera.event.wait()\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame - continue\", get_ident())\r\n        Camera.event.clear()\r\n\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame - Returning frame\", get_ident())\r\n        return Camera.frame\r\n\r\n    def get_photoFrame_hr(self):\r\n        \"\"\"Return photo frame, assuming that camera is runnuing \"\"\"\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame_hr\", get_ident())\r\n        frame = None\r\n        cfg = CameraCfg()\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    buffer = io.BytesIO()\r\n                    Camera.cam.capture_file(buffer, format=\"jpeg\", name=cfg.photoConfig.stream)\r\n                    frame = buffer.getvalue()\r\n                except Exception as e:\r\n                    logger.error(\"Camera.get_photoFrame_hr - Error %s\", e)\r\n            else:\r\n                err = \"Camera not started\"\r\n                logger.error(\"Camera.get_photoFrame_hr - Error %s\", err)\r\n        else:\r\n            if Camera.cam.isOpened() == True:\r\n                frame, frameRaw = Camera().get_frame()\r\n            else:\r\n                err = \"USB Camera not started\"\r\n                logger.error(\"Camera.get_photoFrame_hr - Error %s\", err)\r\n        return frame\r\n\r\n    def get_photoFrame2(self):\r\n        \"\"\"Return the current camera 2 frame.\"\"\"\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame2\", get_ident())\r\n        if Camera.cam2:\r\n            with Camera.thread2Lock:\r\n                Camera.last_access2 = time.time()\r\n\r\n            # wait for a signal from the camera thread\r\n            logger.debug(\r\n                \"Thread %s: Camera.get_photoFrame2 - waiting for frame\", get_ident()\r\n            )\r\n            Camera.event2.wait()\r\n            logger.debug(\"Thread %s: Camera.get_photoFrame2 - continue\", get_ident())\r\n            Camera.event2.clear()\r\n\r\n            logger.debug(\r\n                \"Thread %s: Camera.get_photoFrame2 - Returning frame\", get_ident()\r\n            )\r\n            return Camera.frame2\r\n        else:\r\n            return None\r\n\r\n    def get_photoFrame2_hr(self):\r\n        \"\"\"Return photo frame, assuming that camera is runnuing \"\"\"\r\n        logger.debug(\"Thread %s: Camera.get_photoFrame2_hr\", get_ident())\r\n        frame = None\r\n        cfg = CameraCfg()\r\n        strc = cfg.streamingCfg\r\n        if Camera.cam2IsUsb == False:\r\n            if Camera.cam2.started:\r\n                camNum2Str = str(Camera.camNum2)\r\n                if camNum2Str in strc:\r\n                    scfg = strc[camNum2Str]\r\n                    if \"photoconfig\" in scfg:\r\n                        stream = scfg[\"photoconfig\"].stream\r\n                        try:\r\n                            buffer = io.BytesIO()\r\n                            Camera.cam2.capture_file(buffer, format=\"jpeg\", name=stream)\r\n                            frame = buffer.getvalue()\r\n                        except Exception as e:\r\n                            logger.error(\"Camera.get_photoFrame2_hr - Error %s\", e)\r\n                    else:\r\n                        err = \"Camera 2 photo config not found\"\r\n                        logger.error(\"Camera.get_photoFrame2_hr - Error %s\", err)\r\n                else:\r\n                    err = \"Camera 2 config not found\"\r\n                    logger.error(\"Camera.get_photoFrame2_hr - Error %s\", err)\r\n            else:\r\n                err = \"Camera 2 not started\"\r\n                logger.error(\"Camera.get_photoFrame2_hr - Error %s\", err)\r\n        else:\r\n            if Camera.cam2.isOpened() == True:\r\n                frame, frameRaw = Camera().get_frame2()\r\n            else:\r\n                err = \"USB Camera not started\"\r\n                logger.error(\"Camera.get_photoFrame2_hr - Error %s\", err)\r\n        return frame\r\n\r\n    @staticmethod\r\n    def loadCameraSpecifics():\r\n        \"\"\"Load camera specific parameters into configuration, if not already done\"\"\"\r\n        logger.debug(\"Thread %s: Camera.loadCameraSpecifics\", get_ident())\r\n        cfg = CameraCfg()\r\n        cfgProps = cfg.cameraProperties\r\n        cfgCtrls = cfg.controls\r\n        cfgSensorModes = cfg.sensorModes\r\n        cfgRawFormats = cfg.rawFormats\r\n\r\n        # Load Camera Properties\r\n        if cfgProps.model is None:\r\n            camPprops = Camera.cam.camera_properties\r\n            cfgProps.model = camPprops[\"Model\"]\r\n            if \"UnitCellSize\" in camPprops:\r\n                cfgProps.unitCellSize = camPprops[\"UnitCellSize\"]\r\n            cfgProps.location = camPprops[\"Location\"]\r\n            cfgProps.rotation = camPprops[\"Rotation\"]\r\n            cfgProps.pixelArraySize = camPprops[\"PixelArraySize\"]\r\n            cfgProps.pixelArrayActiveAreas = camPprops[\"PixelArrayActiveAreas\"]\r\n            cfgProps.colorFilterArrangement = camPprops[\"ColorFilterArrangement\"]\r\n            cfgProps.scalerCropMaximum = camPprops[\"ScalerCropMaximum\"]\r\n            cfgProps.systemDevices = camPprops[\"SystemDevices\"]\r\n            if \"SensorSensitivity\" in camPprops:\r\n                cfgProps.sensorSensitivity = camPprops[\"SensorSensitivity\"]\r\n\r\n            cfgProps.hasFocus = \"AfMode\" in Camera.cam.camera_controls\r\n            cfgProps.hasFlicker = \"AeFlickerMode\" in Camera.cam.camera_controls\r\n            cfgProps.hasHdr = \"HdrMode\" in Camera.cam.camera_controls\r\n\r\n            if cfgCtrls.include_scalerCrop == False:\r\n                cfgCtrls.scalerCrop = (\r\n                    0,\r\n                    0,\r\n                    camPprops[\"PixelArraySize\"][0],\r\n                    camPprops[\"PixelArraySize\"][1],\r\n                )\r\n                # This must be updated after the camera has been started\r\n                Camera.resetScalerCropRequested = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadCameraSpecifics loaded to config\", get_ident()\r\n            )\r\n\r\n        # Load Sensor Modes\r\n        if len(cfgSensorModes) == 0:\r\n            sensorModes = Camera.cam.sensor_modes\r\n            ind = 0\r\n            for mode in sensorModes:\r\n                fmt = str(mode[\"format\"])\r\n                if not fmt in cfgRawFormats:\r\n                    cfgRawFormats.append(fmt)\r\n                fmt = str(mode[\"unpacked\"])\r\n                if not fmt in cfgRawFormats:\r\n                    cfgRawFormats.append(fmt)\r\n                cfgMode = SensorMode()\r\n                cfgMode.id = str(ind)\r\n                cfgMode.format = mode[\"format\"]\r\n                cfgMode.unpacked = mode[\"unpacked\"]\r\n                cfgMode.bit_depth = mode[\"bit_depth\"]\r\n                cfgMode.size = mode[\"size\"]\r\n                cfgMode.fps = mode[\"fps\"]\r\n                cfgMode.crop_limits = mode[\"crop_limits\"]\r\n                cfgMode.exposure_limits = mode[\"exposure_limits\"]\r\n                cfgSensorModes.append(cfgMode)\r\n                ind = ind + 1\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadCameraSpecifics: %s sensor modes found\",\r\n                get_ident(),\r\n                len(cfg.sensorModes),\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadCameraSpecifics: %s raw formats found\",\r\n                get_ident(),\r\n                len(cfg.rawFormats),\r\n            )\r\n\r\n            # Set some Sensor Mode specific parameters for standard configurations\r\n            maxModei = len(cfg.sensorModes) - 1\r\n            maxMode = str(maxModei)\r\n            # For Live View\r\n            # Initially set the stream size to (640, 480). Use Sensor Mode, if possible\r\n            # If stream_size is set, keep the settings. They have been loeaded from stored config\r\n            if cfg.liveViewConfig.stream_size is None:\r\n                sizeWidth = 640\r\n                sizeHeight = int(\r\n                    sizeWidth * cfgProps.pixelArraySize[1] / cfgProps.pixelArraySize[0]\r\n                )\r\n                if (sizeHeight % 2) != 0:\r\n                    sizeHeight += 1\r\n                cfg.liveViewConfig.stream_size = (sizeWidth, sizeHeight)\r\n                cfg.liveViewConfig.stream_size_align = False\r\n                if (\r\n                    cfgSensorModes[0].size[0] == sizeWidth\r\n                    and cfgSensorModes[0].size[1] == sizeHeight\r\n                ):\r\n                    cfg.liveViewConfig.sensor_mode = \"0\"\r\n                else:\r\n                    cfg.liveViewConfig.sensor_mode = \"custom\"\r\n            # For photo\r\n            if cfg.photoConfig.stream_size is None:\r\n                cfg.photoConfig.sensor_mode = maxMode\r\n                cfg.photoConfig.stream_size = cfgSensorModes[maxModei].size\r\n            # For raw photo\r\n            if cfg.rawConfig.stream_size is None:\r\n                cfg.rawConfig.sensor_mode = maxMode\r\n                cfg.rawConfig.stream_size = cfgSensorModes[maxModei].size\r\n                cfg.rawConfig.format = str(cfgSensorModes[maxModei].format)\r\n            # For Video\r\n            if cfg.videoConfig.stream_size is None:\r\n                # For Pi < 5 set video and photo resolution to lowest value\r\n                if (\r\n                    cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\")\r\n                    or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\")\r\n                    or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\")\r\n                    or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\")\r\n                    or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\")\r\n                ):\r\n                    cfg.videoConfig.sensor_mode = 0\r\n                    cfg.videoConfig.stream_size = cfgSensorModes[0].size\r\n                    cfg.photoConfig.sensor_mode = 0\r\n                    cfg.photoConfig.stream_size = cfgSensorModes[0].size\r\n                else:\r\n                    cfg.videoConfig.sensor_mode = maxMode\r\n                    cfg.videoConfig.stream_size = cfgSensorModes[maxModei].size\r\n\r\n            # Sync aspect ratio for CSI cameras\r\n            cfg.serverConfig.syncAspectRatio = True\r\n\r\n    @staticmethod\r\n    def loadUsbCameraSpecifics() -> bool:\r\n        \"\"\"Load USB camera specific parameters into configuration, if not already done\r\n\r\n        Returns:\r\n            bool: True if USB camera specifics were loaded, False otherwise\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.loadUsbCameraSpecifics\", get_ident())\r\n\r\n        cfg = CameraCfg()\r\n\r\n        # Load Camera Properties\r\n        if cfg.cameraProperties.model is None:\r\n            if cfg.setUsbCameraProperties() == False:\r\n                return False\r\n\r\n            if cfg.controls.include_scalerCrop == False:\r\n                cfg.controls.scalerCrop = cfg.cameraProperties.scalerCropMaximum\r\n                # This must be updated after the camera has been started\r\n                Camera.resetScalerCropRequested = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadUsbCameraSpecifics loaded to config\", get_ident()\r\n            )\r\n\r\n        # Load Sensor Modes\r\n        if len(cfg.sensorModes) == 0:\r\n            if cfg.setUsbSensorModes() == False:\r\n                return False\r\n\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadUsbCameraSpecifics: %s sensor modes found\",\r\n                get_ident(),\r\n                len(cfg.sensorModes),\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.loadUsbCameraSpecifics: %s raw formats found\",\r\n                get_ident(),\r\n                len(cfg.rawFormats),\r\n            )\r\n\r\n            # Set some Sensor Mode specific parameters for standard configurations\r\n            maxModei = len(cfg.sensorModes) - 1\r\n            maxMode = str(maxModei)\r\n            # For Live View\r\n            # Initially set the stream size to the size of the first Sensor Mode\r\n            if cfg.liveViewConfig.stream_size is None:\r\n                cfg.liveViewConfig.sensor_mode = \"0\"\r\n                sizeWidth = cfg.sensorModes[0].size[0]\r\n                sizeHeight = cfg.sensorModes[0].size[1]\r\n                cfg.liveViewConfig.stream_size = (sizeWidth, sizeHeight)\r\n            cfg.liveViewConfig.colour_space = cfg.cameraProperties.colorSpace\r\n            cfg.liveViewConfig.buffer_count = 1\r\n            cfg.liveViewConfig.queue = False\r\n            cfg.liveViewConfig.stream = \"main\"\r\n            cfg.liveViewConfig.stream_size_align = False\r\n            cfg.liveViewConfig.format = cfg.sensorModes[0].format\r\n            cfg.liveViewConfig.display = None\r\n            cfg.liveViewConfig.encode = None\r\n            # For photo\r\n            if cfg.photoConfig.stream_size is None:\r\n                cfg.photoConfig.sensor_mode = maxMode\r\n                cfg.photoConfig.stream_size = cfg.sensorModes[maxModei].size\r\n            cfg.photoConfig.colour_space = cfg.cameraProperties.colorSpace\r\n            cfg.photoConfig.buffer_count = 1\r\n            cfg.photoConfig.queue = False\r\n            cfg.photoConfig.stream = \"main\"\r\n            cfg.photoConfig.stream_size_align = False\r\n            cfg.photoConfig.format = cfg.sensorModes[maxModei].format\r\n            cfg.photoConfig.display = None\r\n            cfg.photoConfig.encode = None\r\n            # For raw photo\r\n            if cfg.rawConfig.stream_size is None:\r\n                cfg.rawConfig.sensor_mode = maxMode\r\n                cfg.rawConfig.stream_size = cfg.sensorModes[maxModei].size\r\n                cfg.rawConfig.format = \"tiff\"\r\n            cfg.rawConfig.colour_space = cfg.cameraProperties.colorSpace\r\n            cfg.rawConfig.buffer_count = 1\r\n            cfg.rawConfig.queue = False\r\n            cfg.rawConfig.stream = \"main\"\r\n            cfg.rawConfig.stream_size_align = False\r\n            # For Video\r\n            if cfg.videoConfig.stream_size is None:\r\n                cfg.videoConfig.sensor_mode = maxMode\r\n                cfg.videoConfig.stream_size = cfg.sensorModes[maxModei].size\r\n            cfg.videoConfig.colour_space = cfg.cameraProperties.colorSpace\r\n            cfg.videoConfig.buffer_count = 1\r\n            cfg.videoConfig.queue = False\r\n            cfg.videoConfig.stream = \"main\"\r\n            cfg.videoConfig.stream_size_align = False\r\n            cfg.videoConfig.format = cfg.sensorModes[maxModei].format\r\n            cfg.videoConfig.display = None\r\n            cfg.videoConfig.encode = None\r\n\r\n            # Do not sync aspect ratio for USB cameras\r\n            cfg.serverConfig.syncAspectRatio = False\r\n\r\n        # Load USB Camera Controls\r\n        if len(cfg.controls.usbCamControls) == 0:\r\n            cfg.setUsbCamControls()\r\n            cfg.cameraProperties.hasFocus = \"AfMode\" in cfg.controls.usbCamControls\r\n\r\n        return True\r\n\r\n    @classmethod\r\n    def setSecondCamera(cls):\r\n        \"\"\"Set the second camera\"\"\"\r\n        logger.debug(\"Thread %s: Camera.setSecondCamera\", get_ident())\r\n        cls.camNum2 = None\r\n        cls.cam2 = None\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        sc.errorc2 = None\r\n        camNum2 = None\r\n        secondCamIsUsb = False\r\n        secondCamUsbDev = \"\"\r\n        secondCamHasAi = False\r\n        secondCamModel = \"\"\r\n\r\n        # Check camera list for registered second camera\r\n        if not sc.secondCamera is None:\r\n            secondCam = None\r\n            for cfgCam in cfg.cameras:\r\n                if cfgCam.num == sc.secondCamera \\\r\n                and cfgCam.model == sc.secondCameraModel:\r\n                    secondCam = cfgCam.num\r\n                    camNum2 = cfgCam.num\r\n                    secondCamIsUsb = cfgCam.isUsb\r\n                    secondCamUsbDev = cfgCam.usbDev\r\n                    secondCamHasAi = cfgCam.hasAi\r\n                    secondCamModel = cfgCam.model\r\n                    break\r\n            if secondCam is None:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.setSecondCamera - Registered second camera %s not found\",\r\n                    get_ident(),\r\n                    sc.secondCamera,\r\n                )\r\n                sc.unsavedChanges = True\r\n                sc.addChangeLogEntry(\r\n                    f\"Second camera was reset Camera {sc.secondCamera}: {sc.secondCameraModel} - not found\"\r\n                )\r\n                sc.secondCamera = None\r\n\r\n        # If no registered second camera, take the first available which is not the active camera\r\n        if sc.secondCamera is None:\r\n            for cfgCam in cfg.cameras:\r\n                if cfgCam.num != cls.camNum and camNum2 is None:\r\n                    # Take the first camera which is not the active camera if USB is OK\r\n                    if not cfgCam.isUsb or sc.supportsUsbCamera == True:\r\n                        camNum2 = cfgCam.num\r\n                        secondCamIsUsb = cfgCam.isUsb\r\n                        secondCamUsbDev = cfgCam.usbDev\r\n                        secondCamHasAi = cfgCam.hasAi\r\n                        secondCamModel = cfgCam.model\r\n                        break\r\n        logger.debug(\r\n            \"Thread %s: Camera.setSecondCamera - found second camera: %s model: %s\",\r\n            get_ident(),\r\n            camNum2,\r\n            secondCamModel,\r\n        )\r\n        if not camNum2 is None:\r\n            try:\r\n                cls.camNum2 = camNum2\r\n                cls.cam2IsUsb = secondCamIsUsb\r\n                cls.cam2UsbDev = secondCamUsbDev\r\n                cls.cam2HasAi = secondCamHasAi\r\n                sc.secondCamera = camNum2\r\n                sc.secondCameraIsUsb = secondCamIsUsb\r\n                sc.secondCameraUsbDev = secondCamUsbDev\r\n                sc.secondCameraHasAi = secondCamHasAi\r\n                sc.secondCameraModel = secondCamModel\r\n                sc.secondCameraInfo = (\r\n                    \"Camera \" + str(camNum2) + \" (\" + secondCamModel + \")\"\r\n                )\r\n                strc = cfg.streamingCfg\r\n                camNum2Str = str(camNum2)\r\n                if secondCamIsUsb == False:\r\n                    if camNum2Str in strc:\r\n                        scfg = strc[camNum2Str]\r\n                        if \"tuningconfig\" in scfg:\r\n                            tc = scfg[\"tuningconfig\"]\r\n                            if tc.loadTuningFile == False:\r\n                                cls.cam2 = Picamera2(cls.camNum2)\r\n                                prgLogger.debug(\"picam2 = Picamera2(%s)\", cls.camNum2)\r\n                            else:\r\n                                tuning = Picamera2.load_tuning_file(\r\n                                    tc.tuningFile, tc.tuningFolder\r\n                                )\r\n                                cls.cam2 = Picamera2(cls.camNum2, tuning=tuning)\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera.setSecondCamera - Initialized camera %s with tuning file %s\",\r\n                                    get_ident(),\r\n                                    cls.camNum2,\r\n                                    tc.tuningFilePath,\r\n                                )\r\n                                prgLogger.debug(\r\n                                    \"tuning = Picamera2.load_tuning_file(%s, %s)\",\r\n                                    tc.tuningFile,\r\n                                    tc.tuningFolder,\r\n                                )\r\n                                prgLogger.debug(\r\n                                    \"picam2 = Picamera2(%s, tuning=tuning)\", cls.camNum2\r\n                                )\r\n                        else:\r\n                            cls.cam2 = Picamera2(cls.camNum2)\r\n                            prgLogger.debug(\"picam2 = Picamera2(%s)\", cls.camNum2)\r\n                    else:\r\n                        cls.cam2 = Picamera2(cls.camNum2)\r\n                        prgLogger.debug(\"picam2 = Picamera2(%s)\", cls.camNum2)\r\n                else:\r\n                    cls.cam2 = cv2.VideoCapture(cls.cam2UsbDev, cv2.CAP_V4L2)\r\n                    if not cls.cam2 or not cls.cam2.isOpened():\r\n                        raise RuntimeError(\"USB camera not opened\")\r\n                cls.ctrl2 = CameraController(cls.cam2IsUsb, cls.cam2UsbDev, forActiveCamera=False)\r\n                cls.event2 = CameraEvent()\r\n                logger.debug(\r\n                    \"Thread %s: Camera.setSecondCamera - second camera initialized %s\",\r\n                    get_ident(),\r\n                    cls.camNum2,\r\n                )\r\n                cfg.serverConfig.isLiveStream2 = False\r\n            except RuntimeError as e:\r\n                logger.error(\r\n                    \"Thread %s: Camera.setSecondCamera - Error %s\", get_ident(), e\r\n                )\r\n                if not sc.errorc2:\r\n                    sc.errorc2 = \"Error while initializing camera: \" + str(e)\r\n                    sc.errorc22 = \"Probably another process is using the camera.\"\r\n                    if secondCamIsUsb == False:\r\n                        sc.errorc2Source = \"Picamera2\"\r\n                    else:\r\n                        sc.errorc2Source = \"CV2\"\r\n            except Exception as e:\r\n                logger.error(\r\n                    \"Thread %s: Camera.setSecondCamera - Error %s\", get_ident(), e\r\n                )\r\n                if not sc.errorc2:\r\n                    sc.errorc2 = \"Error while initializing camera: \" + str(e)\r\n                    if secondCamIsUsb == False:\r\n                        sc.errorc2Source = \"Picamera2\"\r\n                    else:\r\n                        sc.errorc2Source = \"CV2\"\r\n\r\n        cls.setStreamingConfigs()\r\n        logger.debug(\r\n            \"Thread %s: Camera.setSecondCamera - second camera set to %s\",\r\n            get_ident(),\r\n            cls.camNum2,\r\n        )\r\n\r\n        cameraPhotoSubPath = \"photos/\" + \"camera_\" + str(camNum2)\r\n        fp = sc.photoRoot + \"/\" + cameraPhotoSubPath\r\n        if not os.path.exists(fp):\r\n            os.makedirs(fp)\r\n            logger.debug(\r\n                \"Thread %s: Camera.setSecondCamera - Photo directory created %s\",\r\n                get_ident(),\r\n                fp,\r\n            )\r\n\r\n    @classmethod\r\n    def setStreamingConfigs(cls):\r\n        \"\"\"Set the configuration for streaming which will be used when cameras are switched\"\"\"\r\n        logger.debug(\"Thread %s: Camera.setStreamingConfigs\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        trc = cfg.triggerConfig\r\n        strc = cfg.streamingCfg\r\n        logger.debug(\r\n            \"Thread %s: Camera.setStreamingConfigs - current streamingCfg: %s\",\r\n            get_ident(),\r\n            strc,\r\n        )\r\n\r\n        # For active camera\r\n        cn = str(sc.activeCamera)\r\n        logger.debug(\r\n            \"Thread %s: Camera.setStreamingConfigs - for active camera %s\",\r\n            get_ident(),\r\n            cn,\r\n        )\r\n        resetActive = False\r\n        if cn in strc:\r\n            scfg = strc[cn]\r\n            logger.debug(\r\n                \"Thread %s: Camera.setStreamingConfigs - found in strc. scfg: %s\",\r\n                get_ident(),\r\n                scfg,\r\n            )\r\n            if \"camerainfo\" in scfg:\r\n                if scfg[\"camerainfo\"] != sc.activeCameraInfo:\r\n                    resetActive = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.setStreamingConfigs - Resetting active camera config for camera %s\",\r\n                        get_ident(),\r\n                        cn,\r\n                    )\r\n                    sc.unsavedChanges = True\r\n                    sc.addChangeLogEntry(\r\n                        f\"Streaming configuration for {sc.activeCameraInfo} was reset due to camera model change\"\r\n                    )\r\n                else:\r\n                    # Check whether camera properties are available\r\n                    if not \"cameraproperties\" in scfg:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.setStreamingConfigs - StreamingConfig for active camera %s does not have camera properties\",\r\n                            get_ident(),\r\n                            cn,\r\n                        )\r\n                        scfg[\"cameraproperties\"] = copy.deepcopy(cfg.cameraProperties)\r\n                        sc.unsavedChanges = True\r\n                        sc.addChangeLogEntry(\r\n                            f\"Streaming configuration for {sc.activeCameraInfo} was extended with camera properties\"\r\n                        )\r\n                # For USB cameras, check the status\r\n                # The streaming config needs to be reset if it was initially created for the second camera\r\n                # And has never been updated for the active camera\r\n                if cls.camIsUsb == True:\r\n                    if \"is_ok\" in scfg:\r\n                        isOK = scfg[\"is_ok\"]\r\n                        if isOK == False:\r\n                            resetActive = True\r\n                            logger.debug(\r\n                                \"Thread %s: Camera.setStreamingConfigs - Resetting active camera config for camera %s due to is_OK=False\",\r\n                                get_ident(),\r\n                                cn,\r\n                            )\r\n                            sc.unsavedChanges = True\r\n                            sc.addChangeLogEntry(\r\n                                f\"Streaming configuration for {sc.activeCameraInfo} was reset due to camera status change\"\r\n                            )\r\n                    else:\r\n                        resetActive = True\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.setStreamingConfigs - Resetting active camera config for camera %s due to missing is_OK\",\r\n                            get_ident(),\r\n                            cn,\r\n                        )\r\n                        sc.unsavedChanges = True\r\n                        sc.addChangeLogEntry(\r\n                            f\"Streaming configuration for {sc.activeCameraInfo} was reset due to camera status change\"\r\n                        )\r\n                if not \"triggercamera\" in scfg:\r\n                    resetActive = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.setStreamingConfigs - StreamingConfig for active camera %s does not have triggercamera\",\r\n                        get_ident(),\r\n                        cn,\r\n                    )\r\n                    sc.unsavedChanges = True\r\n                    sc.addChangeLogEntry(\r\n                        f\"Streaming configuration for {sc.activeCameraInfo} was extended with trigger camera settings\"\r\n                    )\r\n                if not \"aiconfig\" in scfg:\r\n                    resetActive = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.setStreamingConfigs - StreamingConfig for active camera %s does not have aiconfig\",\r\n                        get_ident(),\r\n                        cn,\r\n                    )\r\n                    sc.unsavedChanges = True\r\n                    sc.addChangeLogEntry(\r\n                        f\"Streaming configuration for {sc.activeCameraInfo} was extended with AI settings\"\r\n                    )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.setStreamingConfigs - not found in strc.\",\r\n                    get_ident(),\r\n                )\r\n                resetActive = True\r\n        else:\r\n            resetActive = True\r\n        if resetActive == True:\r\n            logger.debug(\r\n                \"Thread %s: Camera.setStreamingConfigs - Active camera strc must be reset\",\r\n                get_ident(),\r\n            )\r\n            scfg = {}\r\n            scfg[\"camnum\"] = sc.activeCamera\r\n            scfg[\"is_ok\"] = True\r\n            scfg[\"camerainfo\"] = copy.copy(sc.activeCameraInfo)\r\n            scfg[\"cameraproperties\"] = copy.deepcopy(cfg.cameraProperties)\r\n            scfg[\"hasfocus\"] = cfg.cameraProperties.hasFocus\r\n            if cls.camIsUsb == False:\r\n                scfg[\"tuningconfig\"] = copy.deepcopy(cfg.tuningConfig)\r\n            scfg[\"liveconfig\"] = copy.deepcopy(cfg.liveViewConfig)\r\n            scfg[\"photoconfig\"] = copy.deepcopy(cfg.photoConfig)\r\n            scfg[\"rawconfig\"] = copy.deepcopy(cfg.rawConfig)\r\n            scfg[\"videoconfig\"] = copy.deepcopy(cfg.videoConfig)\r\n            scfg[\"controls\"] = copy.deepcopy(cfg.controls)\r\n            scfg[\"triggercamera\"] = copy.deepcopy(trc.cameraSettings)\r\n            scfg[\"aiconfig\"] = copy.deepcopy(cfg.aiConfig)\r\n            strc[cn] = scfg\r\n            logger.debug(\r\n                \"Thread %s: Camera.setStreamingConfigs - created  entry for active camera %s\",\r\n                get_ident(),\r\n                cn,\r\n            )\r\n        else:\r\n            if cn in strc:\r\n                scfg = strc[cn]\r\n                if not \"camnum\" in scfg:\r\n                    scfg[\"camnum\"] = sc.activeCamera\r\n                    strc[cn] = scfg\r\n        # Reset streaming config invalidation flag\r\n        cfg.streamingCfgInvalid = False\r\n\r\n        # For second camera\r\n        if cls.cam2:\r\n            cn = str(cls.camNum2)\r\n            resetSecond = False\r\n            if cn in strc:\r\n                scfg = strc[cn]\r\n                if \"camerainfo\" in scfg:\r\n                    model = \"\"\r\n                    for cfgCam in cfg.cameras:\r\n                        if cfgCam.num == cls.camNum2:\r\n                            model = cfgCam.model\r\n                            break\r\n                    newCamInfo = \"Camera \" + str(cls.camNum2) + \" (\" + model + \")\"\r\n                    if scfg[\"camerainfo\"] != newCamInfo:\r\n                        resetSecond = True\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.setStreamingConfigs - Resetting second camera config for camera %s\",\r\n                            get_ident(),\r\n                            cn,\r\n                        )\r\n                        sc.unsavedChanges = True\r\n                        sc.addChangeLogEntry(\r\n                            f\"Streaming configuration for {newCamInfo} was reset due to camera model change\"\r\n                        )\r\n                else:\r\n                    resetSecond = True\r\n            else:\r\n                resetSecond = True\r\n            if resetSecond == True:\r\n                scfg = {}\r\n                model = \"\"\r\n                for cfgCam in cfg.cameras:\r\n                    if cfgCam.num == cls.camNum2:\r\n                        model = cfgCam.model\r\n                        break\r\n                scfg[\"camnum\"] = cls.camNum2\r\n                scfg[\"camerainfo\"] = \"Camera \" + cn + \" (\" + model + \")\"\r\n\r\n                if cls.cam2IsUsb == False:\r\n                    scfg[\"is_ok\"] = True\r\n                    camPprops = cls.cam2.camera_properties\r\n                    hasFocus = \"AfMode\" in cls.cam2.camera_controls\r\n                    pixelArraySize = copy.copy(camPprops[\"PixelArraySize\"])\r\n                    sensorModes = copy.copy(cls.cam2.sensor_modes)\r\n                    maxMode = len(sensorModes) - 1\r\n                    liveViewConfig = CameraConfig()\r\n                    liveViewConfig.id = \"LIVE\"\r\n                    liveViewConfig.use_case = \"Live view\"\r\n                    liveViewConfig.stream = \"lores\"\r\n                    liveViewConfig.buffer_count = 6\r\n                    liveViewConfig.encode = \"main\"\r\n                    liveViewConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n                    if liveViewConfig.stream_size is None:\r\n                        sizeWidth = 640\r\n                        sizeHeight = int(\r\n                            sizeWidth * pixelArraySize[1] / pixelArraySize[0]\r\n                        )\r\n                        if (sizeHeight % 2) != 0:\r\n                            sizeHeight += 1\r\n                        liveViewConfig.stream_size = (sizeWidth, sizeHeight)\r\n                        liveViewConfig.stream_size_align = False\r\n                        if (\r\n                            sensorModes[0][\"size\"][0] == sizeWidth\r\n                            and sensorModes[0][\"size\"][1] == sizeHeight\r\n                        ):\r\n                            liveViewConfig.sensor_mode = \"0\"\r\n                        else:\r\n                            liveViewConfig.sensor_mode = \"custom\"\r\n\r\n                    videoConfig = CameraConfig()\r\n                    videoConfig.id = \"VIDO\"\r\n                    videoConfig.use_case = \"Video\"\r\n                    videoConfig.buffer_count = 6\r\n                    videoConfig.encode = \"main\"\r\n                    videoConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n\r\n                    if (\r\n                        cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\")\r\n                    ):\r\n                        videoConfig.sensor_mode = 0\r\n                        videoConfig.stream_size = sensorModes[0][\"size\"]\r\n                        videoConfig.buffer_count = 2\r\n                        liveViewConfig.buffer_count = 2\r\n                    else:\r\n                        videoConfig.sensor_mode = str(maxMode)\r\n                        videoConfig.stream_size = sensorModes[maxMode][\"size\"]\r\n\r\n                    photoConfig = CameraConfig()\r\n                    photoConfig.id = \"FOTO\"\r\n                    photoConfig.use_case = \"Photo\"\r\n                    photoConfig.buffer_count = 1\r\n                    photoConfig.encode = \"main\"\r\n                    photoConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n\r\n                    if (\r\n                        cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\")\r\n                    ):\r\n                        photoConfig.sensor_mode = 0\r\n                        photoConfig.stream_size = sensorModes[0][\"size\"]\r\n                    else:\r\n                        photoConfig.sensor_mode = str(maxMode)\r\n                        photoConfig.stream_size = sensorModes[maxMode][\"size\"]\r\n\r\n                    rawConfig = CameraConfig()\r\n                    rawConfig.id = \"PRAW\"\r\n                    rawConfig.use_case = \"Raw Photo\"\r\n                    rawConfig.buffer_count = 1\r\n                    rawConfig.encode = \"raw\"\r\n                    rawConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n                    rawConfig.sensor_mode = str(maxMode)\r\n                    rawConfig.stream_size = sensorModes[maxMode][\"size\"]\r\n\r\n                    scfg[\"hasfocus\"] = hasFocus\r\n                    scfg[\"tuningconfig\"] = TuningConfig()\r\n                    scfg[\"liveconfig\"] = liveViewConfig\r\n                    scfg[\"photoconfig\"] = photoConfig\r\n                    scfg[\"rawconfig\"] = rawConfig\r\n                    scfg[\"videoconfig\"] = videoConfig\r\n                    scfg[\"controls\"] = copy.deepcopy(cfg.controls)\r\n                else:\r\n                    scfg[\"is_ok\"] = False\r\n                    hasFocus = False\r\n                    pixelArraySize = None\r\n                    sensorModes = []\r\n                    maxMode = len(sensorModes) - 1\r\n\r\n                    liveViewConfig = CameraConfig()\r\n                    liveViewConfig.id = \"LIVE\"\r\n                    liveViewConfig.use_case = \"Live view\"\r\n                    liveViewConfig.stream = \"main\"\r\n                    liveViewConfig.colour_space = \"sRGB\"\r\n                    liveViewConfig.buffer_count = 1\r\n                    liveViewConfig.queue = False\r\n                    liveViewConfig.encode = \"main\"\r\n                    liveViewConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n                    sizeWidth = cfg.sensorModes[0].size[0]\r\n                    sizeHeight = cfg.sensorModes[0].size[1]\r\n                    liveViewConfig.stream_size = (sizeWidth, sizeHeight)\r\n                    liveViewConfig.stream_size_align = False\r\n                    liveViewConfig.sensor_mode = \"0\"\r\n                    liveViewConfig.format = \"YUYV\"\r\n\r\n                    videoConfig = CameraConfig()\r\n                    videoConfig.id = \"VIDO\"\r\n                    videoConfig.use_case = \"Video\"\r\n                    videoConfig.colour_space = \"sRGB\"\r\n                    videoConfig.buffer_count = 1\r\n                    videoConfig.queue = False\r\n                    videoConfig.encode = \"main\"\r\n                    videoConfig.controls[\"FrameDurationLimits\"] = (33333, 33333)\r\n                    videoConfig.stream = \"main\"\r\n                    videoConfig.sensor_mode = 0\r\n                    videoConfig.stream_size = (640, 480)\r\n                    videoConfig.stream_size_align = False\r\n                    videoConfig.format = \"YUYV\"\r\n\r\n                    photoConfig = CameraConfig()\r\n                    photoConfig.id = \"FOTO\"\r\n                    photoConfig.use_case = \"Photo\"\r\n                    photoConfig.colour_space = \"sRGB\"\r\n                    photoConfig.buffer_count = 1\r\n                    photoConfig.queue = False\r\n                    photoConfig.encode = \"main\"\r\n                    photoConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n                    photoConfig.stream = \"main\"\r\n                    photoConfig.sensor_mode = 0\r\n                    photoConfig.stream_size = (640, 480)\r\n                    photoConfig.stream_size_align = False\r\n                    photoConfig.format = \"YUYV\"\r\n\r\n                    rawConfig = CameraConfig()\r\n                    rawConfig.id = \"PRAW\"\r\n                    rawConfig.use_case = \"Raw Photo\"\r\n                    rawConfig.colour_space = \"sRGB\"\r\n                    rawConfig.buffer_count = 1\r\n                    rawConfig.queue = False\r\n                    rawConfig.encode = \"raw\"\r\n                    rawConfig.controls[\"FrameDurationLimits\"] = (100, 1000000000)\r\n                    rawConfig.stream = \"main\"\r\n                    rawConfig.sensor_mode = 0\r\n                    rawConfig.stream_size = (640, 480)\r\n                    rawConfig.stream_size_align = False\r\n                    rawConfig.format = \"tiff\"\r\n\r\n                    scfg[\"hasfocus\"] = hasFocus\r\n                    scfg[\"liveconfig\"] = liveViewConfig\r\n                    scfg[\"photoconfig\"] = photoConfig\r\n                    scfg[\"rawconfig\"] = rawConfig\r\n                    scfg[\"videoconfig\"] = videoConfig\r\n                    scfg[\"controls\"] = copy.deepcopy(cfg.controls)\r\n\r\n                strc[cn] = scfg\r\n                logger.debug(\r\n                    \"Thread %s: Camera.setStreamingConfigs - created  entry for second camera %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            else:\r\n                if cn in strc:\r\n                    scfg = strc[cn]\r\n                    if not \"camnum\" in scfg:\r\n                        scfg[\"camnum\"] = cls.camNum2\r\n                        strc[cn] = scfg\r\n\r\n    @classmethod\r\n    def restoreConfigFromStreamingConfig(cls):\r\n        \"\"\"Restore active configuration and controls from a previously saved streaming config\"\"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        trc = cfg.triggerConfig\r\n        strc = cfg.streamingCfg\r\n        logger.debug(\"Thread %s: Camera.restoreConfigFromStreamingConfig for camera %s\", get_ident(), sc.activeCamera)\r\n\r\n        # For active camera\r\n        cn = str(sc.activeCamera)\r\n        if cn in strc:\r\n            scfg = strc[cn]\r\n            if sc.activeCameraIsUsb:\r\n                if \"is_ok\" in scfg:\r\n                    isOK = scfg[\"is_ok\"]\r\n                    if isOK == False:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.restoreConfigFromStreamingConfig - Streaming config for active camera %s is not OK, skipping restore\",\r\n                            get_ident(),\r\n                            cn,\r\n                        )\r\n                        return\r\n            if \"liveconfig\" in scfg:\r\n                cfg.liveViewConfig = copy.deepcopy(scfg[\"liveconfig\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored liveViewConfig from streaming config %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            if \"photoconfig\" in scfg:\r\n                cfg.photoConfig = copy.deepcopy(scfg[\"photoconfig\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored photoconfig from streaming config %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            if \"rawconfig\" in scfg:\r\n                cfg.rawConfig = copy.deepcopy(scfg[\"rawconfig\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored rawconfig from streaming config %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            if \"videoconfig\" in scfg:\r\n                cfg.videoConfig = copy.deepcopy(scfg[\"videoconfig\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored videoconfig from streaming config %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            if \"controls\" in scfg:\r\n                cfg.controls = copy.deepcopy(scfg[\"controls\"])\r\n                # Camera.resetScalerCropRequested = False\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored controls from streaming config %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - cfgCtrls=%s\",\r\n                    get_ident(),\r\n                    scfg[\"controls\"].__dict__,\r\n                )\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - cfg.controls=%s\",\r\n                    get_ident(),\r\n                    cfg.controls.__dict__,\r\n                )\r\n            if \"triggercamera\" in scfg:\r\n                trc.cameraSettings = copy.deepcopy(scfg[\"triggercamera\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - Trigger camera settings restored from streaming config for camera %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            else:\r\n                trc.setCameraSettingsToDefault()\r\n                logger.debug(\r\n                    \"Thread %s: Camera.restoreConfigFromStreamingConfig - Trigger camera settings set to defaults for camera %s\",\r\n                    get_ident(),\r\n                    cn,\r\n                )\r\n            logger.debug(\r\n                \"Thread %s: Camera.restoreConfigFromStreamingConfig - restored config and controls from streaming config %s\",\r\n                get_ident(),\r\n                cn,\r\n            )\r\n        else:\r\n            trc.setCameraSettingsToDefault()\r\n            logger.debug(\r\n                \"Thread %s: Camera.restoreConfigFromStreamingConfig - Trigger camera settings set to defaults for camera %s\",\r\n                get_ident(),\r\n                cn,\r\n            )\r\n\r\n    @staticmethod\r\n    def configure(cfg: CameraConfig, cfgPhoto: CameraConfig):\r\n        \"\"\"The function creates and configures a CameraConfiguration\r\n        based on given configuration settings cfg.\r\n\r\n        The fully configured configuration is returned\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.configure\", get_ident())\r\n        # We start configuration with a new blank CameraConfiguration object\r\n        camCfg = CameraConfiguration()\r\n\r\n        camCfg.use_case = cfg.use_case\r\n        camCfg.transform = Transform(\r\n            vflip=cfg.transform_vflip, hflip=cfg.transform_hflip\r\n        )\r\n        camCfg.buffer_count = cfg.buffer_count\r\n        cosp = cfg.colour_space\r\n        if cosp == \"sYCC\":\r\n            colourSpace = ColorSpace.Sycc()\r\n        elif cosp == \"Smpte170m\":\r\n            colourSpace = ColorSpace.Smpte170m()\r\n        elif cosp == \"Rec709\":\r\n            colourSpace = ColorSpace.Rec709()\r\n        else:\r\n            colourSpace = ColorSpace.Sycc()\r\n        camCfg.colour_space = colourSpace\r\n        camCfg.queue = cfg.queue\r\n        camCfg.display = cfg.display\r\n        camCfg.encode = cfg.encode\r\n        # The mainStream is configured here from the photo configuration (e.g. jpg)\r\n        # to allow for a jpeg in addition to a dng from the raw stream\r\n        mainStream = StreamConfiguration()\r\n        mainStream.format = cfgPhoto.format\r\n        # However the size shall be that of the target configuration\r\n        # so that the formats of both, jpg and dng are the same\r\n        mainStream.size = cfg.stream_size\r\n        stream = StreamConfiguration()\r\n        stream.size = cfg.stream_size\r\n        stream.format = cfg.format\r\n        if cfg.stream == \"main\":\r\n            camCfg.main = stream\r\n            camCfg.lores = None\r\n            camCfg.raw = None\r\n        if cfg.stream == \"lores\":\r\n            camCfg.main = mainStream\r\n            camCfg.lores = stream\r\n            camCfg.raw = None\r\n        if cfg.stream == \"raw\":\r\n            camCfg.main = mainStream\r\n            camCfg.lores = None\r\n            camCfg.raw = stream\r\n        ctrls = cfg.controls\r\n        if len(ctrls) == 0:\r\n            raise ValueError(\"controls in camera configuration must not be empty\")\r\n        else:\r\n            camCfg.controls = ctrls\r\n        logger.debug(\r\n            \"Thread %s: Camera.configure: configuration completed\", get_ident()\r\n        )\r\n\r\n        # Automatically align the stream size, if selected\r\n        if cfg.stream_size_align and cfg.sensor_mode == \"custom\":\r\n            logger.debug(\r\n                \"Thread %s: Camera.configure: Aligning camera configuration. Old size: %s\",\r\n                get_ident(),\r\n                cfg.stream_size,\r\n            )\r\n            camCfg.align()\r\n            logger.debug(\r\n                \"Thread %s: Camera.configure: Alignment successful. Adjusting stream size\",\r\n                get_ident(),\r\n            )\r\n            cfg.stream_size = camCfg.size\r\n            logger.debug(\r\n                \"Thread %s: Camera.configure: Stream size adjusted to %s\",\r\n                get_ident(),\r\n                cfg.stream_size,\r\n            )\r\n\r\n        return camCfg\r\n\r\n    @staticmethod\r\n    def requiresTimeForAutoAlgos() -> bool:\r\n        \"\"\"Check if the camera requires time for auto algorithms to settle\r\n        Returns True, if the camera is a Pi 4 or Pi 5\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.requiresTimeForAutoAlgos\", get_ident())\r\n        cfgCtrls = CameraCfg().controls\r\n        res = False\r\n        if cfgCtrls.include_aeEnable and cfgCtrls.aeEnable == True:\r\n            res = True\r\n        if cfgCtrls.include_awbEnable and cfgCtrls.awbEnable == True:\r\n            res = True\r\n        if CameraCfg().cameraProperties.hasFocus == True:\r\n            if cfgCtrls.include_afMode and cfgCtrls.afMode != 0:\r\n                res = True\r\n        return res\r\n\r\n    @staticmethod\r\n    def applyMappedControlToUsbCamera(\r\n        ctrl: str,\r\n        ctrls: dict,\r\n        isBool: bool,\r\n        usbCc: dict,\r\n        camDev: str,\r\n    ):\r\n        \"\"\"Apply a mapped control to a USB camera\r\n\r\n        Values of the raspiCamSrv Control are mapped to USB camera control values.\r\n\r\n        ctrl        : The control to be applied\r\n        ctrls       : The controls to be applied\r\n        isBool      : Indicates if the control value is boolean\r\n        usbCc       : The USB camera controls mapping\r\n        camDev      : The camera device identifier  \r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyMappedControlToUsbCamera - ctrl: %s\", get_ident(), ctrl\r\n        )\r\n        if ctrl in ctrls and ctrls[ctrl] is not None:\r\n            logger.debug(\"Thread %s: Camera.applyMappedControlToUsbCamera - applying: %s \", get_ident(), ctrl)\r\n            if isBool == True:\r\n                if ctrls[ctrl] == True:\r\n                    cfgVal = \"1\"\r\n                else:\r\n                    cfgVal = \"0\"\r\n            else:\r\n                cfgVal = str(ctrls[ctrl])\r\n            if \"mapping\" in usbCc[ctrl]:\r\n                mapping = usbCc[ctrl][\"mapping\"]\r\n                if cfgVal in mapping:\r\n                    camVal = mapping[cfgVal]\r\n                    ctrlName = usbCc[ctrl][\"ctrlName\"]\r\n                    try:\r\n                        subprocess.run([\"v4l2-ctl\", \"-d\", camDev, f\"--set-ctrl={ctrlName}={camVal}\"])\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.applyMappedControlToUsbCamera - camDev: %s set ctrl %s to %s\",\r\n                            get_ident(),\r\n                            camDev,\r\n                            ctrlName,\r\n                            camVal,\r\n                        )\r\n                    except Exception as e:\r\n                        logger.error(\r\n                            \"Camera.applyMappedControlToUsbCamera - camDev: %s Error setting %s to %s: %s\",\r\n                            camDev,\r\n                            ctrlName,\r\n                            camVal,\r\n                            e,\r\n                        )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.applyMappedControlToUsbCamera - ctrl: %s not applied (not in ctrls)\",\r\n                get_ident(),\r\n                ctrl,\r\n            )\r\n\r\n    @staticmethod\r\n    def applyDirectControlToUsbCamera(\r\n        ctrl: str,\r\n        ctrls: dict,\r\n        usbCc: dict,\r\n        camDev: str,\r\n    ):\r\n        \"\"\"Apply a control directly to a USB camera\r\n\r\n        Values of the raspiCamSrv Control are scaled to USB camera control values.\r\n\r\n        ctrl        : The control to be applied\r\n        ctrls       : The controls to be applied\r\n        usbCc       : The USB camera controls mapping\r\n        camDev      : The camera device identifier  \r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyDirectControlToUsbCamera - ctrl: %s\", get_ident(), ctrl\r\n        )\r\n        if ctrl in ctrls and ctrls[ctrl] is not None:\r\n            logger.debug(\"Thread %s: Camera.applyDirectControlToUsbCamera - applying: %s \", get_ident(), ctrl)\r\n            camVal = ctrls[ctrl]\r\n            if ctrl == \"LensPosition\":\r\n                camVal = 1.0 / camVal\r\n            if usbCc[ctrl][\"type\"] == \"int\":\r\n                camVal = int(camVal)\r\n            ctrlName = usbCc[ctrl][\"ctrlName\"]\r\n            try:\r\n                subprocess.run([\"v4l2-ctl\", \"-d\", camDev, f\"--set-ctrl={ctrlName}={camVal}\"])\r\n                logger.debug(\r\n                    \"Thread %s: Camera.applyScaledControlToUsbCamera - camDev: %s set ctrl %s to %s\",\r\n                    get_ident(),\r\n                    camDev,\r\n                    ctrlName,\r\n                    camVal,\r\n                )\r\n            except Exception as e:\r\n                logger.error(\r\n                    \"Camera.applyScaledControlToUsbCamera - camDev: %s Error setting %s to %s: %s\",\r\n                    camDev,\r\n                    ctrlName,\r\n                    camVal,\r\n                    e,\r\n                )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.applyScaledControlToUsbCamera - ctrl: %s not applied (not in ctrls)\",\r\n                get_ident(),\r\n                ctrl,\r\n            )\r\n\r\n    @staticmethod\r\n    def applyControlsToUsbCamera(\r\n        ctrls: dict,\r\n        toCam2: bool = False\r\n    ):\r\n        \"\"\"Apply controls to a USB camera\r\n\r\n        This method is called before images are captured from a USB camera.\r\n        It can be used to set camera properties, e.g. via v4l2-ctl commands.\r\n        ctrls       : The controls to be applied\r\n        toCam2      : If true, controls are set for the second camera\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControlsToUsbCamera - toCam2: %s ctrls: %s\", get_ident(), toCam2, ctrls\r\n        )\r\n        cfg = CameraCfg()\r\n        cc = cfg.controls\r\n        usbCc = cc.usbCamControls\r\n        if toCam2 == False:\r\n            camNum = Camera.camNum\r\n            camDev = Camera.camUsbDev\r\n        else:\r\n            camNum = Camera.camNum2\r\n            camDev = Camera.cam2UsbDev\r\n        logger.debug(\"Thread %s: Camera.applyControlsToUsbCamera - camNum: %s camDev: %s\", get_ident(), camNum, camDev)\r\n\r\n        # Auto White Balance\r\n        Camera.applyMappedControlToUsbCamera(\"AwbEnable\", ctrls, True, usbCc, camDev)\r\n\r\n        # Auto White Balance Mode\r\n        Camera.applyMappedControlToUsbCamera(\"AwbMode\", ctrls, False, usbCc, camDev)\r\n\r\n        # Sharpness\r\n        Camera.applyDirectControlToUsbCamera(\"Sharpness\", ctrls, usbCc, camDev)\r\n\r\n        # Brightness\r\n        Camera.applyDirectControlToUsbCamera(\"Brightness\", ctrls, usbCc, camDev)\r\n\r\n        # Contrast\r\n        Camera.applyDirectControlToUsbCamera(\"Contrast\", ctrls, usbCc, camDev)\r\n\r\n        # Saturation\r\n        Camera.applyDirectControlToUsbCamera(\"Saturation\", ctrls, usbCc, camDev)\r\n\r\n        # AfMode\r\n        Camera.applyMappedControlToUsbCamera(\"AfMode\", ctrls, False, usbCc, camDev)\r\n\r\n        # LensPosition\r\n        Camera.applyDirectControlToUsbCamera(\"LensPosition\", ctrls, usbCc, camDev)\r\n\r\n    @staticmethod\r\n    def usbFrameApplyControls(\r\n        frame,\r\n        log = False,\r\n        exceptCtrl=None, exceptValue=None, toCam2=None\r\n    ):\r\n        \"\"\"Apply the currently selected camera controls to a frame captured from a USB camera\r\n\r\n        frame       : Frame captured from the USB camera\r\n        log         : If true, log debug information (to prevent logging for each frame in video mode)\r\n        exceptCtrl  : Exception control. Optionally, one exceptional control can be specified\r\n                      If specified, the exceptValue will replace the value fom CameraCfg().controls\r\n                      Currently supported:\r\n                      - ExposureTime\r\n                      - AnalogueGain\r\n                      - FocalDistance -> LensPosition = 1 / FocalDistance\r\n        toCam2      : If true, controls are set for the second camera with control data from streamingCfg\r\n\r\n        Returns     : The frame with applied controls\r\n        \"\"\"\r\n        if toCam2 is None:\r\n            toCam2 = False\r\n\r\n        if log:\r\n            logger.debug(\r\n                \"Thread %s: Camera.usbFrameApplyControls - toCam2: %s\", get_ident(), toCam2\r\n            )\r\n\r\n        cfg = CameraCfg()\r\n        if toCam2 is False:\r\n            cfgCtrls = cfg.controls\r\n        else:\r\n            cfgCtrls = cfg.streamingCfg[str(Camera.camNum2)][\"controls\"]\r\n        \r\n        if log:\r\n            logger.debug(\r\n                \"Thread %s: Camera.usbFrameApplyControls - cfgCtrls=%s\",\r\n                get_ident(),\r\n                cfgCtrls.__dict__,\r\n            )\r\n        \r\n        newFrame = frame\r\n\r\n        ctrls = {}\r\n        cnt = 0\r\n\r\n        # Apply selected controls\r\n        # Scaler crop\r\n        if cfgCtrls.include_scalerCrop:\r\n            ctrls[\"ScalerCrop\"] = cfgCtrls.scalerCrop\r\n            cnt += 1\r\n\r\n            hFrame, wFrame = frame.shape[:2]\r\n            if log:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.usbFrameApplyControls - Frame size: width=%s height=%s\",\r\n                    get_ident(),\r\n                    wFrame,\r\n                    hFrame,\r\n                )\r\n            X, Y, W, H = Camera.getUsbScalerCrop(wFrame, hFrame, log=log, forCam2=toCam2)\r\n            x, y, w, h = cfgCtrls.scalerCrop\r\n            if log:\r\n                logger.debug(\"Thread %s: Camera.usbFrameApplyControls - ScalerCrop Frame is %s\", get_ident(), (X, Y, W, H))\r\n                logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Cropping to %s\", get_ident(), (x, y, w, h))\r\n            xc = x + int(w/2)\r\n            yc = y + int(h/2)\r\n            if log:\r\n                logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Crop center is %s\", get_ident(), (xc, yc))\r\n\r\n            aspectRatioFrame = W / H\r\n            aspectRatioCrop = w / h\r\n            if aspectRatioFrame > aspectRatioCrop:\r\n                # Frame is wider than crop aspect ratio -> increase width\r\n                wNew = int(h * aspectRatioFrame)\r\n                hNew = h\r\n                if log:\r\n                    logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Frame is wider than crop aspect ratio. New size is %s\", get_ident(), (wNew, hNew))\r\n            else:\r\n                # Frame is taller than crop aspect ratio -> increase height\r\n                wNew = w\r\n                hNew = int(w / aspectRatioFrame)\r\n                if log:\r\n                    logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Frame is taller than crop aspect ratio. New size is %s\", get_ident(), (wNew, hNew))\r\n            if wNew > W:\r\n                wNew = W\r\n            if hNew > H:\r\n                hNew = H\r\n\r\n            scaleToFrame = W / wFrame\r\n            wNew = int(wNew / scaleToFrame)\r\n            hNew = int(hNew / scaleToFrame)\r\n            xc = int((xc - X) / scaleToFrame)\r\n            yc = int((yc - Y) / scaleToFrame)\r\n\r\n            x1 = xc - int(wNew / 2)\r\n            if x1 < 0:\r\n                x1 = 0\r\n            y1 = yc - int(hNew / 2)\r\n            if y1 < 0:\r\n                y1 = 0\r\n            x2 = x1 + wNew\r\n            if x2 > wFrame:\r\n                x2 = wFrame\r\n            y2 = y1 + hNew\r\n            if y2 > hFrame:\r\n                y2 = hFrame\r\n            if log:\r\n                logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Cropping coordinates are x1=%s y1=%s x2=%s y2=%s\", get_ident(), x1, y1, x2, y2)\r\n            cropped = frame[y1:y2, x1:x2]\r\n            newFrame = cv2.resize(cropped, (wFrame, hFrame), interpolation=cv2.INTER_LINEAR)\r\n            if log:\r\n                logger.debug(\"Thread %s: Camera.usbFrameApplyControls - Cropping and resizing done\", get_ident())\r\n        return newFrame\r\n\r\n    @staticmethod\r\n    def applyControls(\r\n        camCfg: CameraConfig, exceptCtrl=None, exceptValue=None, toCam2=None\r\n    ):\r\n        \"\"\"Apply the currently selected camera controls\r\n        camCfg      : Configuration from which controls shall be taken with priority\r\n        exceptCtrl  : Exception control. Optionally, one exceptional control can be specified\r\n                      If specified, the exceptValue will replace the value fom CameraCfg().controls\r\n                      Currently supported:\r\n                      - ExposureTime\r\n                      - AnalogueGain\r\n                      - FocalDistance -> LensPosition = 1 / FocalDistance\r\n        toCam2      : If true, controls are set for the second camera with control data from streamingCfg\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - toCam2: %s\", get_ident(), toCam2\r\n        )\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - camCfg.controls=%s\",\r\n            get_ident(),\r\n            camCfg.controls,\r\n        )\r\n        cfg = CameraCfg()\r\n        if toCam2 is None:\r\n            cfgCtrls = cfg.controls\r\n        else:\r\n            cfgCtrls = cfg.streamingCfg[str(Camera.camNum2)][\"controls\"]\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - cfgCtrls=%s\",\r\n            get_ident(),\r\n            cfgCtrls.__dict__,\r\n        )\r\n\r\n        # Initialize controls dict with controls included in configuration\r\n        # ctrls = copy.deepcopy(camCfg.controls)\r\n        ctrls = {}\r\n        logger.debug(\"Thread %s: Camera.applyControls - ctrls=%s\", get_ident(), ctrls)\r\n        cnt = 0\r\n\r\n        # Apply selected controls with precedence of controls from configuration\r\n        # Auto exposure controls\r\n        if cfgCtrls.include_aeEnable and \"AeEnable\" not in camCfg.controls:\r\n            ctrls[\"AeEnable\"] = cfgCtrls.aeEnable\r\n            cnt += 1\r\n        if cfgCtrls.include_aeMeteringMode and \"AeMeteringMode\" not in camCfg.controls:\r\n            ctrls[\"AeMeteringMode\"] = cfgCtrls.aeMeteringMode\r\n            cnt += 1\r\n        if cfgCtrls.include_aeExposureMode and \"AeExposureMode\" not in camCfg.controls:\r\n            ctrls[\"AeExposureMode\"] = cfgCtrls.aeExposureMode\r\n            cnt += 1\r\n        if (\r\n            cfgCtrls.include_aeConstraintMode\r\n            and \"AeConstraintMode\" not in camCfg.controls\r\n        ):\r\n            ctrls[\"AeConstraintMode\"] = cfgCtrls.aeConstraintMode\r\n            cnt += 1\r\n        if cfgCtrls.include_aeFlickerMode and \"AeFlickerMode\" not in camCfg.controls:\r\n            ctrls[\"AeFlickerMode\"] = cfgCtrls.aeFlickerMode\r\n            cnt += 1\r\n        if (\r\n            cfgCtrls.include_aeFlickerPeriod\r\n            and \"AeFlickerPeriod\" not in camCfg.controls\r\n        ):\r\n            ctrls[\"AeFlickerPeriod\"] = cfgCtrls.aeFlickerPeriod\r\n            cnt += 1\r\n        # Exposure controls\r\n        if cfgCtrls.include_exposureTime and \"ExposureTime\" not in camCfg.controls:\r\n            ctrls[\"ExposureTime\"] = cfgCtrls.exposureTime\r\n            cnt += 1\r\n        if cfgCtrls.include_exposureValue and \"ExposureValue\" not in camCfg.controls:\r\n            ctrls[\"ExposureValue\"] = cfgCtrls.exposureValue\r\n            cnt += 1\r\n        if cfgCtrls.include_analogueGain and \"AnalogueGain\" not in camCfg.controls:\r\n            ctrls[\"AnalogueGain\"] = cfgCtrls.analogueGain\r\n            cnt += 1\r\n        if cfgCtrls.include_colourGains and \"ColourGains\" not in camCfg.controls:\r\n            ctrls[\"ColourGains\"] = (cfgCtrls.colourGainRed, cfgCtrls.colourGainBlue)\r\n            cnt += 1\r\n        if (\r\n            cfgCtrls.include_frameDurationLimits\r\n            and \"FrameDurationLimits\" not in camCfg.controls\r\n        ):\r\n            ctrls[\"FrameDurationLimits\"] = (\r\n                cfgCtrls.frameDurationLimitMax,\r\n                cfgCtrls.frameDurationLimitMin,\r\n            )\r\n            cnt += 1\r\n        if cfgCtrls.include_hdrMode and \"HdrMode\" not in camCfg.controls:\r\n            ctrls[\"HdrMode\"] = cfgCtrls.hdrMode\r\n            cnt += 1\r\n        # Image controls\r\n        if cfgCtrls.include_awbEnable and \"AwbEnable\" not in camCfg.controls:\r\n            ctrls[\"AwbEnable\"] = cfgCtrls.awbEnable\r\n            cnt += 1\r\n        if cfgCtrls.include_awbMode and \"AwbMode\" not in camCfg.controls:\r\n            ctrls[\"AwbMode\"] = cfgCtrls.awbMode\r\n            cnt += 1\r\n        if (\r\n            cfgCtrls.include_noiseReductionMode\r\n            and \"NoiseReductionMode\" not in camCfg.controls\r\n        ):\r\n            ctrls[\"NoiseReductionMode\"] = cfgCtrls.noiseReductionMode\r\n            cnt += 1\r\n        if cfgCtrls.include_sharpness and \"Sharpness\" not in camCfg.controls:\r\n            ctrls[\"Sharpness\"] = cfgCtrls.sharpness\r\n            cnt += 1\r\n        if cfgCtrls.include_contrast and \"Contrast\" not in camCfg.controls:\r\n            ctrls[\"Contrast\"] = cfgCtrls.contrast\r\n            cnt += 1\r\n        if cfgCtrls.include_saturation and \"Saturation\" not in camCfg.controls:\r\n            ctrls[\"Saturation\"] = cfgCtrls.saturation\r\n            cnt += 1\r\n        if cfgCtrls.include_brightness and \"Brightness\" not in camCfg.controls:\r\n            ctrls[\"Brightness\"] = cfgCtrls.brightness\r\n            cnt += 1\r\n        # Scaler crop\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - cfg.liveViewConfig.controls=%s\",\r\n            get_ident(),\r\n            cfg.liveViewConfig.controls,\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - include_scalerCrop=%s\",\r\n            get_ident(),\r\n            cfgCtrls.include_scalerCrop,\r\n        )\r\n        if cfgCtrls.include_scalerCrop and \"ScalerCrop\" not in camCfg.controls:\r\n            ctrls[\"ScalerCrop\"] = cfgCtrls.scalerCrop\r\n            cnt += 1\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - cfg.liveViewConfig.controls=%s\",\r\n            get_ident(),\r\n            cfg.liveViewConfig.controls,\r\n        )\r\n        # Focus\r\n        if toCam2 is None:\r\n            hasFocus = cfg.cameraProperties.hasFocus\r\n        else:\r\n            hasFocus = cfg.streamingCfg[str(Camera.camNum2)][\"hasfocus\"]\r\n        if hasFocus:\r\n            if cfgCtrls.include_afMode and \"AfMode\" not in camCfg.controls:\r\n                ctrls[\"AfMode\"] = cfgCtrls.afMode\r\n                cnt += 1\r\n            if cfgCtrls.include_lensPosition and \"LensPosition\" not in camCfg.controls:\r\n                ctrls[\"LensPosition\"] = cfgCtrls.lensPosition\r\n                cnt += 1\r\n            if cfgCtrls.include_afMetering and \"AfMetering\" not in camCfg.controls:\r\n                ctrls[\"AfMetering\"] = cfgCtrls.afMetering\r\n                cnt += 1\r\n            if cfgCtrls.include_afPause and \"AfPause\" not in camCfg.controls:\r\n                ctrls[\"AfPause\"] = cfgCtrls.afPause\r\n                cnt += 1\r\n            if cfgCtrls.include_afRange and \"AfRange\" not in camCfg.controls:\r\n                ctrls[\"AfRange\"] = cfgCtrls.afRange\r\n                cnt += 1\r\n            if cfgCtrls.include_afSpeed and \"AfSpeed\" not in camCfg.controls:\r\n                ctrls[\"AfSpeed\"] = cfgCtrls.afSpeed\r\n                cnt += 1\r\n            if cfgCtrls.include_afTrigger and \"AfTrigger\" not in camCfg.controls:\r\n                ctrls[\"AfTrigger\"] = cfgCtrls.afTrigger\r\n                cnt += 1\r\n            if cfgCtrls.include_afWindows and \"AfWindows\" not in camCfg.controls:\r\n                ctrls[\"AfWindows\"] = cfgCtrls.afWindows\r\n                cnt += 1\r\n            # Consider exception control\r\n            if exceptCtrl:\r\n                if exceptCtrl == \"FocalDistance\":\r\n                    if not \"LensPosition\" in camCfg.controls:\r\n                        cnt += 1\r\n                    ctrls[\"LensPosition\"] = 1.0 / exceptValue\r\n\r\n        # Consider exception control\r\n        if exceptCtrl:\r\n            if exceptCtrl != \"FocalDistance\":\r\n                if not exceptCtrl in camCfg.controls:\r\n                    cnt += 1\r\n                if exceptCtrl == \"ExposureTime\":\r\n                    ctrls[exceptCtrl] = int(exceptValue)\r\n                else:\r\n                    ctrls[exceptCtrl] = exceptValue\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControls - Applying %s controls\", get_ident(), cnt\r\n        )\r\n        logger.debug(\"Thread %s: Camera.applyControls - ctrls=%s\", get_ident(), ctrls)\r\n        if toCam2 is None:\r\n            if Camera.camIsUsb == False:\r\n                camCtrls = Controls(Camera.cam)\r\n                prgLogger.debug(\"camCtrls = Controls(picam2)\")\r\n                prgLogger.debug(\"ctrls = %s\", ctrls)\r\n                camCtrls.set_controls(ctrls)\r\n                prgLogger.debug(\"camCtrls.set_controls(ctrls)\")\r\n                Camera.cam.controls = camCtrls\r\n                # Camera.cam.controls.set_controls(ctrls)\r\n                prgLogger.debug(\"picam2.controls = camCtrls\")\r\n                logger.debug(\r\n                    \"Thread %s: Camera.applyControls - id(Camera)=%s id(Camera.cam)=%s id(Camera.cam.controls)=%s\",\r\n                    get_ident(),\r\n                    id(Camera),\r\n                    id(Camera.cam),\r\n                    id(Camera.cam.controls),\r\n                )\r\n                logger.debug(\r\n                    \"Thread %s: Camera.applyControls - Camera.cam.controls=%s\",\r\n                    get_ident(),\r\n                    Camera.cam.controls,\r\n                )\r\n\r\n                ai = cfg.aiConfig\r\n                if ai.enable == True:\r\n                    # Register the callback to parse and draw classification results for AI camera\r\n                    if ai.task == \"classification\":\r\n                        if Camera.cam_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                            Camera.cam_imx500.set_auto_aspect_ratio()\r\n                        Camera.cam.pre_callback = Camera.parse_and_draw_classification_results\r\n                    elif ai.task == \"object detection\":\r\n                        if Camera.cam_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                            Camera.cam_imx500.set_auto_aspect_ratio()\r\n                        Camera.cam.pre_callback = Camera.draw_detections\r\n                    elif ai.task == \"pose estimation\":\r\n                        Camera.set_drawer()\r\n                        Camera.cam_imx500.set_auto_aspect_ratio()\r\n                        Camera.cam.pre_callback = Camera.picamera2_pre_callback\r\n                    elif ai.task == \"segmentation\":\r\n                        Camera.cam.pre_callback = Camera.create_and_draw_masks\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.applyControls - Registered pre_callback for AI camera\",\r\n                        get_ident(),\r\n                    )\r\n\r\n            else:\r\n                camCtrls = ctrls\r\n                Camera.applyControlsToUsbCamera(ctrls)\r\n        else:\r\n            if Camera.cam2IsUsb == False:\r\n                camCtrls = Controls(Camera.cam2)\r\n                camCtrls.set_controls(ctrls)\r\n                Camera.cam2.controls = camCtrls\r\n                logger.debug(\r\n                    \"Thread %s: Camera.applyControls - Camera.cam2.controls=%s\",\r\n                    get_ident(),\r\n                    Camera.cam2.controls,\r\n                )\r\n                scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n                if \"aiconfig\" in scfg:\r\n                    ai = scfg[\"aiconfig\"]\r\n                    if ai.enable == True:\r\n                        # Register the callback to parse and draw classification results for AI camera\r\n                        if ai.task == \"classification\":\r\n                            if Camera.cam2_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                                Camera.cam2_imx500.set_auto_aspect_ratio()\r\n                            Camera.cam2.pre_callback = Camera.cam2_parse_and_draw_classification_results\r\n                        elif ai.task == \"object detection\":\r\n                            if Camera.cam2_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                                Camera.cam2_imx500.set_auto_aspect_ratio()\r\n                            Camera.cam2.pre_callback = Camera.cam2_draw_detections\r\n                        elif ai.task == \"pose estimation\":\r\n                            Camera.cam2_set_drawer()\r\n                            Camera.cam2_imx500.set_auto_aspect_ratio()\r\n                            Camera.cam2.pre_callback = Camera.cam2_picamera2_pre_callback\r\n                        elif ai.task == \"segmentation\":\r\n                            Camera.cam2.pre_callback = Camera.cam2_create_and_draw_masks\r\n                        logger.debug(\r\n                            \"Thread %s: Camera.applyControls - Registered pre_callback for AI camera\",\r\n                            get_ident(),\r\n                        )\r\n            else:\r\n                camCtrls = ctrls\r\n                Camera.applyControlsToUsbCamera(ctrls, toCam2=True)\r\n        return camCtrls\r\n\r\n    @staticmethod\r\n    def applyControlsForAfCycle(camCfg: CameraConfig):\r\n        \"\"\"Apply camera controls required for AF cycle\"\"\"\r\n        logger.debug(\"Thread %s: Camera.applyControlsForAfCycle\", get_ident())\r\n\r\n        cfg = CameraCfg()\r\n        cfgCtrls = cfg.controls\r\n\r\n        # Initialize controls dict with controls included in configuration\r\n        # ctrls = copy.deepcopy(camCfg.controls)\r\n        ctrls = {}\r\n        cnt = 0\r\n        # Focus\r\n        if cfg.cameraProperties.hasFocus:\r\n            if cfgCtrls.include_afMode and \"AfMode\" not in camCfg.controls:\r\n                ctrls[\"AfMode\"] = cfgCtrls.afMode\r\n                cnt += 1\r\n            if cfgCtrls.include_afMetering and \"AfMetering\" not in camCfg.controls:\r\n                ctrls[\"AfMetering\"] = cfgCtrls.afMetering\r\n                cnt += 1\r\n            if cfgCtrls.include_afPause and \"AfPause\" not in camCfg.controls:\r\n                ctrls[\"AfPause\"] = cfgCtrls.afPause\r\n                cnt += 1\r\n            if cfgCtrls.include_afRange and \"AfRange\" not in camCfg.controls:\r\n                ctrls[\"AfRange\"] = cfgCtrls.afRange\r\n                cnt += 1\r\n            if cfgCtrls.include_afSpeed and \"AfSpeed\" not in camCfg.controls:\r\n                ctrls[\"AfSpeed\"] = cfgCtrls.afSpeed\r\n                cnt += 1\r\n            if cfgCtrls.include_afWindows and \"AfWindows\" not in camCfg.controls:\r\n                ctrls[\"AfWindows\"] = cfgCtrls.afWindows\r\n                cnt += 1\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControlsForAfCycle - Applying %s controls\",\r\n            get_ident(),\r\n            cnt,\r\n        )\r\n        camCtrls = Controls(Camera.cam)\r\n        prgLogger.debug(\"camCtrls = Controls(picam2)\")\r\n        prgLogger.debug(\"ctrls = %s\", ctrls)\r\n        camCtrls.set_controls(ctrls)\r\n        prgLogger.debug(\"camCtrls.set_controls(ctrls)\")\r\n        Camera.cam.controls = camCtrls\r\n        prgLogger.debug(\"picam2.controls = camCtrls\")\r\n        logger.debug(\r\n            \"Thread %s: Camera.applyControlsForAfCycle - Camera.cam.controls=%s\",\r\n            get_ident(),\r\n            Camera.cam.controls,\r\n        )\r\n\r\n    @staticmethod\r\n    def applyControlsForLivestream(wait: float = None):\r\n        \"\"\"Apply active controls if livestream is active\"\"\"\r\n        logger.debug(\"Thread %s: Camera.applyControlsForLivestream\", get_ident())\r\n        if Camera.thread:\r\n            if wait:\r\n                time.sleep(wait)\r\n            Camera.applyControls(Camera.ctrl.configuration)\r\n            if Camera.camIsUsb:\r\n                Camera.logUsbFrameApplyControls = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.applyControlsForLivestream - Controlls applied\",\r\n                get_ident(),\r\n            )\r\n\r\n    @staticmethod\r\n    def stopCameraSystem():\r\n        logger.debug(\"Thread %s: Camera.stopCameraSystem\", get_ident())\r\n        logger.debug(\r\n            \"Thread %s: Camera.stopCameraSystem: Stopping Live view thread\", get_ident()\r\n        )\r\n        Camera.stopRequested = True\r\n        if Camera.thread:\r\n            cnt = 0\r\n            while Camera.thread:\r\n                time.sleep(0.01)\r\n                cnt += 1\r\n                if cnt > 200:\r\n                    break\r\n            if Camera.thread:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Live view thread did not stop within 2 sec\",\r\n                    get_ident(),\r\n                )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Live view thread successfully stopped\",\r\n                    get_ident(),\r\n                )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.stopCameraSystem: Live view thread was not active\",\r\n                get_ident(),\r\n            )\r\n        Camera.stopRequested = False\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.stopCameraSystem: Stopping Video thread\", get_ident()\r\n        )\r\n        Camera.stopVideoRequested = True\r\n        if Camera.videoThread:\r\n            cnt = 0\r\n            while Camera.videoThread:\r\n                time.sleep(0.01)\r\n                cnt += 1\r\n                if cnt > 200:\r\n                    break\r\n            if Camera.videoThread:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Video thread did not stop within 2 sec\",\r\n                    get_ident(),\r\n                )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Video thread successfully stopped\",\r\n                    get_ident(),\r\n                )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.stopCameraSystem: Video thread was not active\",\r\n                get_ident(),\r\n            )\r\n        Camera.stopVideoRequested = False\r\n        Camera.videoDuration = 0\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera.stopCameraSystem: Stopping Photoseries thread\",\r\n            get_ident(),\r\n        )\r\n        Camera.stopPhotoSeriesRequested = True\r\n        if Camera.photoSeriesThread:\r\n            cnt = 0\r\n            while Camera.photoSeriesThread:\r\n                time.sleep(0.01)\r\n                cnt += 1\r\n                if cnt > 500:\r\n                    break\r\n            if Camera.photoSeriesThread:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Photoseries thread did not stop within 5 sec\",\r\n                    get_ident(),\r\n                )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Photoseries thread successfully stopped\",\r\n                    get_ident(),\r\n                )\r\n        else:\r\n            logger.debug(\r\n                \"Thread %s: Camera.stopCameraSystem: Photoseries thread was not active\",\r\n                get_ident(),\r\n            )\r\n        Camera.stopPhotoSeriesRequested = False\r\n\r\n        if Camera.ctrl:\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n\r\n        if Camera.cam2:\r\n            Camera.stopRequested2 = True\r\n            if Camera.thread2:\r\n                cnt = 0\r\n                while Camera.thread2:\r\n                    time.sleep(0.01)\r\n                    cnt += 1\r\n                    if cnt > 200:\r\n                        break\r\n                if Camera.thread2:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.stopCameraSystem: Live view thread 2 did not stop within 2 sec\",\r\n                        get_ident(),\r\n                    )\r\n                else:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.stopCameraSystem: Live view thread 2 successfully stopped\",\r\n                        get_ident(),\r\n                    )\r\n            else:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.stopCameraSystem: Live view thread 2 was not active\",\r\n                    get_ident(),\r\n                )\r\n            Camera.stopRequested2 = False\r\n            if Camera.ctrl2:\r\n                Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n\r\n    @classmethod\r\n    def _thread(cls):\r\n        \"\"\"Camera background thread.\"\"\"\r\n        logger.debug(\"Thread %s: Camera._thread\", get_ident())\r\n        frames_iterator = None\r\n\r\n        if Camera().when_streaming_1_starts:\r\n            Camera().when_streaming_1_starts()\r\n\r\n        ai = CameraCfg().aiConfig\r\n        if ai.enable == True:\r\n            if ai.task == \"object detection\":\r\n                Camera.cam_imx500_last_results = None\r\n\r\n        try:\r\n            if Camera.camIsUsb == False:\r\n                frames_iterator = cls.frames()\r\n            else:\r\n                frames_iterator = cls.framesUsb()\r\n            logger.debug(\r\n                \"Thread %s: Camera._thread - frames_iterator instantiated\", get_ident()\r\n            )\r\n            for frame, frameRaw in frames_iterator:\r\n                Camera.frame = frame\r\n                Camera.frameRaw = frameRaw\r\n                # logger.debug(\"Thread %s: Camera._thread - received frame from camera -> notifying clients\", get_ident())\r\n                Camera.event.set()  # send signal to clients\r\n                Camera.camWaitingForFirstFrame = False\r\n                time.sleep(0)\r\n\r\n                if ai.enable == True:\r\n                    if ai.task == \"object detection\":\r\n                        Camera.cam_imx500_last_results = Camera.parse_detections(Camera.cam.capture_metadata())\r\n\r\n                Camera.threadLock.acquire()\r\n\r\n                stop = False\r\n                # Check whether stop is requested\r\n                if Camera.stopRequested:\r\n                    frames_iterator.close()\r\n                    Camera.stopRequested = False\r\n                    stop = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._thread - Thread is requested to stop.\",\r\n                        get_ident(),\r\n                    )\r\n                    break\r\n\r\n                # if there hasn't been any clients asking for frames in\r\n                # the last 10 seconds then stop the thread\r\n                if time.time() - Camera.last_access > 10:\r\n                    frames_iterator.close()\r\n                    stop = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._thread - Stopping camera thread due to inactivity.\",\r\n                        get_ident(),\r\n                    )\r\n                    break\r\n\r\n                # Release lock if not stopping\r\n                if stop == False:\r\n                    Camera.threadLock.release()\r\n        except UsbCameraNoFrameReceivedError as fe:\r\n            Camera.threadLock.acquire()\r\n            if frames_iterator:\r\n                frames_iterator.close()\r\n            Camera.event.set()\r\n            Camera.camWaitingForFirstFrame = False\r\n            Camera.event.clear()\r\n        except UsbCameraOpenError as ue:\r\n            Camera.threadLock.acquire()\r\n            if frames_iterator:\r\n                frames_iterator.close()\r\n            Camera.event.set()\r\n            Camera.camWaitingForFirstFrame = False\r\n            Camera.event.clear()\r\n        except Exception as e:\r\n            Camera.threadLock.acquire()\r\n            logger.error(\"Thread %s: Camera._thread - Exception: %s\", get_ident(), e)\r\n            if frames_iterator:\r\n                frames_iterator.close()\r\n            Camera.event.set()\r\n            Camera.camWaitingForFirstFrame = False\r\n            Camera.event.clear()\r\n            CameraCfg().serverConfig.error = \"Error in live view: \" + str(e)\r\n            CameraCfg().serverConfig.error2 = (\r\n                \"Probably, a different camera configuration can solve the problem.\"\r\n            )\r\n            CameraCfg().serverConfig.errorSource = \"Camera._thread\"\r\n\r\n        sc = CameraCfg().serverConfig\r\n\r\n        closeCam = True\r\n        if sc.isVideoRecording == True or cls.isVideoRecording() == True:\r\n            closeCam = False\r\n            logger.debug(\r\n                \"Thread %s: Camera._thread - isVideoRecording -> Camera not closing\",\r\n                get_ident(),\r\n            )\r\n        if sc.isPhotoSeriesRecording == True:\r\n            ser = Camera.photoSeries\r\n            if ser:\r\n                if ser.isExposureSeries == True or ser.isFocusStackingSeries == True:\r\n                    closeCam = False\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._thread - Exposure- or PhotoStack series -> Camera not closing\",\r\n                        get_ident(),\r\n                    )\r\n                else:\r\n                    nextTime = ser.nextTime()\r\n                    curTime = datetime.datetime.now()\r\n                    timedif = nextTime - curTime\r\n                    timedifSec = timedif.total_seconds()\r\n                    if timedifSec < 60:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._thread - Photo series next shot within 60 sec -> Camera not closing\",\r\n                            get_ident(),\r\n                        )\r\n                        closeCam = False\r\n        if closeCam == True:\r\n            logger.debug(\"Thread %s: Camera._thread - Closing camera\", get_ident())\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n        sc.isLiveStream = False\r\n\r\n        if Camera().when_streaming_1_stops:\r\n            Camera().when_streaming_1_stops()\r\n\r\n        if Camera.threadLock.locked():\r\n            Camera.threadLock.release()\r\n\r\n        Camera.thread = None\r\n        logger.debug(\"Thread %s: Camera._thread - Terminated\", get_ident())\r\n\r\n    @classmethod\r\n    def _thread2(cls):\r\n        \"\"\"Camera background thread 2.\"\"\"\r\n        logger.debug(\"Thread %s: Camera._thread2\", get_ident())\r\n        frames_iterator = None\r\n\r\n        if Camera().when_streaming_2_starts:\r\n            Camera().when_streaming_2_starts()\r\n\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        if ai.enable == True:\r\n            if ai.task == \"object detection\":\r\n                Camera.cam2_imx500_last_results = None\r\n\r\n        try:\r\n            if Camera.cam2IsUsb == False:\r\n                frames_iterator = cls.frames2()\r\n            else:\r\n                frames_iterator = cls.frames2Usb()\r\n            logger.debug(\r\n                \"Thread %s: Camera._thread2 - frames_iterator instantiated\", get_ident()\r\n            )\r\n            for frame, frameRaw in frames_iterator:\r\n                Camera.frame2 = frame\r\n                Camera.frame2Raw = frameRaw\r\n                # logger.debug(\"Thread %s: Camera._thread2 - received frame from camera -> notifying clients\", get_ident())\r\n                Camera.event2.set()  # send signal to clients\r\n                Camera.cam2WaitingForFirstFrame = False\r\n                time.sleep(0)\r\n\r\n                if ai.enable == True:\r\n                    if ai.task == \"object detection\":\r\n                        Camera.cam2_imx500_last_results = Camera.cam2_parse_detections(Camera.cam2.capture_metadata())\r\n\r\n                # Acquire lock to avoid clients accessing the stream while it is closing down\r\n                # logger.debug(\"Thread %s: Camera._thread2 - About to acquire Lock: thread2Lock=%s.\", get_ident(), Camera.thread2Lock.locked())\r\n                Camera.thread2Lock.acquire()\r\n                # logger.debug(\"Thread %s: Camera._thread2 - Lock acquired: thread2Lock=%s.\", get_ident(), Camera.thread2Lock.locked())\r\n                stop = False\r\n                # Check whether stop is requested\r\n                if Camera.stopRequested2:\r\n                    frames_iterator.close()\r\n                    Camera.stopRequested2 = False\r\n                    stop = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._thread2 - Thread is requested to stop.\",\r\n                        get_ident(),\r\n                    )\r\n                    break\r\n\r\n                # if there hasn't been any clients asking for frames in\r\n                # the last 10 seconds then stop the thread\r\n                if time.time() - Camera.last_access2 > 10:\r\n                    frames_iterator.close()\r\n                    stop = True\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._thread2 - Stopping camera thread due to inactivity.\",\r\n                        get_ident(),\r\n                    )\r\n                    break\r\n\r\n                # Release lock if not stopping\r\n                if stop == False:\r\n                    Camera.thread2Lock.release()\r\n                    # logger.debug(\"Thread %s: Camera._thread2 - Lock released: thread2Lock=%s.\", get_ident(), Camera.thread2Lock.locked())\r\n        except Exception as e:\r\n            Camera.thread2Lock.acquire()\r\n            logger.error(\"Thread %s: Camera._thread2 - Exception: %s\", get_ident(), e)\r\n            if frames_iterator:\r\n                frames_iterator.close()\r\n            Camera.event2.set()\r\n            Camera.cam2WaitingForFirstFrame = False\r\n            Camera.event2.clear()\r\n            CameraCfg().serverConfig.errorc2 = \"Error in camera 2 stream: \" + str(e)\r\n            CameraCfg().serverConfig.errorc22 = (\r\n                \"Probably, a different camera configuration can solve the problem.\"\r\n            )\r\n            CameraCfg().serverConfig.errorc2Source = \"Camera._thread2\"\r\n\r\n        Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n        CameraCfg().serverConfig.isLiveStream2 = False\r\n\r\n        if Camera().when_streaming_2_stops:\r\n            Camera().when_streaming_2_stops()\r\n\r\n        if Camera.thread2Lock.locked():\r\n            Camera.thread2Lock.release()\r\n            logger.debug(\r\n                \"Thread %s: Camera._thread2 - Lock released: thread2Lock=%s.\",\r\n                get_ident(),\r\n                Camera.thread2Lock.locked(),\r\n            )\r\n\r\n        Camera.thread2 = None\r\n\r\n        logger.debug(\"Thread %s: Camera._thread2 - Exit.\", get_ident())\r\n\r\n    @staticmethod\r\n    def framesUsb():\r\n        logger.debug(\"Thread %s: Camera.framesUsb\", get_ident())\r\n        srvCam = CameraCfg()\r\n        imx500 = None\r\n\r\n        try:\r\n            cc, cr = Camera.ctrl.requestConfig(srvCam.photoConfig)\r\n            if cc:\r\n                # If the request for photoConfig caused a configuration change, restart with a new configuration\r\n                Camera.ctrl.clearConfig()\r\n                Camera.ctrl.requestConfig(srvCam.photoConfig)\r\n            Camera.ctrl.requestConfig(srvCam.rawConfig, cfgPhoto=srvCam.photoConfig)\r\n            Camera.ctrl.requestConfig(srvCam.liveViewConfig)\r\n            Camera.cam, started, imx500 = Camera.ctrl.requestStart(\r\n                Camera.cam,\r\n                Camera.camNum,\r\n                Camera.camIsUsb,\r\n                Camera.camUsbDev,\r\n                forActiveCamera=True,\r\n            )\r\n            if not started:\r\n                Camera.cam, excl, imx500 = Camera.ctrl.requestCameraForConfig(\r\n                    Camera.cam, Camera.camNum, cfg=None, forLiveStream=True\r\n                )\r\n            else:\r\n                if Camera.cam.isOpened():\r\n                    logger.debug(\r\n                        \"Thread %s: Camera.framesUsb - camera started\", get_ident()\r\n                    )\r\n                else:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.framesUsb - camera not opened\", get_ident()\r\n                    )\r\n                    raise RuntimeError(\"USB camera could not be opened\")\r\n\r\n            Camera.cam_imx500 = imx500\r\n            # Camera.applyControls(Camera.ctrl.configuration)\r\n            # logger.debug(\"Thread %s: Camera.framesUsb - controls applied\", get_ident())\r\n            # time.sleep(0.5)\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.framesUsb - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n        cfg = Camera.ctrl.configuration\r\n        hflip = cfg.transform.hflip\r\n        vflip = cfg.transform.vflip\r\n        logger.debug(\r\n            \"Thread %s: Camera.framesUsb - hflip=%s vflip=%s\",\r\n            get_ident(),\r\n            hflip,\r\n            vflip,\r\n        )\r\n        gotScalerCropLiveView = False\r\n        try:\r\n            cnt = 0\r\n            Camera.logUsbFrameApplyControls = True\r\n            while True:\r\n                if Camera.cam.isOpened() == False:\r\n                    raise UsbCameraOpenError(\"USB camera not open during live view\")\r\n                success, frame = Camera.cam.read()\r\n                if not success:\r\n                    time.sleep(0.01)\r\n                    cnt += 1\r\n                    if cnt > 100:\r\n                        raise UsbCameraNoFrameReceivedError(\"No frame received from USB camera for live view\")\r\n                else:\r\n                    if gotScalerCropLiveView == False:\r\n                        # Get the live view scaler crop\r\n                        if Camera.resetScalerCropRequested == True:\r\n                            Camera.resetScalerCropUsb()\r\n                        metadata = Camera.getUsbCamMetadata(Camera.cam)\r\n                        srvCam.scalerCropLiveView = metadata[\"ScalerCrop\"]\r\n                        gotScalerCropLiveView = True\r\n                    # logger.debug(\"Thread %s: Camera.framesUsb - Received frame from camera\", get_ident())\r\n                    # Apply controls for each frame to allow dynamic changes\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=Camera.logUsbFrameApplyControls)\r\n                    Camera.logUsbFrameApplyControls = False\r\n                    # Encode frame as JPEG\r\n                    ret, buffer = cv2.imencode(\".jpg\", frame)\r\n                    frameEncoded = buffer.tobytes()\r\n                    yield frameEncoded, frame\r\n        except UsbCameraNoFrameReceivedError as ue:\r\n            logger.debug(\r\n                \"Thread %s: Camera.framesUsb - No frame received after 1 sec\",\r\n                get_ident(),\r\n            )\r\n            raise\r\n        except UsbCameraOpenError as ue:\r\n            logger.debug(\r\n                        \"Thread %s: Camera.framesUsb - camera not opened during streaming\",\r\n                        get_ident(),\r\n                    )\r\n            raise\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.framesUsb - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n    @staticmethod\r\n    def frames():\r\n        logger.debug(\"Thread %s: Camera.frames\", get_ident())\r\n        srvCam = CameraCfg()\r\n        piModelLower5 = srvCam.serverConfig.raspiModelLower5\r\n        imx500 = None\r\n        try:\r\n            cc, cr = Camera.ctrl.requestConfig(srvCam.photoConfig)\r\n            if cc:\r\n                # If the request for photoConfig caused a configuration change, restart with a new configuration\r\n                Camera.ctrl.clearConfig()\r\n                Camera.ctrl.requestConfig(srvCam.photoConfig)\r\n            if piModelLower5 == False:\r\n                Camera.ctrl.requestConfig(srvCam.rawConfig, cfgPhoto=srvCam.photoConfig)\r\n            Camera.ctrl.requestConfig(srvCam.liveViewConfig)\r\n            Camera.cam, started, imx500 = Camera.ctrl.requestStart(\r\n                Camera.cam,\r\n                Camera.camNum,\r\n                Camera.camIsUsb,\r\n                Camera.camUsbDev,\r\n                forActiveCamera=True,\r\n            )\r\n            if not started:\r\n                Camera.cam, excl, imx500 = Camera.ctrl.requestCameraForConfig(\r\n                    Camera.cam, Camera.camNum, cfg=None, forLiveStream=True\r\n                )\r\n            else:\r\n                logger.debug(\"Thread %s: Camera.frames - camera started\", get_ident())\r\n\r\n            if Camera.resetScalerCropRequested == True:\r\n                Camera.resetScalerCrop()\r\n\r\n            Camera.cam_imx500 = imx500\r\n            Camera.applyControls(Camera.ctrl.configuration)\r\n            logger.debug(\"Thread %s: Camera.frames - controls applied\", get_ident())\r\n            time.sleep(0.5)\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.frames - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n        try:\r\n            Camera.streamOutput = StreamingOutput()\r\n            prgLogger.debug(\"output = None\")\r\n            encoder = MJPEGEncoder()\r\n            prgLogger.debug(\"encoder = MJPEGEncoder()\")\r\n            Camera.cam.start_encoder(\r\n                encoder,\r\n                FileOutput(Camera.streamOutput),\r\n                name=srvCam.liveViewConfig.stream,\r\n            )\r\n            prgLogger.debug(\r\n                'picam2.start_encoder(encoder, FileOutput(output), name=\"%s\")',\r\n                srvCam.liveViewConfig.stream,\r\n            )\r\n            prgLogger.debug(\"time.sleep(videoDuration)\")\r\n            Camera.ctrl.registerEncoder(Camera.ENCODER_LIVESTREAM, encoder)\r\n            logger.debug(\"Thread %s: Camera.frames - encoder started\", get_ident())\r\n\r\n            # Get the live view scaler crop\r\n            metadata = Camera.cam.capture_metadata()\r\n            srvCam.serverConfig.scalerCropLiveView = metadata[\"ScalerCrop\"]\r\n            while True:\r\n                # logger.debug(\"Thread %s: Camera.frames - Receiving camera stream\", get_ident())\r\n                with Camera.streamOutput.condition:\r\n                    # logger.debug(\"Thread %s: Camera.frames - waiting\", get_ident())\r\n                    Camera.streamOutput.condition.wait()\r\n                    # logger.debug(\"Thread %s: Camera.frames - waiting done\", get_ident())\r\n                    frame = Camera.streamOutput.frame\r\n                    l = len(frame)\r\n                # logger.debug(\"Thread %s: Camera.frames - got frame with length %s\", get_ident(), l)\r\n                yield frame, None\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.frames - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n    @staticmethod\r\n    def frames2Usb():\r\n        logger.debug(\"Thread %s: Camera.frames2Usb\", get_ident())\r\n        srvCam = CameraCfg()\r\n        imx500 = None\r\n\r\n        Camera.ctrl2.requestConfig(\r\n            srvCam.streamingCfg[str(Camera.camNum2)][\"videoconfig\"]\r\n        )\r\n        Camera.ctrl2.requestConfig(\r\n            srvCam.streamingCfg[str(Camera.camNum2)][\"liveconfig\"]\r\n        )\r\n        Camera.cam2, started, imx500 = Camera.ctrl2.requestStart(\r\n            Camera.cam2,\r\n            Camera.camNum2,\r\n            Camera.cam2IsUsb,\r\n            Camera.cam2UsbDev,\r\n            forActiveCamera=False,\r\n        )\r\n        if not started:\r\n            logger.error(\"Second camera did not start\")\r\n            raise RuntimeError(\"Second camera did not start\")\r\n        else:\r\n            logger.debug(\"Thread %s: Camera.frames2Usb - camera started\", get_ident())\r\n            Camera.cam2_imx500 = imx500\r\n\r\n        cfg = Camera.ctrl2.configuration\r\n        hflip = cfg.transform.hflip\r\n        vflip = cfg.transform.vflip\r\n        Camera.logUsbFrame2ApplyControls = True\r\n        try:\r\n            while True:\r\n                # logger.debug(\"Thread %s: Camera.frames2Usb - Receiving camera stream\", get_ident())\r\n                success, frame = Camera.cam2.read()\r\n                if not success:\r\n                    break\r\n                else:\r\n                    # Apply controls for each frame to allow dynamic changes\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=Camera.logUsbFrame2ApplyControls, toCam2=True)\r\n                    Camera.logUsbFrame2ApplyControls = False\r\n                    # Encode frame as JPEG\r\n                    ret, buffer = cv2.imencode(\".jpg\", frame)\r\n                    frameEncoded = buffer.tobytes()\r\n                yield frameEncoded, frame\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.frames2Usb - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n    @staticmethod\r\n    def frames2():\r\n        logger.debug(\"Thread %s: Camera.frames2\", get_ident())\r\n        srvCam = CameraCfg()\r\n        imx500 = None\r\n\r\n        Camera.ctrl2.requestConfig(\r\n            srvCam.streamingCfg[str(Camera.camNum2)][\"videoconfig\"]\r\n        )\r\n        Camera.ctrl2.requestConfig(\r\n            srvCam.streamingCfg[str(Camera.camNum2)][\"liveconfig\"]\r\n        )\r\n\r\n        Camera.cam2, started, imx500 = Camera.ctrl2.requestStart(\r\n            Camera.cam2,\r\n            Camera.camNum2,\r\n            Camera.cam2IsUsb,\r\n            Camera.cam2UsbDev,\r\n            forActiveCamera=False,\r\n        )\r\n        if not started:\r\n            logger.error(\"Second camera did not start\")\r\n            raise RuntimeError(\"Second camera did not start\")\r\n        else:\r\n            logger.debug(\"Thread %s: Camera.frames2 - camera started\", get_ident())\r\n            Camera.cam2_imx500 = imx500\r\n\r\n        Camera.applyControls(Camera.ctrl2.configuration, toCam2=True)\r\n        logger.debug(\"Thread %s: Camera.frames2 - controls applied\", get_ident())\r\n        time.sleep(0.5)\r\n\r\n        try:\r\n            Camera.stream2Output = StreamingOutput()\r\n            encoder = MJPEGEncoder()\r\n            Camera.cam2.start_encoder(\r\n                encoder,\r\n                FileOutput(Camera.stream2Output),\r\n                name=srvCam.streamingCfg[str(Camera.camNum2)][\"liveconfig\"].stream,\r\n            )\r\n            Camera.ctrl2.registerEncoder(Camera.ENCODER_LIVESTREAM, encoder)\r\n            logger.debug(\"Thread %s: Camera.frames2 - encoder started\", get_ident())\r\n\r\n            while True:\r\n                # logger.debug(\"Thread %s: Camera.frames2 - Receiving camera stream\", get_ident())\r\n                with Camera.stream2Output.condition:\r\n                    # logger.debug(\"Thread %s: Camera.frames2 - waiting\", get_ident())\r\n                    Camera.stream2Output.condition.wait()\r\n                    # logger.debug(\"Thread %s: Camera.frames2 - waiting done\", get_ident())\r\n                    frame = Camera.stream2Output.frame\r\n                    l = len(frame)\r\n                # logger.debug(\"Thread %s: Camera.frames2 - got frame with length %s\", get_ident(), l)\r\n                yield frame, None\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.frames2 - Exception: %s\", get_ident(), e)\r\n            raise\r\n\r\n    @staticmethod\r\n    def getUsbScalerCrop(width: int, height: int, log=True, forCam2=None) -> tuple:\r\n        \"\"\"Get ScalerCrop for a given size for USB camera\r\n        \r\n            Determine ScalerCrop assuming that the camera will first crop to the requested aspect ratio\r\n            and then scale to the requested resolution\r\n        \"\"\"\r\n        if forCam2 is None:\r\n            forCam2 = False\r\n        if log:\r\n            logger.debug(\"Thread %s: Camera.getUsbScalerCrop - width: %d, height: %d forCam2: %s\", get_ident(), width, height, forCam2)\r\n        aspectRatio = width / height\r\n        cfg = CameraCfg()\r\n        if forCam2 == False:\r\n            sensorWidth = cfg.cameraProperties.pixelArraySize[0]\r\n            sensorHeight = cfg.cameraProperties.pixelArraySize[1]\r\n        else:\r\n            cam2Str = str(Camera.camNum2)\r\n            strCfg = cfg.streamingCfg[cam2Str]\r\n            if \"cameraproperties\" in strCfg:\r\n                cam2Props = strCfg[\"cameraproperties\"]\r\n                sensorWidth = cam2Props.pixelArraySize[0]\r\n                sensorHeight = cam2Props.pixelArraySize[1]\r\n            else:\r\n                sensorWidth = cfg.cameraProperties.pixelArraySize[0]\r\n                sensorHeight = cfg.cameraProperties.pixelArraySize[1]\r\n        if log:\r\n            logger.debug(\"Thread %s: Camera.getUsbScalerCrop - sensorWidth: %d, sensorHeight: %d\", get_ident(), sensorWidth, sensorHeight)\r\n        sensorAspectRatio = sensorWidth / sensorHeight\r\n        if aspectRatio > sensorAspectRatio:\r\n            # Crop height\r\n            cropHeight = sensorWidth / aspectRatio\r\n            cropY = (sensorHeight - cropHeight) / 2\r\n            scalerCrop = (\r\n                0,\r\n                int(cropY),\r\n                sensorWidth,\r\n                int(cropHeight),\r\n            )\r\n        else:\r\n            # Crop width\r\n            cropWidth = sensorHeight * aspectRatio\r\n            cropX = (sensorWidth - cropWidth) / 2\r\n            scalerCrop = (\r\n                int(cropX),\r\n                0,\r\n                int(cropWidth),\r\n                sensorHeight,\r\n            )\r\n        if log:\r\n            logger.debug(\"Thread %s: Camera.getUsbScalerCrop - scalerCrop: %s\", get_ident(), scalerCrop)\r\n        return scalerCrop\r\n\r\n    @staticmethod\r\n    def getUsbCamMetadata(cam, log=True) -> dict:\r\n        \"\"\"Get metadata from USB camera using OpenCV\"\"\"\r\n        logger.debug(\"Thread %s: Camera.getUsbCamMetadata\", get_ident())\r\n\r\n        width = int(cam.get(cv2.CAP_PROP_FRAME_WIDTH))\r\n        height = int(cam.get(cv2.CAP_PROP_FRAME_HEIGHT))\r\n\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        cc = cfg.controls\r\n        if width > 0 and height > 0:\r\n            if cc.include_scalerCrop == True:\r\n                scalerCrop = cc.scalerCrop\r\n                # Map scalerCrop for LiveView to current resolution\r\n                if width != cfg.liveViewConfig.stream_size[0] or height != cfg.liveViewConfig.stream_size[1]:\r\n                    aspectRatioLiveView = cfg.liveViewConfig.stream_size[0] / cfg.liveViewConfig.stream_size[1]\r\n                    aspectRatioCurrent = width / height\r\n                    if aspectRatioLiveView != aspectRatioCurrent:\r\n                        x1, y1, w, h = scalerCrop\r\n                        if aspectRatioCurrent > aspectRatioLiveView:\r\n                            # Extend width\r\n                            newW = h * aspectRatioCurrent\r\n                            newX1 = x1 - (newW - w) / 2\r\n                            scalerCrop = (\r\n                                int(newX1),\r\n                                y1,\r\n                                int(newW),\r\n                                h,\r\n                            )\r\n                        else:\r\n                            # Extend height\r\n                            newH = w / aspectRatioCurrent\r\n                            newY1 = y1 - (newH - h) / 2\r\n                            scalerCrop = (\r\n                                x1,\r\n                                int(newY1),\r\n                                w,\r\n                                int(newH),\r\n                            )\r\n            else:\r\n                scalerCrop = Camera.getUsbScalerCrop(width, height)\r\n        else:\r\n            scalerCrop = sc.scalerCropMax\r\n\r\n        metadata = {\r\n            \"Width\": width,\r\n            \"Height\": height,\r\n            \"ScalerCrop\": scalerCrop,\r\n            \"FPS\": cam.get(cv2.CAP_PROP_FPS),\r\n            \"Format (FOURCC)\": int(cam.get(cv2.CAP_PROP_FOURCC)),\r\n            \"Format\": \"\".join(\r\n                [chr((int(cam.get(cv2.CAP_PROP_FOURCC)) >> 8 * i) & 0xFF) for i in range(4)]\r\n            ),\r\n            \"Brightness\": cam.get(cv2.CAP_PROP_BRIGHTNESS),\r\n            \"Contrast\": cam.get(cv2.CAP_PROP_CONTRAST),\r\n            \"Saturation\": cam.get(cv2.CAP_PROP_SATURATION),\r\n            \"Hue\": cam.get(cv2.CAP_PROP_HUE),\r\n            \"Gain\": cam.get(cv2.CAP_PROP_GAIN),\r\n            \"Exposure\": cam.get(cv2.CAP_PROP_EXPOSURE),\r\n            \"Exposure\": cam.get(cv2.CAP_PROP_EXPOSURE),\r\n            \"White Balance Temperature\": cam.get(cv2.CAP_PROP_WB_TEMPERATURE),\r\n            \"White Balance Auto WB\": cam.get(cv2.CAP_PROP_AUTO_WB),\r\n            \"Focus\": cam.get(cv2.CAP_PROP_FOCUS),\r\n            \"Autofocus\": cam.get(cv2.CAP_PROP_AUTOFOCUS),\r\n            }\r\n        return metadata\r\n\r\n    @staticmethod\r\n    def takeImage(\r\n        filename: str,\r\n        keepExclusive: bool = False,\r\n        noEvents: bool = False,\r\n        alternatePath: str = \"\",\r\n    ) -> str:\r\n        \"\"\"Takes a photo with the specified file name and returns the path\r\n\r\n        filename:       file name for the photo\r\n        keepExclusive:  If True, keep the exclusive mode\r\n                        This can be used for example if a jpg photo shall be taken\r\n                        before a video is recorded\r\n        noEvents:       If True, no events are triggered\r\n        alternatePath:  If not empty, the file path of the photo,\r\n                        otherwise the standard photo path is taken\r\n                        and the display buffer is not updated\r\n\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.takeImage - filename: %s keepExclusive: %s\",\r\n            get_ident(),\r\n            filename,\r\n            keepExclusive,\r\n        )\r\n        fp = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        if noEvents == False:\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage Checking for callback: when_photo_taken=%s\",\r\n                get_ident(),\r\n                Camera().when_photo_taken,\r\n            )\r\n            if Camera().when_photo_taken:\r\n                Camera().when_photo_taken()\r\n        try:\r\n            forceExclusive = False\r\n            if Camera.camIsUsb == True:\r\n                forceExclusive = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage Requesting camera for photoConfig\",\r\n                get_ident(),\r\n            )\r\n            Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n                Camera.cam, Camera.camNum, cfg.photoConfig, forceExclusive=forceExclusive\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage Got camera for photoConfig exclusive: %s\",\r\n                get_ident(),\r\n                exclusive,\r\n            )\r\n\r\n            Camera.applyControls(Camera.ctrl.configuration)\r\n            logger.debug(\"Thread %s: Camera.takeImage - controls applied\", get_ident())\r\n\r\n            if Camera.camIsUsb == False:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.takeImage - Camera.cam.controls=%s\",\r\n                    get_ident(),\r\n                    Camera.cam.controls,\r\n                )\r\n                request = Camera.cam.capture_request()\r\n                prgLogger.debug(\"request = picam2.capture_request()\")\r\n                logger.debug(\"Thread %s: Camera.takeImage: Request started\", get_ident())\r\n            path = sc.photoRoot + \"/\" + sc.cameraPhotoSubPath\r\n            if alternatePath != \"\":\r\n                path = alternatePath\r\n            fp = path + \"/\" + filename\r\n            if Camera.camIsUsb == False:\r\n                request.save(cfg.photoConfig.stream, fp)\r\n                prgLogger.debug(\r\n                    'request.save(\"%s\", \"%s\")',\r\n                    cfg.photoConfig.stream,\r\n                    sc.prgOutputPath + \"/\" + filename,\r\n                )\r\n            else:\r\n                # For USB cameras, save the image using OpenCV\r\n                if Camera.cam.isOpened() == False:\r\n                    raise RuntimeError(\"USB camera is not opened\")\r\n                success, frame = Camera.cam.read()\r\n                if success:\r\n                    conf = Camera.ctrl.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=True)\r\n                    cv2.imwrite(fp, frame)\r\n                else:\r\n                    raise RuntimeError(\"Failed to capture image from USB camera\")\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage: Image saved as %s\", get_ident(), fp\r\n            )\r\n            if alternatePath == \"\":\r\n                sc.displayFile = filename\r\n                sc.displayPhoto = sc.cameraPhotoSubPath + \"/\" + filename\r\n                sc.isDisplayHidden = False\r\n                if Camera.camIsUsb == False:\r\n                    metadata = request.get_metadata()\r\n                    prgLogger.debug(\"metadata = request.get_metadata()\")\r\n                else:\r\n                    metadata = Camera.getUsbCamMetadata(Camera.cam)\r\n                sc.displayMeta = {\"Camera\": sc.activeCameraInfo}\r\n                sc.displayMeta.update(metadata)\r\n                sc.displayMetaFirst = 0\r\n                if len(metadata) < 11:\r\n                    sc._displayMetaLast = 999\r\n                else:\r\n                    sc.displayMetaLast = 10\r\n                sc.displayHistogram = None\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage: Image metedata captured\", get_ident()\r\n            )\r\n            if Camera.camIsUsb == False:\r\n                request.release()\r\n                prgLogger.debug(\"request.release()\")\r\n                logger.debug(\"Thread %s: Camera.takeImage: Request released\", get_ident())\r\n\r\n            if not keepExclusive:\r\n                Camera.cam = Camera.ctrl.restoreLivestream(Camera.cam, exclusive)\r\n                if (\r\n                    sc.isPhotoSeriesRecording == False\r\n                    and sc.isVideoRecording == False\r\n                    and sc.isLiveStream == False\r\n                ):\r\n                    Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.takeImage: Error %s\", get_ident(), e)\r\n            if not sc.error:\r\n                sc.error = \"Phototaking caused error: \" + str(e)\r\n                sc.errorSource = \"Camera.takeImage\"\r\n        Camera.liveViewDeactivated = False\r\n        return fp\r\n\r\n    @staticmethod\r\n    def takeImage2(\r\n        filename: str,\r\n        keepExclusive: bool = False,\r\n        noEvents: bool = False,\r\n        alternatePath: str = \"\",\r\n    ) -> str:\r\n        \"\"\"Takes a photo with second camera with the specified file name and returns the path\r\n\r\n        filename:       file name for the photo\r\n        keepExclusive:  If True, keep the exclusive mode\r\n                        This can be used for example if a jpg photo shall be taken\r\n                        before a video is recorded\r\n        noEvents:       If True, no events are triggered\r\n        alternatePath:  If not empty, the file path of the photo,\r\n                        otherwise the standard photo path is taken\r\n                        and the display buffer is not updated\r\n\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.takeImage2 - filename: %s keepExclusive: %s\",\r\n            get_ident(),\r\n            filename,\r\n            keepExclusive,\r\n        )\r\n        fp = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        if noEvents == False:\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage2 Checking for callback: when_photo_taken=%s\",\r\n                get_ident(),\r\n                Camera().when_photo_taken,\r\n            )\r\n            if Camera().when_photo_2_taken:\r\n                Camera().when_photo_2_taken()\r\n        try:\r\n            photoConfig = cfg.streamingCfg[str(Camera.camNum2)][\"photoconfig\"]\r\n            forceExclusive = False\r\n            if Camera.cam2IsUsb == True:\r\n                forceExclusive = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage2 Requesting camera for photoConfig\",\r\n                get_ident(),\r\n            )\r\n            Camera.cam2, exclusive, Camera.cam2_imx500 = Camera.ctrl2.requestCameraForConfig(\r\n                Camera.cam2, Camera.camNum2, photoConfig, forActiveCamera=False, forceExclusive=forceExclusive\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage2 Got camera for photoConfig exclusive: %s\",\r\n                get_ident(),\r\n                exclusive,\r\n            )\r\n\r\n            Camera.applyControls(Camera.ctrl2.configuration, toCam2=True)\r\n            logger.debug(\"Thread %s: Camera.takeImage2 - controls applied\", get_ident())\r\n\r\n            if Camera.cam2IsUsb == False:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.takeImage2 - Camera.cam2.controls=%s\",\r\n                    get_ident(),\r\n                    Camera.cam2.controls,\r\n                )\r\n                request = Camera.cam2.capture_request()\r\n                prgLogger.debug(\"request = picam2.capture_request()\")\r\n                logger.debug(\"Thread %s: Camera.takeImage2: Request started\", get_ident())\r\n            cameraPhotoSubPath = \"photos/\" + \"camera_\" + str(Camera.camNum2)\r\n            path = sc.photoRoot + \"/\" + cameraPhotoSubPath\r\n            if alternatePath != \"\":\r\n                path = alternatePath\r\n            fp = path + \"/\" + filename\r\n            if Camera.cam2IsUsb == False:\r\n                request.save(photoConfig.stream, fp)\r\n                prgLogger.debug(\r\n                    'request.save(\"%s\", \"%s\")',\r\n                    photoConfig.stream,\r\n                    sc.prgOutputPath + \"/\" + filename,\r\n                )\r\n            else:\r\n                # For USB cameras, save the image using OpenCV\r\n                if Camera.cam2.isOpened() == False:\r\n                    raise RuntimeError(\"USB camera 2 is not opened\")\r\n                success, frame = Camera.cam2.read()\r\n                if success:\r\n                    conf = Camera.ctrl2.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=True, toCam2=True)\r\n                    cv2.imwrite(fp, frame)\r\n                else:\r\n                    raise RuntimeError(\"Failed to capture image from USB camera\")\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage2: Image saved as %s\", get_ident(), fp\r\n            )\r\n            if Camera.cam2IsUsb == False:\r\n                request.release()\r\n                prgLogger.debug(\"request.release()\")\r\n                logger.debug(\"Thread %s: Camera.takeImage2: Request released\", get_ident())\r\n\r\n            if not keepExclusive:\r\n                Camera.cam2 = Camera.ctrl2.restoreLivestream2(Camera.cam2, exclusive)\r\n                if sc.isVideoRecording2 == False and sc.isLiveStream2 == False:\r\n                    Camera.cam2, done = Camera.ctrl2.requestStop(\r\n                        Camera.cam2, close=True\r\n                    )\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.takeImage2: Error %s\", get_ident(), e)\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Phototaking caused error: \" + str(e)\r\n                sc.errorc2Source = \"Camera.takeImage2\"\r\n        Camera.liveView2Deactivated = False\r\n        return fp\r\n\r\n    @staticmethod\r\n    def quickPhoto(fp: str, saveImage: bool = True) -> tuple:\r\n        \"\"\"Take a photo assuming that the camera is started\r\n        \r\n        Parameters:\r\n            fp:         File path where the photo shall be saved\r\n            saveImage:  True: save image to file\r\n                        False: do not save image but return frame\r\n        Returns:\r\n            done:   True if photo was saved to file\r\n            err:    Error message if any\r\n            img:    Image frame if saveImage is False\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.quickPhoto - filename: %s\", get_ident(), fp)\r\n        done = False\r\n        err = \"\"\r\n        frameRaw = None\r\n        cfg = CameraCfg()\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    if saveImage == True:\r\n                        request = Camera.cam.capture_request()\r\n                        request.save(cfg.photoConfig.stream, fp)\r\n                        request.release()\r\n                        done = True\r\n                    else:\r\n                        request = Camera.cam.capture_request()\r\n                        frameRaw = Camera.cam.capture_array(cfg.liveViewConfig.stream)\r\n                        if cfg.liveViewConfig.format == \"YUV420\":\r\n                            if cv2Available == True:\r\n                                frameRaw = cv2.cvtColor(frameRaw, cv2.COLOR_YUV2BGR_I420)\r\n                        request.release()\r\n                except Exception as e:\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            if Camera.cam.isOpened() == True:\r\n                frame, frameRaw = Camera().get_frame()\r\n                if saveImage == True:\r\n                    cv2.imwrite(fp, frameRaw)\r\n                    done = True\r\n            else:\r\n                err = \"USB Camera not started\"\r\n        return (done, err, copy.copy(frameRaw))\r\n\r\n    @staticmethod\r\n    def quickUsbVideoThread(out):\r\n        \"\"\"Record a video from a USB camera\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.quickUsbVideoThread - starting recording\", get_ident()\r\n        )\r\n        done = False\r\n        if Camera.cam.isOpened() == False:\r\n            logger.error(\r\n                \"Thread %s: Camera.quickUsbVideoThread - USB camera not opened\", get_ident()\r\n            )\r\n            done = True\r\n        if out.isOpened() == False:\r\n            logger.error(\r\n                \"Thread %s: Camera.quickUsbVideoThread - VideoWriter not opened\", get_ident()\r\n            )\r\n            done = True\r\n        while not done:\r\n            logger.debug(\"Thread %s: Camera.quickUsbVideoThread - acquiring lock - locked: %s\", get_ident(), Camera.threadUsbVideoLock.locked())\r\n            Camera.threadUsbVideoLock.acquire()\r\n            logger.debug(\"Thread %s: Camera.quickUsbVideoThread - getting frame\", get_ident())\r\n            frame, frameRaw = Camera().get_frame()\r\n            logger.debug(\"Thread %s: Camera.quickUsbVideoThread - got frame\", get_ident())\r\n            out.write(frameRaw)\r\n            logger.debug(\"Thread %s: Camera.quickUsbVideoThread - wrote frame\", get_ident())\r\n            if Camera.stopUsbVideoRequested == True:\r\n                done = True\r\n            Camera.threadUsbVideoLock.release()\r\n        logger.debug(\r\n            \"Thread %s: Camera.quickUsbVideoThread - stopping recording\", get_ident()\r\n        )\r\n\r\n        if Camera.threadUsbVideoLock.locked():\r\n            Camera.threadUsbVideoLock.release()\r\n        Camera.threadUsbVideo = None\r\n\r\n    @staticmethod\r\n    def quickVideoStart(fp: str) -> tuple:\r\n        \"\"\"Record a video assuming that the camera is started\"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.quickVideoStart - filename: %s\", get_ident(), fp\r\n        )\r\n        encoder = None\r\n        done = False\r\n        err = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    encoder = H264Encoder()\r\n                    output = fp\r\n                    if output.lower().endswith(\".mp4\"):\r\n                        if sc.recordAudio == False:\r\n                            encoder.output = FfmpegOutput(output, audio=False)\r\n                        else:\r\n                            encoder.output = FfmpegOutput(\r\n                                output, audio=True, audio_sync=sc.audioSync\r\n                            )\r\n                    else:\r\n                        encoder.output = FileOutput(output)\r\n\r\n                    stream = cfg.videoConfig.stream\r\n                    # For Pi Zero take video with liveView (lowres stream)\r\n                    # The lower buffer size of these devices is too small for full size video\r\n                    # and we do not want to switch mode\r\n                    if (\r\n                        cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi Zero\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 4\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 3\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 2\")\r\n                        or cfg.serverConfig.raspiModelFull.startswith(\"Raspberry Pi 1\")\r\n                    ):\r\n                        stream = cfg.liveViewConfig.stream\r\n                    Camera.cam.start_encoder(encoder, name=stream)\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.quickVideoStart - error when starting encoder: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            if Camera.cam.isOpened() == True:\r\n                frameRate = 30\r\n                Camera.cam.set(cv2.CAP_PROP_FPS, frameRate)\r\n                fourcc = cv2.VideoWriter_fourcc(*\"avc1\")\r\n                width = int(Camera.cam.get(cv2.CAP_PROP_FRAME_WIDTH))\r\n                height = int(Camera.cam.get(cv2.CAP_PROP_FRAME_HEIGHT))\r\n                out = cv2.VideoWriter(fp, fourcc, frameRate, (width, height))\r\n                logger.debug(\r\n                    \"Thread %s: Camera.quickVideoStart - starting quickUsbVideoThread\", get_ident()\r\n                )\r\n                Camera.stopUsbVideoRequested = False\r\n                Camera.threadUsbVideo = threading.Thread(\r\n                    target=Camera.quickUsbVideoThread, args=(out,)\r\n                )\r\n                Camera.threadUsbVideo.start()\r\n                encoder = out\r\n                done = True\r\n            else:\r\n                err = \"USB Camera not started\"\r\n        return (done, encoder, err)\r\n\r\n    @staticmethod\r\n    def quickVideoStop(encoder) -> tuple:\r\n        \"\"\"Stop a video recording that the camera is started\"\"\"\r\n        logger.debug(\"Thread %s: Camera.quickVideoStop\", get_ident())\r\n        done = False\r\n        err = \"\"\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    Camera.cam.stop_encoder(encoder)\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.quickVideoStop - error when stopping encoder: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            if Camera.threadUsbVideo:\r\n                logger.debug(\r\n                    \"Thread %s: Camera.quickVideoStop - stopping quickUsbVideoThread\", get_ident()\r\n                )\r\n                with Camera.threadUsbVideoLock:\r\n                    Camera.stopUsbVideoRequested = True\r\n                while Camera.threadUsbVideo:\r\n                    time.sleep(0.1)\r\n                encoder.release()\r\n                logger.debug(\r\n                    \"Thread %s: Camera.quickVideoStop - quickUsbVideoThread stopped\",\r\n                    get_ident(),\r\n                )\r\n                Camera.stopUsbVideoRequested = False\r\n                done = True\r\n            else:\r\n                err = \"USB video thread not running\"\r\n        return (done, err)\r\n\r\n    @staticmethod\r\n    def startCircular(buffersizeSec=5) -> tuple:\r\n        \"\"\"Start encoder for circular output\"\"\"\r\n        logger.debug(\"Thread %s: Camera.startCircular\", get_ident())\r\n        encoder = None\r\n        circ = None\r\n        done = False\r\n        err = \"\"\r\n        cfg = CameraCfg()\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    encoder = H264Encoder()\r\n                    sm = cfg.videoConfig.sensor_mode\r\n                    if sm == \"custom\":\r\n                        buffersize = 150\r\n                    else:\r\n                        buffersize = cfg.sensorModes[sm].fps * buffersizeSec\r\n                    circ = CircularOutput(buffersize=buffersize)\r\n                    encoder.output = [circ]\r\n                    Camera.cam.encoders = encoder\r\n                    Camera.cam.start_encoder(encoder, name=cfg.videoConfig.stream)\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.startCircular - error when starting encoder: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            err = \"USB camera does not support circular recording\"\r\n        return (done, circ, encoder, err)\r\n\r\n    @staticmethod\r\n    def stopCircular(encoder) -> tuple:\r\n        \"\"\"Stop encoder for circular output\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopCircular\", get_ident())\r\n        done = False\r\n        err = \"\"\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    Camera.cam.stop_encoder(encoder)\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.stopCircular - error when stopping encoder: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            err = \"USB camera does not support circular recording\"\r\n        return (done, err)\r\n\r\n    @staticmethod\r\n    def recordCircular(circ: CircularOutput, fp: str) -> tuple:\r\n        \"\"\"Start recording circular output\"\"\"\r\n        logger.debug(\"Thread %s: Camera.recordCircular - file: %s\", get_ident(), fp)\r\n        done = False\r\n        err = \"\"\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    circ.fileoutput = fp\r\n                    circ.start()\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.recordCircular - error when starting circular: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            err = \"USB camera does not support circular recording\"\r\n        return (done, err)\r\n\r\n    @staticmethod\r\n    def stopRecordingCircular(circ: CircularOutput) -> tuple:\r\n        \"\"\"Start recording circular output\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopRecordingCircular\", get_ident())\r\n        done = False\r\n        err = \"\"\r\n        if Camera.camIsUsb == False:\r\n            if Camera.cam.started:\r\n                try:\r\n                    circ.stop()\r\n                    done = True\r\n                except Exception as e:\r\n                    logger.error(\r\n                        \"Thread %s: Camera.stopRecordingCircular - error when stopping circular: %s\",\r\n                        get_ident(),\r\n                        e,\r\n                    )\r\n                    err = str(e)\r\n            else:\r\n                err = \"Camera not started\"\r\n        else:\r\n            err = \"USB camera does not support circular recording\"\r\n        return (done, err)\r\n\r\n    @staticmethod\r\n    def takeRawImage(\r\n        filenameRaw: str, filename: str, noEvents: bool = False, alternatePath: str = \"\"\r\n    ):\r\n        \"\"\"Takes a photo as well as a raw image with the specified file names\r\n        and returns the path for the raw photo\r\n        filenameRaw: file name for the raw image\r\n        filename:    file name for the photo\r\n        noEvents:       If True, no events are triggered\r\n        alternatePath:  If not empty, the file path of the photo,\r\n                        otherwise the standard photo path is taken\r\n                        and the display buffer is not updated\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.takeRawImage\", get_ident())\r\n        fpr = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        piModelLower5 = sc.raspiModelLower5\r\n\r\n        if noEvents == False:\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeImage Checking for callback: when_photo_taken=%s\",\r\n                get_ident(),\r\n                Camera().when_photo_taken,\r\n            )\r\n            if Camera().when_photo_taken:\r\n                Camera().when_photo_taken()\r\n\r\n        try:\r\n            forceExclusive = False\r\n            if Camera.camIsUsb == True:\r\n                forceExclusive = True\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage Requesting camera for rawConfig\",\r\n                get_ident(),\r\n            )\r\n            Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n                Camera.cam, Camera.camNum, cfg.rawConfig, cfg.photoConfig, forceExclusive=forceExclusive\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage Got camera for rawConfig exclusive: %s\",\r\n                get_ident(),\r\n                exclusive,\r\n            )\r\n\r\n            Camera.applyControls(Camera.ctrl.configuration)\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage: controls applied\", get_ident()\r\n            )\r\n\r\n            if Camera.camIsUsb == False:\r\n                request = Camera.cam.capture_request()\r\n                prgLogger.debug(\"request = picam2.capture_request()\")\r\n                logger.debug(\"Thread %s: Camera.takeRawImage: Request started\", get_ident())\r\n            path = sc.photoRoot + \"/\" + sc.cameraPhotoSubPath\r\n            if alternatePath != \"\":\r\n                path = alternatePath\r\n            fp = path + \"/\" + filename\r\n            fpr = path + \"/\" + filenameRaw\r\n            if Camera.camIsUsb == False:\r\n                request.save(\"main\", fp)\r\n                prgLogger.debug(\r\n                    'request.save(\"main\", \"%s\")', sc.prgOutputPath + \"/\" + filename\r\n                )\r\n                request.save_dng(fpr)\r\n                prgLogger.debug('request.save_dng(\"%s\")', fpr)\r\n            else:\r\n                # For USB cameras, save the image using OpenCV\r\n                if Camera.cam.isOpened() == False:\r\n                    raise RuntimeError(\"USB camera is not opened\")\r\n                success, frame = Camera.cam.read()\r\n                if success:\r\n                    conf = Camera.ctrl.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=True)\r\n                    cv2.imwrite(fp, frame)\r\n                    cv2.imwrite(fpr, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1])\r\n                else:\r\n                    raise RuntimeError(\"Failed to capture image from USB camera\")\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage: Raw Image saved as %s\",\r\n                get_ident(),\r\n                fpr,\r\n            )\r\n            if alternatePath == \"\":\r\n                sc.displayFile = filenameRaw\r\n                sc.displayPhoto = sc.cameraPhotoSubPath + \"/\" + filename\r\n                sc.isDisplayHidden = False\r\n                if Camera.camIsUsb == False:\r\n                    metadata = request.get_metadata()\r\n                    prgLogger.debug(\"metadata = request.get_metadata()\")\r\n                else:\r\n                    metadata = Camera.getUsbCamMetadata(Camera.cam)\r\n                sc.displayMeta = {\"Camera\": sc.activeCameraInfo}\r\n                sc.displayMeta.update(metadata)\r\n                sc.displayMetaFirst = 0\r\n                if len(metadata) < 11:\r\n                    sc._displayMetaLast = 999\r\n                else:\r\n                    sc.displayMetaLast = 10\r\n                sc.displayHistogram = None\r\n                logger.debug(\r\n                    \"Thread %s: Camera.takeRawImage: Raw Image metedata captured\",\r\n                    get_ident(),\r\n                )\r\n            if Camera.camIsUsb == False:\r\n                request.release()\r\n                prgLogger.debug(\"request.release()\")\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage: Request released\", get_ident()\r\n            )\r\n            \r\n            if piModelLower5 == True:\r\n                Camera.ctrl.clearConfig()\r\n            Camera.cam = Camera.ctrl.restoreLivestream(Camera.cam, exclusive)\r\n            if (\r\n                sc.isPhotoSeriesRecording == False\r\n                and sc.isVideoRecording == False\r\n                and sc.isLiveStream == False\r\n            ):\r\n                Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.takeRawImage: Error %s\", get_ident(), e)\r\n            if not sc.error:\r\n                sc.error = \"Taking raw photo caused error: \" + str(e)\r\n                sc.errorSource = \"Camera.takeRawImage\"\r\n        Camera.liveViewDeactivated = False\r\n        return fpr\r\n\r\n    @staticmethod\r\n    def takeRawImage2(\r\n        filenameRaw: str, filename: str, noEvents: bool = False, alternatePath: str = \"\"\r\n    ):\r\n        \"\"\"Takes a photo as well as a raw image with the specified file names\r\n        and returns the path for the raw photo\r\n        filenameRaw: file name for the raw image\r\n        filename:    file name for the photo\r\n        noEvents:       If True, no events are triggered\r\n        alternatePath:  If not empty, the file path of the photo,\r\n                        otherwise the standard photo path is taken\r\n                        and the display buffer is not updated\r\n        \"\"\"\r\n        logger.debug(\"Thread %s: Camera.takeRawImage2\", get_ident())\r\n        fpr = \"\"\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        if noEvents == False:\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage2 Checking for callback: when_photo_2_taken=%s\",\r\n                get_ident(),\r\n                Camera().when_photo_2_taken,\r\n            )\r\n            if Camera().when_photo_2_taken:\r\n                Camera().when_photo_2_taken()\r\n\r\n        try:\r\n            forceExclusive = False\r\n            if Camera.cam2IsUsb == True:\r\n                forceExclusive = True\r\n            rawConfig = cfg.streamingCfg[str(Camera.camNum2)][\"rawconfig\"]\r\n            photoConfig = cfg.streamingCfg[str(Camera.camNum2)][\"photoconfig\"]\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage2 Requesting camera for rawConfig\",\r\n                get_ident(),\r\n            )\r\n            Camera.cam2, exclusive, Camera.cam2_imx500 = Camera.ctrl2.requestCameraForConfig(\r\n                Camera.cam2,\r\n                Camera.camNum2,\r\n                rawConfig,\r\n                photoConfig,\r\n                forActiveCamera=False,\r\n                forceExclusive=forceExclusive,\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage2 Got camera for rawConfig exclusive: %s\",\r\n                get_ident(),\r\n                exclusive,\r\n            )\r\n\r\n            Camera.applyControls(Camera.ctrl2.configuration, toCam2=True)\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage2: controls applied\", get_ident()\r\n            )\r\n\r\n            if Camera.cam2IsUsb == False:\r\n                request = Camera.cam2.capture_request()\r\n                prgLogger.debug(\"request = picam2.capture_request()\")\r\n                logger.debug(\r\n                    \"Thread %s: Camera.takeRawImage2: Request started\", get_ident()\r\n                )\r\n            cameraPhotoSubPath = \"photos/\" + \"camera_\" + str(Camera.camNum2)\r\n            path = sc.photoRoot + \"/\" + cameraPhotoSubPath\r\n            if alternatePath != \"\":\r\n                path = alternatePath\r\n            fp = path + \"/\" + filename\r\n            fpr = path + \"/\" + filenameRaw\r\n            if Camera.cam2IsUsb == False:\r\n                request.save(photoConfig.stream, fp)\r\n                prgLogger.debug(\r\n                    'request.save(\"%s\", \"%s\")',\r\n                    photoConfig.stream,\r\n                    sc.prgOutputPath + \"/\" + filename,\r\n                )\r\n                request.save_dng(fpr)\r\n                prgLogger.debug('request.save_dng(\"%s\")', fpr)\r\n            else:\r\n                # For USB cameras, save the image using OpenCV\r\n                if Camera.cam2.isOpened() == False:\r\n                    raise RuntimeError(\"USB camera is not opened\")\r\n                success, frame = Camera.cam2.read()\r\n                if success:\r\n                    conf = Camera.ctrl2.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, log=True, toCam2=True)\r\n                    cv2.imwrite(fp, frame)\r\n                    cv2.imwrite(fpr, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1])\r\n                else:\r\n                    raise RuntimeError(\"Failed to capture image from USB camera\")\r\n            logger.debug(\r\n                \"Thread %s: Camera.takeRawImage2: Raw Image saved as %s\",\r\n                get_ident(),\r\n                fpr,\r\n            )\r\n            if Camera.cam2IsUsb == False:\r\n                request.release()\r\n                prgLogger.debug(\"request.release()\")\r\n                logger.debug(\r\n                    \"Thread %s: Camera.takeRawImage2: Request released\", get_ident()\r\n                )\r\n\r\n            Camera.cam2 = Camera.ctrl2.restoreLivestream2(Camera.cam2, exclusive)\r\n            if sc.isVideoRecording2 == False and sc.isLiveStream2 == False:\r\n                Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n        except Exception as e:\r\n            logger.error(\"Thread %s: Camera.takeRawImage2: Error %s\", get_ident(), e)\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Taking raw photo caused error: \" + str(e)\r\n                sc.errorc2Source = \"Camera.takeRawImage2\"\r\n        Camera.liveView2Deactivated = False\r\n        return fpr\r\n\r\n    @staticmethod\r\n    def _videoThreadUsb():\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThreadUsb - Requesting camera for videoConfig\",\r\n            get_ident(),\r\n        )\r\n        Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n            Camera.cam, Camera.camNum, cfg.videoConfig, forceExclusive=True\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThreadUsb - Got camera for videoConfig exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n\r\n        Camera.applyControls(Camera.ctrl.configuration)\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb - controls applied\", get_ident())\r\n\r\n        # frameRate = Camera.cam.get(cv2.CAP_PROP_FPS)\r\n        frameRate = 14.5\r\n        Camera.cam.set(cv2.CAP_PROP_FPS, frameRate)\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb - frameRate is %s\", get_ident(), frameRate)\r\n\r\n        # Codec for MP4 (most compatible)\r\n        fourcc = cv2.VideoWriter_fourcc(*\"avc1\")\r\n        width = int(Camera.cam.get(cv2.CAP_PROP_FRAME_WIDTH))\r\n        height = int(Camera.cam.get(cv2.CAP_PROP_FRAME_HEIGHT))\r\n\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb - width:%s, height:%s\", get_ident(), width, height)\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb - videoOutput:%s\", get_ident(), Camera.videoOutput)\r\n\r\n        out = cv2.VideoWriter(Camera.videoOutput, fourcc, frameRate, (width, height))\r\n        logger.debug(\"Thread %s: Camera._videoThreadUsb - VideoWriter created\", get_ident())\r\n\r\n        try:\r\n            videoStart = time.time()\r\n            duration = float(Camera.videoDuration)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThreadUsb - video started at %s, duration is %s\",\r\n                get_ident(),\r\n                videoStart,\r\n                duration,\r\n            )\r\n\r\n            if duration > 0.0:\r\n                elapsed = time.time() - videoStart\r\n                while elapsed <= duration:\r\n                    ret, frame = Camera.cam.read()\r\n                    if not ret:\r\n                        break\r\n                    conf = Camera.ctrl.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame)\r\n                    out.write(frame)\r\n                    if Camera.stopVideoRequested == True:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._videoThreadUsb - stop video requested\", get_ident()\r\n                        )\r\n                        break\r\n                    elapsed = time.time() - videoStart\r\n                sc.isVideoRecording = False\r\n                sc.isAudioRecording = False\r\n            else:\r\n                while Camera.stopVideoRequested == False:\r\n                    ret, frame = Camera.cam.read()\r\n                    if not ret:\r\n                        break\r\n                    conf = Camera.ctrl.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame)\r\n                    out.write(frame)\r\n                    if Camera.stopVideoRequested == True:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._videoThreadUsb - stop video requested\", get_ident()\r\n                        )\r\n                        break\r\n            out.release()\r\n            Camera.stopVideoRequested = False\r\n            Camera.videoDuration = 0\r\n        except Exception as e:\r\n            logger.error(\r\n                \"Thread %s: Camera._videoThreadUsb - Exception: %s\", get_ident(), e\r\n            )\r\n            Camera.liveViewDeactivated = False\r\n            if not sc.error:\r\n                sc.error = \"Error in video recording: \" + str(e)\r\n                sc.errorSource = \"Camera._videoThreadUsb\"\r\n\r\n        Camera.videoThread = None\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread - _videoThreadUsb terminated\", get_ident()\r\n        )\r\n\r\n        Camera.cam = Camera.ctrl.restoreLivestream(Camera.cam, exclusive)\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThreadUsb - sc.error: %s)\", get_ident(), sc.error\r\n        )\r\n\r\n        if sc.isPhotoSeriesRecording == False and sc.isLiveStream == False:\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n\r\n    @staticmethod\r\n    def _videoThread():\r\n        logger.debug(\"Thread %s: Camera._videoThread\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread - Requesting camera for videoConfig\",\r\n            get_ident(),\r\n        )\r\n        Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n            Camera.cam, Camera.camNum, cfg.videoConfig\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread - Got camera for videoConfig exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n\r\n        Camera.applyControls(Camera.ctrl.configuration)\r\n        logger.debug(\"Thread %s: Camera._videoThread - controls applied\", get_ident())\r\n\r\n        sc.checkMicrophone()\r\n\r\n        encoder = H264Encoder()\r\n        prgLogger.debug(\"encoder = H264Encoder()\")\r\n        output = Camera.videoOutput\r\n        prgLogger.debug('output=\"%s\"', Camera.prgVideoOutput)\r\n        if output.lower().endswith(\".mp4\"):\r\n            if sc.recordAudio == False:\r\n                encoder.output = FfmpegOutput(output, audio=False)\r\n                prgLogger.debug(\"encoder.output = FfmpegOutput(output, audio=False)\")\r\n            else:\r\n                encoder.output = FfmpegOutput(\r\n                    output, audio=True, audio_sync=sc.audioSync\r\n                )\r\n                prgLogger.debug(\r\n                    \"encoder.output = FfmpegOutput(output, audio=True, audio_sync=%s)\",\r\n                    sc.audioSync,\r\n                )\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - mp4 Video output to %s\",\r\n                get_ident(),\r\n                output,\r\n            )\r\n        else:\r\n            encoder.output = FileOutput(output)\r\n            prgLogger.debug(\"encoder.output = FileOutput(output)\")\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - h264 Video output to %s\",\r\n                get_ident(),\r\n                output,\r\n            )\r\n        try:\r\n            videoStart = time.time()\r\n            duration = float(Camera.videoDuration)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - video started at %s, duration is %s\",\r\n                get_ident(),\r\n                videoStart,\r\n                duration,\r\n            )\r\n            Camera.cam.start_encoder(encoder, name=cfg.videoConfig.stream)\r\n            prgLogger.debug(\r\n                'picam2.start_encoder(encoder, name=\"%s\")', cfg.videoConfig.stream\r\n            )\r\n            prgLogger.debug(\"time.sleep(videoDuration)\")\r\n            Camera.ctrl.registerEncoder(Camera.ENCODER_VIDEO, encoder)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - Encoder started\", get_ident()\r\n            )\r\n            if duration > 0.0:\r\n                elapsed = time.time() - videoStart\r\n                while elapsed <= duration:\r\n                    if Camera.stopVideoRequested == True:\r\n                        break\r\n                    time.sleep(0.1)\r\n                    elapsed = time.time() - videoStart\r\n                sc.isVideoRecording = False\r\n                sc.isAudioRecording = False\r\n            else:\r\n                while Camera.stopVideoRequested == False:\r\n                    time.sleep(0.1)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - stop video requested\", get_ident()\r\n            )\r\n            Camera.ctrl.stopEncoder(Camera.cam, Camera.ENCODER_VIDEO)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - encoder stopped\", get_ident()\r\n            )\r\n            Camera.stopVideoRequested = False\r\n            Camera.videoDuration = 0\r\n        except ProcessLookupError as e:\r\n            logger.error(\"Thread %s: Camera._videoThread - Error: %s\", get_ident(), e)\r\n            Camera.liveViewDeactivated = False\r\n            if not sc.error:\r\n                sc.error = \"Error in encoder: \" + str(e)\r\n                sc.error2 = \"Probably, the requested resolution is too high.\"\r\n                sc.errorSource = \"Camera._videoThread\"\r\n        except RuntimeError as e:\r\n            logger.error(\"Thread %s: Camera._videoThread - Error: %s)\", get_ident(), e)\r\n            Camera.liveViewDeactivated = False\r\n            if not sc.error:\r\n                sc.error = \"Error in encoder: \" + str(e)\r\n                sc.error2 = \"Probably, there is not sufficient memory for the requested resolution.\"\r\n                sc.errorSource = \"Camera._videoThread\"\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread - sc.error: %s)\", get_ident(), sc.error\r\n            )\r\n        except Exception as e:\r\n            logger.error(\r\n                \"Thread %s: Camera._videoThread - Exception: %s\", get_ident(), e\r\n            )\r\n            Camera.liveViewDeactivated = False\r\n            if not sc.error:\r\n                sc.error = \"Error in video recording: \" + str(e)\r\n                sc.errorSource = \"Camera._videoThread\"\r\n\r\n        Camera.videoThread = None\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread - videoThread terminated\", get_ident()\r\n        )\r\n\r\n        Camera.cam = Camera.ctrl.restoreLivestream(Camera.cam, exclusive)\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread - sc.error: %s)\", get_ident(), sc.error\r\n        )\r\n\r\n        if sc.isPhotoSeriesRecording == False and sc.isLiveStream == False:\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n\r\n    @staticmethod\r\n    def _videoThread2Usb():\r\n        logger.debug(\"Thread %s: Camera._videoThread2Usb\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - Requesting camera for videoConfig\",\r\n            get_ident(),\r\n        )\r\n        videoConfig = cfg.streamingCfg[str(Camera.camNum2)][\"videoconfig\"]\r\n        Camera.cam2, exclusive, Camera.cam2_imx500 = Camera.ctrl2.requestCameraForConfig(\r\n            Camera.cam2, Camera.camNum2, videoConfig, forActiveCamera=False, forceExclusive=True\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - Got camera for videoConfig exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n\r\n        Camera.applyControls(Camera.ctrl2.configuration, toCam2=True)\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - controls applied\", get_ident()\r\n        )\r\n\r\n        # frameRate = Camera.cam.get(cv2.CAP_PROP_FPS)\r\n        frameRate = 14.5\r\n        Camera.cam2.set(cv2.CAP_PROP_FPS, frameRate)\r\n        logger.debug(\"Thread %s: Camera._videoThread2Usb - frameRate is %s\", get_ident(), frameRate)\r\n\r\n        # Codec for MP4 (most compatible)\r\n        fourcc = cv2.VideoWriter_fourcc(*\"avc1\")\r\n        width = int(Camera.cam2.get(cv2.CAP_PROP_FRAME_WIDTH))\r\n        height = int(Camera.cam2.get(cv2.CAP_PROP_FRAME_HEIGHT))\r\n\r\n        logger.debug(\"Thread %s: Camera._videoThread2Usb - width:%s, height:%s\", get_ident(), width, height)\r\n        logger.debug(\"Thread %s: Camera._videoThread2Usb - videoOutput2:%s\", get_ident(), Camera.videoOutput2)\r\n\r\n        out = cv2.VideoWriter(Camera.videoOutput2, fourcc, frameRate, (width, height))\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - VideoWriter created\", get_ident()\r\n        )\r\n\r\n        try:\r\n            videoStart = time.time()\r\n            duration = float(Camera.videoDuration2)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2Usb - video started at %s, duration is %s\",\r\n                get_ident(),\r\n                videoStart,\r\n                duration,\r\n            )\r\n\r\n            if duration > 0.0:\r\n                elapsed = time.time() - videoStart\r\n                while elapsed <= duration:\r\n                    ret, frame = Camera.cam2.read()\r\n                    if not ret:\r\n                        break\r\n                    conf = Camera.ctrl2.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, toCam2=True)\r\n                    out.write(frame)\r\n                    if Camera.stopVideoRequested2 == True:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._videoThread2Usb - stop video requested\",\r\n                            get_ident(),\r\n                        )\r\n                        break\r\n                    elapsed = time.time() - videoStart\r\n                sc.isVideoRecording2 = False\r\n                sc.isAudioRecording = False\r\n            else:\r\n                while Camera.stopVideoRequested2 == False:\r\n                    ret, frame = Camera.cam2.read()\r\n                    if not ret:\r\n                        break\r\n                    conf = Camera.ctrl2.configuration\r\n                    hflip = conf.transform.hflip\r\n                    vflip = conf.transform.vflip\r\n                    if hflip == True:\r\n                        frame = cv2.flip(frame, 1)\r\n                    if vflip == True:\r\n                        frame = cv2.flip(frame, 0)\r\n                    # Apply controls\r\n                    frame = Camera.usbFrameApplyControls(frame, toCam2=True)\r\n                    out.write(frame)\r\n                    if Camera.stopVideoRequested2 == True:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._videoThread2Usb - stop video requested\",\r\n                            get_ident(),\r\n                        )\r\n                        break\r\n            out.release()\r\n            Camera.stopVideoRequested2 = False\r\n            Camera.videoDuration2 = 0\r\n        except Exception as e:\r\n            logger.error(\r\n                \"Thread %s: Camera._videoThread2Usb - Exception: %s\", get_ident(), e\r\n            )\r\n            Camera.liveView2Deactivated = False\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Error in video recording: \" + str(e)\r\n                sc.errorc2Source = \"Camera._videoThread2Usb\"\r\n\r\n        Camera.videoThread2 = None\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - _videoThread2Usb terminated\",\r\n            get_ident(),\r\n        )\r\n\r\n        Camera.cam2 = Camera.ctrl2.restoreLivestream2(Camera.cam2, exclusive)\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2Usb - sc.errorc2: %s)\", get_ident(), sc.errorc2\r\n        )\r\n\r\n        if sc.isLiveStream2 == False:\r\n            Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n\r\n    @staticmethod\r\n    def _videoThread2():\r\n        logger.debug(\"Thread %s: Camera._videoThread2\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2 - Requesting camera for videoConfig\",\r\n            get_ident(),\r\n        )\r\n        videoConfig = cfg.streamingCfg[str(Camera.camNum2)][\"videoconfig\"]\r\n        Camera.cam2, exclusive, Camera.cam2_imx500 = Camera.ctrl2.requestCameraForConfig(\r\n            Camera.cam2, Camera.camNum2, videoConfig\r\n        )\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2 - Got camera for videoConfig exclusive: %s\",\r\n            get_ident(),\r\n            exclusive,\r\n        )\r\n\r\n        Camera.applyControls(Camera.ctrl2.configuration, toCam2=True)\r\n        logger.debug(\"Thread %s: Camera._videoThread2 - controls applied\", get_ident())\r\n        time.sleep(0.5)\r\n\r\n        encoder = H264Encoder()\r\n        prgLogger.debug(\"encoder = H264Encoder()\")\r\n        output = Camera.videoOutput2\r\n        prgLogger.debug('output=\"%s\"', Camera.prgVideoOutput)\r\n        if output.lower().endswith(\".mp4\"):\r\n            encoder.output = FfmpegOutput(output, audio=False)\r\n            prgLogger.debug(\"encoder.output = FfmpegOutput(output, audio=False)\")\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - mp4 Video output to %s\",\r\n                get_ident(),\r\n                output,\r\n            )\r\n        else:\r\n            encoder.output = FileOutput(output)\r\n            prgLogger.debug(\"encoder.output = FileOutput(output)\")\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - h264 Video output to %s\",\r\n                get_ident(),\r\n                output,\r\n            )\r\n        try:\r\n            videoStart = time.time()\r\n            duration = float(Camera.videoDuration2)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - video started at %s, duration is %s\",\r\n                get_ident(),\r\n                videoStart,\r\n                duration,\r\n            )\r\n            Camera.cam2.start_encoder(encoder, name=videoConfig.stream)\r\n            prgLogger.debug(\r\n                'picam2.start_encoder(encoder, name=\"%s\")', videoConfig.stream\r\n            )\r\n            prgLogger.debug(\"time.sleep(videoDuration)\")\r\n            Camera.ctrl2.registerEncoder(Camera.ENCODER_VIDEO, encoder)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - Encoder started\", get_ident()\r\n            )\r\n            if duration > 0.0:\r\n                elapsed = time.time() - videoStart\r\n                while elapsed <= duration:\r\n                    if Camera.stopVideoRequested2 == True:\r\n                        break\r\n                    time.sleep(0.1)\r\n                    elapsed = time.time() - videoStart\r\n                sc.isVideoRecording2 = False\r\n            else:\r\n                while Camera.stopVideoRequested2 == False:\r\n                    time.sleep(0.1)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - stop video requested\", get_ident()\r\n            )\r\n            Camera.ctrl2.stopEncoder(Camera.cam2, Camera.ENCODER_VIDEO)\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - encoder stopped\", get_ident()\r\n            )\r\n            Camera.stopVideoRequested2 = False\r\n            Camera.videoDuration2 = 0\r\n        except ProcessLookupError as e:\r\n            logger.error(\"Thread %s: Camera._videoThread2 - Error: %s\", get_ident(), e)\r\n            Camera.liveView2Deactivated = False\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Error in encoder: \" + str(e)\r\n                sc.errorc22 = \"Probably, the requested resolution is too high.\"\r\n                sc.errorc2Source = \"Camera._videoThread2\"\r\n        except RuntimeError as e:\r\n            logger.error(\"Thread %s: Camera._videoThread2 - Error: %s)\", get_ident(), e)\r\n            Camera.liveView2Deactivated = False\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Error in encoder: \" + str(e)\r\n                sc.errorc22 = \"Probably, there is not sufficient memory for the requested resolution.\"\r\n                sc.errorc2Source = \"Camera._videoThread2\"\r\n            logger.debug(\r\n                \"Thread %s: Camera._videoThread2 - sc.errorc2: %s)\",\r\n                get_ident(),\r\n                sc.errorc2,\r\n            )\r\n        except Exception as e:\r\n            logger.error(\r\n                \"Thread %s: Camera._videoThread2 - Exception: %s\", get_ident(), e\r\n            )\r\n            Camera.liveView2Deactivated = False\r\n            if not sc.errorc2:\r\n                sc.errorc2 = \"Error in video recording: \" + str(e)\r\n                sc.errorc2Source = \"Camera._videoThread2\"\r\n\r\n        Camera.videoThread2 = None\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2 - videoThread2 terminated\", get_ident()\r\n        )\r\n\r\n        Camera.cam2 = Camera.ctrl2.restoreLivestream2(Camera.cam2, exclusive)\r\n        logger.debug(\r\n            \"Thread %s: Camera._videoThread2 - sc.errorc2: %s)\", get_ident(), sc.errorc2\r\n        )\r\n\r\n        if sc.isLiveStream2 == False:\r\n            Camera.cam2, done = Camera.ctrl2.requestStop(Camera.cam2, close=True)\r\n\r\n    @staticmethod\r\n    def recordVideo(\r\n        filenameVid: str,\r\n        filename: str,\r\n        duration: int = 0,\r\n        noEvents: bool = False,\r\n        alternatePath: str = \"\",\r\n    ):\r\n        \"\"\"Start recrding video in an own thread\r\n\r\n        Args:\r\n            filenameVid (str): File name for video\r\n            filename (str): filename for placeholder image\r\n                            If empty, no placeholder image is created\r\n            duration (int, optional): Video duration. Defaults to 0.\r\n            noEvents (bool, optional): Dont fire events. Defaults to False.\r\n            alternatePath (str, optional): Alternate path.\r\n                        If set, display buffer will not be upfated\r\n                        Defaults to \"\".\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.recordVideo. filename=%s, duration=%s\",\r\n            get_ident(),\r\n            filename,\r\n            duration,\r\n        )\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        # First take a normal photo as placeholder\r\n        if filename != \"\":\r\n            Camera.takeImage(\r\n                filename, keepExclusive=True, noEvents=True, alternatePath=alternatePath\r\n            )\r\n            if alternatePath == \"\":\r\n                sc.displayFile = filenameVid\r\n\r\n        # Configure output for video file\r\n        path = sc.photoRoot + \"/\" + sc.cameraPhotoSubPath\r\n        if alternatePath != \"\":\r\n            path = alternatePath\r\n        output = path + \"/\" + filenameVid\r\n        prgoutput = sc.prgOutputPath + \"/\" + filenameVid\r\n\r\n        if Camera.videoThread is None:\r\n            Camera.videoOutput = output\r\n            Camera.prgVideoOutput = prgoutput\r\n            Camera.videoDuration = duration\r\n            logger.debug(\r\n                \"Thread %s: Camera.recordVideo - Starting new videoThread\", get_ident()\r\n            )\r\n            if Camera.camIsUsb == False:\r\n                Camera.videoThread = threading.Thread(\r\n                    target=Camera._videoThread, daemon=True\r\n                )\r\n            else:\r\n                Camera.videoThread = threading.Thread(\r\n                    target=Camera._videoThreadUsb, daemon=True\r\n                )\r\n            Camera.videoThread.start()\r\n            logger.debug(\r\n                \"Thread %s: Camera.recordVideo - videoThread started\", get_ident()\r\n            )\r\n\r\n            if noEvents == False:\r\n                if Camera().when_recording_starts:\r\n                    Camera().when_recording_starts()\r\n        return output\r\n\r\n    @staticmethod\r\n    def recordVideo2(\r\n        filenameVid: str,\r\n        filename: str,\r\n        duration: int = 0,\r\n        noEvents: bool = False,\r\n        alternatePath: str = \"\",\r\n    ):\r\n        \"\"\"Start recording video with second camera in an own thread\r\n\r\n        Args:\r\n            filenameVid (str): File name for video\r\n            filename (str): filename for placeholder image\r\n                            If empty, no placeholder image is created\r\n            duration (int, optional): Video duration. Defaults to 0.\r\n            noEvents (bool, optional): Dont fire events. Defaults to False.\r\n            alternatePath (str, optional): Alternate path.\r\n                        If set, display buffer will not be upfated\r\n                        Defaults to \"\".\r\n        \"\"\"\r\n        logger.debug(\r\n            \"Thread %s: Camera.recordVideo2. filename=%s, duration=%s\",\r\n            get_ident(),\r\n            filename,\r\n            duration,\r\n        )\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        # First take a normal photo as placeholder\r\n        if filename != \"\":\r\n            Camera.takeImage2(\r\n                filename, keepExclusive=True, noEvents=True, alternatePath=alternatePath\r\n            )\r\n\r\n        # Configure output for video file\r\n        cameraPhotoSubPath = \"photos/\" + \"camera_\" + str(Camera.camNum2)\r\n        path = sc.photoRoot + \"/\" + cameraPhotoSubPath\r\n        if alternatePath != \"\":\r\n            path = alternatePath\r\n        output = path + \"/\" + filenameVid\r\n        prgoutput = sc.prgOutputPath + \"/\" + filenameVid\r\n\r\n        if Camera.videoThread2 is None:\r\n            Camera.videoOutput2 = output\r\n            Camera.prgVideoOutput2 = prgoutput\r\n            Camera.videoDuration2 = duration\r\n            logger.debug(\r\n                \"Thread %s: Camera.recordVideo2 - Starting new videoThread with output=%s\",\r\n                get_ident(),\r\n                Camera.prgVideoOutput2,\r\n            )\r\n            if Camera.cam2IsUsb == False:\r\n                Camera.videoThread2 = threading.Thread(\r\n                    target=Camera._videoThread2, daemon=True\r\n                )\r\n            else:\r\n                Camera.videoThread2 = threading.Thread(\r\n                    target=Camera._videoThread2Usb, daemon=True\r\n                )\r\n            Camera.videoThread2.start()\r\n            logger.debug(\r\n                \"Thread %s: Camera.recordVideo2 - videoThread2 started\", get_ident()\r\n            )\r\n\r\n            if noEvents == False:\r\n                if Camera().when_recording_2_starts:\r\n                    Camera().when_recording_2_starts()\r\n        return output\r\n\r\n    @staticmethod\r\n    def stopVideoRecording(noEvents: bool = False):\r\n        \"\"\"stops the video recording\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopVideoRecording\", get_ident())\r\n        Camera.stopVideoRequested = True\r\n        Camera.videoDuration = 0\r\n        cnt = 0\r\n        while Camera.videoThread:\r\n            time.sleep(0.01)\r\n            cnt += 1\r\n            if cnt > 500:\r\n                raise TimeoutError(\"Video thread did not stop within 5 sec\")\r\n        logger.debug(\r\n            \"Thread %s: Camera.stopVideoRecording: Thread has stopped\", get_ident()\r\n        )\r\n\r\n        if noEvents == False:\r\n            if Camera().when_recording_stops:\r\n                Camera().when_recording_stops()\r\n        Camera.liveViewDeactivated = False\r\n        time.sleep(0.1)\r\n        Camera.startLiveStream()\r\n\r\n    @staticmethod\r\n    def stopVideoRecording2(noEvents: bool = False):\r\n        \"\"\"stops the video recording for second camera\"\"\"\r\n        logger.debug(\"Thread %s: Camera.stopVideoRecording2\", get_ident())\r\n        Camera.stopVideoRequested2 = True\r\n        Camera.videoDurations = 0\r\n        cnt = 0\r\n        while Camera.videoThread2:\r\n            time.sleep(0.01)\r\n            cnt += 1\r\n            if cnt > 500:\r\n                raise TimeoutError(\"Video thread 2 did not stop within 5 sec\")\r\n        logger.debug(\r\n            \"Thread %s: Camera.stopVideoRecording2: Thread has stopped\", get_ident()\r\n        )\r\n\r\n        if noEvents == False:\r\n            if Camera().when_recording_2_stops:\r\n                Camera().when_recording_2_stops()\r\n        Camera.liveView2Deactivated = False\r\n\r\n    @staticmethod\r\n    def isVideoRecording() -> bool:\r\n        return Camera.videoThread is not None\r\n\r\n    @staticmethod\r\n    def isVideoRecording2() -> bool:\r\n        return Camera.videoThread2 is not None\r\n\r\n    @staticmethod\r\n    def getLensPosition() -> float:\r\n        metadata = Camera.cam.capture_metadata()\r\n        if \"LensPosition\" in metadata:\r\n            return metadata[\"LensPosition\"]\r\n        else:\r\n            return 0.0\r\n\r\n    @staticmethod\r\n    def getMetaData() -> dict:\r\n        logger.debug(\"Thread %s: Camera.getMetaData\", get_ident())\r\n        if Camera.camIsUsb == False:\r\n            return Camera.cam.capture_metadata()\r\n        else:\r\n            return Camera.getUsbCamMetadata(Camera.cam)\r\n\r\n    @staticmethod\r\n    def _photoSeriesThread():\r\n        logger.debug(\"Thread %s: Camera._photoSeriesThread\", get_ident())\r\n        ser = Camera.photoSeries\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n\r\n        logger.debug(\r\n            \"Thread %s: Camera._photoSeriesThread Requesting camera for photo series of type %s\",\r\n            get_ident(),\r\n            ser.type,\r\n        )\r\n        exclusive = False\r\n        try:\r\n            if Camera.camIsUsb == False:\r\n                forceExclusive = False\r\n            else:\r\n                forceExclusive = True\r\n            if ser.type == \"jpg\":\r\n                Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n                    Camera.cam, Camera.camNum, cfg.photoConfig, forceExclusive=forceExclusive\r\n                )\r\n            else:\r\n                Camera.cam, exclusive, Camera.cam_imx500 = Camera.ctrl.requestCameraForConfig(\r\n                    Camera.cam, Camera.camNum, cfg.rawConfig, cfg.photoConfig, forceExclusive=forceExclusive\r\n                )\r\n            logger.debug(\r\n                \"Thread %s: Camera._photoSeriesThread Got camera for photo series exclusive: %s\",\r\n                get_ident(),\r\n                exclusive,\r\n            )\r\n        except Exception as e:\r\n            logger.error(\r\n                \"Thread %s: Camera._photoSeriesThread error: %s\", get_ident(), e\r\n            )\r\n            if not sc.error:\r\n                sc.error = \"Error while requesting camera: \" + str(e)\r\n                sc.errorSource = \"Camera._photoSeriesThread\"\r\n\r\n        if not sc.error:\r\n            sc.isPhotoSeriesRecording = True\r\n\r\n            exceptCtrl = None\r\n            exceptValue = None\r\n            exceptValueRaw = None\r\n            # Special handling for exposure series\r\n            if ser.isExposureSeries:\r\n                if sc.useHistograms:\r\n                    import numpy as np\r\n                    from matplotlib import pyplot as plt\r\n                if ser.isExpGainFix:\r\n                    exceptCtrl = \"ExposureTime\"\r\n                    exceptValue = ser.expTimeStart\r\n                    if ser.expTimeStep == 0:\r\n                        expFact = 2\r\n                    elif ser.expTimeStep == 1:\r\n                        expFact = 2 ** (1.0 / 3)\r\n                    elif ser.expTimeStep == 2:\r\n                        expFact = 4\r\n                    else:\r\n                        expFact = 2\r\n                else:\r\n                    exceptCtrl = \"AnalogueGain\"\r\n                    exceptValue = ser.expGainStart\r\n                    if ser.expGainStep == 0:\r\n                        expFact = 2\r\n                    elif ser.expGainStep == 1:\r\n                        expFact = 2 ** (1.0 / 3)\r\n                    elif ser.expGainStep == 2:\r\n                        expFact = 4\r\n                    else:\r\n                        expFact = 2\r\n                if ser.curShots:\r\n                    if ser.curShots > 1:\r\n                        n = 0\r\n                        while n < ser.curShots:\r\n                            n += 1\r\n                            exceptValue = exceptValue * expFact\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._photoSeriesThread - Exposure Series for %s: Restart after %s shots\",\r\n                            get_ident(),\r\n                            exceptCtrl,\r\n                            ser.curShots,\r\n                        )\r\n                logger.debug(\r\n                    \"Thread %s: Camera._photoSeriesThread - Exposure Series for %s: %s Factor: %s\",\r\n                    get_ident(),\r\n                    exceptCtrl,\r\n                    exceptValue,\r\n                    expFact,\r\n                )\r\n\r\n            # Special handling for focus series\r\n            if ser.isFocusStackingSeries:\r\n                exceptCtrl = \"LensPosition\"\r\n                exceptValueRaw = ser.focalDistStart\r\n                exceptValue = 1.0 / exceptValueRaw\r\n                if ser.curShots:\r\n                    if ser.curShots > 1:\r\n                        exceptValueRaw = (\r\n                            ser.focalDistStart + (ser.curShots - 1) * ser.focalDistStep\r\n                        )\r\n                        exceptValue = 1.0 / exceptValueRaw\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._photoSeriesThread - Focus Series: Restart after %s shots\",\r\n                            get_ident(),\r\n                            ser.curShots,\r\n                        )\r\n                logger.debug(\r\n                    \"Thread %s: Camera._photoSeriesThread - Focus Series for %s: %s (focal dist: %s, interval: %s)\",\r\n                    get_ident(),\r\n                    exceptCtrl,\r\n                    exceptValue,\r\n                    exceptValueRaw,\r\n                    ser.focalDistStep,\r\n                )\r\n\r\n            photoseriesCtrls = Camera.applyControls(\r\n                Camera.ctrl.configuration, exceptCtrl, exceptValue\r\n            )\r\n            logger.debug(\r\n                \"Thread %s: Camera._photoSeriesThread - selected controls applied\",\r\n                get_ident(),\r\n            )\r\n\r\n            lastTime = None\r\n            stop = False\r\n            while not stop:\r\n                nextTime = ser.nextTime(lastTime)\r\n                curShots, nextPhoto, serMetaData = ser.nextPhoto()\r\n                logger.debug(\r\n                    \"Thread %s: Camera._photoSeriesThread - nextPhoto: %s nextTime %s\",\r\n                    get_ident(),\r\n                    nextPhoto,\r\n                    str(nextTime),\r\n                )\r\n                if nextPhoto == \"\" or nextTime is None or ser.status == \"FINISHED\":\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._photoSeriesThread - Series done: nextPhoto=%s, nextTime=%s, status=%s\",\r\n                        get_ident(),\r\n                        nextPhoto,\r\n                        str(nextTime),\r\n                        ser.status,\r\n                    )\r\n                    stop = True\r\n                else:\r\n                    curTime = datetime.datetime.now()\r\n                    timedif = nextTime - curTime\r\n                    timedifSec = timedif.total_seconds()\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._photoSeriesThread - Seconds to wait: %s\",\r\n                        get_ident(),\r\n                        timedifSec,\r\n                    )\r\n\r\n                    camClosed = False\r\n                    if (\r\n                        ser.isFocusStackingSeries == False\r\n                        and ser.isExposureSeries == False\r\n                    ):\r\n                        if sc.isVideoRecording == False and sc.isLiveStream == False:\r\n                            if timedifSec > 60:\r\n                                Camera.cam, camClosed = Camera.ctrl.requestStop(\r\n                                    Camera.cam, close=True\r\n                                )\r\n\r\n                    while timedifSec > 2.0:\r\n                        time.sleep(2.0)\r\n                        curTime = datetime.datetime.now()\r\n                        timedif = nextTime - curTime\r\n                        timedifSec = timedif.total_seconds()\r\n                        if camClosed:\r\n                            timedifSec -= 2.0\r\n\r\n                        if Camera.stopPhotoSeriesRequested:\r\n                            stop = True\r\n                            break\r\n                    if stop == False and timedifSec > 0.0:\r\n                        time.sleep(timedifSec)\r\n                if Camera.stopPhotoSeriesRequested:\r\n                    logger.debug(\r\n                        \"Thread %s: Camera._photoSeriesThread - Stop requested\",\r\n                        get_ident(),\r\n                    )\r\n                    stop = True\r\n                if not stop:\r\n                    try:\r\n                        logger.debug(\r\n                            \"Thread %s: Camera._photoSeriesThread - Starting next shot\",\r\n                            get_ident(),\r\n                        )\r\n                        if Camera.cam is None:\r\n                            camClosed = True\r\n                        else:\r\n                            if Camera.camIsUsb == False:\r\n                                if Camera.cam.started == False:\r\n                                    camClosed = True\r\n                            else:\r\n                                camClosed = Camera.cam.isOpened() == False\r\n                        if camClosed:\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - Preparing closed camera\",\r\n                                get_ident(),\r\n                            )\r\n                            if ser.type == \"jpg\":\r\n                                Camera.cam, exclusive, Camera.cam_imx500 = (\r\n                                    Camera.ctrl.requestCameraForConfig(\r\n                                        Camera.cam, Camera.camNum, cfg.photoConfig, forceExclusive=forceExclusive\r\n                                    )\r\n                                )\r\n                            else:\r\n                                Camera.cam, exclusive, Camera.cam_imx500 = (\r\n                                    Camera.ctrl.requestCameraForConfig(\r\n                                        Camera.cam,\r\n                                        Camera.camNum,\r\n                                        cfg.rawConfig,\r\n                                        cfg.photoConfig,\r\n                                        forceExclusive=forceExclusive\r\n                                    )\r\n                                )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread Got camera for photo series exclusive: %s\",\r\n                                get_ident(),\r\n                                exclusive,\r\n                            )\r\n                            photoseriesCtrls = Camera.applyControls(\r\n                                Camera.ctrl.configuration, exceptCtrl, exceptValue\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - selected controls applied\",\r\n                                get_ident(),\r\n                            )\r\n                            time.sleep(1.5)\r\n                            curTime = datetime.datetime.now()\r\n                            timedif = nextTime - curTime\r\n                            timedifSec = timedif.total_seconds()\r\n                            if timedifSec > 0:\r\n                                time.sleep(timedifSec)\r\n\r\n                        lastTime = datetime.datetime.now()\r\n                        if Camera.camIsUsb == False:\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - Preparing request\",\r\n                                get_ident(),\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - id(Camera)=%s id(Camera.cam)=%s id(Camera.cam.controls)=%s\",\r\n                                get_ident(),\r\n                                id(Camera),\r\n                                id(Camera.cam),\r\n                                id(Camera.cam.controls),\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - Camera.cam.controls=%s\",\r\n                                get_ident(),\r\n                                Camera.cam.controls,\r\n                            )\r\n                            request = Camera.cam.capture_request()\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - capture_request completed\",\r\n                                get_ident(),\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - id(Camera)=%s id(Camera.cam)=%s id(Camera.cam.controls)=%s\",\r\n                                get_ident(),\r\n                                id(Camera),\r\n                                id(Camera.cam),\r\n                                id(Camera.cam.controls),\r\n                            )\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - Camera.cam.controls=%s\",\r\n                                get_ident(),\r\n                                Camera.cam.controls,\r\n                            )\r\n                            prgLogger.debug(\"request = picam2.capture_request()\")\r\n                            fpjpg = ser.path + \"/\" + nextPhoto + \".jpg\"\r\n                            fpraw = ser.path + \"/\" + nextPhoto + \".dng\"\r\n                            request.save(\"main\", fpjpg)\r\n                            prgLogger.debug(\r\n                                'request.save(\"main\", \"%s\")',\r\n                                sc.prgOutputPath + \"/\" + nextPhoto + \".jpg\",\r\n                            )\r\n                            if ser.type == \"raw+jpg\":\r\n                                request.save_dng(fpraw)\r\n                                prgLogger.debug(\r\n                                    'request.save_dng(\"%s\")',\r\n                                    sc.prgOutputPath + \"/\" + nextPhoto + \".dng\",\r\n                                )\r\n                            metadata = request.get_metadata()\r\n                            prgLogger.debug(\"metadata = request.get_metadata()\")\r\n                            request.release()\r\n                            prgLogger.debug(\"request.release()\")\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - Request released\",\r\n                                get_ident(),\r\n                            )\r\n                        else:\r\n                            # For USB cameras, save the image using OpenCV\r\n                            fpjpg = ser.path + \"/\" + nextPhoto + \".jpg\"\r\n                            fpraw = ser.path + \"/\" + nextPhoto + \".tiff\"\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - USB camera capture image %s\",\r\n                                get_ident(),\r\n                                fpjpg,\r\n                            )\r\n                            if Camera.cam.isOpened() == False:\r\n                                raise RuntimeError(\"USB camera is not opened\")\r\n                            success, frame = Camera.cam.read()\r\n                            if success:\r\n                                metadata = Camera.getUsbCamMetadata(Camera.cam)\r\n                                conf = Camera.ctrl.configuration\r\n                                hflip = conf.transform.hflip\r\n                                vflip = conf.transform.vflip\r\n                                if hflip == True:\r\n                                    frame = cv2.flip(frame, 1)\r\n                                if vflip == True:\r\n                                    frame = cv2.flip(frame, 0)\r\n                                # Apply controls\r\n                                frame = Camera.usbFrameApplyControls(frame)\r\n                                cv2.imwrite(fpjpg, frame)\r\n                                if ser.type == \"raw+jpg\":\r\n                                    cv2.imwrite(fpraw, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1])\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera._photoSeriesThread - USB camera capture done\",\r\n                                    get_ident()\r\n                                )\r\n                            else:\r\n                                raise RuntimeError(\"Failed to capture image from USB camera\")\r\n                        ser.curShots = curShots\r\n                        ser.logPhoto(nextPhoto, lastTime, metadata, serMetaData)\r\n                        if (\r\n                            ser.isFocusStackingSeries == False\r\n                            and ser.isExposureSeries == False\r\n                        ):\r\n                            if Camera().when_series_photo_taken:\r\n                                Camera().when_series_photo_taken()\r\n                    except Exception as e:\r\n                        ser.nextStatus(\"pause\")\r\n                        stop = True\r\n                        logger.error(\r\n                            \"Thread %s: Camera._photoSeriesThread - Error: %s\",\r\n                            get_ident(),\r\n                            e,\r\n                        )\r\n                        ser.error = \"Error in photoseries: \" + str(e)\r\n                        ser.errorSource = \"Camera._photoSeriesThread\"\r\n\r\n                    if not sc.error and not ser.error:\r\n                        # Draw histogram\r\n                        if ser.isExposureSeries and sc.useHistograms:\r\n                            dest = ser.histogramPath + \"/\" + nextPhoto + \".jpg\"\r\n                            plt.figure()\r\n                            img = cv2.imread(fpjpg)\r\n                            color = (\"b\", \"g\", \"r\")\r\n                            for i, col in enumerate(color):\r\n                                histr = cv2.calcHist([img], [i], None, [256], [0, 256])\r\n                                plt.plot(histr, color=col)\r\n                                plt.xlim([0, 256])\r\n                            plt.savefig(dest)\r\n                            logger.debug(\r\n                                \"Thread %s: Camera._photoSeriesThread - histogram created: %s\",\r\n                                get_ident(),\r\n                                dest,\r\n                            )\r\n                            plt.close()\r\n\r\n                        # For exposure series apply controls\r\n                        if ser.isExposureSeries:\r\n                            ser.logCamCfgCtrl(\r\n                                nextPhoto,\r\n                                Camera.ctrl.configuration.make_dict(),\r\n                                photoseriesCtrls.make_dict(),\r\n                            )\r\n                            if not stop:\r\n                                exceptValue = expFact * exceptValue\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera._photoSeriesThread - Exposure Series for %s: %s\",\r\n                                    get_ident(),\r\n                                    exceptCtrl,\r\n                                    exceptValue,\r\n                                )\r\n                                photoseriesCtrls = Camera.applyControls(\r\n                                    Camera.ctrl.configuration, exceptCtrl, exceptValue\r\n                                )\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera._photoSeriesThread - selected controls applied\",\r\n                                    get_ident(),\r\n                                )\r\n\r\n                        # For focus series apply controls\r\n                        if ser.isFocusStackingSeries:\r\n                            ser.logCamCfgCtrl(\r\n                                nextPhoto,\r\n                                Camera.ctrl.configuration.make_dict(),\r\n                                photoseriesCtrls.make_dict(),\r\n                            )\r\n                            if not stop:\r\n                                exceptValueRaw = exceptValueRaw + ser.focalDistStep\r\n                                exceptValue = 1.0 / exceptValueRaw\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera._photoSeriesThread - Focus Series for %s: %s (focal dist: %s)\",\r\n                                    get_ident(),\r\n                                    exceptCtrl,\r\n                                    exceptValue,\r\n                                    exceptValueRaw,\r\n                                )\r\n                                photoseriesCtrls = Camera.applyControls(\r\n                                    Camera.ctrl.configuration, exceptCtrl, exceptValue\r\n                                )\r\n                                logger.debug(\r\n                                    \"Thread %s: Camera._photoSeriesThread - selected controls applied\",\r\n                                    get_ident(),\r\n                                )\r\n\r\n        Camera.photoSeriesThread = None\r\n        Camera.stopPhotoSeriesRequested = False\r\n        sc.isPhotoSeriesRecording = False\r\n        Camera.cam = Camera.ctrl.restoreLivestream(Camera.cam, exclusive)\r\n        if sc.isVideoRecording == False and sc.isLiveStream == False:\r\n            Camera.cam, done = Camera.ctrl.requestStop(Camera.cam, close=True)\r\n        logger.debug(\r\n            \"Thread %s: Camera._photoSeriesThread - photoSeriesThread terminated\",\r\n            get_ident(),\r\n        )\r\n\r\n    @staticmethod\r\n    def startPhotoSeries(ser: Series):\r\n        \"\"\"Run photoseries in an own thread\"\"\"\r\n        logger.debug(\"Thread %s: startPhotoSeries - series=%s\", get_ident(), ser.name)\r\n\r\n        if Camera.photoSeriesThread is None:\r\n            logger.debug(\r\n                \"Thread %s: startPhotoSeries - Starting new photoSeriesThread\",\r\n                get_ident(),\r\n            )\r\n            Camera.photoSeries = ser\r\n            Camera.photoSeriesThread = threading.Thread(\r\n                target=Camera._photoSeriesThread, daemon=True\r\n            )\r\n            Camera.photoSeriesThread.start()\r\n            logger.debug(\r\n                \"Thread %s: startPhotoSeries - photoSeriesThread started\", get_ident()\r\n            )\r\n\r\n    @staticmethod\r\n    def stopPhotoSeries():\r\n        \"\"\"stops the photo series\"\"\"\r\n        logger.debug(\"Thread %s: stopPhotoSeries\", get_ident())\r\n        Camera.stopPhotoSeriesRequested = True\r\n        cnt = 0\r\n        while Camera.photoSeriesThread:\r\n            time.sleep(0.01)\r\n            cnt += 1\r\n            if cnt > 500:\r\n                Camera.photoSeriesThread = None\r\n                CameraCfg().serverConfig.isPhotoSeriesRecording = False\r\n                # raise TimeoutError(\"Photoseries thread did not stop within 5 sec\")\r\n                logger.debug(\r\n                    \"Thread %s: stopPhotoSeries: Thread seams to be dead\", get_ident()\r\n                )\r\n                break\r\n        logger.debug(\"Thread %s: stopPhotoSeries: Thread has stopped\", get_ident())\r\n        Camera.stopPhotoSeriesRequested = False\r\n\r\n    @classmethod\r\n    def cameraStatus(cls, camNum) -> str:\r\n        status = \"\"\r\n        sc = CameraCfg().serverConfig\r\n        if camNum == cls.camNum:\r\n            if cls.camIsUsb == False:\r\n                if cls.cam.is_open == True:\r\n                    status = \"open\"\r\n                    if cls.cam.started == True:\r\n                        status = status + \" - started\"\r\n                        mode = \"unknown\"\r\n                        if useSensorConfiguration:\r\n                            sc = cls.cam.camera_config[\"sensor\"]\r\n                            for sm in CameraCfg().sensorModes:\r\n                                if (\r\n                                    sc[\"output_size\"] == sm.size\r\n                                    and sc[\"bit_depth\"] == sm.bit_depth\r\n                                ):\r\n                                    mode = str(sm.id)\r\n                        status = status + \" - current Sensor Mode: \" + mode\r\n                    else:\r\n                        status = status + \" - stopped\"\r\n                else:\r\n                    status = \"closed\"\r\n            else:\r\n                if cls.cam.isOpened() == True:\r\n                    status = \"open\"\r\n                else:\r\n                    status = \"closed\"\r\n        elif camNum == cls.camNum2:\r\n            if cls.cam2IsUsb == False:\r\n                if cls.cam2.is_open == True:\r\n                    status = \"open\"\r\n                    if cls.cam2.started == True:\r\n                        status = status + \" - started\"\r\n                    else:\r\n                        status = status + \" - stopped\"\r\n                else:\r\n                    status = \"closed\"\r\n            else:\r\n                if cls.cam2.isOpened() == True:\r\n                    status = \"open\"\r\n                else:\r\n                    status = \"closed\"\r\n        else:\r\n            if sc.supportsUsbCamera == True:\r\n                if sc.useUsbCameras == True:\r\n                    status = \"inactive\"\r\n                else:\r\n                    status = \"excluded\"\r\n            else:\r\n                status = \"not supported (OpenCV missing)\"\r\n        return status\r\n\r\n    @classmethod\r\n    def resetScalerCrop(cls):\r\n        logger.debug(\"Thread %s: Camera.resetScalerCrop\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        cc = cfg.controls\r\n        cp = cfg.cameraProperties\r\n        scInf = cls.cam.camera_controls[\"ScalerCrop\"]\r\n        sc.scalerCropMin = scInf[0]\r\n        sc.scalerCropMax = scInf[1]\r\n        sc.scalerCropDef = scInf[2]\r\n        sc.zoomFactor = 100\r\n        sc.scalerCropLiveView = sc.scalerCropDef\r\n        if cc.scalerCrop != sc.scalerCropDef:\r\n            cc.include_scalerCrop = True\r\n            sc.zoomFactor = sc.zoomFactorStep * math.floor(\r\n                (100 * cc.scalerCrop[2] / cp.pixelArraySize[0]) / sc.zoomFactorStep)\r\n        else:\r\n            cc.include_scalerCrop = False\r\n        cls.resetScalerCropRequested = False\r\n\r\n    @classmethod\r\n    def resetScalerCropUsb(cls):\r\n        logger.debug(\"Thread %s: Camera.resetScalerCropUsb\", get_ident())\r\n        cfg = CameraCfg()\r\n        sc = cfg.serverConfig\r\n        cc = cfg.controls\r\n        cp = cfg.cameraProperties\r\n        ref = cfg.liveViewConfig.stream_size\r\n        sc.scalerCropMax = Camera.getUsbScalerCrop(ref[0], ref[1])\r\n        sc.scalerCropMin = (0, 0, sc.scalerCropMax[2] / 100, sc.scalerCropMax[3] / 100)\r\n        sc.scalerCropDef = sc.scalerCropMax\r\n        sc.zoomFactor = 100\r\n        sc.scalerCropLiveView = sc.scalerCropDef\r\n        if cc.scalerCrop == cfg.cameraProperties.scalerCropMaximum:\r\n            cc.scalerCrop = sc.scalerCropDef\r\n        if cc.scalerCrop != sc.scalerCropDef:\r\n            cc.include_scalerCrop = True\r\n            sc.zoomFactor = sc.zoomFactorStep * math.floor(\r\n                (100 * cc.scalerCrop[2] / cp.pixelArraySize[0]) / sc.zoomFactorStep)\r\n        else:\r\n            cc.include_scalerCrop = False\r\n        cls.resetScalerCropRequested = False\r\n\r\n    @staticmethod\r\n    def resetAiCache():\r\n        logger.debug(\"Thread %s: Camera.resetAiCache\", get_ident())\r\n        Camera.cam_imx500 = None\r\n        Camera.cam_imx500_last_detections = []\r\n        Camera.cam_imx500_last_results = None\r\n        Camera.cam_imx500_labels = None\r\n        Camera.cam_imx500_last_boxes = None\r\n        Camera.cam_imx500_last_scores = None\r\n        Camera.cam_imx500_last_keypoints = None\r\n        Camera.cam_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n        Camera.cam_drawer = None\r\n\r\n    @staticmethod\r\n    def resetAiCache2():\r\n        logger.debug(\"Thread %s: Camera.resetAiCache2\", get_ident())\r\n        Camera.cam2_imx500 = None\r\n        Camera.cam2_imx500_last_detections = []\r\n        Camera.cam2_imx500_last_results = None\r\n        Camera.cam2_imx500_labels = None\r\n        Camera.cam2_imx500_last_boxes = None\r\n        Camera.cam2_imx500_last_scores = None\r\n        Camera.cam2_imx500_last_keypoints = None\r\n        Camera.cam2_imx500_WINDOW_SIZE_H_W = (480, 640)\r\n        Camera.cam2_drawer = None\r\n\r\n\r\n    @staticmethod\r\n    def get_label(request: CompletedRequest, idx: int) -> str:\r\n        \"\"\"Classification: Retrieve the label corresponding to the classification index.\"\"\"\r\n        if Camera.cam_imx500_labels is None:\r\n            Camera.cam_imx500_labels = Camera.cam_imx500.network_intrinsics.labels\r\n            output_tensor_size = Camera.cam_imx500.get_output_shapes(request.get_metadata())[0][0]\r\n            if output_tensor_size == 1000:\r\n                Camera.cam_imx500_labels = Camera.cam_imx500_labels[1:]  # Ignore the background label if present\r\n        return Camera.cam_imx500_labels[idx]\r\n\r\n\r\n    @staticmethod\r\n    def parse_and_draw_classification_results(request: CompletedRequest):\r\n        \"\"\"Classification: Analyse and draw the classification results in the output tensor.\"\"\"\r\n        results = Camera.parse_classification_results(request)\r\n        Camera.draw_classification_results(request, results)\r\n\r\n    @staticmethod\r\n    def parse_classification_results(request: CompletedRequest) -> List[Classification]:\r\n        \"\"\"Classification: Parse the output tensor into the classification results above the threshold.\"\"\"\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        np_outputs = Camera.cam_imx500.get_outputs(request.get_metadata())\r\n        if np_outputs is None:\r\n            return Camera.cam_imx500_last_detections\r\n        np_output = np_outputs[0]\r\n        if Camera.cam_imx500.network_intrinsics.softmax:\r\n            np_output = softmax(np_output)\r\n        top_indices = np.argpartition(-np_output, ai.topK)[:ai.topK]  # Get top K indices with the highest scores\r\n        top_indices = top_indices[np.argsort(-np_output[top_indices])]  # Sort the top K indices by their scores\r\n        Camera.cam_imx500_last_detections = [Classification(index, np_output[index]) for index in top_indices]\r\n        return Camera.cam_imx500_last_detections\r\n\r\n\r\n    @staticmethod\r\n    def draw_classification_results(request: CompletedRequest, results: List[Classification], stream: str = \"lores\"):\r\n        \"\"\"Classification: Draw the classification results for this request onto the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    if Camera.cam_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                        # Drawing ROI box\r\n                        b_x, b_y, b_w, b_h = Camera.cam_imx500.get_roi_scaled(request, stream)\r\n                        color = (255, 0, 0)  # red\r\n                        cv2.putText(m.array, \"ROI\", (b_x + 5, b_y + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)\r\n                        cv2.rectangle(m.array, (b_x, b_y), (b_x + b_w, b_y + b_h), (255, 0, 0, 0))\r\n                        text_left, text_top = b_x, b_y + 20\r\n                    else:\r\n                        text_left, text_top = 0, 0\r\n                    # Drawing labels (in the ROI box if it exists)\r\n                    for index, result in enumerate(results):\r\n                        label = Camera.get_label(request, idx=result.idx)\r\n                        text = f\"{label}: {result.score:.3f}\"\r\n\r\n                        # Calculate text size and position\r\n                        (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)\r\n                        text_x = text_left + 5\r\n                        text_y = text_top + 15 + index * 20\r\n\r\n                        # Create a copy of the array to draw the background with opacity\r\n                        overlay = m.array.copy()\r\n\r\n                        # Draw the background rectangle on the overlay\r\n                        cv2.rectangle(overlay,\r\n                                    (text_x, text_y - text_height),\r\n                                    (text_x + text_width, text_y + baseline),\r\n                                    (255, 255, 255),  # Background color (white)\r\n                                    cv2.FILLED)\r\n\r\n                        alpha = 0.3\r\n                        cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array)\r\n\r\n                        # Draw text on top of the background\r\n                        cv2.putText(m.array, text, (text_x, text_y),\r\n                                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)\r\n\r\n\r\n    @staticmethod\r\n    def ai_output_tensor_parse(metadata: dict):\r\n        \"\"\"Pose Estimation: Parse the output tensor into a number of detected objects, scaled to the ISP output.\"\"\"\r\n        np_outputs = Camera.cam_imx500.get_outputs(metadata=metadata, add_batch=True)\r\n        if np_outputs is not None:\r\n            keypoints, scores, boxes = postprocess_higherhrnet(outputs=np_outputs,\r\n                                                            img_size=Camera.cam_imx500_WINDOW_SIZE_H_W,\r\n                                                            img_w_pad=(0, 0),\r\n                                                            img_h_pad=(0, 0),\r\n                                                            detection_threshold=CameraCfg().aiConfig.detectionThreshold,\r\n                                                            network_postprocess=True)\r\n\r\n            if scores is not None and len(scores) > 0:\r\n                Camera.cam_imx500_last_keypoints = np.reshape(np.stack(keypoints, axis=0), (len(scores), 17, 3))\r\n                Camera.cam_imx500_last_boxes = [np.array(b) for b in boxes]\r\n                Camera.cam_imx500_last_scores = np.array(scores)\r\n        return Camera.cam_imx500_last_boxes, Camera.cam_imx500_last_scores, Camera.cam_imx500_last_keypoints\r\n\r\n\r\n    @staticmethod\r\n    def ai_output_tensor_draw(request: CompletedRequest, boxes, scores, keypoints, stream='lores'):\r\n        \"\"\"Pose Estimation: Draw the detections for this request onto the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        detection_threshold = ai.detectionThreshold\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    if boxes is not None and len(boxes) > 0:\r\n                        Camera.cam_imx500_drawer.annotate_image(m.array, boxes, scores,\r\n                                            np.zeros(scores.shape), keypoints, detection_threshold,\r\n                                            detection_threshold, request.get_metadata(), Camera.cam, stream)\r\n\r\n\r\n    @staticmethod\r\n    def picamera2_pre_callback(request: CompletedRequest):\r\n        \"\"\"Pose Estimation: Analyse the detected objects in the output tensor and draw them on the main output image.\"\"\"\r\n        boxes, scores, keypoints = Camera.ai_output_tensor_parse(request.get_metadata())\r\n        Camera.ai_output_tensor_draw(request, boxes, scores, keypoints)\r\n\r\n\r\n    @staticmethod\r\n    def set_drawer():\r\n        \"\"\"Pose Estimation: Set up the drawer for IMX500 pose estimation.\"\"\"\r\n        categories = Camera.cam_imx500.network_intrinsics.labels\r\n        categories = [c for c in categories if c and c != \"-\"]\r\n        Camera.cam_imx500_drawer = COCODrawer(categories, Camera.cam_imx500, needs_rescale_coords=False)\r\n\r\n\r\n    @staticmethod\r\n    def parse_detections(metadata: dict):\r\n        # logger.debug(\"Thread %s: Camera.parse_detections\", get_ident())\r\n        \"\"\"Object Detection: Parse the output tensor into a number of detected objects, scaled to the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        bbox_normalization = Camera.cam_imx500.network_intrinsics.bbox_normalization\r\n        bbox_order = Camera.cam_imx500.network_intrinsics.bbox_order\r\n        threshold = ai.detectionThreshold\r\n        iou = ai.iouThreshold\r\n        max_detections = ai.maxDetections\r\n\r\n        np_outputs = Camera.cam_imx500.get_outputs(metadata, add_batch=True)\r\n        input_w, input_h = Camera.cam_imx500.get_input_size()\r\n        # logger.debug(\"Thread %s: Camera.parse_detections - got input_size\", get_ident())\r\n        if np_outputs is None:\r\n            # logger.debug(\"Thread %s: Camera.parse_detections - np_outputs is None\", get_ident())\r\n            return Camera.cam_imx500_last_detections\r\n        if Camera.cam_imx500.network_intrinsics.postprocess == \"nanodet\":\r\n            # logger.debug(\"Thread %s: Camera.parse_detections - postprocess == nanodet\", get_ident())\r\n            boxes, scores, classes = \\\r\n                postprocess_nanodet_detection(outputs=np_outputs[0], conf=threshold, iou_thres=iou,\r\n                                            max_out_dets=max_detections)[0]\r\n            from picamera2.devices.imx500.postprocess import scale_boxes\r\n            boxes = scale_boxes(boxes, 1, 1, input_h, input_w, False, False)\r\n        else:\r\n            # logger.debug(\"Thread %s: Camera.parse_detections - postprocess != nanodet\", get_ident())\r\n            boxes, scores, classes = np_outputs[0][0], np_outputs[1][0], np_outputs[2][0]\r\n            if bbox_normalization:\r\n                boxes = boxes / input_h\r\n\r\n            if bbox_order == \"xy\":\r\n                boxes = boxes[:, [1, 0, 3, 2]]\r\n            boxes = np.array_split(boxes, 4, axis=1)\r\n            boxes = zip(*boxes)\r\n\r\n        # logger.debug(\"Thread %s: Camera.parse_detections - Registring last detections\", get_ident())\r\n        Camera.cam_imx500_last_detections = [\r\n            Detection(box, category, score, metadata)\r\n            for box, score, category in zip(boxes, scores, classes)\r\n            if score > threshold\r\n        ]\r\n        # logger.debug(\"Thread %s: Camera.parse_detections - found %s detections\", get_ident(), len(Camera.cam_imx500_last_detections))\r\n        return Camera.cam_imx500_last_detections\r\n\r\n\r\n    @staticmethod\r\n    @lru_cache\r\n    def get_labels():\r\n        \"\"\"Object Detection: get labels.\"\"\"\r\n        labels = Camera.cam_imx500.network_intrinsics.labels\r\n\r\n        if Camera.cam_imx500.network_intrinsics.ignore_dash_labels:\r\n            labels = [label for label in labels if label and label != \"-\"]\r\n        return labels\r\n\r\n\r\n    @staticmethod\r\n    def draw_detections(request, stream=\"main\"):\r\n        \"\"\"Object Detection: Draw the detections for this request onto the ISP output.\"\"\"\r\n        # logger.debug(\"Thread %s: Camera.draw_detections\", get_ident())\r\n        detections = Camera.cam_imx500_last_results\r\n        if detections is None:\r\n            return\r\n        if len(detections) == 0:\r\n            return\r\n        labels = Camera.get_labels()\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                # logger.debug(\"Thread %s: Camera.draw_detections - drawing on %s\", get_ident(), stream)\r\n                with MappedArray(request, stream) as m:\r\n                    for detection in detections:\r\n                        detectionOK = False\r\n                        if stream == \"lores\":\r\n                            if detection.box is not None:\r\n                                detectionOK = True\r\n                                x, y, w, h = detection.box\r\n                        else:\r\n                            if detection.box_main is not None:\r\n                                detectionOK = True\r\n                                x, y, w, h = detection.box_main\r\n                        if detectionOK:\r\n                            label = f\"{labels[int(detection.category)]} ({detection.conf:.2f})\"\r\n\r\n                            # Calculate text size and position\r\n                            (text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)\r\n                            text_x = x + 5\r\n                            text_y = y + 15\r\n\r\n                            # Create a copy of the array to draw the background with opacity\r\n                            overlay = m.array.copy()\r\n\r\n                            # Draw the background rectangle on the overlay\r\n                            cv2.rectangle(overlay,\r\n                                        (text_x, text_y - text_height),\r\n                                        (text_x + text_width, text_y + baseline),\r\n                                        (255, 255, 255),  # Background color (white)\r\n                                        cv2.FILLED)\r\n\r\n                            alpha = 0.30\r\n                            cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array)\r\n\r\n                            # Draw text on top of the background\r\n                            cv2.putText(m.array, label, (text_x, text_y),\r\n                                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)\r\n                            # Draw detection box\r\n                            cv2.rectangle(m.array, (x, y), (x + w, y + h), (0, 255, 0, 0), thickness=2)\r\n\r\n                    if Camera.cam_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                        b_x, b_y, b_w, b_h = Camera.cam_imx500.get_roi_scaled(request, stream)\r\n                        color = (255, 0, 0)  # red\r\n                        cv2.putText(m.array, \"ROI\", (b_x + 5, b_y + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)\r\n                        cv2.rectangle(m.array, (b_x, b_y), (b_x + b_w, b_y + b_h), (255, 0, 0, 0))\r\n\r\n\r\n    @staticmethod\r\n    def create_and_draw_masks(request: CompletedRequest):\r\n        \"\"\"Segmentation: Create masks from the output tensor and draw them on the main output image.\"\"\"\r\n        masks = Camera.create_masks(request)\r\n        if masks:\r\n            Camera.cam_imx500_last_overlay = Camera.compose_overlay(masks)\r\n        if Camera.cam_imx500_last_overlay is not None:\r\n            Camera.draw_masks(request, Camera.cam_imx500_last_overlay)\r\n\r\n\r\n    @staticmethod\r\n    def create_masks(request: CompletedRequest) -> Dict[int, np.ndarray]:\r\n        \"\"\"Segmentation: Create masks from the output tensor, scaled to the ISP output.\"\"\"\r\n        res = {}\r\n        np_outputs = Camera.cam_imx500.get_outputs(metadata=request.get_metadata())\r\n        input_w, input_h = Camera.cam_imx500.get_input_size()\r\n        if np_outputs is None:\r\n            return res\r\n        mask = np_outputs[0]\r\n        found_indices = np.unique(mask)\r\n\r\n        for i in found_indices:\r\n            if i == 0:\r\n                continue\r\n            output_shape = [input_h, input_w, 4]\r\n            colour = [(0, 0, 0, 0), Camera.COLOURS[int(i)]]\r\n            colour[1][3] = 150  # update the alpha value here, to save setting it later\r\n            overlay = np.array(mask == i, dtype=np.uint8)\r\n            overlay = np.array(colour)[overlay].reshape(output_shape).astype(np.uint8)\r\n            # No need to resize the overlay, it will be stretched to the output window.\r\n            res[i] = overlay\r\n        return res\r\n\r\n\r\n    @staticmethod\r\n    def compose_overlay(masks):\r\n        \"\"\"Segmentation: Compose overlay from masks.\"\"\"\r\n        input_w, input_h = Camera.cam_imx500.get_input_size()\r\n        overlay = np.zeros((input_h, input_w, 4), dtype=np.uint8)\r\n        for v in masks.values():\r\n            overlay += v\r\n        return overlay\r\n\r\n\r\n    @staticmethod\r\n    def draw_masks(request: CompletedRequest, overlay: np.ndarray):\r\n        \"\"\"Segmentation: Draw masks.\"\"\"\r\n        alpha = overlay[:, :, 3:4] / 255.0\r\n        overlay_rgb = overlay[:, :, :3]\r\n\r\n        cfg = CameraCfg()\r\n        ai = cfg.aiConfig\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    frame = m.array  # HxWx3 RGB\r\n                    h, w, _ = frame.shape\r\n\r\n                    ov = cv2.resize(overlay_rgb, (w, h), interpolation=cv2.INTER_NEAREST)\r\n                    a = cv2.resize(alpha, (w, h), interpolation=cv2.INTER_NEAREST)[:, :, np.newaxis]\r\n\r\n                    frame[:] = (a * ov + (1.0 - a) * frame).astype(np.uint8)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_get_label(request: CompletedRequest, idx: int) -> str:\r\n        \"\"\"Classification: Retrieve the label corresponding to the classification index.\"\"\"\r\n        if Camera.cam2_imx500_labels is None:\r\n            Camera.cam2_imx500_labels = Camera.cam2_imx500.network_intrinsics.labels\r\n            output_tensor_size = Camera.cam2_imx500.get_output_shapes(request.get_metadata())[0][0]\r\n            if output_tensor_size == 1000:\r\n                Camera.cam2_imx500_labels = Camera.cam2_imx500_labels[1:]  # Ignore the background label if present\r\n        return Camera.cam2_imx500_labels[idx]\r\n\r\n\r\n    @staticmethod\r\n    def cam2_parse_and_draw_classification_results(request: CompletedRequest):\r\n        \"\"\"Classification: Analyse and draw the classification results in the output tensor.\"\"\"\r\n        results = Camera.cam2_parse_classification_results(request)\r\n        Camera.cam2_draw_classification_results(request, results)\r\n\r\n    @staticmethod\r\n    def cam2_parse_classification_results(request: CompletedRequest) -> List[Classification]:\r\n        \"\"\"Classification: Parse the output tensor into the classification results above the threshold.\"\"\"\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        np_outputs = Camera.cam2_imx500.get_outputs(request.get_metadata())\r\n        if np_outputs is None:\r\n            return Camera.cam2_imx500_last_detections\r\n        np_output = np_outputs[0]\r\n        if Camera.cam2_imx500.network_intrinsics.softmax:\r\n            np_output = softmax(np_output)\r\n        top_indices = np.argpartition(-np_output, ai.topK)[:ai.topK]  # Get top 3 indices with the highest scores\r\n        top_indices = top_indices[np.argsort(-np_output[top_indices])]  # Sort the top 3 indices by their scores\r\n        Camera.cam2_imx500_last_detections = [Classification(index, np_output[index]) for index in top_indices]\r\n        return Camera.cam2_imx500_last_detections\r\n\r\n\r\n    @staticmethod\r\n    def cam2_draw_classification_results(request: CompletedRequest, results: List[Classification], stream: str = \"lores\"):\r\n        \"\"\"Classification: Draw the classification results for this request onto the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    if Camera.cam2_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                        # Drawing ROI box\r\n                        b_x, b_y, b_w, b_h = Camera.cam2_imx500.get_roi_scaled(request, stream)\r\n                        color = (255, 0, 0)  # red\r\n                        cv2.putText(m.array, \"ROI\", (b_x + 5, b_y + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)\r\n                        cv2.rectangle(m.array, (b_x, b_y), (b_x + b_w, b_y + b_h), (255, 0, 0, 0))\r\n                        text_left, text_top = b_x, b_y + 20\r\n                    else:\r\n                        text_left, text_top = 0, 0\r\n                    # Drawing labels (in the ROI box if it exists)\r\n                    for index, result in enumerate(results):\r\n                        label = Camera.cam2_get_label(request, idx=result.idx)\r\n                        text = f\"{label}: {result.score:.3f}\"\r\n\r\n                        # Calculate text size and position\r\n                        (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)\r\n                        text_x = text_left + 5\r\n                        text_y = text_top + 15 + index * 20\r\n\r\n                        # Create a copy of the array to draw the background with opacity\r\n                        overlay = m.array.copy()\r\n\r\n                        # Draw the background rectangle on the overlay\r\n                        cv2.rectangle(overlay,\r\n                                    (text_x, text_y - text_height),\r\n                                    (text_x + text_width, text_y + baseline),\r\n                                    (255, 255, 255),  # Background color (white)\r\n                                    cv2.FILLED)\r\n\r\n                        alpha = 0.3\r\n                        cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array)\r\n\r\n                        # Draw text on top of the background\r\n                        cv2.putText(m.array, text, (text_x, text_y),\r\n                                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_ai_output_tensor_parse(metadata: dict):\r\n        \"\"\"Pose Estimation: Parse the output tensor into a number of detected objects, scaled to the ISP output.\"\"\"\r\n        np_outputs = Camera.cam2_imx500.get_outputs(metadata=metadata, add_batch=True)\r\n        if np_outputs is not None:\r\n            cfg = CameraCfg()\r\n            scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n            if \"aiconfig\" in scfg:\r\n                ai = scfg[\"aiconfig\"]\r\n            else:\r\n                ai = AiConfig()\r\n            keypoints, scores, boxes = postprocess_higherhrnet(outputs=np_outputs,\r\n                                                            img_size=Camera.cam2_imx500_WINDOW_SIZE_H_W,\r\n                                                            img_w_pad=(0, 0),\r\n                                                            img_h_pad=(0, 0),\r\n                                                            detection_threshold=ai.detectionThreshold,\r\n                                                            network_postprocess=True)\r\n\r\n            if scores is not None and len(scores) > 0:\r\n                Camera.cam2_imx500_last_keypoints = np.reshape(np.stack(keypoints, axis=0), (len(scores), 17, 3))\r\n                Camera.cam2_imx500_last_boxes = [np.array(b) for b in boxes]\r\n                Camera.cam2_imx500_last_scores = np.array(scores)\r\n        return Camera.cam2_imx500_last_boxes, Camera.cam2_imx500_last_scores, Camera.cam2_imx500_last_keypoints\r\n\r\n    @staticmethod\r\n    def cam2_ai_output_tensor_draw(request: CompletedRequest, boxes, scores, keypoints, stream='lores'):\r\n        \"\"\"Pose Estimation: Draw the detections for this request onto the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        detection_threshold = ai.detectionThreshold\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    if boxes is not None and len(boxes) > 0:\r\n                        Camera.cam2_imx500_drawer.annotate_image(m.array, boxes, scores,\r\n                                            np.zeros(scores.shape), keypoints, detection_threshold,\r\n                                            detection_threshold, request.get_metadata(), Camera.cam2, stream)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_picamera2_pre_callback(request: CompletedRequest):\r\n        \"\"\"Pose Estimation: Analyse the detected objects in the output tensor and draw them on the main output image.\"\"\"\r\n        boxes, scores, keypoints = Camera.cam2_ai_output_tensor_parse(request.get_metadata())\r\n        Camera.cam2_ai_output_tensor_draw(request, boxes, scores, keypoints)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_set_drawer():\r\n        \"\"\"Pose Estimation: Set up the drawer for IMX500 pose estimation.\"\"\"\r\n        categories = Camera.cam2_imx500.network_intrinsics.labels\r\n        categories = [c for c in categories if c and c != \"-\"]\r\n        Camera.cam2_imx500_drawer = COCODrawer(categories, Camera.cam2_imx500, needs_rescale_coords=False)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_parse_detections(metadata: dict):\r\n        \"\"\"Object Detection: Parse the output tensor into a number of detected objects, scaled to the ISP output.\"\"\"\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        bbox_normalization = Camera.cam2_imx500.network_intrinsics.bbox_normalization\r\n        bbox_order = Camera.cam2_imx500.network_intrinsics.bbox_order\r\n        threshold = ai.detectionThreshold\r\n        iou = ai.iouThreshold\r\n        max_detections = ai.maxDetections\r\n\r\n        np_outputs = Camera.cam2_imx500.get_outputs(metadata, add_batch=True)\r\n        input_w, input_h = Camera.cam2_imx500.get_input_size()\r\n        if np_outputs is None:\r\n            return Camera.cam2_imx500_last_detections\r\n        if Camera.cam2_imx500.network_intrinsics.postprocess == \"nanodet\":\r\n            boxes, scores, classes = \\\r\n                postprocess_nanodet_detection(outputs=np_outputs[0], conf=threshold, iou_thres=iou,\r\n                                            max_out_dets=max_detections)[0]\r\n            from picamera2.devices.imx500.postprocess import scale_boxes\r\n            boxes = scale_boxes(boxes, 1, 1, input_h, input_w, False, False)\r\n        else:\r\n            boxes, scores, classes = np_outputs[0][0], np_outputs[1][0], np_outputs[2][0]\r\n            if bbox_normalization:\r\n                boxes = boxes / input_h\r\n\r\n            if bbox_order == \"xy\":\r\n                boxes = boxes[:, [1, 0, 3, 2]]\r\n            boxes = np.array_split(boxes, 4, axis=1)\r\n            boxes = zip(*boxes)\r\n\r\n        Camera.cam2_imx500_last_detections = [\r\n            Cam2Detection(box, category, score, metadata)\r\n            for box, score, category in zip(boxes, scores, classes)\r\n            if score > threshold\r\n        ]\r\n        return Camera.cam2_imx500_last_detections\r\n\r\n\r\n    @staticmethod\r\n    @lru_cache\r\n    def cam2_get_labels():\r\n        \"\"\"Object Detection: get labels.\"\"\"\r\n        labels = Camera.cam2_imx500.network_intrinsics.labels\r\n\r\n        if Camera.cam2_imx500.network_intrinsics.ignore_dash_labels:\r\n            labels = [label for label in labels if label and label != \"-\"]\r\n        return labels\r\n\r\n\r\n    @staticmethod\r\n    def cam2_draw_detections(request, stream=\"main\"):\r\n        \"\"\"Object Detection: Draw the detections for this request onto the ISP output.\"\"\"\r\n        detections = Camera.cam2_imx500_last_results\r\n        if detections is None:\r\n            return\r\n        if len(detections) == 0:\r\n            return\r\n        labels = Camera.cam2_get_labels()\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    for detection in detections:\r\n                        if stream == \"lores\":\r\n                            x, y, w, h = detection.box\r\n                        else:\r\n                            x, y, w, h = detection.box_main\r\n                        label = f\"{labels[int(detection.category)]} ({detection.conf:.2f})\"\r\n\r\n                        # Calculate text size and position\r\n                        (text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)\r\n                        text_x = x + 5\r\n                        text_y = y + 15\r\n\r\n                        # Create a copy of the array to draw the background with opacity\r\n                        overlay = m.array.copy()\r\n\r\n                        # Draw the background rectangle on the overlay\r\n                        cv2.rectangle(overlay,\r\n                                    (text_x, text_y - text_height),\r\n                                    (text_x + text_width, text_y + baseline),\r\n                                    (255, 255, 255),  # Background color (white)\r\n                                    cv2.FILLED)\r\n\r\n                        alpha = 0.30\r\n                        cv2.addWeighted(overlay, alpha, m.array, 1 - alpha, 0, m.array)\r\n\r\n                        # Draw text on top of the background\r\n                        cv2.putText(m.array, label, (text_x, text_y),\r\n                                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)\r\n\r\n                        # Draw detection box\r\n                        cv2.rectangle(m.array, (x, y), (x + w, y + h), (0, 255, 0, 0), thickness=2)\r\n\r\n                    if Camera.cam2_imx500.network_intrinsics.preserve_aspect_ratio:\r\n                        b_x, b_y, b_w, b_h = Camera.cam2_imx500.get_roi_scaled(request, stream)\r\n                        color = (255, 0, 0)  # red\r\n                        cv2.putText(m.array, \"ROI\", (b_x + 5, b_y + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)\r\n                        cv2.rectangle(m.array, (b_x, b_y), (b_x + b_w, b_y + b_h), (255, 0, 0, 0))\r\n\r\n\r\n\r\n    @staticmethod\r\n    def cam2_create_and_draw_masks(request: CompletedRequest):\r\n        \"\"\"Segmentation: Create masks from the output tensor and draw them on the main output image.\"\"\"\r\n        masks = Camera.cam2_create_masks(request)\r\n        if masks:\r\n            Camera.cam2_imx500_last_overlay = Camera.cam2_compose_overlay(masks)\r\n        if Camera.cam2_imx500_last_overlay is not None:\r\n            Camera.cam2_draw_masks(request, Camera.cam2_imx500_last_overlay)\r\n\r\n\r\n    @staticmethod\r\n    def cam2_create_masks(request: CompletedRequest) -> Dict[int, np.ndarray]:\r\n        \"\"\"Segmentation: Create masks from the output tensor, scaled to the ISP output.\"\"\"\r\n        res = {}\r\n        np_outputs = Camera.cam2_imx500.get_outputs(metadata=request.get_metadata())\r\n        input_w, input_h = Camera.cam2_imx500.get_input_size()\r\n        if np_outputs is None:\r\n            return res\r\n        mask = np_outputs[0]\r\n        found_indices = np.unique(mask)\r\n\r\n        for i in found_indices:\r\n            if i == 0:\r\n                continue\r\n            output_shape = [input_h, input_w, 4]\r\n            colour = [(0, 0, 0, 0), Camera.COLOURS[int(i)]]\r\n            colour[1][3] = 150  # update the alpha value here, to save setting it later\r\n            overlay = np.array(mask == i, dtype=np.uint8)\r\n            overlay = np.array(colour)[overlay].reshape(output_shape).astype(np.uint8)\r\n            # No need to resize the overlay, it will be stretched to the output window.\r\n            res[i] = overlay\r\n        return res\r\n\r\n\r\n    @staticmethod\r\n    def cam2_compose_overlay(masks):\r\n        \"\"\"Segmentation: Compose overlay from masks.\"\"\"\r\n        input_w, input_h = Camera.cam2_imx500.get_input_size()\r\n        overlay = np.zeros((input_h, input_w, 4), dtype=np.uint8)\r\n        for v in masks.values():\r\n            overlay += v\r\n        return overlay\r\n\r\n\r\n\r\n    @staticmethod\r\n    def cam2_draw_masks(request: CompletedRequest, overlay: np.ndarray):\r\n        \"\"\"Segmentation: Draw masks.\"\"\"\r\n        alpha = overlay[:, :, 3:4] / 255.0\r\n        overlay_rgb = overlay[:, :, :3]\r\n\r\n        cfg = CameraCfg()\r\n        scfg = cfg.streamingCfg[str(Camera.camNum2)]\r\n        if \"aiconfig\" in scfg:\r\n            ai = scfg[\"aiconfig\"]\r\n        else:\r\n            ai = AiConfig()\r\n        for stream in [\"lores\", \"main\"]:\r\n            if (stream == \"lores\" and ai.drawOnLores == True) or (stream == \"main\" and ai.drawOnMain == True):\r\n                with MappedArray(request, stream) as m:\r\n                    frame = m.array  # HxWx3 RGB\r\n                    h, w, _ = frame.shape\r\n\r\n                    ov = cv2.resize(overlay_rgb, (w, h), interpolation=cv2.INTER_NEAREST)\r\n                    a = cv2.resize(alpha, (w, h), interpolation=cv2.INTER_NEAREST)[:, :, np.newaxis]\r\n\r\n                    frame[:] = (a * ov + (1.0 - a) * frame).astype(np.uint8)\r\n\r\n"
  },
  {
    "path": "raspiCamSrv/config.py",
    "content": "from flask import (\n    Blueprint,\n    Response,\n    flash,\n    g,\n    redirect,\n    render_template,\n    request,\n    url_for,\n    current_app,\n)\nfrom flask import send_file\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camCfg import CameraCfg\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.version import version\nfrom picamera2 import Picamera2\nimport os\nimport shutil\nimport json\nimport time\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\n# Try to import platform, which does not exist in Bullseye Picamera2 distributions\ntry:\n    import picamera2.platform as Platform\n\n    usePlatform = True\nexcept ImportError:\n    usePlatform = False\n\n\nbp = Blueprint(\"config\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n\n@bp.route(\"/config\")\n@login_required\ndef main():\n    g.hostname = request.host\n    g.version = version\n    # Although not directly needed here, the camara needs to be initialized\n    # in order to load the camera-specific parameters into configuration\n    cam = Camera().cam\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.curMenu = \"config\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\ndef doSyncTransform(hflip: bool, vflip: bool, tgt: list) -> bool:\n    \"\"\"Synchronize the transform settings of target configurations with reference\n\n    Parameters:\n    hflip:    horizontal flip\n    vflip:    vertical flip\n    tgt  :    list of configurations for which to adjust the aspect ratio\n\n    Return:\n    True if transform settings for Live View was changed\n    \"\"\"\n    logger.debug(\"In doSyncTransform\")\n    ret = False\n    cfg = CameraCfg()\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    for conf in tgt:\n        if conf == \"Live View\":\n            if cfglive.transform_hflip != hflip or cfglive.transform_vflip != vflip:\n                ret = True\n            cfglive.transform_hflip = hflip\n            cfglive.transform_vflip = vflip\n        elif conf == \"Photo\":\n            cfgphoto.transform_hflip = hflip\n            cfgphoto.transform_vflip = vflip\n        elif conf == \"Raw Photo\":\n            cfgraw.transform_hflip = hflip\n            cfgraw.transform_vflip = vflip\n        elif conf == \"Video\":\n            cfgvideo.transform_hflip = hflip\n            cfgvideo.transform_vflip = vflip\n    logger.debug(\"doSyncTransform %s\", ret)\n    return ret\n\n\ndef doSyncAspectRatio(ref: tuple, tgt: list) -> bool:\n    \"\"\"Synchronize the aspect ratio of target configurations with reference\n\n    Parameters:\n    ref:    reference size (width, height)\n    tgt:    list of configurations for which to adjust the aspect ratio\n\n    Return:\n    True if Stream Size for Live View was changed\n    \"\"\"\n    logger.debug(\"In doSyncAspectRatio\")\n    ret = False\n    cfg = CameraCfg()\n    aspRatioRef = ref[0] / ref[1]\n    for conf in tgt:\n        if conf == \"Live View\":\n            size = cfg.liveViewConfig.stream_size\n        elif conf == \"Photo\":\n            size = cfg.photoConfig.stream_size\n        elif conf == \"Raw Photo\":\n            size = cfg.rawConfig.stream_size\n        elif conf == \"Video\":\n            size = cfg.videoConfig.stream_size\n        else:\n            size = None\n        if not size is None:\n            log = f\"Changed Stream Size for {conf} from {size} to \"\n            aspRatio = size[0] / size[1]\n            if aspRatio != aspRatioRef:\n                width = size[0]\n                height = round(size[0] / aspRatioRef)\n                if not (height % 2) == 0:\n                    height += 1\n                if height > cfg.cameraProperties.pixelArraySize[1]:\n                    height = cfg.cameraProperties.pixelArraySize[1]\n                    width = round(height * aspRatioRef)\n                    if not (width % 2) == 0:\n                        width += 1\n                size = (width, height)\n\n                sm = \"custom\"\n                for mode in cfg.sensorModes:\n                    if mode.size[0] == width and mode.size[1] == height:\n                        sm = str(mode.id)\n                        break\n\n                logger.debug(log + str(size))\n                if conf == \"Live View\":\n                    cfg.liveViewConfig.stream_size = size\n                    cfg.liveViewConfig.sensor_mode = sm\n                    ret = True\n                elif conf == \"Photo\":\n                    cfg.photoConfig.stream_size = size\n                    cfg.photoConfig.sensor_mode = sm\n                elif conf == \"Raw Photo\":\n                    cfg.rawConfig.stream_size = size\n                    cfg.rawConfig.sensor_mode = sm\n                elif conf == \"Video\":\n                    cfg.videoConfig.stream_size = size\n                    cfg.videoConfig.sensor_mode = sm\n                else:\n                    pass\n    logger.debug(\"doSyncAspectRatio %s\", ret)\n    return ret\n\n\n@bp.route(\"/syncAspectRatio\", methods=(\"GET\", \"POST\"))\n@login_required\ndef syncAspectRatio():\n    logger.debug(\"In syncAspectRatio\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        lastTab = sc.lastConfigTab\n        selTab = request.form.get(\"activecfgtab\")\n        if selTab != \"-\":\n            lastTab = selTab\n        sc.lastConfigTab = lastTab\n        syncAspectRatio = not request.form.get(\"syncaspectratio\") is None\n        sc.syncAspectRatio = syncAspectRatio\n        logger.debug(\"syncAspectRatio - lastTab: %s\", lastTab)\n        if syncAspectRatio == True:\n            if lastTab == \"cfglive\":\n                aspRef = cfglive.stream_size\n                aspTgt = [\"Photo\", \"Raw Photo\", \"Video\"]\n                doSyncAspectRatio(aspRef, aspTgt)\n            elif lastTab == \"cfgphoto\":\n                aspRef = cfgphoto.stream_size\n                aspTgt = [\"Live View\", \"Raw Photo\", \"Video\"]\n                doSyncAspectRatio(aspRef, aspTgt)\n            elif lastTab == \"cfgraw\":\n                aspRef = cfgraw.stream_size\n                aspTgt = [\"Live View\", \"Photo\", \"Video\"]\n                doSyncAspectRatio(aspRef, aspTgt)\n            elif lastTab == \"cfgvideo\":\n                aspRef = cfgvideo.stream_size\n                aspTgt = [\"Live View\", \"Photo\", \"Raw Photo\"]\n                doSyncAspectRatio(aspRef, aspTgt)\n            else:\n                pass\n            Camera.resetScalerCropRequested = True\n            Camera().restartLiveStream()\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Sync Aspect Ratio set to {sc.syncAspectRatio}\")\n        cfg.streamingCfgInvalid = True\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\ndef findTuningFile(tuning_file: str, dir=None) -> str:\n    \"\"\"Find the given tuning file and return its path\n\n    Code has been copied from Picamera2.load_tuning_file(...)\n    Args:\n        - tuning_file (str): filename of tuning file\n        - dir (str, optional): Directory to search. If None, search standard installation dirs\n\n    Returns:\n        - str: Path of tuning file; None, if not found\n    \"\"\"\n    tfPath = None\n    if dir is not None:\n        dirs = [dir]\n    else:\n        if usePlatform:\n            platform_dir = (\n                \"vc4\" if Picamera2.platform == Platform.Platform.VC4 else \"pisp\"\n            )\n            dirs = [\n                os.path.expanduser(\"~/libcamera/src/ipa/rpi/\" + platform_dir + \"/data\"),\n                \"/usr/local/share/libcamera/ipa/rpi/\" + platform_dir,\n                \"/usr/share/libcamera/ipa/rpi/\" + platform_dir,\n            ]\n        else:\n            dirs = [\n                os.path.expanduser(\"~/libcamera/src/ipa/rpi/vc4/data\"),\n                \"/usr/local/share/libcamera/ipa/rpi/vc4\",\n                \"/usr/share/libcamera/ipa/rpi/vc4\",\n            ]\n    for directory in dirs:\n        file = os.path.join(directory, tuning_file)\n        if os.path.isfile(file):\n            tfPath = file\n    return tfPath\n\n\ndef isTuningFile(file: str, folder: str) -> bool:\n    logger.debug(\"In isTuningFile\")\n    logger.debug(\"isTuningFile - file=%s\", file)\n    logger.debug(\"isTuningFile - folder=%s\", folder)\n    res = False\n    try:\n        tf = Picamera2.load_tuning_file(file, folder)\n        res = True\n    except RuntimeError as e:\n        res = False\n    logger.debug(\"isTuningFile - res=%s\", res)\n    return res\n\n\ndef getTuningFiles(folder, defFile) -> list:\n    \"\"\"Create a list of all .json files in the given folder\n\n    Args:\n        - folder (str): Folder to search\n        - defFile (str): Name of default file\n    Returns:\n        - list: list with filenames of .json files\n    \"\"\"\n    tfl = []\n    defFileFound = False\n    if folder is not None:\n        if os.path.exists(folder):\n            for f in os.listdir(folder):\n                if os.path.isfile(os.path.join(folder, f)):\n                    if f == defFile:\n                        defFileFound = True\n                    nam, ext = os.path.splitext(f)\n                    if ext.lower() == \".json\":\n                        tfl.append(f)\n    if defFile:\n        if defFileFound == False:\n            tfl.append(defFile)\n    return tfl\n\n\n@bp.route(\"/tuningCfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef tuningCfg():\n    logger.debug(\"In tuningCfg\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        msg = \"\"\n        restart = False\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the tuning configuration\"\n            msg = err\n        if sc.isPhotoSeriesRecording:\n            err = \"Please go to 'Photo Series' and stop the active process before changing the tuning configuration\"\n            msg = err\n        if sc.isVideoRecording == True:\n            err = \"Please stop video recording before changing the tuning configuration\"\n            msg = err\n        if not err:\n            loadTuningFile = not request.form.get(\"loadtuningfile\") is None\n            fd = request.form[\"tuningfolder\"]\n            if fd == \"\":\n                fd = None\n            fn = request.form[\"tuningfile\"]\n            if loadTuningFile:\n                if isTuningFile(fn, fd) == True:\n                    tc.tuningFolder = fd\n                    tc.tuningFile = fn\n                    tc.loadTuningFile = loadTuningFile\n                    restart = True\n                else:\n                    msg = \"Specify an existing tuning file before activating to load it\"\n            else:\n                tc.tuningFolder = fd\n                tc.tuningFile = fn\n                if tc.loadTuningFile != loadTuningFile:\n                    restart = True\n                tc.loadTuningFile = loadTuningFile\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Configuration for tuning changed\")\n            cfg.streamingCfgInvalid = True\n        if restart:\n            Camera().restartLiveStream()\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/customTuning\", methods=(\"GET\", \"POST\"))\n@login_required\ndef customTuning():\n    logger.debug(\"In customTuning\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        msg = \"\"\n        restart = False\n        if tc.loadTuningFile == True:\n            if sc.isTriggerRecording:\n                err = \"Please go to 'Trigger' and stop the active process before changing the tuning configuration\"\n                msg = err\n            if sc.isPhotoSeriesRecording:\n                err = \"Please go to 'Photo Series' and stop the active process before changing the tuning configuration\"\n                msg = err\n            if sc.isVideoRecording == True:\n                err = \"Please stop video recording before changing the tuning configuration\"\n                msg = err\n        if not err:\n            fd = tc.tuningFolder\n            fn = tc.tuningFile\n            fdCustom = current_app.static_folder + \"/tuning\"\n            if fd == fdCustom:\n                msg = \"No changes. Custom folder was already set.\"\n            else:\n                try:\n                    os.makedirs(fdCustom, exist_ok=True)\n                    tc.tuningFolder = fdCustom\n                except Exception as e:\n                    msg = \"Error while creating custom folder \" + fdCustom + \":\" + e\n                if msg == \"\":\n                    if fn != \"\":\n                        if isTuningFile(fn, fdCustom) == True:\n                            if tc.loadTuningFile == True:\n                                msg = \"Tuning file switched to custom file.\"\n                                restart = True\n                        else:\n                            if isTuningFile(fn, None) == True:\n                                fpCustom = os.path.join(fdCustom, fn)\n                                fpDefault = findTuningFile(fn, None)\n                                if fpDefault is not None:\n                                    try:\n                                        shutil.copyfile(fpDefault, fpCustom)\n                                        msg = (\n                                            \"Tuning file \"\n                                            + fn\n                                            + \" copied to custom directory.\"\n                                        )\n                                        if tc.loadTuningFile == True:\n                                            restart = True\n                                    except Exception as e:\n                                        logger.debug(\n                                            \"error while copying tuning file: %s\",\n                                            str(e),\n                                        )\n                                        msg = (\n                                            \"Tuning file directory switched to custom directory, but tuning file \"\n                                            + fn\n                                            + \" could not be copied.\"\n                                        )\n                                        fn = \"\"\n                                        if tc.loadTuningFile == True:\n                                            restart = True\n                                        tc.loadTuningFile = False\n                                else:\n                                    tc.tuningFile = \"\"\n                                    if tc.loadTuningFile == True:\n                                        restart = True\n                                        tc.loadTuningFile = False\n                            else:\n                                tc.tuningFile = \"\"\n                                if tc.loadTuningFile == True:\n                                    restart = True\n                                    tc.loadTuningFile = False\n                        sc.unsavedChanges = True\n                        sc.addChangeLogEntry(f\"Tuning folder set to custom folder\")\n                        cfg.streamingCfgInvalid = True\n        if restart:\n            Camera().restartLiveStream()\n        if len(msg) > 0:\n            flash(msg)\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/defaultTuning\", methods=(\"GET\", \"POST\"))\n@login_required\ndef defaultTuning():\n    logger.debug(\"In defaultTuning\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        msg = \"\"\n        restart = False\n        if tc.loadTuningFile == True:\n            if sc.isTriggerRecording:\n                err = \"Please go to 'Trigger' and stop the active process before changing the tuning configuration\"\n                msg = err\n            if sc.isPhotoSeriesRecording:\n                err = \"Please go to 'Photo Series' and stop the active process before changing the tuning configuration\"\n                msg = err\n            if sc.isVideoRecording == True:\n                err = \"Please stop video recording before changing the tuning configuration\"\n                msg = err\n        if not err:\n            fd = tc.tuningFolder\n            fn = tc.tuningFile\n            fdDefault = tc.tuningFolderDef\n            if fd == fdDefault:\n                msg = \"No changes. Default folder was already set.\"\n            else:\n                tc.tuningFolder = fdDefault\n                if fn != \"\":\n                    if isTuningFile(fn, fd) == True:\n                        if tc.loadTuningFile == True:\n                            msg = \"Tuning file switched to default file.\"\n                            restart = True\n                    else:\n                        fn = sc.activeCameraModel + \".json\"\n                        if isTuningFile(fn, fd) == True:\n                            tc.tuningFile = fn\n                            if tc.loadTuningFile == True:\n                                msg = \"Tuning file switched to default file.\"\n                                restart = True\n                        else:\n                            tc.tuningFile = \"\"\n                            if tc.loadTuningFile == True:\n                                restart = True\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(f\"Tuning folder set to default folder\")\n                cfg.streamingCfgInvalid = True\n        if restart:\n            Camera().restartLiveStream()\n        if len(msg) > 0:\n            flash(msg)\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/deleteTuningFile\", methods=(\"GET\", \"POST\"))\n@login_required\ndef deleteTuningFile():\n    logger.debug(\"In deleteTuningFile\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        restart = False\n        fp = None\n        if tc.isDefaultFolder == True:\n            msg = \"You cannot delete a tuning file from the default folder\"\n        else:\n            fd = tc.tuningFolder\n            fn = tc.tuningFile\n            fp = findTuningFile(fn, fd)\n            if fp is not None:\n                os.remove(fp)\n                msg = f\"Tuning File deleted: {fp}\"\n            else:\n                msg = \"Tuning file not found\"\n            tc.tuningFile = \"\"\n            if tc.loadTuningFile == True:\n                restart = True\n                tc.loadTuningFile = False\n        tfl = getTuningFiles(tc.tuningFolder, None)\n        if len(tfl) > 0:\n            fn = sc.activeCameraModel + \".json\"\n            found = False\n            for f in tfl:\n                if f == fn:\n                    tc.tuningFile = fn\n                    found = True\n                    break\n            if found == False:\n                tc.tuningFile = tfl[0]\n        else:\n            logger.debug(\"deleteTuningFile - No more tuning files in custom folder\")\n            fn = sc.activeCameraModel + \".json\"\n            tc.tuningFolder = None\n            logger.debug(\n                \"deleteTuningFile - fn=%s tuningFolder=%s isTuningFile=%s\",\n                fn,\n                tc.tuningFolder,\n                isTuningFile(fn, tc.tuningFolder),\n            )\n            if isTuningFile(fn, tc.tuningFolder) == True:\n                tc.tuningFile = fn\n                logger.debug(\n                    \"deleteTuningFile - tc.tuningFile set to %s\", tc.tuningFile\n                )\n            else:\n                tc.loadTuningFile = False\n        tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n        if not fp is None:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Tuning file deleted: {fp}\")\n            cfg.streamingCfgInvalid = True\n        if restart:\n            Camera().restartLiveStream()\n        if msg != \"\":\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/downloadTuningFile\", methods=(\"GET\", \"POST\"))\n@login_required\ndef downloadTuningFile():\n    logger.debug(\"In downloadTuningFile\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        fd = tc.tuningFolder\n        fn = tc.tuningFile\n        fp = findTuningFile(fn, fd)\n        if fp is not None:\n            msg = f\"Downloading {fn}\"\n            flash(msg)\n            return send_file(fp, as_attachment=True, download_name=fn)\n        else:\n            msg = \"Tuning file not found\"\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/uploadTuningFile\", methods=(\"GET\", \"POST\"))\n@login_required\ndef uploadTuningFile():\n    logger.debug(\"In uploadTuningFile\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgtuning\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        msg = \"\"\n        if tc.tuningFolder is None:\n            msg = \"You may only upload to a custom folder!\"\n        else:\n            if os.path.exists(tc.tuningFolder) == False:\n                try:\n                    os.makedirs(tc.tuningFolder)\n                except Exception as e:\n                    msg = f\"Error creating folder {tc.tuningFolder}: {str(e)}\"\n        if msg == \"\":\n            if \"tuningfile\" not in request.files:\n                msg = \"No file to save\"\n            else:\n                files = request.files.getlist(\"tuningfile\")\n                countSel = len(files)\n                # tf = request.files[\"tuningfile\"]\n                logger.debug(\"uploadTuningFile - %s files selected\", countSel)\n                countUp = 0\n                for tf in files:\n                    fn = tf.filename\n                    logger.debug(\"uploadTuningFile - selected file: %s\", fn)\n                    nam, ext = os.path.splitext(fn)\n                    if ext.lower() == \".json\":\n                        fp = os.path.join(tc.tuningFolder, fn)\n                        tf.save(fp)\n                        msg = f\"Tuning file saved as {fp}.\"\n                        countUp += 1\n                if countSel > 1:\n                    msg = f\"{countUp} of {countSel} files uploaded.\"\n                    if countUp < countSel:\n                        msg = msg + \" Not all files were .json files.\"\n        if msg != \"\":\n            flash(msg)\n        tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/liveViewCfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef liveViewCfg():\n    logger.debug(\"In liveViewCfg\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfglive\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n            msg = err\n        if not err:\n            transform_hflip = not request.form.get(\"LIVE_transform_hflip\") is None\n            cfglive.transform_hflip = transform_hflip\n            transform_vflip = not request.form.get(\"LIVE_transform_vflip\") is None\n            cfglive.transform_vflip = transform_vflip\n            colour_space = request.form[\"LIVE_colour_space\"]\n            cfglive.colour_space = colour_space\n            buffer_count = int(request.form[\"LIVE_buffer_count\"])\n            cfglive.buffer_count = buffer_count\n            queue = not request.form.get(\"LIVE_queue\") is None\n            cfglive.queue = queue\n            stream = request.form[\"LIVE_stream\"]\n            sensor_mode = request.form[\"LIVE_sensor_mode\"]\n            format = request.form[\"LIVE_format\"]\n            cfglive.format = format\n            if sensor_mode == \"custom\":\n                size_width = int(request.form[\"LIVE_stream_size_width\"])\n                if not (size_width % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                size_height = int(request.form[\"LIVE_stream_size_height\"])\n                if not (size_height % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                if stream == \"lores\":\n                    if cfgphoto.stream == \"main\":\n                        if (\n                            size_width > cfgphoto.stream_size[0]\n                            or size_height > cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Photo)\"\n                    if not err and cfgvideo.stream == \"main\":\n                        if (\n                            size_width > cfgvideo.stream_size[0]\n                            or size_height > cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Video)\"\n                if stream == \"main\":\n                    if cfgphoto.stream == \"lores\":\n                        if (\n                            size_width < cfgphoto.stream_size[0]\n                            or size_height < cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Photo) must not exceed main Stream Size\"\n                    if not err and cfgvideo.stream == \"lores\":\n                        if (\n                            size_width < cfgvideo.stream_size[0]\n                            or size_height < cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Video) must not exceed main Stream Size\"\n                if not err:\n                    cfglive.stream = stream\n                    cfglive.sensor_mode = sensor_mode\n                    cfglive.stream_size = (size_width, size_height)\n                    cfglive.stream_size_align = (\n                        not request.form.get(\"LIVE_stream_size_align\") is None\n                    )\n            else:\n                mode = sm[int(sensor_mode)]\n                if stream == \"lores\":\n                    if cfgphoto.stream == \"main\":\n                        if (\n                            mode.size[0] > cfgphoto.stream_size[0]\n                            or mode.size[1] > cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Photo)\"\n                    if not err and cfgvideo.stream == \"main\":\n                        if (\n                            mode.size[0] > cfgvideo.stream_size[0]\n                            or mode.size[1] > cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Video)\"\n                if stream == \"main\":\n                    if cfgphoto.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfgphoto.stream_size[0]\n                            or mode.size[1] < cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Photo) must not exceed main Stream Size\"\n                    if not err and cfgvideo.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfgvideo.stream_size[0]\n                            or mode.size[1] < cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Video) must not exceed main Stream Size\"\n                if sc.activeCameraIsUsb == True:\n                    format = mode.format\n                    cfglive.format = format\n                if not err:\n                    cfglive.stream = stream\n                    cfglive.sensor_mode = sensor_mode\n                    cfglive.stream_size = mode.size\n                    cfglive.stream_size_align = (\n                        not request.form.get(\"LIVE_stream_size_align\") is None\n                    )\n            cfglive.display = None\n            if sc.activeCameraIsUsb == False:\n                cfglive.encode = cfglive.stream\n            else:\n                cfglive.encode = None\n            if sc.syncAspectRatio == True:\n                doSyncAspectRatio(cfglive.stream_size, [\"Photo\", \"Raw Photo\", \"Video\"])\n            Camera.resetScalerCropRequested = True\n            doSyncTransform(\n                transform_hflip, transform_vflip, [\"Photo\", \"Raw Photo\", \"Video\"]\n            )\n            Camera().restartLiveStream()\n\n            msg = \"\"\n            if err:\n                msg = err\n            if sc.raspiModelLower5:\n                if cfglive.stream == \"lores\":\n                    if format == \"YUV420\":\n                        cfglive.format = format\n                    else:\n                        if msg != \"\":\n                            msg = msg + \"\\n\"\n                        msg = (\n                            msg\n                            + \"For Raspberry Pi models < 5, the lowres stream format must be YUV\"\n                        )\n                else:\n                    cfglive.format = format\n            else:\n                cfglive.format = format\n\n            if cfglive.stream != \"lores\":\n                if msg != \"\":\n                    msg = msg + \"\\n\"\n                if sc.activeCameraIsUsb == False:\n                    msg = (\n                        msg\n                        + \"WARNING: If you do not set Stream to 'lores', the Live Stream cannot be shown parallel to other activities!\"\n                    )\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Configuration for Live View changed\")\n            cfg.streamingCfgInvalid = True\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/addLiveViewControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef addLiveViewControls():\n    logger.debug(\"In addLiveViewControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfglive\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            for key, value in cc.dict().items():\n                if value[0] == True:\n                    if key not in cfg.liveViewConfig.controls:\n                        cfg.liveViewConfig.controls[key] = value[1]\n            Camera().restartLiveStream()\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Controls added to Configuration for Live View\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/remLiveViewControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remLiveViewControls():\n    logger.debug(\"In remLiveViewControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfglive\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            cnt = 0\n            for ctrl in cfg.liveViewConfig.controls:\n                logger.debug(\"Checking checkbox ID:\" + \"sel_LIVE_\" + ctrl)\n                if request.form.get(\"sel_LIVE_\" + ctrl) is not None:\n                    cnt += 1\n            logger.debug(\n                \"Nr controls: %s - selected: %s\", len(cfg.liveViewConfig.controls), cnt\n            )\n            if cnt > 0:\n                if cnt < len(cfg.liveViewConfig.controls):\n                    while cnt > 0:\n                        for ctrl in cfg.liveViewConfig.controls:\n                            if request.form.get(\"sel_LIVE_\" + ctrl) is not None:\n                                ctrlDel = ctrl\n                                break\n                        del cfg.liveViewConfig.controls[ctrlDel]\n                        cnt -= 1\n                    Camera().restartLiveStream()\n                else:\n                    msg = \"At least one control must remain in the configuration\"\n                    flash(msg)\n            else:\n                msg = \"No controls were selected\"\n                flash(msg)\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Controls removed from Configuration for Live View\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/photoCfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef photoCfg():\n    logger.debug(\"In photoCfg\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgphoto\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            transform_hflip = not request.form.get(\"FOTO_transform_hflip\") is None\n            cfgphoto.transform_hflip = transform_hflip\n            transform_vflip = not request.form.get(\"FOTO_transform_vflip\") is None\n            cfgphoto.transform_vflip = transform_vflip\n            colour_space = request.form[\"FOTO_colour_space\"]\n            cfgphoto.colour_space = colour_space\n            buffer_count = int(request.form[\"FOTO_buffer_count\"])\n            cfgphoto.buffer_count = buffer_count\n            queue = not request.form.get(\"FOTO_queue\") is None\n            cfgphoto.queue = queue\n            stream = request.form[\"FOTO_stream\"]\n            sensor_mode = request.form[\"FOTO_sensor_mode\"]\n            format = request.form[\"FOTO_format\"]\n            cfgphoto.format = format\n            if sensor_mode == \"custom\":\n                size_width = int(request.form[\"FOTO_stream_size_width\"])\n                if not (size_width % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                size_height = int(request.form[\"FOTO_stream_size_height\"])\n                if not (size_height % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                if stream == \"lores\":\n                    if cfglive.stream == \"main\":\n                        if (\n                            size_width > cfglive.stream_size[0]\n                            or size_height > cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Live View)\"\n                    if not err and cfgvideo.stream == \"main\":\n                        if (\n                            size_width > cfgvideo.stream_size[0]\n                            or size_height > cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Video)\"\n                if stream == \"main\":\n                    if cfglive.stream == \"lores\":\n                        if (\n                            size_width < cfglive.stream_size[0]\n                            or size_height < cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Live View) must not exceed main Stream Size\"\n                    if not err and cfgvideo.stream == \"lores\":\n                        if (\n                            size_width < cfgvideo.stream_size[0]\n                            or size_height < cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Video) must not exceed main Stream Size\"\n                if not err:\n                    cfgphoto.stream = stream\n                    cfgphoto.sensor_mode = sensor_mode\n                    cfgphoto.stream_size = (size_width, size_height)\n                    cfgphoto.stream_size_align = (\n                        not request.form.get(\"FOTO_stream_size_align\") is None\n                    )\n            else:\n                mode = sm[int(sensor_mode)]\n                if stream == \"lores\":\n                    if cfglive.stream == \"main\":\n                        if (\n                            mode.size[0] > cfglive.stream_size[0]\n                            or mode.size[1] > cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Live View)\"\n                    if not err and cfgvideo.stream == \"main\":\n                        if (\n                            mode.size[0] > cfgvideo.stream_size[0]\n                            or mode.size[1] > cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Video)\"\n                if stream == \"main\":\n                    if cfglive.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfglive.stream_size[0]\n                            or mode.size[1] < cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Live View) must not exceed main Stream Size\"\n                    if not err and cfgvideo.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfgvideo.stream_size[0]\n                            or mode.size[1] < cfgvideo.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Video) must not exceed main Stream Size\"\n                if sc.activeCameraIsUsb == True:\n                    format = mode.format\n                    cfgphoto.format = format\n                if not err:\n                    cfgphoto.stream = stream\n                    cfgphoto.sensor_mode = sensor_mode\n                    cfgphoto.stream_size = mode.size\n                    cfgphoto.stream_size_align = (\n                        not request.form.get(\"FOTO_stream_size_align\") is None\n                    )\n            cfgphoto.display = None\n            if sc.activeCameraIsUsb == False:\n                cfgphoto.encode = \"main\"\n            else:\n                cfgphoto.encode = None\n            cc, cr = Camera().ctrl.requestConfig(cfgphoto, test=True)\n            if cc:\n                msg = (\n                    \"This modification will cause the live stream to be interrupted when a photo is taken!\\nReason: \"\n                    + cr\n                )\n                flash(msg)\n            if sc.syncAspectRatio == True:\n                doSyncAspectRatio(\n                    cfgphoto.stream_size, [\"Live View\", \"Raw Photo\", \"Video\"]\n                )\n            Camera.resetScalerCropRequested = True\n            doSyncTransform(\n                transform_hflip, transform_vflip, [\"Live View\", \"Raw Photo\", \"Video\"]\n            )\n            Camera().restartLiveStream()\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Configuration for Photo changed\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/addPhotoControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef addPhotoControls():\n    logger.debug(\"In addPhotoControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgphoto\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            for key, value in cc.dict().items():\n                if value[0] == True:\n                    if key not in cfg.photoConfig.controls:\n                        cfg.photoConfig.controls[key] = value[1]\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Controls added to Configuration for Photo\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/remPhotoControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remPhotoControls():\n    logger.debug(\"In remPhotoControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgphoto\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            cnt = 0\n            for ctrl in cfg.photoConfig.controls:\n                if request.form.get(\"sel_FOTO_\" + ctrl) is not None:\n                    cnt += 1\n            if cnt > 0:\n                if cnt < len(cfg.photoConfig.controls):\n                    while cnt > 0:\n                        for ctrl in cfg.photoConfig.controls:\n                            if request.form.get(\"sel_FOTO_\" + ctrl) is not None:\n                                ctrlDel = ctrl\n                                break\n                        del cfg.photoConfig.controls[ctrlDel]\n                        cnt -= 1\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Controls removed from Configuration for Photo\"\n                    )\n                    cfg.streamingCfgInvalid = True\n                else:\n                    msg = \"At least one control must remain in the configuration\"\n                    flash(msg)\n            else:\n                msg = \"No controls were selected\"\n                flash(msg)\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/rawCfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef rawCfg():\n    logger.debug(\"In rawCfg\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgraw\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg.rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            transform_hflip = not request.form.get(\"PRAW_transform_hflip\") is None\n            cfgraw.transform_hflip = transform_hflip\n            transform_vflip = not request.form.get(\"PRAW_transform_vflip\") is None\n            cfgraw.transform_vflip = transform_vflip\n            colour_space = request.form[\"PRAW_colour_space\"]\n            cfgraw.colour_space = colour_space\n            queue = not request.form.get(\"PRAW_queue\") is None\n            cfgraw.queue = queue\n            format = request.form[\"PRAW_format\"]\n            cfgraw.format = format\n            sensor_mode = request.form[\"PRAW_sensor_mode\"]\n            if sensor_mode == \"custom\":\n                size_width = int(request.form[\"PRAW_stream_size_width\"])\n                if not (size_width % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                size_height = int(request.form[\"PRAW_stream_size_height\"])\n                if not (size_height % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                if not err:\n                    cfgraw.sensor_mode = sensor_mode\n                    cfgraw.stream_size = (size_width, size_height)\n                    cfgraw.stream_size_align = (\n                        not request.form.get(\"PRAW_stream_size_align\") is None\n                    )\n            else:\n                mode = sm[int(sensor_mode)]\n                if not err:\n                    cfgraw.sensor_mode = sensor_mode\n                    cfgraw.stream_size = mode.size\n                    cfgraw.stream_size_align = (\n                        not request.form.get(\"PRAW_stream_size_align\") is None\n                    )\n            if sc.activeCameraIsUsb == True:\n                cfgraw.format = \"tiff\"\n            cfgraw.sensor_mode = sensor_mode\n            cfgraw.display = None\n            cfgraw.encode = None\n            if sc.syncAspectRatio == True:\n                doSyncAspectRatio(cfgraw.stream_size, [\"Live View\", \"Photo\", \"Video\"])\n            Camera.resetScalerCropRequested = True\n            doSyncTransform(\n                transform_hflip, transform_vflip, [\"Live View\", \"Photo\", \"Video\"]\n            )\n            Camera().restartLiveStream()\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Configuration for Raw Photo changed\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/addRawControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef addRawControls():\n    logger.debug(\"In addRawControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgraw\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            for key, value in cc.dict().items():\n                if value[0] == True:\n                    if key not in cfg.rawConfig.controls:\n                        cfg.rawConfig.controls[key] = value[1]\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Controls added to Configuration for Raw Photo\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/remRawControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remRawControls():\n    logger.debug(\"In remRawControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgraw\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            cnt = 0\n            for ctrl in cfg.rawConfig.controls:\n                if request.form.get(\"sel_PRAW_\" + ctrl) is not None:\n                    cnt += 1\n            if cnt > 0:\n                if cnt < len(cfg.rawConfig.controls):\n                    while cnt > 0:\n                        for ctrl in cfg.rawConfig.controls:\n                            if request.form.get(\"sel_PRAW_\" + ctrl) is not None:\n                                ctrlDel = ctrl\n                                break\n                        del cfg.rawConfig.controls[ctrlDel]\n                        cnt -= 1\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Controls removed from Configuration for Raw Photo\"\n                    )\n                    cfg.streamingCfgInvalid = True\n                else:\n                    msg = \"At least one control must remain in the configuration\"\n                    flash(msg)\n            else:\n                msg = \"No controls were selected\"\n                flash(msg)\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/videoCfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef videoCfg():\n    logger.debug(\"In videoCfg\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgvideo\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            transform_hflip = not request.form.get(\"VIDO_transform_hflip\") is None\n            cfgvideo.transform_hflip = transform_hflip\n            transform_vflip = not request.form.get(\"VIDO_transform_vflip\") is None\n            cfgvideo.transform_vflip = transform_vflip\n            colour_space = request.form[\"VIDO_colour_space\"]\n            cfgvideo.colour_space = colour_space\n            buffer_count = int(request.form[\"VIDO_buffer_count\"])\n            cfgvideo.buffer_count = buffer_count\n            queue = not request.form.get(\"VIDO_queue\") is None\n            cfgvideo.queue = queue\n            stream = request.form[\"VIDO_stream\"]\n            sensor_mode = request.form[\"VIDO_sensor_mode\"]\n            format = request.form[\"VIDO_format\"]\n            cfgvideo.format = format\n            if sensor_mode == \"custom\":\n                size_width = int(request.form[\"VIDO_stream_size_width\"])\n                if not (size_width % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                size_height = int(request.form[\"VIDO_stream_size_height\"])\n                if not (size_height % 2) == 0:\n                    err = \"Stream Size (width, height) must be even\"\n                if stream == \"lores\":\n                    if cfglive.stream == \"main\":\n                        if (\n                            size_width > cfglive.stream_size[0]\n                            or size_height > cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Live View)\"\n                    if not err and cfgphoto.stream == \"main\":\n                        if (\n                            size_width > cfgphoto.stream_size[0]\n                            or size_height > cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Photo)\"\n                if stream == \"main\":\n                    if cfglive.stream == \"lores\":\n                        if (\n                            size_width < cfglive.stream_size[0]\n                            or size_height < cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Live View) must not exceed main Stream Size\"\n                    if not err and cfgphoto.stream == \"lores\":\n                        if (\n                            size_width < cfgphoto.stream_size[0]\n                            or size_height < cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Photo) must not exceed main Stream Size\"\n                if not err:\n                    cfgvideo.stream = stream\n                    cfgvideo.sensor_mode = sensor_mode\n                    cfgvideo.stream_size = (size_width, size_height)\n                    cfgvideo.stream_size_align = (\n                        not request.form.get(\"VIDO_stream_size_align\") is None\n                    )\n            else:\n                mode = sm[int(sensor_mode)]\n                if stream == \"lores\":\n                    if cfglive.stream == \"main\":\n                        if (\n                            mode.size[0] > cfglive.stream_size[0]\n                            or mode.size[1] > cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Live View)\"\n                    if not err and cfgphoto.stream == \"main\":\n                        if (\n                            mode.size[0] > cfgphoto.stream_size[0]\n                            or mode.size[1] > cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size must not exceed main Stream Size (Photo)\"\n                if stream == \"main\":\n                    if cfglive.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfglive.stream_size[0]\n                            or mode.size[1] < cfglive.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Live View) must not exceed main Stream Size\"\n                    if not err and cfgphoto.stream == \"lores\":\n                        if (\n                            mode.size[0] < cfgphoto.stream_size[0]\n                            or mode.size[1] < cfgphoto.stream_size[1]\n                        ):\n                            err = \"lores Stream Size (Photo) must not exceed main Stream Size\"\n                if sc.activeCameraIsUsb == True:\n                    format = mode.format\n                    cfgvideo.format = format\n                if not err:\n                    cfgvideo.stream = stream\n                    cfgvideo.sensor_mode = sensor_mode\n                    cfgvideo.stream_size = mode.size\n                    cfgvideo.stream_size_align = (\n                        not request.form.get(\"VIDO_stream_size_align\") is None\n                    )\n            cfgvideo.display = None\n            if sc.activeCameraIsUsb == False:\n                cfgvideo.encode = \"main\"\n            else:\n                cfgvideo.encode = None\n            if sc.syncAspectRatio == True:\n                doSyncAspectRatio(\n                    cfgvideo.stream_size, [\"Live View\", \"Photo\", \"Raw Photo\"]\n                )\n            Camera.resetScalerCropRequested = True\n            doSyncTransform(\n                transform_hflip, transform_vflip, [\"Live View\", \"Photo\", \"Raw Photo\"]\n            )\n            Camera().restartLiveStream()\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Configuration for Video changed\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/addVideoControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef addVideoControls():\n    logger.debug(\"In addVideoControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgvideo\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            for key, value in cc.dict().items():\n                if value[0] == True:\n                    if key not in cfg.videoConfig.controls:\n                        cfg.videoConfig.controls[key] = value[1]\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Controls added to Configuration for Video\")\n            cfg.streamingCfgInvalid = True\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/remVideoControls\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remVideoControls():\n    logger.debug(\"In remVideoControls\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgvideo\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not err:\n            cnt = 0\n            for ctrl in cfg.videoConfig.controls:\n                if request.form.get(\"sel_VIDO_\" + ctrl) is not None:\n                    cnt += 1\n            if cnt > 0:\n                if cnt < len(cfg.videoConfig.controls):\n                    while cnt > 0:\n                        for ctrl in cfg.videoConfig.controls:\n                            if request.form.get(\"sel_VIDO_\" + ctrl) is not None:\n                                ctrlDel = ctrl\n                                break\n                        del cfg.videoConfig.controls[ctrlDel]\n                        cnt -= 1\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Controls removed from Configuration for Video\"\n                    )\n                    cfg.streamingCfgInvalid = True\n                else:\n                    msg = \"At least one control must remain in the configuration\"\n                    flash(msg)\n            else:\n                msg = \"No controls were selected\"\n                flash(msg)\n        if err:\n            flash(err)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/getAiModelFiles\", methods=(\"GET\", \"POST\"))\n@login_required\ndef getAiModelFiles():\n    logger.debug(\"In getAiModelFiles\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgai\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        msg = \"\"\n        # Try to import IMX500\n        try:\n            from picamera2.devices import IMX500\n            logger.debug(\"In getAiModelFiles - imported IMX500 successfully\")\n        except ImportError:\n            msg = \"The class IMX500 could not be imported.\"\n            msg += \"\\n Maybe, the IMX500 firmware is not installed.\"\n            msg += \"\\n Try installing with 'sudo apt install imx500-all'.\"\n\n        if msg == \"\":\n            modelFolder = request.form.get(\"modelfolder\")\n            if modelFolder.strip() == \"\":\n                modelfolder = ai.modelFolderDef\n            if os.path.isdir(modelFolder) == False:\n                msg = \"The specified AI Model Folder does not exist\"\n                msg += \"\\n Maybe, the IMX500 firmware is not installed.\"\n                msg += \"\\n Try installing with 'sudo apt install imx500-all'.\"\n        if sc.isLiveStream == True \\\n        or sc.isPhotoSeriesRecording == True \\\n        or sc.isTriggerRecording == True \\\n        or sc.isVideoRecording == True:\n            msg = \"This setting cannot be changed while the AI camera is active. Please wait and repeat the action when the camera has stopped.\"\n        if msg == \"\":\n            ai.modelFolder = modelFolder\n            task = request.form.get(\"aitask\").lower()\n            ai.task = task\n            logger.debug(\"In getAiModelFiles - searching %s for model files having task '%s'\", ai.modelFolder, ai.task)\n            modelFiles = os.listdir(modelFolder)\n            modelFiles.sort(reverse=False)\n            ai.modelFiles = []\n            for mf in modelFiles:\n                if mf.endswith(\".rpk\"):\n                    mfp = os.path.join(modelFolder, mf)\n                    imx500 = IMX500(mfp)\n                    intrinsics = imx500.network_intrinsics\n                    intrTask = \"\"\n                    if intrinsics:\n                        intrTask = intrinsics.task\n                        if intrTask:\n                            intrTask = intrTask.lower()\n                    if intrTask == ai.task:\n                        logger.debug(\"In getAiModelFiles - found model file: %s\", mf)\n                        ai.modelFiles.append(mf)\n                    else:\n                        logger.debug(\"In getAiModelFiles - skipping model file: %s having task '%s'\", mf, intrTask)\n                        continue\n            imx500 = None\n            ai.modelFile = \"\"\n            ai.modelIntrinsics = {}\n            if len(ai.modelFiles) == 0:\n                msg = \"No AI model files (*.rpk) for the given task were found in the specified folder\"\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/setAiModelFile\", methods=(\"GET\", \"POST\"))\n@login_required\ndef setAiModelFile():\n    logger.debug(\"In setAiModelFile\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgai\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        msg = \"\"\n        # Try to import IMX500\n        try:\n            from picamera2.devices import IMX500\n            logger.debug(\"In setAiModelFile - imported IMX500 successfully\")\n        except ImportError:\n            msg = \"The class IMX500 could not be imported.\"\n            msg += \"\\n Maybe, the IMX500 firmware is not installed.\"\n            msg += \"\\n Try installing with 'sudo apt install imx500-all'.\"\n        if sc.isLiveStream == True \\\n        or sc.isPhotoSeriesRecording == True \\\n        or sc.isTriggerRecording == True \\\n        or sc.isVideoRecording == True:\n            msg = \"This setting cannot be changed while the AI camera is active. Please wait and repeat the action when the camera has stopped.\"\n        if msg == \"\":\n            modelFolder = ai.modelFolder\n            task = ai.task\n            mf = request.form.get(\"aimodelfile\")\n            if mf.endswith(\".rpk\"):\n                mfp = os.path.join(modelFolder, mf)\n                imx500 = IMX500(mfp)\n                intrinsics = imx500.network_intrinsics\n                intrTask = \"\"\n                if intrinsics:\n                    intrTask = intrinsics.task\n                    if intrTask:\n                        intrTask = intrTask.lower()\n                if intrTask != task:\n                    msg = \"The selected AI model file does not match the given Task\"\n                else:\n                    ai.modelFile = mf\n                    logger.debug(\"In setAiModelFile - selected model file: %s\", mf)\n                    \n                    modelIntrinsics = intrinsics.__dict__.copy()\n                    if \"_NetworkIntrinsics__intrinsics\" in modelIntrinsics:\n                        modelIntrinsics = modelIntrinsics[\"_NetworkIntrinsics__intrinsics\"]\n                        if \"classes\" in modelIntrinsics:\n                            modelIntrinsics.pop(\"classes\")\n                        if \"task\" in modelIntrinsics:\n                            modelIntrinsics.pop(\"task\")\n                    else:\n                        modelIntrinsics = {}\n                    ai.modelIntrinsics = modelIntrinsics\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(f\"AI model file changed for camera {sc.activeCameraInfo} to {mf}\")\n                    cfg.streamingCfgInvalid = True\n            else:\n                msg = \"The selected AI model file is not a valid .rpk file\"\n            imx500 = None\n            if len(ai.modelFiles) == 0:\n                msg = \"No AI model files (*.rpk) for the given task were found in the specified folder\"\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/enableAi\", methods=(\"GET\", \"POST\"))\n@login_required\ndef enableAi():\n    logger.debug(\"In enableAi\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgai\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        msg = \"\"\n        restart = False\n        if sc.isTriggerRecording:\n            msg = \"Please go to 'Trigger' and stop the active process before enabling AI processing\"\n        if sc.isPhotoSeriesRecording:\n            msg = \"Please go to 'Photo Series' and stop the active process before enabling AI processing\"\n        if sc.isVideoRecording == True:\n            msg = \"Please stop video recording before enabling AI processing\"\n        if msg == \"\":\n            enableAi = not request.form.get(\"enableai\") is None\n            if ai.enable == True:\n                if ai.drawOnLores == True:\n                    if (cfglive.stream != \"lores\") \\\n                    and (cfgphoto.stream != \"lores\"):\n                        msg = \"AI drawing on lores stream is enabled, but no configuration is set to use the lores stream.\"\n                if ai.drawOnMain == True:\n                    if (cfglive.stream != \"main\") \\\n                    and (cfgphoto.stream != \"main\"):\n                        msg = \"AI drawing on main stream is enabled, but no configuration is set to use the main stream.\"\n        if msg == \"\":\n            if ai.enable != enableAi:\n                restart = True\n                Camera().liveViewDeactivated = True\n                Camera().stopLiveStream()\n                ai.enable = enableAi\n                logger.debug(\"In enableAi - set enable AI to %s\", ai.enable)\n                sc.unsavedChanges = True\n                if ai.enable == False:\n                    sc.addChangeLogEntry(f\"AI disabled for camera {sc.activeCameraInfo}\")\n                else:\n                    sc.addChangeLogEntry(f\"AI enabled for camera {sc.activeCameraInfo}\")\n                cfg.streamingCfgInvalid = True\n            else:\n                logger.debug(\"In enableAi - left enable AI at %s\", ai.enable)\n        if restart:\n            Camera.resetAiCache()\n            Camera().liveViewDeactivated = False\n            Camera().startLiveStream()\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n\n\n@bp.route(\"/ai_settings\", methods=(\"GET\", \"POST\"))\n@login_required\ndef ai_settings():\n    logger.debug(\"In ai_settings\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    rf = cfg.rawFormats\n    sc = cfg.serverConfig\n    tc = cfg.tuningConfig\n    ai = cfg.aiConfig\n    sc.lastConfigTab = \"cfgai\"\n    cfgs = cfg.cameraConfigs\n    cfglive = cfg.liveViewConfig\n    cfgphoto = cfg.photoConfig\n    cfgraw = cfg._rawConfig\n    cfgvideo = cfg.videoConfig\n    cfgrf = cfg.rawFormats\n    if tc.tuningFile == \"\":\n        fn = sc.activeCameraModel + \".json\"\n        if isTuningFile(fn, tc.tuningFolder) == True:\n            tc.tuningFile = fn\n    tfl = getTuningFiles(tc.tuningFolder, tc.tuningFile)\n    if request.method == \"POST\":\n        msg = \"\"\n        restart = False\n        if msg == \"\":\n            if ai.task == \"classification\":\n                ai.topK = int(request.form[\"topk\"])\n            if ai.task == \"object detection\" \\\n            or ai.task == \"pose estimation\":\n                ai.detectionThreshold = float(request.form[\"detectionthreshold\"])\n            if ai.task == \"object detection\":\n                ai.iouThreshold = float(request.form[\"iouthreshold\"])\n                ai.maxDetections = int(request.form[\"maxdetections\"])\n            ai.drawOnLores = not request.form.get(\"drawonlores\") is None\n            ai.drawOnMain = not request.form.get(\"drawonmain\") is None\n            if ai.drawOnLores == True:\n                if (cfglive.stream != \"lores\") \\\n                and (cfgphoto.stream != \"lores\"):\n                    msg = \"AI drawing on lores stream is enabled, but no configuration is set to use the lores stream.\"\n            if ai.drawOnMain == True:\n                if (cfglive.stream != \"main\") \\\n                and (cfgphoto.stream != \"main\"):\n                    msg = \"AI drawing on main stream is enabled, but no configuration is set to use the main stream.\"\n        if msg == \"\":\n            if ai.enable == True:\n                restart = True\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"AI setting 'Draw on lores stream' set to {ai.drawOnLores} for camera {sc.activeCameraInfo}\")\n            cfg.streamingCfgInvalid = True\n        if restart:\n            Camera().restartLiveStream()\n        if len(msg) > 0:\n            flash(msg)\n    return render_template(\n        \"config/main.html\",\n        sc=sc,\n        tc=tc,\n        ai=ai,\n        cp=cp,\n        sm=sm,\n        rf=rf,\n        cfglive=cfglive,\n        cfgphoto=cfgphoto,\n        cfgraw=cfgraw,\n        cfgvideo=cfgvideo,\n        cfgrf=cfgrf,\n        cfgs=cfgs,\n        tfl=tfl,\n    )\n"
  },
  {
    "path": "raspiCamSrv/console.py",
    "content": "from flask import Blueprint, Response, flash, g, redirect, render_template, request, url_for\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg\nfrom raspiCamSrv.version import version\nimport subprocess\nfrom subprocess import CalledProcessError\nfrom raspiCamSrv.triggerHandler import TriggerHandler\n\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\nbp = Blueprint(\"console\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/console\")\n@login_required\ndef console():\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    if sc.vButtonHasCommandLine == True:\n        if sc.vButtonCommand is None:\n            sc.vButtonCommand = \"\"\n    sc.curMenu = \"console\"\n    return render_template(\"console/console.html\", sc=sc)\n\n@bp.route(\"/execute/<row>/<col>\", methods=(\"GET\", \"POST\"))\n@login_required\ndef execute(row:None, col=None):\n    logger.debug(\"In execute - row=%s, col=%s\", row, col)\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    sc.curMenu = \"console\"\n    sc.vButtonCommand = None\n    sc.vButtonArgs = None\n    sc.vButtonReturncode = None\n    sc.vButtonStderr = None\n    sc.vButtonStdout = None\n    sc.lastConsoleTab = \"versbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        r = int(row)\n        c = int(col)\n        btn = sc.vButtons[r][c]\n        cmd = btn.buttonExec\n        sc.vButtonCommand = cmd\n        args = cmd.rsplit(\" \")\n        sc.vButtonArgs = args\n        \n        msg = \"Command successfully executed.\"\n        result = None\n        if cmd != \"\":\n            try:\n                result = subprocess.run(args, capture_output=True, text=True, check=False)            \n            except CalledProcessError as e:\n                msg = f\"Command executed with error: {e}.\"\n            except Exception as e:\n                msg = f\"Command executed with error: {e}.\"\n            if result:\n                sc.vButtonReturncode = result.returncode\n                sc.vButtonStdout = result.stdout\n                sc.vButtonStderr = result.stderr\n        else:\n            msg = \"No command executed\"\n        \n        if msg != \"\":\n            flash(msg)\n    return render_template(\"console/console.html\", sc=sc)\n\n@bp.route(\"/execCommandline\", methods=(\"GET\", \"POST\"))\n@login_required\ndef execCommandline():\n    logger.debug(\"In execCommandline\")\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    sc.curMenu = \"console\"\n    sc.vButtonCommand = None\n    sc.vButtonArgs = None\n    sc.vButtonReturncode = None\n    sc.vButtonStderr = None\n    sc.vButtonStdout = None\n    sc.lastConsoleTab = \"versbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        cmd = request.form[\"commandline\"]\n        sc.vButtonCommand = cmd\n        args = cmd.rsplit(\" \")\n        sc.vButtonArgs = args\n        \n        msg = \"Command successfully executed.\"\n        result = None\n        if cmd != \"\":\n            try:\n                result = subprocess.run(args, capture_output=True, text=True, check=False)            \n            except CalledProcessError as e:\n                msg = f\"Command executed with error: {e}.\"\n            except Exception as e:\n                msg = f\"Command executed with error: {e}.\"\n            if result:\n                sc.vButtonReturncode = result.returncode\n                sc.vButtonStdout = result.stdout\n                sc.vButtonStderr = result.stderr\n        else:\n            msg = \"No command executed\"\n        \n        if msg != \"\":\n            flash(msg)\n    return render_template(\"console/console.html\", sc=sc)\n\n@bp.route(\"/do_action/<row>/<col>\", methods=(\"GET\", \"POST\"))\n@login_required\ndef do_action(row:None, col=None):\n    logger.debug(\"In do_action - row=%s, col=%s\", row, col)\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    sc.curMenu = \"console\"\n    sc.vButtonCommand = None\n    sc.vButtonArgs = None\n    sc.vButtonReturncode = None\n    sc.vButtonStderr = None\n    sc.vButtonStdout = None\n    sc.lastConsoleTab = \"actionbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n#        if sc.isEventhandling == False:\n#            msg = \"Event handling is not active. Activate 'Configured Triggers' in Trigger/Control and press Start.\"\n        if msg == \"\":\n            r = int(row)\n            c = int(col)\n            btn = sc.aButtons[r][c]\n            action = btn.buttonAction\n            \n            msg = \"Action successfully executed.\"\n            result = None\n            if action != \"\":\n                msg = TriggerHandler.doAction(action)\n            else:\n                msg = \"No Action executed\"\n        \n        if msg != \"\":\n            flash(msg)\n    return render_template(\"console/console.html\", sc=sc)\n"
  },
  {
    "path": "raspiCamSrv/db.py",
    "content": "import sqlite3\n\nimport click\nfrom flask import current_app, g\nfrom raspiCamSrv.camCfg import CameraCfg\nimport logging\n\nlogger = logging.getLogger(__name__)\n\ndef get_db():\n    if \"db\" not in g:\n        g.db = sqlite3.connect(\n            current_app.config[\"DATABASE\"], detect_types=sqlite3.PARSE_DECLTYPES\n        )\n        g.db.row_factory = sqlite3.Row\n\n    return g.db\n\n\ndef close_db(e=None):\n    db = g.pop(\"db\", None)\n\n    if db is not None:\n        db.close()\n\n\ndef init_db():\n    db = get_db()\n\n    with current_app.open_resource(\"schema.sql\") as f:\n        db.executescript(f.read().decode(\"utf8\"))\n\n\n@click.command(\"init-db\")\ndef init_db_command():\n    \"\"\"Clear the existing data and create new tables.\"\"\"\n    init_db()\n    click.echo(\"Initialized the database.\")\n\n\ndef init_app(app):\n    app.teardown_appcontext(close_db)\n    app.cli.add_command(init_db_command)\n"
  },
  {
    "path": "raspiCamSrv/dbx.py",
    "content": "import sqlite3\n\nimport raspiCamSrv.camCfg as camCfg\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_dbx() -> sqlite3.Connection:\n    \"\"\" Get database outside of application context\n    \"\"\"\n    database = camCfg.CameraCfg().serverConfig.database\n    logger.debug(\"get_dbx - database: %s\", database)\n    db = sqlite3.connect(database, detect_types=sqlite3.PARSE_DECLTYPES)\n    db.row_factory = sqlite3.Row\n    return db\n"
  },
  {
    "path": "raspiCamSrv/gpioDeviceTypes.py",
    "content": "gpioDeviceTypes = [\n    {\n        \"type\": \"Button\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#button\",\n        \"image\": \"device_button.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"pull_up\": {\"value\": True, \"type\": \"boolOrNone\"},\n            \"active_state\": {\"value\": None, \"type\": \"boolOrNone\"},\n            \"bounce_time\": {\n                \"value\": None,\n                \"type\": \"floatOrNone\",\n                \"min\": 0.0,\n                \"max\": 10.0,\n            },\n            \"hold_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0, \"max\": 10.0},\n            \"hold_repeat\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"is_pressed\", \"value\"],\n        \"events\": [\"when_pressed\", \"when_released\"],\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"RotaryEncoder\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#rotaryencoder\",\n        \"image\": \"device_RotaryEncoder.jpg\",\n        \"params\": {\n            \"a\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"b\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"bounce_time\": {\n                \"value\": None,\n                \"type\": \"floatOrNone\",\n                \"min\": 0.0,\n                \"max\": 10.0,\n            },\n            \"max_steps\": {\"value\": 16, \"type\": \"int\", \"min\": 0, \"max\": 100},\n            \"threshold_steps\": {\"value\": (0, 0), \"type\": \"tuple(int)\"},\n            \"wrap\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"steps\", \"value\"],\n        \"testStepDuration\": 3,\n        \"events\": [\n            \"when_rotated\",\n            \"when_rotated_clockwise\",\n            \"when_rotated_counter_clockwise\",\n        ],\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"LightSensor\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#lightsensor-ldr\",\n        \"image\": \"device_LightSensor.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"queue_len\": {\"value\": 5, \"type\": \"int\", \"min\": 0, \"max\": 100},\n            \"charge_time_limit\": {\n                \"value\": 0.01,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 100.0,\n            },\n            \"threshold\": {\"value\": 0.1, \"type\": \"float\", \"min\": 0.0, \"max\": 100.0},\n            \"partial\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"light_detected\", \"value\"],\n        \"events\": [\"when_dark\", \"when_light\"],\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"MotionSensor\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#motionsensor-d-sun-pir\",\n        \"image\": \"device_motionSensor.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"pull_up\": {\"value\": True, \"type\": \"boolOrNone\"},\n            \"active_state\": {\"value\": None, \"type\": \"boolOrNone\"},\n            \"queue_len\": {\"value\": 1, \"type\": \"int\", \"min\": 0, \"max\": 100},\n            \"sample_rate\": {\"value\": 10.0, \"type\": \"float\", \"min\": 0.0, \"max\": 1000.0},\n            \"threshold\": {\"value\": 0.5, \"type\": \"float\", \"min\": 0.0, \"max\": 1000.0},\n            \"partial\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"motion_detected\", \"value\"],\n        \"events\": [\"when_motion\", \"when_no_motion\"],\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"LineSensor\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#linesensor-trct5000\",\n        \"image\": \"device_LineSensor.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"pull_up\": {\"value\": False, \"type\": \"boolOrNone\"},\n            \"active_state\": {\"value\": None, \"type\": \"boolOrNone\"},\n            \"queue_len\": {\"value\": 5, \"type\": \"int\", \"min\": 0, \"max\": 100},\n            \"sample_rate\": {\"value\": 100.0, \"type\": \"float\", \"min\": 0.0, \"max\": 1000.0},\n            \"threshold\": {\"value\": 0.5, \"type\": \"float\", \"min\": 0.0, \"max\": 100.0},\n            \"partial\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"value\"],\n        \"events\": [\"when_line\", \"when_no_line\"],\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"DistanceSensor\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#distancesensor-hc-sr04\",\n        \"image\": \"device_DistanceSensor.jpg\",\n        \"params\": {\n            \"echo\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"trigger\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"queue_len\": {\"value\": 9, \"type\": \"int\", \"min\": 0, \"max\": 99},\n            \"max_distance\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0, \"max\": 100.0},\n            \"threshold_distance\": {\n                \"value\": 0.3,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 100.0,\n            },\n            \"partial\": {\"value\": False, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\"distance\", \"value\"],\n        \"events\": [\"when_in_range\", \"when_out_of_range\"],\n        \"eventSettings\": {\"threshold_distance\": 0.0},\n        \"control\": {\"bounce_time\": 0.0, \"event_log\": False},\n    },\n    {\n        \"type\": \"DigitalInputDevice\",\n        \"usage\": \"Input\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_input.html#digitalinputdevice\",\n        \"image\": \"device_DigitalInputDevice.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"pull_up\": {\"value\": False, \"type\": \"boolOrNone\"},\n            \"active_state\": {\"value\": None, \"type\": \"boolOrNone\"},\n            \"bounce_time\": {\n                \"value\": None,\n                \"type\": \"floatOrNone\",\n                \"min\": 0.0,\n                \"max\": 10.0,\n            },\n        },\n        \"testMethods\": [\"value\", \"active_time\"],\n        \"events\": [\"when_activated\", \"when_deactivated\"],\n        \"eventSettings\": {},\n        \"control\": {},\n    },\n    {\n        \"type\": \"LED\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#led\",\n        \"image\": \"device_LED.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": False, \"type\": \"boolOrNone\"},\n        },\n        \"testMethods\": [\"on\", \"value\"],\n        \"testDuration\": 2,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"toggle\", \"params\": {}, \"control\": {}},\n            {\n                \"method\": \"blink\",\n                \"params\": {\n                    \"on_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"off_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": {\"value\": True, \"type\": \"bool\"}\n                },\n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"value\",\n                \"params\": {\n                    \"value\": {\"value\": 1, \"type\": \"int\", \"min\": 0, \"max\": 1}\n                },\n                \"control\": {}\n            },\n        ],\n    },\n    {\n        \"type\": \"PWMLED\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#pwmled\",\n        \"image\": \"device_LED.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": 0.0, \"type\": \"float\", \"min\": 0.0, \"max\": 1.0},\n            \"frequency\": {\"value\": \"100\", \"type\": \"int\", \"min\": 0, \"max\": 1000},\n        },\n        \"testMethods\": [\"on\", \"off\", \"pulse\", \"value\", \"off\"],\n        \"testStepDuration\": 2,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"toggle\", \"params\": {}, \"control\": {}},\n            {\n                \"method\": \"blink\",\n                \"params\": {\n                    \"on_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"off_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"fade_in_time\": {\"value\": 0.0, \"type\": \"float\", \"min\": 0.0},\n                    \"fade_out_time\": {\"value\": 0.0, \"type\": \"float\", \"min\": 0.0},\n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": {\"value\": True, \"type\": \"bool\"}\n                },\n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"pulse\", \n                \"params\": {\n                    \"fade_in_time\": 1.0, \n                    \"fade_out_time\": 1.0,\n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": True\n                }, \n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"value\",\n                \"params\": {\n                    \"value\": {\n                        \"value\": 1.0,\n                        \"type\": \"float\",\n                        \"min\": 0.0,\n                        \"max\": 1.0,\n                    }\n                },\n                \"control\": {}\n            },\n        ],\n    },\n    {\n        \"type\": \"RGBLED\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#rgbled\",\n        \"image\": \"device_RGBLED.jpg\",\n        \"params\": {\n            \"red\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"green\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"blue\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": (0.0, 0.0, 0.0), \"type\": \"tuple(float)\"},\n            \"pwm\": {\"value\": True, \"type\": \"bool\"},\n        },\n        \"testMethods\": [\n            \"on\",\n            {\"color\": (1, 0, 0)},\n            {\"color\": (0, 1, 0)},\n            {\"color\": (0, 0, 1)},\n            \"is_lit\",\n            \"value\",\n        ],\n        \"testStepDuration\": 1,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"toggle\", \"params\": {}, \"control\": {}},\n            {\n                \"method\": \"blink\",\n                \"params\": {\n                    \"on_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"off_time\": {\"value\": 1.0, \"type\": \"float\", \"min\": 0.0},\n                    \"fade_in_time\": {\"value\": 0.0, \"type\": \"float\", \"min\": 0.0},\n                    \"fade_out_time\": {\"value\": 0.0, \"type\": \"float\", \"min\": 0.0},\n                    \"on_color\": (1.0, 1.0, 1.0),\n                    \"off_color\": (0.0, 0.0, 0.0),\n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": {\"value\": True, \"type\": \"bool\"}\n                },\n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"pulse\", \n                \"params\": {\n                    \"fade_in_time\": 1.0, \n                    \"fade_out_time\": 1.0,\n                    \"on_color\": (1.0, 1.0, 1.0),\n                    \"off_color\": (0.0, 0.0, 0.0),\n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": True\n                }, \n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"value\",\n                \"params\": {\"value\": (0.0, 0.0, 0.0)},\n                \"control\": {\"duration\": 0.0},\n            },\n        ],\n    },\n    {\n        \"type\": \"Buzzer\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#buzzer\",\n        \"image\": \"device_Buzzer.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": False, \"type\": \"boolOrNone\"},\n        },\n        \"testMethods\": [\"on\", \"value\"],\n        \"testDuration\": 2,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"toggle\", \"params\": {}, \"control\": {}},\n            {\n                \"method\": \"beep\",\n                \"params\": {\n                    \"on_time\": 1.0, \n                    \"off_time\": 1.0, \n                    \"n\": {\"value\": None, \"type\": \"intOrNone\", \"min\": 1},\n                    \"background\": True\n                },\n                \"control\": {\"duration\": 0.0}\n            },\n            {\n                \"method\": \"value\",\n                \"params\": {\"value\": 1, \"type\": \"int\", \"min\": 0, \"max\": 1},\n                \"control\": {\"duration\": 0.0}\n            },\n        ],\n    },\n    {\n        \"type\": \"TonalBuzzer\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#tonalbuzzer\",\n        \"image\": \"device_TonalBuzzer.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"initial_value\": {\n                \"value\": None,\n                \"type\": \"floatOrNone\",\n                \"min\": -1.0,\n                \"max\": 1.0,\n            },\n            \"mid_tone\": {\"value\": 69, \"type\": \"int\", \"min\": 0, \"max\": 127},\n            \"octaves\": {\"value\": 1, \"type\": \"int\", \"min\": 0, \"max\": 127},\n        },\n        \"testMethods\": [{\"play\": 60}, {\"play\": 64}, {\"play\": 67}, \"value\", \"stop\"],\n        \"testStepDuration\": 1,\n        \"actionTargets\": [\n            {\"method\": \"play\", \"params\": {\"tone\": 69}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"stop\", \"params\": {}, \"control\": {}},\n        ],\n    },\n    {\n        \"type\": \"Servo\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#servo\",\n        \"image\": \"device_Servo.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"initial_value\": {\n                \"value\": 0.0,\n                \"type\": \"floatOrNone\",\n                \"min\": -1.0,\n                \"max\": 1.0,\n            },\n            \"min_pulse_width\": {\n                \"value\": 0.001,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n            \"max_pulse_width\": {\n                \"value\": 0.002,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n            \"frame_width\": {\n                \"value\": 0.020,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n        },\n        \"testMethods\": [\"min\", \"max\", \"mid\", \"is_active\", \"value\"],\n        \"testStepDuration\": 1,\n        \"actionTargets\": [\n            {\n                \"method\": \"value\",\n                \"params\": {\"value\": 0.0},\n                \"control\": {\"duration\": 0.0, \"steps\": 1},\n            },\n        ],\n    },\n    {\n        \"type\": \"AngularServo\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#angularservo\",\n        \"image\": \"device_Servo.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"initial_angle\": {\"value\": 0.0, \"type\": \"float\", \"min\": -90.0, \"max\": 90.0},\n            \"min_angle\": {\"value\": -90.0, \"type\": \"float\", \"min\": -360.0, \"max\": 360.0},\n            \"max_angle\": {\"value\": 90.0, \"type\": \"float\", \"min\": -360.0, \"max\": 360.0},\n            \"min_pulse_width\": {\n                \"value\": 0.001,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n            \"max_pulse_width\": {\n                \"value\": 0.002,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n            \"frame_width\": {\n                \"value\": 0.020,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n        },\n        \"testMethods\": [\"min\", \"max\", \"mid\", \"is_active\", \"angle\", \"value\"],\n        \"testStepDuration\": 1,\n        \"actionTargets\": [\n            {\n                \"method\": \"angle\",\n                \"params\": {\"angle\": 0.0},\n                \"control\": {\"duration\": 0.0, \"steps\": 1},\n            },\n        ],\n    },\n    {\n        \"type\": \"Motor\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#motor\",\n        \"image\": \"device_Motor.jpg\",\n        \"params\": {\n            \"forward\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"backward\": {\n                \"value\": \"\",\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 27,\n                \"isPin\": True,\n            },\n            \"enable\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"pwm\": {\"value\": True, \"type\": \"bool\"},\n        },\n        \"testMethods\": [{\"forward\": 1}, {\"backward\": 1}, \"stop\"],\n        \"testStepDuration\": 3,\n        \"actionTargets\": [\n            {\n                \"method\": \"forward\",\n                \"params\": {\"speed\": 1.0},\n                \"control\": {\"duration\": 0.0, \"steps\": 1},\n            },\n            {\n                \"method\": \"backward\",\n                \"params\": {\"speed\": 1.0},\n                \"control\": {\"duration\": 0.0, \"steps\": 1},\n            },\n            {\"method\": \"reverse\", \"params\": {}, \"control\": {}},\n            {\"method\": \"stop\", \"params\": {}, \"control\": {}},\n        ],\n    },\n    {\n        \"type\": \"StepperMotor\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://signag.github.io/raspi-cam-srv/latest/gpioDevices/StepperMotor/\",\n        \"image\": \"device_StepperMotor.jpg\",\n        \"params\": {\n            \"in1\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"in2\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"in3\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"in4\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"mode\": {\n                \"value\": 0,\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 1,\n            },\n            \"speed\": {\n                \"value\": 1.0,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 1.0,\n            },\n            \"current_angle\": {\n                \"value\": 0.0,\n                \"type\": \"float\",\n                \"min\": -360.0,\n                \"max\": 360.0,\n            },\n            \"swing_from\": {\n                \"value\": -45.0,\n                \"type\": \"float\",\n                \"min\": -360.0,\n                \"max\": 0.0,\n            },\n            \"swing_to\": {\n                \"value\": 45.0,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 360.0,\n            },\n            \"swing_step\": {\n                \"value\": 9.0,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 360.0,\n            },\n            \"swing_direction\": {\n                \"value\": 1,\n                \"type\": \"int\",\n                \"min\": -1,\n                \"max\": 1,\n            },\n            \"stride_angle\": {\n                \"value\": 5.625,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 360.0,\n            },\n            \"gear_reduction\": {\n                \"value\": 64,\n                \"type\": \"int\",\n                \"min\": 1,\n                \"max\": 1000.0,\n            },\n        },\n        \"testMethods\": [\n            {\"rotate_right\": 90.0},\n            {\"rotate_left\": 90.0},\n            {\"rotate_to\": 0.0},\n        ],\n        \"testStepDuration\": 1,\n        \"calibration\": {\n            \"fbwd\": {\n                \"method\": \"rotate\",\n                \"params\": {\"angle\": -10.0},\n            },\n            \"bwd\": {\n                \"method\": \"rotate\",\n                \"params\": {\"angle\": -1.0},\n            },\n            \"calibrate\": {\n                \"method\": \"value\",\n                \"params\": {\"value\": 0.0},\n            },\n            \"fwd\": {\n                \"method\": \"rotate\",\n                \"params\": {\"angle\": 1.0},\n            },\n            \"ffwd\": {\n                \"method\": \"rotate\",\n                \"params\": {\"angle\": 10.0},\n            },\n        },\n        \"actionTargets\": [\n            {\"method\": \"step\", \"params\": {\"steps\": -1}, \"control\": {}},\n            {\"method\": \"step_forward\", \"params\": {\"steps\": 1}, \"control\": {}},\n            {\"method\": \"step_backward\", \"params\": {\"speed\": 1}, \"control\": {}},\n            {\"method\": \"rotate\", \"params\": {\"angle\": -1.0}, \"control\": {}},\n            {\"method\": \"rotate_right\", \"params\": {\"angle\": 1.0}, \"control\": {}},\n            {\"method\": \"rotate_left\", \"params\": {\"angle\": 1.0}, \"control\": {}},\n            {\"method\": \"rotate_to\", \"params\": {\"target\": 1.0}, \"control\": {}},\n            {\"method\": \"swing\", \"params\": {}, \"control\": {}},\n            {\n                \"method\": \"wipe\",\n                \"params\": {\"angle_from\": -45.0, \"angle_to\": 45.0, \"speed\": 0.0, \"count\": 1},\n                \"control\": {}\n            },\n            {\n                \"method\": \"stop\",\n                \"params\": {},\n                \"control\": {}\n            },\n        ],\n    },\n    {\n        \"type\": \"ServoPWM\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://signag.github.io/raspi-cam-srv/latest/gpioDevices/ServoPWM/\",\n        \"image\": \"device_ServoPWM.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 12, \"max\": 19, \"isPin\": True},\n            \"min_angle\": {\"value\": -90.0, \"type\": \"float\", \"min\": -360.0, \"max\": 360.0},\n            \"max_angle\": {\"value\": 90.0, \"type\": \"float\", \"min\": -360.0, \"max\": 360.0},\n            \"min_pulse_width_us\": {\n                \"value\": 500,\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 1000000.0,\n            },\n            \"max_pulse_width_us\": {\n                \"value\": 2500,\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 1000000.0,\n            },\n            \"frame_width_us\": {\n                \"value\": 20000,\n                \"type\": \"int\",\n                \"min\": 0,\n                \"max\": 1000000.0,\n            },\n            \"speed\": {\n                \"value\": 1.5,\n                \"type\": \"float\",\n                \"min\": 0.0,\n                \"max\": 10000.0,\n            },\n            \"idle_off\": {\"value\": False, \"type\": \"bool\"},\n            \"calibration\": {\"value\": 0.0, \"type\": \"float\", \"min\": -90.0, \"max\": 90.0},\n        },\n        \"testMethods\": [\"min\", \"max\", \"mid\", {\"rotate_to\": 0.0}],\n        \"testStepDuration\": 1,\n        \"calibration\": {\n            \"fbwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": -10.0},\n            },\n            \"bwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": -1.0},\n            },\n            \"calibrate\": {\n                \"param\": \"calibration\",\n            },\n            \"fwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": 1.0},\n            },\n            \"ffwd\": {\n                \"method\": \"rotate_by\",\n                \"params\": {\"angle\": 10.0},\n            },\n        },\n        \"actionTargets\": [\n            {\"method\": \"min\", \"params\": {}, \"control\": {}},\n            {\"method\": \"max\", \"params\": {}, \"control\": {}},\n            {\"method\": \"mid\", \"params\": {}, \"control\": {}},\n            {\"method\": \"rotate_to\", \"params\": {\"angle\": -1.0}, \"control\": {}},\n            {\"method\": \"rotate_by\", \"params\": {\"angle\": -1.0}, \"control\": {}},\n            {\"method\": \"rotate_right\", \"params\": {\"angle\": 1.0}, \"control\": {}},\n            {\"method\": \"rotate_left\", \"params\": {\"angle\": 1.0}, \"control\": {}},\n            {\"method\": \"stop\", \"params\": {}, \"control\": {}},\n            {\"method\": \"close\", \"params\": {}, \"control\": {}},\n        ],\n    },\n    {\n        \"type\": \"DigitalOutputDevice\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#digitaloutputdevice\",\n        \"image\": \"device_DigitalOutputDevice.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": False, \"type\": \"boolOrNone\"},\n        },\n        \"testMethods\": [\"on\", \"value\"],\n        \"testDuration\": 2,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"value\", \"params\": {\"value\": 0}, \"control\": {}},\n        ],\n    },\n    {\n        \"type\": \"OutputDevice\",\n        \"usage\": \"Output\",\n        \"docUrl\": \"https://gpiozero.readthedocs.io/en/stable/api_output.html#outputdevice\",\n        \"image\": \"device_OutputDevice.jpg\",\n        \"params\": {\n            \"pin\": {\"value\": \"\", \"type\": \"int\", \"min\": 0, \"max\": 27, \"isPin\": True},\n            \"active_high\": {\"value\": True, \"type\": \"bool\"},\n            \"initial_value\": {\"value\": False, \"type\": \"boolOrNone\"},\n        },\n        \"testMethods\": [\"on\", \"value\"],\n        \"testDuration\": 2,\n        \"actionTargets\": [\n            {\"method\": \"on\", \"params\": {}, \"control\": {\"duration\": 0.0}},\n            {\"method\": \"off\", \"params\": {}, \"control\": {}},\n            {\"method\": \"toggle\", \"params\": {}, \"control\": {}},\n            {\"method\": \"value\", \"params\": {\"value\": 0}, \"control\": {}},\n        ],\n    },\n]\n"
  },
  {
    "path": "raspiCamSrv/gpioDevices.py",
    "content": "from gpiozero import OutputDevice, PWMOutputDevice\nimport threading\nfrom _thread import allocate_lock\nimport time\nfrom datetime import datetime\nimport math\nimport subprocess\nfrom subprocess import CalledProcessError\nimport logging\n# Try to import rpi_hardware_pwm\ntry:\n    from rpi_hardware_pwm import HardwarePWM\n    useHardwarePwm = True\nexcept ImportError:\n    useHardwarePwm = False\n\nlogger = logging.getLogger(__name__)\n\nclass StepperMotor():\n    \"\"\" This class implements a stepper motor\n    \n        Developped and tested with\n        Stepper motor: 28BYJ-48\n        Motor driver : ULN2003A\n    \"\"\"\n\n    def __init__(self, \\\n            in1:int, \\\n            in2:int, \\\n            in3:int, \\\n            in4:int, \\\n            mode:int=0, \\\n            speed:float=1.0, \\\n            current_angle:float=0.0, \\\n            swing_from:float=-45.0, \\\n            swing_to:float=45.0, \\\n            swing_step:float=9.0, \\\n            swing_direction:int=1, \\\n            stride_angle:float=5.625, \\\n            gear_reduction:int=64\n            ):\n        \"\"\" Constructor for StepperMotor\n\n        Args:\n            in1 (int): GPIO Pin connected to IN1\n            in2 (int): GPIO Pin connected to IN2\n            in3 (int): GPIO Pin connected to IN3\n            in4 (int): GPIO Pin connected to IN4\n            mode (int, optional): \n                0: half-step mode\n                1: full-step mode\n                Defaults to 0.\n            speed (float, optional):\n                0.0 : lowest speed\n                1.0 : highest speed\n                Defaults to 1.0.\n            current_angle (float, optional):\n                current angle of the motor\n                Defaults to 0.0.\n            swing_from (float, optional):\n                left limit of the swing (-360.0 to 0.0)\n                Defaults to -45.0.\n            swing_to (float, optional):\n                right limit of the swing (0.0 to 360.0)\n                Defaults to 45.0.\n            swing_step (float, optional):\n                step size of the swing (0.0 to 360.0)\n                Defaults to 9.0.\n            swing_direction (int, optional):\n                1: clockwise\n                -1: counter-clockwise\n                Defaults to 1.\n            stride_angle (float, optional)\n                angle per step in half-step mode\n                Defaults to 5.625.\n            gear_reduction (int, optional)\n                gear reduction ratio\n                Defaults to 64.\n        \"\"\"\n        # Constant waiting times for highest (1) and lowest (0) speed\n        self._WAIT_HIGH_SPEED = 0.001\n        self._WAIT_LOW_SPEED = 0.040\n\n        # Set the pins\n        self._in1 = OutputDevice(in1)\n        self._in2 = OutputDevice(in2)\n        self._in3 = OutputDevice(in3)\n        self._in4 = OutputDevice(in4)\n        self._mode = mode\n        self._speed = speed\n        self._stride_angle = stride_angle\n        self._gear_reduction = gear_reduction\n        self._wait = self._WAIT_HIGH_SPEED\n\n        self._pins = [self._in1, self._in2, self._in3, self._in4]\n\n        # Step sequence for half-step operation\n        self._seq_half_step = [ \\\n            [1,0,0,0], \n            [1,1,0,0],\n            [0,1,0,0],\n            [0,1,1,0],\n            [0,0,1,0],\n            [0,0,1,1],\n            [0,0,0,1],\n            [1,0,0,1],\n        ]\n        # Step sequence for full-step operation\n        self._seq_full_step = [ \\\n            [1,1,0,0], \n            [0,1,1,0],\n            [0,0,1,1],\n            [1,0,0,1]\n        ]\n        if self._mode == 0:\n            self._seq = self._seq_half_step\n        else:\n            self._seq = self._seq_full_step\n        self._seq_len = len(self._seq)\n\n        # Set the waiting time tepending on the speed\n        if self._speed < 0.0:\n            self._speed = 0.0\n        if self._speed > 1.0:\n            self._speed = 1.0\n\n        self._wait = self._WAIT_LOW_SPEED + self._speed * (self._WAIT_HIGH_SPEED - self._WAIT_LOW_SPEED)\n        # For full-step mode, the wait time is doubled\n        if self._mode == 1:\n            self._wait = self._wait * 2\n\n        # Set the current step\n        self._current_step = 0\n\n        # Set parameters for swinging\n        self._current_angle = current_angle\n        self._swing_from = swing_from\n        self._swing_to = swing_to\n        self._swing_step = swing_step\n        self._swing_direction = swing_direction\n\n        # Set parameters for swiping\n        self.wipe_active = False\n        self.wipeLock = allocate_lock()  # lock for wipe status\n        self.wipeThread = None  # thread for wipe operation\n\n    @property\n    def in1(self) -> int:\n        return self._in1\n\n    @in1.setter\n    def in1(self, value: int):\n        self._in1 = value\n\n    @property\n    def in2(self) -> int:\n        return self._in2\n\n    @in2.setter\n    def in2(self, value: int):\n        self._in2 = value\n\n    @property\n    def in3(self) -> int:\n        return self._in3\n\n    @in3.setter\n    def in3(self, value: int):\n        self._in3 = value\n\n    @property\n    def in4(self) -> int:\n        return self._in4\n\n    @in4.setter\n    def in4(self, value: int):\n        self._in4 = value\n\n    @property\n    def mode(self) -> int:\n        return self._mode\n\n    @mode.setter\n    def mode(self, value: int):\n        self._mode = value\n        if self._mode == 0:\n            self._seq = self._seq_half_step\n        else:\n            self._seq = self._seq_full_step\n        self._seq_len = len(self._seq)\n        # Set the waiting time tepending on the speed\n        self.speed = self.speed\n\n    @property\n    def speed(self) -> float:\n        return self._speed\n\n    @speed.setter\n    def speed(self, value: float):\n        self._speed = value\n\n        if self._speed < 0.0:\n            self._speed = 0.0\n        if self._speed > 1.0:\n            self._speed = 1.0\n\n        self._wait = self._WAIT_LOW_SPEED + self._speed * (self._WAIT_HIGH_SPEED - self._WAIT_LOW_SPEED)\n        # For full-step mode, the wait time is doubled\n        if self._mode == 1:\n            self._wait = self._wait * 2\n\n    @property\n    def stride_angle(self) -> float:\n        return self._stride_angle\n\n    @property\n    def gear_reduction(self) -> float:\n        return self._gear_reduction\n\n    @property\n    def current_angle(self) -> float:\n        return self._current_angle\n\n    @current_angle.setter\n    def current_angle(self, value: float):\n        self._current_angle = value\n\n    @property\n    def value(self) -> float:\n        return self._current_angle\n\n    @value.setter\n    def value(self, value: float):\n        self._current_angle = value\n\n    @property\n    def swing_from(self) -> float:\n        return self._swing_from\n\n    @swing_from.setter\n    def swing_from(self, value: float):\n        self._swing_from = value\n\n    @property\n    def swing_to(self) -> float:\n        return self._swing_to\n\n    @swing_to.setter\n    def swing_to(self, value: float):\n        self._swing_to = value\n\n    @property\n    def swing_step(self) -> float:\n        return self._swing_step\n\n    @swing_step.setter\n    def swing_step(self, value: float):\n        self._swing_step = value\n\n    @property\n    def swing_direction(self) -> float:\n        return self._swing_direction\n\n    @swing_direction.setter\n    def swing_direction(self, value: float):\n        self._swing_direction = value\n\n    def _motor_step(self, direction:int):\n        \"\"\" Do one motor step in the current direction\n        \n        Args:\n            direction (int):\n                 1: forward\n                -1: backward\n        \"\"\"\n        # Move\n        for pin in range(0, 4):\n            if self._seq[self._current_step][pin] != 0:\n                self._pins[pin].on()\n            else:\n                self._pins[pin].off()\n\n        # Proceed\n        self._current_step += direction\n        if self._current_step >= self._seq_len:\n            self._current_step = 0\n        if self._current_step < 0:\n            self._current_step = self._seq_len - 1\n\n        # Wait\n        time.sleep(self._wait)\n\n    def _step(self, direction:int):\n        \"\"\" Do one step in the current direction\n        \n        Args:\n            direction (int):\n                 1: forward\n                -1: backward\n        \"\"\"\n        for motor_step in range(0, self._gear_reduction):\n            self._motor_step(direction)\n        # Update the current angle\n        if self._mode == 0:\n            self._current_angle += direction * self._stride_angle\n        else:\n            self._current_angle += direction * self._stride_angle * 2\n        if self._current_angle > 360.0:\n            self._current_angle -= 360.0\n        if self._current_angle < -360.0:\n            self._current_angle += 360.0\n\n    def step(self, steps:int):\n        \"\"\" step forward or backward by a given number of steps\n\n        Args:\n            steps (int): number of steps to step forward (positive) or backward (negative)\n        \"\"\"\n        nrSteps = abs(steps)\n        if steps < 0:\n            direction = -1\n        else:\n            direction = 1\n        for step in range(0, nrSteps):\n            self._step(direction)\n\n    def step_forward(self, steps:int):\n        \"\"\" step forward by a given number of steps\n\n        Args:\n            steps (int): number of steps to step forward\n        \"\"\"\n        for step in range(0, steps):\n            self._step(1)\n\n    def step_backward(self, steps:int):\n        \"\"\" step forward by a given number of steps\n\n        Args:\n            steps (int): number of steps to step forward\n        \"\"\"\n        for step in range(0, steps):\n            self._step(-1)\n\n    def rotate(self, angle:float):\n        \"\"\" Rotate right by the given angle\n\n        Args:\n            angle (float): angle to rotate. Positive angle is clockwise, negative angle is counter-clockwise\n        \"\"\"\n        abs_angle = abs(angle)\n        dir = 1\n        if angle < 0:\n            dir = -1\n        motor_steps = round(self.gear_reduction * abs_angle / self._stride_angle)\n        if self.mode == 1:\n            motor_steps = round(motor_steps / 2)\n\n        for motor_step in range(0, motor_steps):\n            self._motor_step(dir)\n\n        self._current_angle += angle\n\n    def rotate_right(self, angle:float):\n        \"\"\" Rotate right by the given angle\n\n        Args:\n            angle (float): angle to rotate\n        \"\"\"\n        self.rotate(angle)\n\n    def rotate_left(self, angle:float):\n        \"\"\" Rotate left by the given angle\n\n        Args:\n            angle (float): angle to rotate\n        \"\"\"\n        self.rotate(-angle)\n\n    def rotate_to(self, target:float):\n        \"\"\" Rotate to a given angle\n        Args:\n            angle (float): angle to rotate to\n        \"\"\"\n        angle = target - self._current_angle\n        self.rotate(angle)\n\n    def swing(self):\n        \"\"\" Swing the motor back and forth between the given angles\n        \"\"\"\n        angle_rest = 0.0\n        angle_step = self._swing_direction * self._swing_step\n        angle_new = self._current_angle + angle_step\n        if angle_new > self._swing_to:\n            angle_rest = angle_new - self._swing_to\n            angle_step = self._swing_to - self._current_angle\n        elif angle_new < self._swing_from:\n            angle_rest = angle_new - self._swing_from\n            angle_step = self._swing_from - self._current_angle\n        self.rotate(angle_step)\n        if angle_rest != 0.0:\n            self._swing_direction = -self._swing_direction\n            angle_step = -angle_rest\n            self.rotate(angle_step)\n\n    def wipe(self, angle_from:float=-45, angle_to:float=45, speed:float=1.0, count:int=1):\n        \"\"\"Start swiping in a separate thread\n        Args:\n            angle_from (float): left limit of the wipe\n            angle_to (float): right limit of the wipe\n            duration (float): duration of the wipe in seconds\n            count (int): number of wipes\n        \"\"\"\n        if self.wipeThread is not None and self.wipeThread.is_alive():\n            return\n        self.wipeThread = threading.Thread(target=self._do_wipe, args=(angle_from, angle_to, speed, count))\n        self.wipeThread.start()\n\n    def _do_wipe(self, angle_from, angle_to, speed, count):\n        \"\"\" Wipe the motor back and forth between the given angles\n        Args:\n            angle_from (float): left limit of the wipe\n            angle_to (float): right limit of the wipe\n            duration (float): duration of the wipe in seconds\n            count (int): number of wipes\n        \"\"\"\n        self.wipe_active = True\n        current_angle = self._current_angle\n        current_speed = self._speed\n        self.speed = speed\n        self.rotate_to(angle_from)\n        i = count\n        if i == 0:\n            i = 1\n        while i > 0:\n            self.rotate_to(angle_to)\n            with self.wipeLock:\n                if self.wipe_active == False:\n                    i = 0\n            if i > 0:\n                self.rotate_to(angle_from)\n                if count > 0:\n                    i -= 1\n                with self.wipeLock:\n                    if self.wipe_active == False:\n                        i = 0\n        self.speed = current_speed\n        self.rotate_to(current_angle)\n\n        self.wipe_active = False\n        self.wipeThread = None\n\n    def stop(self):\n        \"\"\" Stop any activity\n        \n        \"\"\"\n        with self.wipeLock:\n            self.wipe_active = False\n\n        while self.wipeThread is not None and self.wipeThread.is_alive():\n            time.sleep(0.1)\n\n    def close(self):\n        \"\"\" Close gpiozero resources associated with pins\n        \n        \"\"\"\n        self.stop()\n        for pin in range(0, 4):\n            self._pins[pin].close()            \n\nclass ServoPWM():\n    \"\"\" This class implements a servo motor using PWM signal\n    \n        Developped and tested with\n        Servo motor: KY66\n\n        Development of this class was motivated by the fact that software-based PWM,\n        provided by the default RPi.GPIO pin factory in gpiozero results in significant jitter for servo motors.\n        The alternative pigpio pin factory does support hardware PWM, but it is currently not compatible\n        with the latest Debian release (Trixie) for Raspberry Pi. \n        Therefore, this class implements the control of the servo motor using the rpi-hardware-pwm library, \n        which provides access to the hardware PWM channels of the Raspberry Pi.\n        (https://github.com/Pioreactor/rpi_hardware_pwm)\n    \"\"\"\n    def __init__(self, \n            pin:int,\n            min_angle:float=-90.0,\n            max_angle:float= 90.0,\n            min_pulse_width_us:int=500, \n            max_pulse_width_us:int=2500,\n            frame_width_us:int=20000,\n            speed:float=2.8,\n            idle_off:bool=False,\n            calibration:float=0.0\n            ):\n        \"\"\" Constructor for ServoPWM\n\n        Args:\n            pin (int): GPIO Pin connected to the servo signal line\n            min_angle (float, optional): minimum angle of the servo. Defaults to -90.0.\n            max_angle (float, optional): maximum angle of the servo. Defaults to 90.0.\n            min_pulse_width_us (int, optional): minimum pulse width corresponding to 0 degree in microseconds. Defaults to 500.\n            max_pulse_width_us (int, optional): maximum pulse width corresponding to 180 degree in microseconds. Defaults to 2500.\n            frame_width_us (int, optional): duration of one PWM frame in microseconds. Defaults to 20000.\n            speed (float, optional): speed of the servo [sec/360°]. Defaults to 0.72.\n            idle_off (bool, optional): whether to turn off the signal when idle. Defaults to True.\n            calibration (float, optional): calibration angle of the servo. Defaults to 0.0.\n        \"\"\"\n        logger.debug(\"ServoPWM.__init__: Initializing on pin %d with calibration=%s, min_angle=%s, max_angle=%s, min_pulse_width_us=%s, max_pulse_width_us=%s, frame_width_us=%s, speed=%s, idle_off=%s\", pin, calibration, min_angle, max_angle, min_pulse_width_us, max_pulse_width_us, frame_width_us, speed, idle_off)\n        if useHardwarePwm == False:\n            raise ImportError(\"rpi_hardware_pwm library is not available. Please run: pip install rpi_hardware_pwm\")\n\n        self._pin = pin\n        match pin:\n            case 12:\n                self._pwm_channel = 0\n            case 13:\n                self._pwm_channel = 1\n            case 18:\n                self._pwm_channel = 2\n            case 19:\n                self._pwm_channel = 3\n            case _:\n                raise ValueError(f\"Invalid pin {pin}. Valid pins are 12, 13, 18, 19.\")\n        if not self.is_pin_ok(pin):\n            raise ValueError(f\"PWM is not routed to pin {pin}. Please check dtoverlay configuration and verify with 'pinctrl get {pin}' command.\")\n        self._min_angle = min_angle\n        self._max_angle = max_angle\n        self._min_pulse_width_us = min_pulse_width_us\n        self._max_pulse_width_us = max_pulse_width_us\n        self._frame_width_us = frame_width_us\n        self._current_angle = 0.0\n        self._current_duty_cycle = 0.0\n        self._frequency = int(1000000 / self._frame_width_us)\n        self._speed = speed\n        self._idle_off = idle_off\n        self._calib_angle = calibration\n        self._pwm = HardwarePWM(pwm_channel=self._pwm_channel, hz=self._frequency)\n        self._pwm.start(0)\n        logger.debug(\"ServoPWM.__init__: Initialization complete. Current duty cycle: %s\", self._current_duty_cycle)\n\n    def is_pin_ok(self, pin:int) -> bool:\n        \"\"\" Check if the given pin is valid for hardware PWM\n\n        Args:\n            pin (int): GPIO pin number to check\n\n        Returns:\n            bool: True if the pin is valid for hardware PWM, False otherwise\n        \"\"\"\n        try:\n            result = subprocess.run(\n                    [\"pinctrl\", \"get\", f\"{pin}\"],\n                    capture_output=True, text=True\n                ).stdout.strip()\n            if result.find(\"PWM\") >= 0:\n                return True\n            else:\n                return False\n        except CalledProcessError as e:\n            logger.error(\"Error checking pin %d: %s\", pin, e)\n            return False\n\n    @property\n    def current_angle(self) -> float:\n        return self._current_angle\n\n    @current_angle.setter\n    def current_angle(self, value: float):\n        \"\"\" Set the new current angle and rotate servo to the given value\n\n        Given limits are regarded\n\n        Args:\n            value (float): target angle to rotate to relative to calibration zero\n        \"\"\"\n        logger.debug(\"ServoPWM.current_angle: Setting current_angle to %s. Actually: %s\", value, self._current_angle)\n        value = value + self._calib_angle\n        logger.debug(\"ServoPWM.current_angle: calibration correction %s\", value)\n        if value < self._min_angle:\n            value = self._min_angle\n        if value > self._max_angle:\n            value = self._max_angle\n        logger.debug(\"ServoPWM.current_angle: Limited to min/max %s\", value)\n        diff = abs(value - self._calib_angle - self._current_angle)\n        logger.debug(\"ServoPWM.current_angle: Difference to current angle %s\", diff)\n        self._current_angle = value - self._calib_angle\n        logger.debug(\"ServoPWM.current_angle: New current angle %s\", self._current_angle)\n        self._current_duty_cycle = self._angle_to_duty_cycle(value)\n        logger.debug(\"ServoPWM.current_angle: New current duty cycle %s\", self._current_duty_cycle)\n        self._pwm.change_duty_cycle(self._current_duty_cycle)\n        logger.debug(\"ServoPWM.current_angle: New duty cycle set: %s\", self._current_duty_cycle)\n        if self._idle_off:\n            duration = diff * self._speed / 360.0 \n            if duration < 0.1:\n                duration = 0.1\n            logger.debug(\"ServoPWM.current_angle: Waiting for %s sec\", duration)\n            time.sleep(duration)\n            self._pwm.change_duty_cycle(0)\n        logger.debug(\"ServoPWM.current_angle: Done\")\n\n    @property\n    def value(self) -> float:\n        return self._current_angle\n\n    @value.setter\n    def value(self, value: float):\n        self.current_angle = value\n\n    def _angle_to_duty_cycle(self, angle:float) -> float:\n        \"\"\" Convert angle to duty cycle\n\n        Args:\n            angle (float): angle to convert\n\n        Returns:\n            float: duty cycle corresponding to the given angle\n        \"\"\"\n        pulse_width_us = self._min_pulse_width_us + (angle - self._min_angle) * (self._max_pulse_width_us - self._min_pulse_width_us) / (self._max_angle - self._min_angle)\n        duty_cycle = 100 * pulse_width_us / self._frame_width_us\n        return duty_cycle\n\n    def _duty_cycle_to_angle(self, duty_cycle:float) -> float:\n        \"\"\" Convert duty cycle to angle\n\n        Args:\n            duty_cycle (float): duty cycle to convert\n\n        Returns:\n            float: angle corresponding to the given duty cycle\n        \"\"\"\n        pulse_width_us = duty_cycle * self._frame_width_us\n        angle = self._min_angle + (pulse_width_us - self._min_pulse_width_us) * (self._max_angle - self._min_angle) / (self._max_pulse_width_us - self._min_pulse_width_us)\n        if angle < self._min_angle:\n            angle = self._min_angle\n        if angle > self._max_angle:\n            angle = self._max_angle\n        return angle\n\n    def min(self):\n        \"\"\" Rotate the servo to the minimum angle\n        \"\"\"\n        self.current_angle = self._min_angle - self._calib_angle\n\n    def max(self):\n        \"\"\" Rotate the servo to the maximum angle\n        \"\"\"\n        self.current_angle = self._max_angle - self._calib_angle\n\n    def mid(self):\n        \"\"\" Rotate the servo to the middle angle\n        \"\"\"\n        self.current_angle = (self._min_angle + self._max_angle) / 2.0 - self._calib_angle\n\n    def rotate_to(self, angle:float):\n        \"\"\" Rotate the servo to the given angle\n\n        Args:\n            angle (float): angle to rotate to\n        \"\"\"\n        self.current_angle = angle\n\n    def rotate_by(self, angle:float):\n        \"\"\" Rotate the servo by the given angle\n\n        Args:\n            angle (float): angle to rotate by\n        \"\"\"\n        self.current_angle = self.current_angle + angle\n\n    def rotate_left(self, angle:float):\n        \"\"\" Rotate the servo left by the given angle\n\n        Args:\n            angle (float): angle to rotate left by\n        \"\"\"\n        self.rotate_by(-angle)\n\n    def rotate_right(self, angle:float):\n        \"\"\" Rotate the servo right by the given angle\n\n        Args:\n            angle (float): angle to rotate right by\n        \"\"\"\n        self.rotate_by(angle)\n\n    def stop(self):\n        \"\"\" Stop any activity\n        \n        \"\"\"\n        self._pwm.stop()\n\n    def close(self):\n        \"\"\" Stop any activity\n        \n        \"\"\"\n        self._pwm.stop()\n\nif __name__ == \"__main__\":\n    testClass=\"ServoPWM\"\n    if testClass == \"StepperMotor\":\n        test = 6\n        print(\"==== Test StepperMotor ======\")\n        sm = StepperMotor(10, 9, 11, 0, 0, 1)\n        #    sm = StepperMotor(14, 15, 18, 23, 0, 1)\n        if test == 3:\n            print(f\"==== Test Calibration ====\")\n            print (f\"==== 1. current_angle:{sm.current_angle}\")\n            sm.step(8)\n            print (f\"==== 2. step(8) ====\")\n            time.sleep(2)\n            print (f\"==== 3. current_angle:{sm.current_angle}\")\n            sm.value = 0.0\n            print (f\"==== 4. value=0.0 ====\")\n            sm.rotate_to(-45.0)\n            print (f\"==== 5. rotate_to(-90.0) ====\")\n            print (f\"==== 6. current_angle:{sm.current_angle}\")\n        if test == 2:\n            print(f\"==== Test swinging ====\")\n            for i in range(0, 40):\n                print (f\"==== Step {i + 1} Start {sm.current_angle} ====\")\n                sm.swing()\n                print (f\"==== Step {i + 1} End   {sm.current_angle} ====\")\n                print(\" \")\n        if test == 3:\n            print(f\"==== Test mode & speed ====\")\n            for mode in range(0, 2):\n                sm.mode = mode\n                for ispeed in range(1, -1, -1):\n                    speed = float(ispeed)\n                    sm.speed = speed\n                    print(f\"==== mode={sm.mode} == speed={sm.speed } ====\")\n                    print(f\"==== step_forward(64) =======\")\n                    sm.step_forward(64)\n                    time.sleep(2)\n                    print(f\"==== step_backward(64)\")\n                    sm.step_backward(64) \n                    time.sleep(2)\n                    print(f\"==== step(64)\")\n                    sm.step(64) \n                    time.sleep(2)\n                    print(f\"==== step(-64)\")\n                    sm.step(-64) \n                    time.sleep(2)\n                    print(f\"==== rotate_right(90) =======\")\n                    sm.rotate_right(90) \n                    time.sleep(2)\n                    print(f\"==== rotate_left(90) =======\")\n                    sm.rotate_left(90) \n                    time.sleep(2)\n                    print(f\"==== rotate(360) =======\")\n                    sm.rotate(360) \n                    time.sleep(2)\n                    print(f\"==== rotate(-360) =======\")\n                    sm.rotate(-360) \n                    time.sleep(2)\n        if test == 4:\n            sm.value = 0.0\n            sm.rotate_to(0)\n            sm.mode = 0\n            sm.speed = 1.0\n            for a in range(0, -91, -15):\n                sm.rotate_to(a)\n                time.sleep(0.5)\n            for a in range(-80, 91, 15):\n                sm.rotate_to(a)\n                time.sleep(0.5)\n            sm.rotate_to(0)\n            \n        if test == 5:\n            print(f\"==== Test wipe ====\")\n            sm.value = 0.0\n            sm.rotate_to(0)\n            sm.mode = 0\n            sm.speed = 0.0\n            sm.wipe(angle_from=-45, angle_to=45, speed=0, count=3)\n            time.sleep(5)\n            sm.stop()\n            \n        if test == 5:\n            print(f\"==== Measuring  Angular Velocity ====\")\n            sm.value = 0.0\n            sm.rotate_to(0)\n            print(\"\")\n            print(f\"==== Half-Step Mode ====\")\n            sm.mode = 0\n            print(f\"==== Slow (speed=0) ====\")\n            sm.speed = 0.0\n            startTime = datetime.now()\n            sm.rotate_to(360)\n            endTime = datetime.now()\n            duration = (endTime - startTime).total_seconds()\n            print(f\"==== Duration: {duration} seconds ====\")\n            print(f\"==== Angular Velocity: {360 / duration} degrees/second ====\")\n            print(f\"==== Fast (speed=1) ====\")\n            sm.speed = 1.0\n            startTime = datetime.now()\n            sm.rotate_to(0)\n            endTime = datetime.now()\n            duration = (endTime - startTime).total_seconds()\n            print(f\"==== Duration: {duration} seconds ====\")\n            print(f\"==== Angular Velocity: {360 / duration} degrees/second ====\")\n            print(\"\")\n            print(f\"==== Full-Step Mode ====\")\n            sm.mode = 1\n            print(f\"==== Slow (speed=0) ====\")\n            sm.speed = 0.0\n            startTime = datetime.now()\n            sm.rotate_to(360)\n            endTime = datetime.now()\n            duration = (endTime - startTime).total_seconds()\n            print(f\"==== Duration: {duration} seconds ====\")\n            print(f\"==== Angular Velocity: {360 / duration} degrees/second ====\")\n            print(f\"==== Fast (speed=1) ====\")\n            sm.speed = 1.0\n            startTime = datetime.now()\n            sm.rotate_to(0)\n            endTime = datetime.now()\n            duration = (endTime - startTime).total_seconds()\n            print(f\"==== Duration: {duration} seconds ====\")\n            print(f\"==== Angular Velocity: {360 / duration} degrees/second ====\")\n        print(f\"==== close ====\")\n        sm.close()\n        print(f\"==== Test completed ====\")\n\n    if testClass == \"ServoPWM\":\n        print(\"==== Test ServoPWM ======\")\n        servo = ServoPWM(pin=12, idle_off=True)\n        print(f\"==== Rotate to Min ====\")\n        servo.min()\n        time.sleep(2)\n        print(f\"==== Rotate to Max ====\")\n        servo.max()\n        time.sleep(2)\n        print(f\"==== Rotate to Mid ====\")\n        servo.mid()\n        time.sleep(2)\n\n        for angle in range(-90, 91, 30):\n            print(f\"==== Rotate to {angle} ====\")\n            servo.rotate_to(angle)\n            time.sleep(2)\n        for angle in range(60, -91, -30):\n            print(f\"==== Rotate to {angle} ====\")\n            servo.rotate_to(angle)\n            time.sleep(1)\n        print(f\"==== Rotate by 45 ====\")\n        servo.rotate_by(45)\n        time.sleep(2)\n        print(f\"==== Rotate by 45 ====\")\n        servo.rotate_by(45)\n        time.sleep(2)\n        print(f\"==== Rotate left by 90 ====\")\n        servo.rotate_left(90)\n        time.sleep(2)\n        print(f\"==== Rotate right by 90 ====\")\n        servo.rotate_right(90)\n        time.sleep(2)\n\n        print(f\"==== stop ====\")\n        servo.stop()\n        print(f\"==== Test completed ====\")\n"
  },
  {
    "path": "raspiCamSrv/home.py",
    "content": "from flask import (\n    current_app,\n    Blueprint,\n    Response,\n    flash,\n    g,\n    redirect,\n    render_template,\n    request,\n    url_for,\n)\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.auth import login_required, login_for_streaming\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg, ServerConfig\nfrom raspiCamSrv.version import version\nfrom raspiCamSrv.triggerHandler import TriggerHandler\nfrom libcamera import controls\nfrom _thread import get_ident\nimport subprocess\nfrom subprocess import CalledProcessError\nimport math\nimport os\nimport datetime\nimport time\nimport logging\n\nbp = Blueprint(\"home\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n\n@bp.route(\"/\")\n@login_required\ndef index():\n    logger.debug(\"Thread %s: In index\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.error = None\n    sc.getLatestVersion(now=True)\n    Camera().startLiveStream()\n    logger.debug(\"Thread %s: Camera instantiated\", get_ident())\n    if sc.noCamera == False:\n        sc.curMenu = \"live\"\n    else:\n        sc.curMenu = \"info\"\n    logger.debug(\"Thread %s: cp.hasFocus is %s\", get_ident(), cp.hasFocus)\n\n    sc.displayBufferCheck()\n\n    if sc.error:\n        msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        flash(msg)\n        if sc.error2:\n            flash(sc.error2)\n    if sc.noCamera == False:\n        return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n    else:\n        return redirect(url_for(\"info.main\"))\n\n\ndef gen(camera):\n    \"\"\"Video streaming generator function.\"\"\"\n    # logger.debug(\"Thread %s: In gen\", get_ident())\n    yield b\"--frame\\r\\n\"\n    while True:\n        frame, frameRaw = camera.get_frame()\n        if frame:\n            # logger.debug(\"Thread %s: gen - Got frame of length %s\", get_ident(), len(frame))\n            yield b\"Content-Type: image/jpeg\\r\\n\\r\\n\" + frame + b\"\\r\\n--frame\\r\\n\"\n\n\ndef gen2(camera):\n    \"\"\"Video streaming generator function.\"\"\"\n    # logger.debug(\"Thread %s: In gen\", get_ident())\n    yield b\"--frame\\r\\n\"\n    while True:\n        frame, frameRaw = camera.get_frame2()\n        if frame:\n            # logger.debug(\"Thread %s: gen - Got frame of length %s\", get_ident(), len(frame))\n            yield b\"Content-Type: image/jpeg\\r\\n\\r\\n\" + frame + b\"\\r\\n--frame\\r\\n\"\n\n\n@bp.route(\"/live_view_feed\")\n@login_required\ndef live_view_feed():\n    logger.debug(\n        \"Thread %s: In live_view_feed - client IP: %s\", get_ident(), request.remote_addr\n    )\n    sc = CameraCfg().serverConfig\n    sc.registerStreamingClient(request.remote_addr, \"live_view\", get_ident())\n    Camera().startLiveStream()\n    return Response(gen(Camera()), mimetype=\"multipart/x-mixed-replace; boundary=frame\")\n\n\n@bp.route(\"/video_feed\")\n@login_for_streaming\ndef video_feed():\n    logger.debug(\n        \"Thread %s: In video_feed - client IP: %s\", get_ident(), request.remote_addr\n    )\n    sc = CameraCfg().serverConfig\n    sc.registerStreamingClient(request.remote_addr, \"video_feed\", get_ident())\n    Camera().startLiveStream()\n    return Response(gen(Camera()), mimetype=\"multipart/x-mixed-replace; boundary=frame\")\n\n\n@bp.route(\"/video_feed2\")\n@login_for_streaming\ndef video_feed2():\n    logger.debug(\n        \"Thread %s: In video_feed2 - client IP: %s\", get_ident(), request.remote_addr\n    )\n    sc = CameraCfg().serverConfig\n    sc.registerStreamingClient(request.remote_addr, \"video_feed2\", get_ident())\n    Camera().startLiveStream2()\n    return Response(\n        gen2(Camera()), mimetype=\"multipart/x-mixed-replace; boundary=frame\"\n    )\n\n\n@bp.route(\"/photos/<photo>\")\n@login_required\ndef displayImage(photo: str):\n    logger.debug(\"In displayImage\")\n    logger.debug(\"photo=%s\", photo)\n    logger.debug(\"current_app.root_path=%s\", current_app.root_path)\n    fp = current_app.root_path + \"/photos/\" + photo\n    logger.debug(\"fp = %s\", fp)\n    return Response(fp, mimetype=\"image/jpg\")\n\n\n@bp.route(\"/focus_control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef focus_control():\n    logger.debug(\"In focus_control\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"focus\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if cp.hasFocus:\n            ctrls = {}\n            if request.form.get(\"include_afmode\") is None:\n                cc.include_afMode = False\n            else:\n                cc.include_afMode = True\n                afMode = int(request.form[\"afmode\"])\n                cc.afMode = afMode\n                ctrls[\"AfMode\"] = afMode\n\n            if request.form.get(\"include_lensposition\") is None:\n                cc.include_lensPosition = False\n            else:\n                cc.include_lensPosition = True\n                fDist = float(request.form[\"fdist\"])\n                cc.focalDistance = fDist\n                lensPosition = cc.lensPosition\n                ctrls[\"LensPosition\"] = lensPosition\n\n            if request.form.get(\"include_afmetering\") is None:\n                cc.include_afMetering = False\n            else:\n                cc.include_afMetering = True\n                afMetering = int(request.form[\"afmetering\"])\n                cc.afMetering = afMetering\n                ctrls[\"AfMetering\"] = afMetering\n\n            if request.form.get(\"include_afpause\") is None:\n                cc.include_afPause = False\n            else:\n                cc.include_afPause = True\n                afPause = int(request.form[\"afpause\"])\n                cc.afPause = afPause\n                ctrls[\"AfPause\"] = afPause\n\n            if request.form.get(\"include_afrange\") is None:\n                cc.include_afRange = False\n            else:\n                cc.include_afRange = True\n                afRange = int(request.form[\"afrange\"])\n                cc.afRange = afRange\n                ctrls[\"AfRange\"] = afRange\n\n            if request.form.get(\"include_afspeed\") is None:\n                cc.include_afSpeed = False\n            else:\n                cc.include_afSpeed = True\n                afSpeed = int(request.form[\"afspeed\"])\n                cc.afSpeed = afSpeed\n                ctrls[\"AfSpeed\"] = afSpeed\n\n            if request.form.get(\"include_afwindows\") is None:\n                cc.include_afWindows = False\n                afWindowsStr = \"()\"\n                cc.afWindowsStr = afWindowsStr\n                ctrls[\"AfWindows\"] = cc.afWindows\n            else:\n                cc.include_afWindows = True\n                afWindowsStr = request.form[\"afwindows\"]\n                cc.afWindowsStr = afWindowsStr\n                ctrls[\"AfWindows\"] = cc.afWindows\n                if len(cc.afWindows) == 0:\n                    cc.include_afWindows = False\n\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Focus handling changed for {sc.activeCameraInfo}\")\n            cfg.streamingCfgInvalid = True\n            Camera().applyControlsForLivestream()\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/trigger_autofocus\", methods=(\"GET\", \"POST\"))\n@login_required\ndef trigger_autofocus():\n    logger.debug(\"In trigger_autofocus\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"focus\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if cp.hasFocus:\n            if cc.afMode == controls.AfModeEnum.Auto:\n                Camera().applyControlsForAfCycle(cfg.liveViewConfig)\n                success = Camera().cam.autofocus_cycle()\n                if success:\n                    lp = Camera().getLensPosition()\n                    # lp = int(100 * lp) / 100\n                    if lp > 0:\n                        cc.lensPosition = lp\n                        cc.include_lensPosition = True\n                        cc.afMode = 0\n                        msg = \"Autofocus successful. See Focal Distance. Autofocus Mode set to 'Manual'.\"\n                        sc.unsavedChanges = True\n                        sc.addChangeLogEntry(\n                            f\"Autofocus triggered for {sc.activeCameraInfo}\"\n                        )\n                        cfg.streamingCfgInvalid = True\n                    else:\n                        msg = \"Camera returned LensPosition 0. Ignored\"\n                else:\n                    msg = \"Autofocus not successful\"\n            else:\n                msg = \"ERROR: Autofocus Mode must be set to 'Auto'!\"\n            flash(msg)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/set_zoom\", methods=(\"GET\", \"POST\"))\n@login_required\ndef set_zoom():\n    logger.debug(\"In set_zoom\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        step = int(request.form[\"zoomfactorstep\"])\n        sc.zoomFactorStep = step\n        logger.debug(\"sc.zoomFactorStep set to %s\", step)\n        if sc.isZoomModeDraw == True:\n            sc.isZoomModeDraw = False\n            scalerCropStr = request.form[\"scalercrop\"]\n            logger.debug(\"Form scalerCrop: %s\", scalerCropStr)\n            sc.scalerCropLiveViewStr = scalerCropStr\n            logger.debug(\"sc.scalerCropLiveView: %s\", sc.scalerCropLiveView)\n            cc.scalerCropStr = scalerCropStr\n            logger.debug(\"cc.scalerCrop: %s\", cc.scalerCrop)\n            cc.include_scalerCrop = True\n            Camera().applyControlsForLivestream()\n            time.sleep(0.5)\n            metadata = Camera.getMetaData()\n            sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n            #zoomFactor = sc.zoomFactorStep * math.floor(\n            #    (100 * cc.scalerCrop[2] / cp.pixelArraySize[0]) / sc.zoomFactorStep\n            #)\n            zoomFactor = round(100 * cc.scalerCrop[2] / cp.pixelArraySize[0], 3)\n            if zoomFactor <= 0:\n                zoomFactor = sc.zoomFactorStep\n            sc.zoomFactor = zoomFactor\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Zoom changed for {sc.activeCameraInfo}\")\n            cfg.streamingCfgInvalid = True\n            if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n                sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/zoom_in\", methods=(\"GET\", \"POST\"))\n@login_required\ndef zoom_in():\n    logger.debug(\"In zoom_in\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    logger.debug(\"cfg.liveViewConfig.controls=%s\", cfg.liveViewConfig.controls)\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        logger.debug(\"ScalerCrop old: %s\", cc.scalerCrop)\n        xCenter = cc.scalerCrop[0] + int(cc.scalerCrop[2] / 2)\n        yCenter = cc.scalerCrop[1] + int(cc.scalerCrop[3] / 2)\n        zfNext = sc.zoomFactor - sc.zoomFactorStep\n        msg = []\n        if zfNext < sc.zoomFactorStep:\n            msg.append(\"WARNING: Minimum zoom factor reached!\")\n            zfNext = sc.zoomFactorStep\n        width = int(sc.scalerCropDef[2] * zfNext / 100)\n        height = int(sc.scalerCropDef[3] * zfNext / 100)\n\n        if width < sc.scalerCropMin[2]:\n            height = int(height * sc.scalerCropMin[2] / width)\n            width = sc.scalerCropMin[2]\n            msg.append(\"WARNING: Smallest ScalerCrop width reached\")\n        if height < sc.scalerCropMin[3]:\n            width = int(width * sc.scalerCropMin[3] / height)\n            height = sc.scalerCropMin[3]\n            msg.append(\"WARNING: Smallest ScalerCrop height reached\")\n\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n\n        sccrop = (int(xCenter - width / 2), int(yCenter - height / 2), width, height)\n        sc.zoomFactor = zfNext\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        logger.debug(\"ScalerCrop new: %s\", cc.scalerCrop)\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Zoom changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\ndef checkScalerCrop(crop: tuple, range: tuple) -> tuple:\n    \"\"\"Check given cropping rectangle with respect to maximum rectangle\n\n    Params:\n        crop:   cropping rectangle to be tested (xOffset, yOffset, width, height)\n        range:  allowed range (xOffset, yOffset, width, height)\n\n    Return:\n        crop: cropping rectangle with initial dimensions but eventually adjusted offset\n        msg:  Message list with modifications made\n    \"\"\"\n    res = crop\n    msg = []\n    x0 = crop[0]\n    y0 = crop[1]\n    width = crop[2]\n    height = crop[3]\n    if x0 < range[0]:\n        msg.append(\"WARNING: left border reached\")\n        x0 = range[0]\n    if y0 < range[1]:\n        msg.append(\"WARNING: upper border reached\")\n        y0 = range[1]\n    if x0 + width > range[0] + range[2]:\n        msg.append(\"WARNING: right border reached\")\n        x0 = range[0] + range[2] - width\n    if y0 + height > range[1] + range[3]:\n        msg.append(\"WARNING: lower border reached\")\n        y0 = range[1] + range[3] - height\n    return ((x0, y0, crop[2], crop[3]), msg)\n\n\n@bp.route(\"/zoom_out\", methods=(\"GET\", \"POST\"))\n@login_required\ndef zoom_out():\n    logger.debug(\"In zoom_out\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        xCenter = cc.scalerCrop[0] + int(cc.scalerCrop[2] / 2)\n        yCenter = cc.scalerCrop[1] + int(cc.scalerCrop[3] / 2)\n        zfNext = sc.zoomFactor + sc.zoomFactorStep\n        msg0 = \"\"\n        if zfNext >= 100:\n            zfNext = 100\n            width = sc.scalerCropDef[2]\n            height = sc.scalerCropDef[3]\n            msg0 = \"WARNING: Maximum zoom reached\"\n        else:\n            width = int(sc.scalerCropDef[2] * zfNext / 100)\n            height = int(sc.scalerCropDef[3] * zfNext / 100)\n\n        ll = (xCenter - int(width / 2), yCenter - int(height / 2))\n        sccrop = (ll[0], ll[1], width, height)\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        if msg0 != \"\":\n            msg.append(msg0)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        sc.zoomFactor = zfNext\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Zoom changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/zoom_full\", methods=(\"GET\", \"POST\"))\n@login_required\ndef zoom_full():\n    logger.debug(\"In zoom_full\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.isZoomModeDraw = False\n        sc.zoomFactor = 100\n        width = sc.scalerCropDef[2]\n        height = sc.scalerCropDef[3]\n        xCenter = cc.scalerCrop[0] + int(cc.scalerCrop[2] / 2)\n        yCenter = cc.scalerCrop[1] + int(cc.scalerCrop[3] / 2)\n        xOffset = int(xCenter - width / 2)\n        yOffset = int(yCenter - height / 2)\n        sccrop = (xOffset, yOffset, width, height)\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Zoom changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/pan_up\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pan_up():\n    logger.debug(\"In pan_up\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        step = int((sc.scalerCropDef[2] * sc.zoomFactorStep) / 100)\n        yOffset = cc.scalerCrop[1] - step\n        sccrop = (cc.scalerCrop[0], yOffset, cc.scalerCrop[2], cc.scalerCrop[3])\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Pan changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/pan_left\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pan_left():\n    logger.debug(\"In pan_left\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        step = int((sc.scalerCropDef[2] * sc.zoomFactorStep) / 100)\n        xOffset = cc.scalerCrop[0] - step\n        sccrop = (xOffset, cc.scalerCrop[1], cc.scalerCrop[2], cc.scalerCrop[3])\n        logger.debug(\"pan_left - scalarCropDef   : %s\", sc.scalerCropDef)\n        logger.debug(\"pan_left - scalarCrop old  : %s\", cc.scalerCrop)\n        logger.debug(\"pan_left - scalarCrop Max  : %s\", sc.scalerCropMax)\n        logger.debug(\"pan_left - step: %s xOffset: %s\", step, xOffset)\n        logger.debug(\"pan_left - scalarCrop Init : %s\", sccrop)\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        logger.debug(\"pan_left - scalarCrop Final: %s\", sccrop)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Pan changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/pan_center\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pan_center():\n    logger.debug(\"In pan_center\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        logger.debug(\"pan_center scalerCropDef: %s\", sc.scalerCropDef)\n        logger.debug(\"pan_center scalerCrop   : %s\", cc.scalerCrop)\n        xOffset = int(\n            sc.scalerCropDef[0] + sc.scalerCropDef[2] / 2 - cc.scalerCrop[2] / 2\n        )\n        yOffset = int(\n            sc.scalerCropDef[1] + sc.scalerCropDef[3] / 2 - cc.scalerCrop[3] / 2\n        )\n        logger.debug(\"pan_center xOffset: %s, yOffset: %s\", xOffset, yOffset)\n        sccrop = (xOffset, yOffset, cc.scalerCrop[2], cc.scalerCrop[3])\n        logger.debug(\"pan_center - sccrop initial: %s\", sccrop)\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        logger.debug(\"pan_center - sccrop final  : %s\", sccrop)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Pan changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/pan_right\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pan_right():\n    logger.debug(\"In pan_right\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        step = int((sc.scalerCropDef[2] * sc.zoomFactorStep) / 100)\n        xOffset = cc.scalerCrop[0] + step\n        sccrop = (xOffset, cc.scalerCrop[1], cc.scalerCrop[2], cc.scalerCrop[3])\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Pan changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/pan_down\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pan_down():\n    logger.debug(\"In pan_down\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        step = int((sc.scalerCropDef[2] * sc.zoomFactorStep) / 100)\n        yOffset = cc.scalerCrop[1] + step\n        sccrop = (cc.scalerCrop[0], yOffset, cc.scalerCrop[2], cc.scalerCrop[3])\n        (sccrop, msg) = checkScalerCrop(sccrop, sc.scalerCropMax)\n        if len(msg) > 0:\n            for m in msg:\n                flash(m)\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        if cc.scalerCrop != sc.scalerCropDef:\n            cc.include_scalerCrop = True\n        else:\n            cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Pan changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/zoom_default\", methods=(\"GET\", \"POST\"))\n@login_required\ndef zoom_default():\n    logger.debug(\"In zoom_default\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.isZoomModeDraw = False\n        sc.zoomFactor = 100\n        sccrop = sc.scalerCropDef\n        cc.scalerCrop = sccrop\n        cc.include_scalerCrop = True\n        Camera().applyControlsForLivestream()\n        time.sleep(0.5)\n        cc.include_scalerCrop = False\n        metadata = Camera().getMetaData()\n        sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Image section set to default for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n            sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/zoom_draw\", methods=(\"GET\", \"POST\"))\n@login_required\ndef zoom_draw():\n    logger.debug(\"In zoom_draw\")\n    g.hostname = request.host\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"zoom\"\n    if request.method == \"POST\":\n        sc.isZoomModeDraw = True\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/ae_control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef ae_control():\n    logger.debug(\"In ae_control\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"autoexposure\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if request.form.get(\"include_aeconstraintmode\") is None:\n            cc.include_aeConstraintMode = False\n        else:\n            cc.include_aeConstraintMode = True\n            aeConstraintMode = int(request.form[\"aeconstraintmode\"])\n            cc.aeConstraintMode = aeConstraintMode\n\n        if request.form.get(\"include_aeenable\") is None:\n            cc.include_aeEnable = False\n        else:\n            cc.include_aeEnable = True\n            aeEnable = not request.form.get(\"aeenable\") is None\n            cc.aeEnable = aeEnable\n\n        if request.form.get(\"include_aeexposuremode\") is None:\n            cc.include_aeExposureMode = False\n        else:\n            cc.include_aeExposureMode = True\n            aeExposureMode = int(request.form[\"aeexposuremode\"])\n            cc.aeExposureMode = aeExposureMode\n\n        if request.form.get(\"include_aemeteringmode\") is None:\n            cc.include_aeMeteringMode = False\n        else:\n            cc.include_aeMeteringMode = True\n            aeMeteringMode = int(request.form[\"aemeteringmode\"])\n            cc.aeMeteringMode = aeMeteringMode\n\n        if cp.hasFlicker:\n            if request.form.get(\"include_aeflickermode\") is None:\n                cc.include_aeFlickerMode = False\n            else:\n                cc.include_aeFlickerMode = True\n                aeFlickerMode = int(request.form[\"aeflickermode\"])\n                cc.aeFlickerMode = aeFlickerMode\n\n            if request.form.get(\"include_aeflickerperiod\") is None:\n                cc.include_aeFlickerPeriod = False\n            else:\n                cc.include_aeFlickerPeriod = True\n                aeFlickerPeriod = int(request.form[\"aeflickerperiod\"])\n                cc.aeFlickerPeriod = aeFlickerPeriod\n\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(\n            f\"Auto-Exposure settings changed for {sc.activeCameraInfo}\"\n        )\n        cfg.streamingCfgInvalid = True\n        Camera().applyControlsForLivestream()\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/exposure_control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef exposure_control():\n    logger.debug(\"In exposure_control\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"exposure\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if request.form.get(\"include_analoguegain\") is None:\n            cc.include_analogueGain = False\n        else:\n            cc.include_analogueGain = True\n            analogueGain = float(request.form[\"analoguegain\"])\n            cc.analogueGain = analogueGain\n\n        if request.form.get(\"include_colourgains\") is None:\n            cc.include_colourGains = False\n        else:\n            cc.include_colourGains = True\n            colourGainRed = float(request.form[\"colourgainred\"])\n            colourGainBlue = float(request.form[\"colourgainblue\"])\n            colourGains = (colourGainRed, colourGainBlue)\n            cc.colourGains = colourGains\n\n        if request.form.get(\"include_exposuretime\") is None:\n            cc.include_exposureTime = False\n        else:\n            cc.include_exposureTime = True\n            exposureTimeSec = float(request.form[\"exposuretimesec\"])\n            cc.exposureTimeSec = exposureTimeSec\n            exposureTime = cc.exposureTime\n\n        if request.form.get(\"include_exposurevalue\") is None:\n            cc.include_exposureValue = False\n        else:\n            cc.include_exposureValue = True\n            exposureValue = float(request.form[\"exposurevalue\"])\n            cc.exposureValue = exposureValue\n\n        if request.form.get(\"include_framedurationlimits\") is None:\n            cc.include_frameDurationLimits = False\n        else:\n            cc.include_frameDurationLimits = True\n            frameDurationLimitMax = int(request.form[\"framedurationlimitmax\"])\n            frameDurationLimitMin = int(request.form[\"framedurationlimitmin\"])\n            frameDurationLimits = (frameDurationLimitMax, frameDurationLimitMin)\n            cc.frameDurationLimits = frameDurationLimits\n\n        if cp.hasHdr:\n            if request.form.get(\"include_hdrmode\") is None:\n                cc.include_hdrMode = False\n            else:\n                cc.include_hdrMode = True\n                hdrMode = int(request.form[\"hdrmode\"])\n                cc.hdrMode = hdrMode\n\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Exposure settings changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        Camera().applyControlsForLivestream()\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/image_control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef image_control():\n    logger.debug(\"In image_control\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"image\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if request.form.get(\"include_noisereductionmode\") is None:\n            cc.include_noiseReductionMode = False\n        else:\n            cc.include_noiseReductionMode = True\n            noiseReductionMode = int(request.form[\"noisereductionmode\"])\n            cc.noiseReductionMode = noiseReductionMode\n\n        if request.form.get(\"include_saturation\") is None:\n            cc.include_saturation = False\n        else:\n            cc.include_saturation = True\n            saturation = float(request.form[\"saturation\"])\n            cc.saturation = saturation\n\n        if request.form.get(\"include_sharpness\") is None:\n            cc.include_sharpness = False\n        else:\n            cc.include_sharpness = True\n            sharpness = float(request.form[\"sharpness\"])\n            cc.sharpness = sharpness\n\n        if request.form.get(\"include_awbenable\") is None:\n            cc.include_awbEnable = False\n        else:\n            cc.include_awbEnable = True\n            awbEnable = not request.form.get(\"awbenable\") is None\n            cc.awbEnable = awbEnable\n\n        if request.form.get(\"include_awbmode\") is None:\n            cc.include_awbMode = False\n        else:\n            cc.include_awbMode = True\n            awbMode = int(request.form[\"awbmode\"])\n            cc.awbMode = awbMode\n\n        if request.form.get(\"include_contrast\") is None:\n            cc.include_contrast = False\n        else:\n            cc.include_contrast = True\n            contrast = float(request.form[\"contrast\"])\n            cc.contrast = contrast\n\n        if request.form.get(\"include_brightness\") is None:\n            cc.include_brightness = False\n        else:\n            cc.include_brightness = True\n            brightness = float(request.form[\"brightness\"])\n            cc.brightness = brightness\n\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Image settings changed for {sc.activeCameraInfo}\")\n        cfg.streamingCfgInvalid = True\n        Camera().applyControlsForLivestream()\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/meta_clear\", methods=(\"GET\", \"POST\"))\n@login_required\ndef meta_clear():\n    logger.debug(\"In meta_clear\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayMeta = None\n        sc.displayPhoto = None\n        sc.displayHistogram = None\n        sc.displayMetaFirst = 0\n        sc.displayMetaLast = 999\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/meta_prev\", methods=(\"GET\", \"POST\"))\n@login_required\ndef meta_prev():\n    logger.debug(\"In meta_prev\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayMetaFirst -= 10\n        if sc.displayMetaFirst < 0:\n            sc.displayMetaFirst = 0\n        sc.displayMetaLast = sc.displayMetaFirst + 10\n        if sc.displayMetaLast > len(sc.displayMeta):\n            sc.displayMetaLast = 999\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/meta_next\", methods=(\"GET\", \"POST\"))\n@login_required\ndef meta_next():\n    logger.debug(\"In meta_next\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayMetaFirst += 10\n        sc.displayMetaLast = sc.displayMetaFirst + 10\n        if sc.displayMetaLast > len(sc.displayMeta):\n            sc.displayMetaLast = 999\n            sc.displayMetaFirst = len(sc.displayMeta) - 10\n            if sc.displayMetaFirst < 0:\n                sc.displayMetaFirst = 0\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n@bp.route(\"/photoBuffer_add\", methods=(\"GET\", \"POST\"))\n@login_required\ndef photoBuffer_add():\n    logger.debug(\"In photoBuffer_add\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayBufferAdd()\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/photoBuffer_remove\", methods=(\"GET\", \"POST\"))\n@login_required\ndef photoBuffer_remove():\n    logger.debug(\"In photoBuffer_remove\")\n    g.hostname = request.host\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayBufferRemove()\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/photoBuffer_prev\", methods=(\"GET\", \"POST\"))\n@login_required\ndef photoBuffer_prev():\n    logger.debug(\"In photoBuffer_prev\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayBufferPrev()\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/photoBuffer_next\", methods=(\"GET\", \"POST\"))\n@login_required\ndef photoBuffer_next():\n    logger.debug(\"In photoBuffer_next\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayBufferNext()\n        if sc.displayContent == \"hist\":\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/show_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef show_photo():\n    logger.debug(\"In show_photo\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.isDisplayHidden = False\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/hide_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef hide_photo():\n    logger.debug(\"In hide_photo\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.isDisplayHidden = True\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/clear_buffer\", methods=(\"GET\", \"POST\"))\n@login_required\ndef clear_buffer():\n    logger.debug(\"In clear_buffer\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayBufferClear()\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/take_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef take_photo():\n    logger.debug(\"Thread %s: In take_photo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image %s\", filename)\n        fp = Camera().takeImage(filename)\n        if not sc.error:\n            logger.debug(\"take_photo - success\")\n            logger.debug(\"take_photo - sc.displayContent: %s\", sc.displayContent)\n            if sc.displayContent == \"hist\":\n                logger.debug(\n                    \"take_photo - sc.displayHistogram: %s\", sc.displayHistogram\n                )\n                if sc.displayHistogram is None:\n                    logger.debug(\"take_photo - sc.displayPhoto: %s\", sc.displayPhoto)\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/take_raw_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef take_raw_photo():\n    logger.debug(\"Thread %s: In take_raw_photo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        if sc.activeCameraIsUsb == False:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n        else:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".tiff\"\n        logger.debug(\"Saving raw image %s\", filenameRaw)\n        fp = Camera().takeRawImage(filenameRaw, filename)\n        if not sc.error:\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/record_video\", methods=(\"GET\", \"POST\"))\n@login_required\ndef record_video():\n    logger.debug(\"Thread %s: In record_video\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording a video %s\", filenameVid)\n        fp = Camera().recordVideo(filenameVid, filename)\n        # TODO: Check sleep time. This might lead to errors when stopping video within that time\n        time.sleep(4)\n        if not sc.error:\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            # Check whether video is being recorded\n            if Camera.isVideoRecording():\n                logger.debug(\"Video recording started\")\n                sc.isVideoRecording = True\n                if sc.recordAudio:\n                    sc.isAudioRecording = True\n                msg = \"Video saved as \" + fp\n                flash(msg)\n            else:\n                logger.debug(\"Video recording did not start\")\n                sc.isVideoRecording = False\n                sc.isAudioRecording = False\n                msg = \"Video recording failed. Requested resolution too high \"\n                flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/stop_recording\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_recording():\n    logger.debug(\"Thread %s: In stop_recording\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        logger.debug(\"Requesting video recording to stop\")\n        Camera().stopVideoRecording()\n        sc.isVideoRecording = False\n        sc.isAudioRecording = False\n        # sleep a little bit to avoid race condition with restoreLiveStream in video thread\n        time.sleep(2)\n        msg = \"Video recording stopped\"\n        flash(msg)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\ndef generateHistogram(sc: ServerConfig):\n    \"\"\"Generate a histogram for the specified image\"\"\"\n    logger.debug(\"In generateHistogram \")\n    import cv2\n    import numpy as np\n    from matplotlib import pyplot as plt\n    import matplotlib\n\n    matplotlib.use(\"agg\")\n\n    source = sc.photoRoot + \"/\" + sc.displayPhoto\n    destPath = sc.photoRoot + \"/\" + sc.cameraHistogramSubPath\n    if not os.path.exists(destPath):\n        os.makedirs(destPath)\n        logger.debug(\"generateHistogram - Created directory %s\", destPath)\n    file = sc.displayFile\n    if not file.endswith(\".jpg\"):\n\n        file = file[: file.find(\".\")] + \".jpg\"\n    dest = destPath + \"/\" + file\n    try:\n        plt.figure()\n        img = cv2.imread(source)\n        color = (\"b\", \"g\", \"r\")\n        for i, col in enumerate(color):\n            histr = cv2.calcHist([img], [i], None, [256], [0, 256], accumulate=False)\n            plt.plot(histr, color=col)\n            plt.xlim([0, 256])\n        plt.savefig(dest)\n        sc.displayHistogram = sc.cameraHistogramSubPath + \"/\" + file\n        logger.debug(\n            \"In generateHistogram - Histogram success: %s\", sc.displayHistogram\n        )\n        plt.close()\n    except Exception as e:\n        sc.displayHistogram = \"histogramfailed.jpg\"\n        logger.error(\"Histogram generation error: %s\", e)\n\n\n@bp.route(\"/show_histogram\", methods=(\"GET\", \"POST\"))\n@login_required\ndef show_histogram():\n    logger.debug(\"In show_histogram\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        if sc.useHistograms:\n            if sc.displayHistogram is None:\n                if sc.displayPhoto:\n                    generateHistogram(sc)\n            sc.displayContent = \"hist\"\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/show_metadata\", methods=(\"GET\", \"POST\"))\n@login_required\ndef show_metadata():\n    logger.debug(\"In show_metadata\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        sc.displayContent = \"meta\"\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n@bp.route(\"/media-viewer\")\n@login_required\ndef media_viewer():\n    src = request.args.get(\"src\")\n    media_type = request.args.get(\"type\", \"image\")\n\n    filename = os.path.basename(src) if src else \"\"\n\n    return render_template(\n        \"media_viewer.html\",\n        src=src,\n        media_type=media_type,\n        filename=filename\n    )\n\n@bp.route(\"/live_direct_control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef live_direct_control():\n    logger.debug(\"In live_direct_control\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.getLatestVersion(now=True)\n    return render_template(\"home/liveDirectPanel.html\", cc=cc, sc=sc, cp=cp)\n\n@bp.route(\"/dc_set_Sharpness\", methods=[\"POST\"])\n@login_required\ndef dc_set_Sharpness():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_Sharpness - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 32.0\n        default = 1.0\n    else:\n        min = float(cc.usbCamControls[\"Sharpness\"][\"min\"])\n        max = float(cc.usbCamControls[\"Sharpness\"][\"max\"])\n        default = float(cc.usbCamControls[\"Sharpness\"][\"default\"])\n    cc.sharpness = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_Contrast\", methods=[\"POST\"])\n@login_required\ndef dc_set_Contrast():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_Contrast - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 32.0\n        default = 1.0\n    else:\n        min = float(cc.usbCamControls[\"Contrast\"][\"min\"])\n        max = float(cc.usbCamControls[\"Contrast\"][\"max\"])\n        default = float(cc.usbCamControls[\"Contrast\"][\"default\"])\n    cc.contrast = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_Saturation\", methods=[\"POST\"])\n@login_required\ndef dc_set_Saturation():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_Saturation - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 32.0\n        default = 1.0\n    else:\n        min = float(cc.usbCamControls[\"Saturation\"][\"min\"])\n        max = float(cc.usbCamControls[\"Saturation\"][\"max\"])\n        default = float(cc.usbCamControls[\"Saturation\"][\"default\"])\n    cc.saturation = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_Brightness\", methods=[\"POST\"])\n@login_required\ndef dc_set_Brightness():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_Brightness - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = -1.0\n        max = 1.0\n        default = 0.0\n    else:\n        min = float(cc.usbCamControls[\"Brightness\"][\"min\"])\n        max = float(cc.usbCamControls[\"Brightness\"][\"max\"])\n        default = float(cc.usbCamControls[\"Brightness\"][\"default\"])\n    cc.brightness = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_exposureTimeSec\", methods=[\"POST\"])\n@login_required\ndef dc_set_exposureTimeSec():\n    logger.debug(\"In dc_set_exposureTimeSec\")\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"dc_set_exposureTimeSec - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 10.0\n        default = 0.0\n    else:\n        min = float(cc.usbCamControls[\"ExposureTime\"][\"min\"])\n        max = float(cc.usbCamControls[\"ExposureTime\"][\"max\"])\n        default = float(cc.usbCamControls[\"ExposureTime\"][\"default\"])\n    cc.exposureTimeSec = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_exposureValue\", methods=[\"POST\"])\n@login_required\ndef dc_set_exposureValue():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_exposureValue - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = -8.0\n        max = 8.0\n        default = 0.0\n    else:\n        min = float(cc.usbCamControls[\"ExposureValue\"][\"min\"])\n        max = float(cc.usbCamControls[\"ExposureValue\"][\"max\"])\n        default = float(cc.usbCamControls[\"ExposureValue\"][\"default\"])\n    cc.exposureValue = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_AnalogueGain\", methods=[\"POST\"])\n@login_required\ndef dc_set_AnalogueGain():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_AnalogueGain - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 1.0\n        max = 99.0\n        default = 1.0\n    else:\n        min = float(cc.usbCamControls[\"AnalogueGain\"][\"min\"])\n        max = float(cc.usbCamControls[\"AnalogueGain\"][\"max\"])\n        default = float(cc.usbCamControls[\"AnalogueGain\"][\"default\"])\n    cc.analogueGain = sc.sliderPosToCtrlVal(min, max, default, spos)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_ColourGainRed\", methods=[\"POST\"])\n@login_required\ndef dc_set_ColourGainRed():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_ColourGainRed - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 32.0\n        default = 0.0\n    else:\n        min = float(cc.usbCamControls[\"ColourGainRed\"][\"min\"])\n        max = float(cc.usbCamControls[\"ColourGainRed\"][\"max\"])\n        default = float(cc.usbCamControls[\"ColourGainRed\"][\"default\"])\n    cc.colourGains = (sc.sliderPosToCtrlVal(min, max, default, spos), cc.colourGains[1])\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_ColourGainBlue\", methods=[\"POST\"])\n@login_required\ndef dc_set_ColourGainBlue():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_ColourGainBlue - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.0\n        max = 32.0\n        default = 0.0\n    else:\n        min = float(cc.usbCamControls[\"ColourGainBlue\"][\"min\"])\n        max = float(cc.usbCamControls[\"ColourGainBlue\"][\"max\"])\n        default = float(cc.usbCamControls[\"ColourGainBlue\"][\"default\"])\n    cc.colourGains = (cc.colourGains[0], sc.sliderPosToCtrlVal(min, max, default, spos))\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_FocalDistance\", methods=[\"POST\"])\n@login_required\ndef dc_set_FocalDistance():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_FocalDistance - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    if sc.activeCameraIsUsb == False:\n        min = 0.001\n        max = 999.0\n    else:\n        min = float(cc.usbCamControls[\"LensPosition\"][\"min\"])\n        max = float(cc.usbCamControls[\"LensPosition\"][\"max\"])\n    cc.focalDistance = round((max * spos**3.0), 3)\n    cfg.streamingCfgInvalid = True\n    Camera().applyControlsForLivestream()\n    return '', 204\n\n@bp.route(\"/dc_set_ZoomFactor\", methods=[\"POST\"])\n@login_required\ndef dc_set_ZoomFactor():\n    data = request.get_json()\n    spos = float(data[\"value\"])\n    logger.debug(\"In dc_set_ZoomFactor - data: %s\", spos)\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cp = cfg.cameraProperties\n\n    zoomFactor = spos\n\n    logger.debug(\"ScalerCrop old: %s\", cc.scalerCrop)\n    xCenter = cc.scalerCrop[0] + int(cc.scalerCrop[2] / 2)\n    yCenter = cc.scalerCrop[1] + int(cc.scalerCrop[3] / 2)\n    width = int(sc.scalerCropDef[2] * zoomFactor / 100)\n    height = int(sc.scalerCropDef[3] * zoomFactor / 100)\n\n    if width < sc.scalerCropMin[2]:\n        height = int(height * sc.scalerCropMin[2] / width)\n        width = sc.scalerCropMin[2]\n    if height < sc.scalerCropMin[3]:\n        width = int(width * sc.scalerCropMin[3] / height)\n        height = sc.scalerCropMin[3]\n\n    if width > cp.pixelArraySize[0]:\n        width = cp.pixelArraySize[0]\n    if height > cp.pixelArraySize[1]:\n        height = cp.pixelArraySize[1]\n\n    x0 = int(xCenter - width / 2)\n    y0 = int(yCenter - height / 2)\n\n    if x0 < 0:\n        x0 = 0\n    if y0 < 0:\n        y0 = 0\n    if x0 + width > cp.pixelArraySize[0]:\n        x0 = cp.pixelArraySize[0] - width\n    if y0 + height > cp.pixelArraySize[1]:\n        y0 = cp.pixelArraySize[1] - height\n\n    sccrop = (x0, y0, width, height)\n    sc.zoomFactor = zoomFactor\n    cc.scalerCrop = sccrop\n    cc.include_scalerCrop = True\n    logger.debug(\"ScalerCrop new: %s\", cc.scalerCrop)\n    Camera().applyControlsForLivestream()\n    time.sleep(0.5)\n    if cc.scalerCrop != sc.scalerCropDef:\n        cc.include_scalerCrop = True\n    else:\n        cc.include_scalerCrop = False\n    metadata = Camera().getMetaData()\n    sc.scalerCropLiveView = metadata[\"ScalerCrop\"]\n    sc.unsavedChanges = True\n    sc.addChangeLogEntry(f\"Zoom changed for {sc.activeCameraInfo}\")\n    cfg.streamingCfgInvalid = True\n    if tc.checkRoisAgainstScalerCropLiveView(sc.scalerCropLiveView) == False:\n        sc.addChangeLogEntry(f\"RoIs or RoNis adjusted for {sc.activeCameraInfo}\")\n    return '', 204\n\n\n@bp.route(\"/live_do_action/<row>/<col>\", methods=(\"GET\", \"POST\"))\n@login_required\ndef live_do_action(row:None, col=None):\n    logger.debug(\"In live_do_action - row=%s, col=%s\", row, col)\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"control\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        msg = \"\"\n        r = int(row)\n        c = int(col)\n        btn = sc.lButtons[r][c]\n        action = btn.buttonAction\n        \n        msg = f\"Action successfully executed: {action}.\"\n        result = None\n        if action != \"\":\n            msg = TriggerHandler.doAction(action)\n        else:\n            msg = \"No Action executed\"\n        \n        if msg != \"\":\n            flash(msg)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n\n\n@bp.route(\"/live_execute/<row>/<col>\", methods=(\"GET\", \"POST\"))\n@login_required\ndef live_execute(row:None, col=None):\n    logger.debug(\"In live_execute - row=%s, col=%s\", row, col)\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cc = cfg.controls\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sc.lastLiveTab = \"control\"\n    sc.getLatestVersion(now=True)\n    if request.method == \"POST\":\n        msg = \"\"\n        r = int(row)\n        c = int(col)\n        btn = sc.lButtons[r][c]\n        cmd = btn.buttonExec\n        args = cmd.rsplit(\" \")\n        sc.vButtonArgs = args\n        \n        msg = f\"Command successfully executed: {cmd}.\"\n        result = None\n        if cmd != \"\":\n            try:\n                result = subprocess.run(args, capture_output=True, text=True, check=True)            \n            except CalledProcessError as e:\n                msg = f\"Command executed with error: {e}.\"\n            except Exception as e:\n                msg = f\"Command executed with error: {e}.\"\n        else:\n            msg = \"No command executed\"\n        \n        if msg != \"\":\n            flash(msg)\n    return render_template(\"home/index.html\", cc=cc, sc=sc, cp=cp)\n"
  },
  {
    "path": "raspiCamSrv/images.py",
    "content": "from flask import Blueprint, Response, flash, g, redirect, render_template, request, url_for\nfrom flask import send_file, send_from_directory\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camCfg import CameraCfg\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.version import version\nimport os\nfrom datetime import datetime, timedelta\nfrom io import BytesIO\nfrom zipfile import ZipFile\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\nbp = Blueprint(\"images\", __name__)\n\nlogger = logging.getLogger(__name__)\n\ndef getFileList() -> list:\n    logger.debug(\"In images/getFileList\")\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    # Get the filelist\n    fp = sc.photoRoot + \"/\" + \"photos/\" + \"camera_\" + str(sc.pvCamera)    \n    fl = os.listdir(fp)\n    # Sort reverse\n    fl.sort(reverse=True)\n    logger.debug(\"%s files found in %s\", len(fl), fp)\n    \n    dl = []\n    p = 1\n    cnt = 0\n    for file in fl:\n        name, ext = os.path.splitext(file)\n        path = \"photos/\" + \"camera_\" + str(sc.pvCamera) + \"/\" + file\n        fpath = os.path.join(fp, file)\n        if ext.lower() != \".dng\" \\\n        and ext.lower() != \".tiff\" \\\n        and ext.lower() != \".mp4\" \\\n        and ext.lower() != \".h264\" \\\n        and (not os.path.isdir(fpath)):\n            nameOK = False\n            try:\n                dat =  datetime.strptime(name, \"%Y%m%d_%H%M%S\")\n                nameOK = True\n            except ValueError:\n                nameOK = False\n            if nameOK == True:\n                include = False\n                if dat >= sc.pvFrom and dat <= sc.pvTo:\n                    include = True\n            else:\n                include = False\n            if include == True:\n                cnt += 1\n                entry = {}\n                entry[\"sel\"] = False\n                entry[\"path\"] = path\n                entry[\"file\"] = file\n                entry[\"name\"] = name\n                entry[\"type\"] = \"photo\"\n                entry[\"detailPath\"] = path\n                dl.append(entry)\n    logger.debug(\"%s distinct files in selected range\", cnt)\n\n    for file in fl:\n        name, ext = os.path.splitext(file)\n        path = \"photos/\" + \"camera_\" + str(sc.pvCamera) + \"/\" + file\n        if ext.lower() == \".dng\" \\\n        or ext.lower() == \".tiff\" \\\n        or ext.lower() == \".mp4\" \\\n        or ext.lower() == \".h264\":\n            # For raw and video, search the placeholder and update\n            for entry in dl:\n                if entry[\"name\"] == name:\n                    if ext.lower() == \".dng\" \\\n                    or ext.lower() == \".tiff\":\n                        entry[\"type\"] = \"raw\"\n                        entry[\"file\"] = file\n                    else:\n                        entry[\"type\"] = \"video\"\n                        entry[\"file\"] = file\n                        if ext.lower() == \".mp4\":\n                            entry[\"detailPath\"] = path\n                    break\n    sc.pvList = dl\n\n@bp.route(\"/images\")\n@login_required\ndef main():\n    logger.debug(\"In images/main\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if sc.pvCamera is None:\n        sc.pvCamera = sc.activeCamera\n    if sc.pvFrom is None:\n        logger.debug(\"images/main - Setting sc.pvFrom to current date\")\n        pvFrom = datetime.now()\n        sc.pvFrom = datetime(year=pvFrom.year, month=pvFrom.month, day=pvFrom.day, hour=0, minute=0, second=0)\n    if sc.pvTo is None:\n        logger.debug(\"images/main - Setting sc.pvTo to current date\")\n        pvTo = datetime.now()\n        sc.pvTo = datetime(year=pvTo.year, month=pvTo.month, day=pvTo.day, hour=23, minute=59, second=59)\n    getFileList()\n    l = len(sc.pvList)\n    if l > 0:\n        msg = f'{l} distinct media files found in specified range (placeholders not included)'\n    else:\n        msg = f'No media files found in specified range'\n    flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/control\", methods=(\"GET\", \"POST\"))\n@login_required\ndef control():\n    logger.debug(\"In images/control\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        pvCamera = request.form[\"camera\"]\n        if pvCamera == \"S\":\n            sc.pvCamera = \"S\"\n        else:\n            sc.pvCamera = int(request.form[\"camera\"])\n        pvFromStr = request.form.get(\"pvfrom\")\n        sc.pvFromStr = pvFromStr\n        pvToStr = request.form.get(\"pvto\")\n        sc.pvToStr = pvToStr\n        getFileList() \n    l = len(sc.pvList)\n    if l > 0:\n        msg = f'{l} distinct media files found in specified range (placeholders not included)'\n    else:\n        msg = f'No media files found in specified range'\n    flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/today\", methods=(\"GET\", \"POST\"))\n@login_required\ndef today():\n    logger.debug(\"In images/today\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        pvFrom = datetime.now()\n        sc.pvFrom = datetime(year=pvFrom.year, month=pvFrom.month, day=pvFrom.day, hour=0, minute=0, second=0)\n        pvTo = datetime.now()\n        sc.pvTo = datetime(year=pvTo.year, month=pvTo.month, day=pvTo.day, hour=23, minute=59, second=59)\n        getFileList()\n    l = len(sc.pvList)\n    if l > 0:\n        msg = f'{l} distinct media files found in specified range (placeholders not included)'\n    else:\n        msg = f'No media files found in specified range'\n    flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/all\", methods=(\"GET\", \"POST\"))\n@login_required\ndef all():\n    logger.debug(\"In images/all\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        sc.pvFrom = datetime(year=1970, month=1, day=1, hour=0, minute=0, second=0)\n        pvTo = datetime.now()\n        sc.pvTo = datetime(year=pvTo.year, month=pvTo.month, day=pvTo.day, hour=23, minute=59, second=59)\n        getFileList()\n    l = len(sc.pvList)\n    if l > 0:\n        msg = f'{l} distinct media files found in specified range (placeholders not included)'\n    else:\n        msg = f'No media files found in specified range'\n    flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/select_all\", methods=(\"GET\", \"POST\"))\n@login_required\ndef select_all():\n    logger.debug(\"In images/select_all\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        for entry in sc.pvList:\n            entry[\"sel\"] = True\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/deselect_all\", methods=(\"GET\", \"POST\"))\n@login_required\ndef deselect_all():\n    logger.debug(\"In images/deselect_all\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        for entry in sc.pvList:\n            entry[\"sel\"] = False\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/select\", methods=(\"GET\", \"POST\"))\n@login_required\ndef select():\n    logger.debug(\"In images/select\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        logger.debug(\"images/select - selecting\")\n        for entry in sc.pvList:\n            name = entry[\"name\"]\n            id = \"photo_\" + name\n            sel = not request.form.get(id) is None\n            entry[\"sel\"] = sel\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/delete_selected\", methods=(\"GET\", \"POST\"))\n@login_required\ndef delete_selected():\n    logger.debug(\"In images/delete_selected\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        logger.debug(\"images/delete_selected - deleting\")\n        cnt = 0\n        cntd = 0\n        cntErr = 0\n        for entry in sc.pvList:\n            name = entry[\"name\"]\n            sel = entry[\"sel\"]\n            if sel == True:\n                cntd += 1\n                fp = sc.photoRoot + \"/\" + \"photos/\" + \"camera_\" + str(sc.pvCamera) + \"/\"\n                fph = sc.photoRoot + \"/\" + \"photos/\" + \"camera_\" + str(sc.pvCamera) + \"/hist/\"\n                # Delete histogram if it exists\n                fnh = fph + name + \".jpg\"\n                cnt, cntErr = deleteFile(fnh, cnt, cntErr)\n                if entry[\"type\"] == \"raw\":\n                    # Detete raw image\n                    fnr = fp + name + \".dng\"\n                    cnt, cntErr = deleteFile(fnr, cnt, cntErr)\n                    fnr = fp + name + \".tiff\"\n                    cnt, cntErr = deleteFile(fnr, cnt, cntErr)\n                if entry[\"type\"] == \"video\":\n                    # Detete video\n                    fnv = fp + name + \".mp4\"\n                    cnt, cntErr = deleteFile(fnv, cnt, cntErr)\n                    fnv = fp + name + \".h264\"\n                    cnt, cntErr = deleteFile(fnv, cnt, cntErr)\n                # Delete photo or placeholder\n                fn = sc.photoRoot + \"/\" + entry[\"path\"]\n                cnt, cntErr = deleteFile(fn, cnt, cntErr)\n                \n        # Clear displaybuffer\n        if cntd > 0:\n            sc.displayBufferCheck()\n\n        getFileList() \n                \n        msg = f\"{cntd} distinct media removed: {cnt} successful deletions, {cntErr} failed deletions\"\n        flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\ndef deleteFile(fp: str, cntOK, cntErr):\n    logger.debug(\"images/deleteFile - trying : %s\", fp)\n    if os.path.exists(fp):\n        try:\n            os.remove(fp)\n            logger.debug(\"images/deleteFile - deleted: %s\", fp)\n            cntOK += 1\n        except:\n            cntErr += 1\n    return cntOK, cntErr\n\n\n@bp.route(\"/download_selected\", methods=(\"GET\", \"POST\"))\n@login_required\ndef download_selected():\n    logger.debug(\"In images/download_selected\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    cs = cfg.cameras\n    sc.curMenu = \"photos\"\n    if request.method == \"POST\":\n        logger.debug(\"images/download_selected - Preparing download\")\n        # Setup filelist for compression\n        fp = sc.photoRoot + \"/\" + \"photos/\" + \"camera_\" + str(sc.pvCamera) + \"/\"\n        zl = []\n        cnt = 0\n        cntPhoto = 0\n        cntRaw = 0\n        cntVideo = 0\n        for entry in sc.pvList:\n            name = entry[\"name\"]\n            sel = entry[\"sel\"]\n            if sel == True:\n                if entry[\"type\"] == \"photo\":\n                    fn = sc.photoRoot + \"/\" + entry[\"path\"]\n                    if os.path.exists(fn):\n                        cnt += 1\n                        cntPhoto += 1\n                        logger.debug(\"images/download_selected - added %s\", fn)\n                        zl.append(fn)\n                if entry[\"type\"] == \"raw\":\n                    fn = fp + name + \".dng\"\n                    if os.path.exists(fn):\n                        cnt += 1\n                        cntRaw += 1\n                        logger.debug(\"images/download_selected - added %s\", fn)\n                        zl.append(fn)\n                    fn = fp + name + \".tiff\"\n                    if os.path.exists(fn):\n                        cnt += 1\n                        cntRaw += 1\n                        logger.debug(\"images/download_selected - added %s\", fn)\n                        zl.append(fn)\n                if entry[\"type\"] == \"video\":\n                    fn = fp + name + \".mp4\"\n                    if os.path.exists(fn):\n                        cnt += 1\n                        cntVideo += 1\n                        logger.debug(\"images/download_selected - added %s\", fn)\n                        zl.append(fn)\n                    fn = fp + name + \".h264\"\n                    if os.path.exists(fn):\n                        cnt += 1\n                        cntVideo += 1\n                        logger.debug(\"images/download_selected - added %s\", fn)\n                        zl.append(fn)\n        if len(zl) > 1:\n            logger.debug(\"images/download_selected - Preparing archive\")\n            stream = BytesIO()\n            with ZipFile(stream, 'w') as zf:\n                for file in zl:\n                    zf.write(file, os.path.basename(file))\n            stream.seek(0)\n            logger.debug(\"images/download_selected - archive done\")\n\n            now = datetime.now()\n            zipName = \"raspiCamSrvMedia_\" + now.strftime(\"%Y%m%d_%H%M%S\") + \".zip\"\n            logger.debug(\"images/download_selected - downloading as %s\", zipName)\n            msg = f\"Downloading archive {zipName} with {cntPhoto} photos, {cntRaw} raw photos and {cntVideo} videos.\"\n            flash(msg)\n            return send_file(\n                stream,\n                as_attachment=True,\n                download_name=zipName\n            )\n        elif len(zl) == 1:\n            fp = zl[0]\n            (path, file) = os.path.split(fp)\n            msg = f\"Downloading {file}\"\n            flash(msg)\n            return send_file(\n                fp,\n                as_attachment=True,\n                download_name=file\n            )\n            \n    msg = \"No files selected for download\"\n    flash(msg)\n    return render_template(\"images/main.html\", sc=sc, cp=cp, cs=cs)\n\n@bp.route(\"/media-viewer\")\n@login_required\ndef media_viewer():\n    src = request.args.get(\"src\")\n    media_type = request.args.get(\"type\", \"image\")\n\n    filename = os.path.basename(src) if src else \"\"\n\n    return render_template(\n        \"media_viewer.html\",\n        src=src,\n        media_type=media_type,\n        filename=filename\n    )\n"
  },
  {
    "path": "raspiCamSrv/info.py",
    "content": "from flask import Blueprint, Response, flash, g, redirect, render_template, request, url_for\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg, TuningConfig\nfrom raspiCamSrv.version import version\nimport threading\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\nbp = Blueprint(\"info\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/info\")\n@login_required\ndef main():\n    logger.debug(\"In info\")\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    cp = cfg.cameraProperties\n    sm = cfg.sensorModes\n    logger.debug(\"In info - len(sm): %s\", len(sm))\n    tcs = {}\n    for c in cs:\n        camnum = str(c.num)\n        if c.num == sc.activeCamera:\n            tcs[camnum] = cfg.tuningConfig\n        else:\n            strc = cfg.streamingCfg\n            if camnum in strc:\n                cstrc = strc[camnum]\n                if \"tuningconfig\" in cstrc:\n                    tcs[camnum] = cstrc[\"tuningconfig\"]\n                else:\n                    tcs[camnum] = []\n            else:\n                tcs[camnum] = TuningConfig()\n        c.status = Camera.cameraStatus(c.num)\n    # Update streaming clients\n    sc.updateStreamingClients()\n    sc.curMenu = \"info\"\n    return render_template(\"info/info.html\", sm=sm, sc=sc, tcs=tcs, cp=cp, cs=cs, cfg=cfg)\n"
  },
  {
    "path": "raspiCamSrv/motionAlgoIB.py",
    "content": "##############################################################################################\n# Motion detection Algorithms after Isaac Berrios\n# Source: https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2\n#\n##############################################################################################\nimport cv2\nimport os\nimport shutil\nfrom glob import glob\nimport copy\nfrom _thread import get_ident\nimport re\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom datetime import datetime\nfrom raspiCamSrv.camCfg import CameraCfg\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nclass MotionDetectAlgoIB():\n    \"\"\" Superclass for group of algorithms according to Isaac Berrios\n    \"\"\"\n    def __init__(self) -> None:\n        # Frames 2:t, 1:t-1\n        self._frame1 = None\n        self._frame2 = None\n        self._frame2o = None\n        self._frame1g = None\n        self._frame2g = None\n        self._detections = None\n        # Algorithn reference and testing\n        self._test = False\n        self._rois = []\n        self._ronis = []\n        self._currentRoI = None\n        self._currentRoiIdx = None\n        self._testFrame1 = None\n        self._testFrame2 = None\n        self._testFrame3 = None\n        self._testFrame4 = None\n        \n        # Variables for video generation\n        self._cfg = CameraCfg()\n        self._tc = self._cfg.triggerConfig\n        self._recordFilename = None\n        self._recordIdx = None\n        self._frameSize = None\n        self._framerate = 20\n        self._recordingStart = None\n        self._recordingActive = False\n        self._video = None\n        self._videoWithRoi = False\n    \n    @property\n    def frame1(self):\n        return self._frame1\n\n    @frame1.setter\n    def frame1(self, value):\n        self._frame1 = value\n    \n    @property\n    def frame2(self):\n        return self._frame2\n\n    @frame2.setter\n    def frame2(self, value):\n        self._frame2 = value\n\n    @property\n    def frame2o(self):\n        return self._frame2o\n\n    @frame2o.setter\n    def frame2o(self, value):\n        self._frame2o = value\n    \n    @property\n    def frame1g(self):\n        return self._frame1g\n\n    @frame1g.setter\n    def frame1g(self, value):\n        self._frame1g = value\n    \n    @property\n    def frame2g(self):\n        return self._frame2g\n\n    @frame2g.setter\n    def frame2g(self, value):\n        self._frame2g = value\n    \n    @property\n    def detections(self):\n        return self._detections\n\n    @detections.setter\n    def detections(self, value):\n        self._detections = value\n    \n    @property\n    def test(self) -> bool:\n        return self._test\n\n    @test.setter\n    def test(self, value:bool):\n        self._test = value\n\n    @property\n    def rois(self):\n        return self._rois\n\n    @rois.setter\n    def rois(self, value):\n        self._rois = value\n\n    @property\n    def ronis(self):\n        return self._ronis\n\n    @ronis.setter\n    def ronis(self, value):\n        self._ronis = value\n\n    @property\n    def currentRoI(self):\n        return self._currentRoI\n\n    @currentRoI.setter\n    def currentRoI(self, value):\n        self._currentRoI = value\n\n    @property\n    def currentRoiIdx(self):\n        return self._currentRoiIdx\n\n    @currentRoiIdx.setter\n    def currentRoiIdx(self, value):\n        self._currentRoiIdx = value\n\n    @property\n    def testFrame1(self):\n        return self._testFrame1\n\n    @testFrame1.setter\n    def testFrame1(self, value):\n        self._testFrame1 = value\n    \n    @property\n    def testFrame2(self):\n        return self._testFrame2\n\n    @testFrame2.setter\n    def testFrame2(self, value):\n        self._testFrame2 = value\n    \n    @property\n    def testFrame3(self):\n        return self._testFrame3\n\n    @testFrame3.setter\n    def testFrame3(self, value):\n        self._testFrame3 = value\n    \n    @property\n    def testFrame4(self):\n        return self._testFrame4\n\n    @testFrame4.setter\n    def testFrame4(self, value):\n        self._testFrame4 = value\n\n    @property\n    def tc(self):\n        return self._tc\n\n    @tc.setter\n    def tc(self, value):\n        self._tc = value\n    \n    @property\n    def recordFilename(self):\n        return self._recordFilename\n\n    @recordFilename.setter\n    def recordFilename(self, value):\n        self._recordFilename = value\n    \n    @property\n    def recordIdx(self):\n        return self._recordIdx\n\n    @recordIdx.setter\n    def recordIdx(self, value):\n        self._recordIdx = value\n    \n    @property\n    def frameSize(self):\n        return self._frameSize\n\n    @frameSize.setter\n    def frameSize(self, value):\n        self._frameSize = value\n    \n    @property\n    def framerate(self):\n        return self._framerate\n\n    @framerate.setter\n    def framerate(self, value):\n        self._framerate = value\n    \n    @property\n    def recordingStart(self):\n        return self._recordingStart\n\n    @recordingStart.setter\n    def recordingStart(self, value):\n        self._recordingStart = value\n    \n    @property\n    def recordingActive(self):\n        return self._recordingActive\n\n    @recordingActive.setter\n    def recordingActive(self, value):\n        self._recordingActive = value\n    \n    @property\n    def video(self):\n        return self._video\n\n    @video.setter\n    def video(self, value):\n        self._video = value\n\n    @property\n    def videoWithRoi(self):\n        return self._videoWithRoi\n\n    @videoWithRoi.setter\n    def videoWithRoi(self, value):\n        self._videoWithRoi = value\n\n    def startRecordMotion(self, fnRaw, includeRoI: bool = False) -> str:\n        \"\"\" Start recording motion\n        \n            Input:\n                fnRaw: Filename without extension\n            Return\n                Filename for video file\n        \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff.startRecordMotion\", get_ident())\n        done = False\n        err = \"\"\n        try:\n            if self.recordingActive == False:\n                self.recordFilename = fnRaw + \".mp4\"\n                save_path = os.path.join(self.tc.actionPath, self.recordFilename)\n                #fourcc = cv2.VideoWriter_fourcc(*'mp4v') \n                fourcc = cv2.VideoWriter_fourcc(*'avc1') \n                fps = self.framerate\n                logger.debug(\"Thread %s: MotionDetectFrameDiff.startRecordMotion - fps:%s framesize:%s\", get_ident(), fps, self.frameSize)\n                self.video = cv2.VideoWriter(save_path, fourcc, fps, self.frameSize)\n                assert self.video.isOpened()\n                self.recordingActive = True\n                self.recordIdx = 0\n            self.videoWithRoi = includeRoI\n            self.recordMotion()\n            done = True\n        except Exception as e:\n            logger.error(\"Thread %s: MotionDetectFrameDiff - error when starting recording: %s\", get_ident(), e)\n            err = str(e)\n        return (done, self.recordFilename, err)\n\n    def stopRecordMotion(self):\n        \"\"\" Stop recording motion\n        \n        \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff.stopRecordMotion\", get_ident())\n        if self.recordingActive == True:\n            self.video.release()\n            logger.debug(\"Thread %s: MotionDetectFrameDiff.stopRecordMotion - video released with %s frames\", get_ident(), self.recordIdx)\n            self.recordingActive = False\n\n    def recordMotion(self):\n        \"\"\" Record motion as series of png - add new frame\n        \n        \"\"\"\n        if self.recordingActive == True:\n            #logger.debug(\"Thread %s: MotionDetectFrameDiff.recordMotion - recordIdx:%s\", get_ident(), self.recordIdx)\n            # Restore RONIs\n            for roni in self.ronis:\n                x = roni[0]\n                y = roni[1]\n                w = roni[2]\n                h = roni[3]\n                self.frame2[y:y+h, x:x+w] = self.frame2o[y:y+h, x:x+w]\n            self._draw_bboxes()\n            if self.videoWithRoi:\n                for roi in self.rois:\n                    x = roi[0]\n                    y = roi[1]\n                    w = roi[2]\n                    h = roi[3]\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), (0,255,0), 2)\n                for roni in self.ronis:\n                    x = roni[0]\n                    y = roni[1]\n                    w = roni[2]\n                    h = roni[3]\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), (255,0,0), 2)\n                if self.currentRoI is not None:\n                    x = self.currentRoI[0]\n                    y = self.currentRoI[1]\n                    w = self.currentRoI[2]\n                    h = self.currentRoI[3]\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), (0,0,255), 2)  \n            if len(self.frame2.shape) == 2:\n                framergb = cv2.cvtColor(self.frame2, cv2.COLOR_YUV2RGB_I420)\n            elif len(self.frame2.shape) == 3:\n                if self.frame2.shape[2] == 4:\n                    framergb = cv2.cvtColor(self.frame2, cv2.COLOR_RGBA2RGB)\n                else:\n                    framergb = self.frame2\n            else:\n                framergb = self.frame2\n            self.video.write(framergb)\n            self.recordIdx += 1\n\n    def _frameToStream(self, frame):\n        \"\"\"Convert frame to bytestream\"\"\"\n        frameb = None\n        (stat, frame_jpg) = cv2.imencode(\".jpg\", frame)\n        if stat == True:\n            frame_jpg_arr = np.array(frame_jpg)\n            frameb = frame_jpg_arr.tobytes()\n        return frameb\n\n    def _draw_bboxes(self):\n        \"\"\" Draw bounding boxes\"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff._draw_bboxes\", get_ident())\n        if not self.detections is None:\n            if self.currentRoI is None:\n                xo = 0\n                yo = 0\n            else:\n                xo = self.currentRoI[0]\n                yo = self.currentRoI[1]\n            for det in self.detections:\n                x1,y1,x2,y2 = det\n                cv2.rectangle(self.frame2, (x1+xo,y1+yo), (x2+xo,y2+yo), (0,255,0), 2)\n\n    def _get_contour_detections(self, mask, thresh=400):\n        \"\"\" Obtains initial proposed detections from contours discoverd on the mask. \n        \n            Scores are taken as the bbox area, larger is higher.\n            Inputs:\n                mask - thresholded image mask\n                thresh - threshold for contour size\n            Outputs:\n                detectons - array of proposed detection bounding boxes and scores [[x1,y1,x2,y2,s]]\n            \"\"\"\n        # get mask contours\n        contours, _ = cv2.findContours(mask, \n                                    cv2.RETR_EXTERNAL, # cv2.RETR_TREE, \n                                    cv2.CHAIN_APPROX_TC89_L1)\n        detections = []\n        for cnt in contours:\n            x,y,w,h = cv2.boundingRect(cnt)\n            area = w*h\n            if area > thresh: \n                detections.append([x,y,x+w,y+h, area])\n\n        return np.array(detections)\n\n    def _non_max_suppression(self, boxes, scores, threshold=1e-1):\n        \"\"\" Perform non-max suppression on a set of bounding boxes and corresponding scores\n        \n            Inputs:\n                boxes: a list of bounding boxes in the format [xmin, ymin, xmax, ymax]\n                scores: a list of corresponding scores \n                threshold: the IoU (intersection-over-union) threshold for merging bounding boxes\n            Outputs:\n                boxes - non-max suppressed boxes\n        \"\"\"\n        # Sort the boxes by score in descending order\n        boxes = boxes[np.argsort(scores)[::-1]]\n\n        # remove all contained bounding boxes and get ordered index\n        order = self._remove_contained_bboxes(boxes)\n\n        keep = []\n        while order:\n            i = order.pop(0)\n            keep.append(i)\n            for j in order:\n                # Calculate the IoU between the two boxes\n                intersection = max(0, min(boxes[i][2], boxes[j][2]) - max(boxes[i][0], boxes[j][0])) * \\\n                            max(0, min(boxes[i][3], boxes[j][3]) - max(boxes[i][1], boxes[j][1]))\n                union = (boxes[i][2] - boxes[i][0]) * (boxes[i][3] - boxes[i][1]) + \\\n                        (boxes[j][2] - boxes[j][0]) * (boxes[j][3] - boxes[j][1]) - intersection\n                iou = intersection / union\n\n                # Remove boxes with IoU greater than the threshold\n                if iou > threshold:\n                    order.remove(j)\n                    \n        return boxes[keep]\n\n    def _remove_contained_bboxes(self, boxes):\n        \"\"\" Removes all smaller boxes that are contained within larger boxes.\n        \n            Requires bboxes to be sorted by area (score)\n            Inputs:\n                boxes - array bounding boxes sorted (descending) by area \n                        [[x1,y1,x2,y2]]\n            Outputs:\n                keep - indexes of bounding boxes that are not entirely contained \n                    in another box\n            \"\"\"\n        check_array = np.array([True, True, False, False])\n        keep = list(range(0, len(boxes)))\n        for i in keep: # range(0, len(bboxes)):\n            for j in range(0, len(boxes)):\n                # check if box j is completely contained in box i\n                if np.all((np.array(boxes[j]) >= np.array(boxes[i])) == check_array):\n                    try:\n                        keep.remove(j)\n                    except ValueError:\n                        continue\n        return keep\n\nclass MotionDetectFrameDiff(MotionDetectAlgoIB):\n    \"\"\" Motion detection by Frame Differencing\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__()\n        \n        # Algorithn reference and testing\n        self.algoReferenceTit = \"Isaac Berrios - Introduction to Motion Detection: Part 1\"\n        self.algoReferenceURL = \"https://medium.com/@itberrios6/introduction-to-motion-detection-part-1-e031b0bb9bb2\"\n        self.testFrame1Title = \"Gray Scale Video\"\n        self.testFrame2Title = \"Gray Scale Frame Difference\"\n        self.testFrame3Title = \"Motion Mask\"\n        self.testFrame4Title = \"Bounding Boxes after Non-Maximal Suppression\"\n\n        # Algorithm parameters\n        self._bbox_threshold = 400\n        self._nms_threshold = 0.001\n\n    @property\n    def bbox_threshold(self):\n        return self._bbox_threshold\n\n    @bbox_threshold.setter\n    def bbox_threshold(self, value):\n        self._bbox_threshold = value\n\n    @property\n    def nms_threshold(self):\n        return self._nms_threshold\n\n    @nms_threshold.setter\n    def nms_threshold(self, value):\n        self._nms_threshold = value\n\n    def detectMotion(self, frame2, frame1, camInfo: str, rois: list, ronis: list):\n        \"\"\" Use frame differencing method to detect motion\n        \n            Inputs:\n                frame2 : frame at t+1\n                frame1 : frame at t\n            Returns:\n                motion : True/False if motion has been detected (#bboxes > 0)\n                trigger: Dict describing trigger\n        \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion\", get_ident())\n        motion = False\n        roiDetected = 0\n\n        triggerParams = {}\n        triggerParams[\"cam\"] = camInfo\n        roiDetected = 0\n\n        self.frame2 = copy.copy(frame2)\n        self.frame1 = copy.copy(frame1)\n\n        self.frame2o = frame2\n        self.rois = rois\n        self.ronis = ronis\n\n        if self.test == True:\n            self.testFrame1 = self._frameToStream(self.frame2)\n            self.testFrame2 = copy.copy(self.frame2)[ :, :, 0]\n            self.testFrame3 = copy.copy(self.frame2)[ :, :, 0]\n            #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - staged frame_gray\", get_ident())\n\n        for roni in ronis:\n            x = roni[0]\n            y = roni[1]\n            w = roni[2]\n            h = roni[3]\n            self.frame2[y:y+h, x:x+w] = (255,0,0)\n            self.frame1[y:y+h, x:x+w] = (255,0,0)\n\n        self.frame2g = cv2.cvtColor(self.frame2, cv2.COLOR_RGB2GRAY)\n        self.frame1g = cv2.cvtColor(self.frame1, cv2.COLOR_RGB2GRAY)\n\n        if len(rois) == 0:\n            self.currentRoI = None\n            self.detections = self._get_detections(self.frame1g, self.frame2g, bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold)\n            #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - got detections: %s\", get_ident(), self.detections)\n            if self.test == True:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        self._draw_bboxes()\n                        #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - done draw_bboxes\", get_ident())\n                self.testFrame4 = self._frameToStream(self.frame2)\n            else:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                        triggerParams[\"IOU_thr\"] = self.nms_threshold\n                        motion = True\n        else:\n            idx = 0\n            for roi in rois:\n                idx += 1\n                self.currentRoI = roi\n                x = roi[0]\n                y = roi[1]\n                w = roi[2]\n                h = roi[3]\n                self.frame2g = cv2.cvtColor(self.frame2[y:y+h, x:x+w], cv2.COLOR_RGB2GRAY)\n                self.frame1g = cv2.cvtColor(self.frame1[y:y+h, x:x+w], cv2.COLOR_RGB2GRAY)\n                self.detections = self._get_detections(self.frame1g, self.frame2g, bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold)\n                #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - got detections: %s\", get_ident(), self.detections)\n                if self.test == True:\n                    color = (0,255,0)\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            color = (0,0,255)\n                            self._draw_bboxes()\n                            #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - done draw_bboxes\", get_ident())\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), color, 2)\n                else:\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            triggerParams[\"roi\"] = idx\n                            triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                            triggerParams[\"IOU_thr\"] = self.nms_threshold\n                            motion = True\n                            roiDetected = idx\n                            motion = True\n                            break\n            if self.test == True:\n                self.testFrame2 = self._frameToStream(self.testFrame2)\n                self.testFrame3 = self._frameToStream(self.testFrame3)\n                self.testFrame4 = self._frameToStream(self.frame2)\n\n        trigger = {\"trigger\":\"Motion Detection\", \"triggertype\":\"Frame Diff.\", \"triggerparam\":triggerParams}\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff.detectMotion - motion:%s\", get_ident(), motion)\n        return (motion, trigger, roiDetected)\n\n    def _get_detections(self, frame1, frame2, bbox_thresh=400, nms_thresh=1e-3, mask_kernel=np.array((9,9), dtype=np.uint8)):\n        \"\"\" Main function to get detections via Frame Differencing\n        \n            Inputs:\n                frame1 - Grayscale frame at time t\n                frame2 - Grayscale frame at time t + 1\n                bbox_thresh - Minimum threshold area for declaring a bounding box \n                nms_thresh - IOU threshold for computing Non-Maximal Supression\n                mask_kernel - kernel for morphological operations on motion mask\n            Outputs:\n                detections - list with bounding box locations of all detections\n                    bounding boxes are in the form of: (xmin, ymin, xmax, ymax)\n            \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff._get_detections\", get_ident())\n        # get image mask for moving pixels\n        mask = self._get_mask(frame1, frame2, mask_kernel)\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff._get_detections got mask\", get_ident())\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame3 = self._frameToStream(mask)\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                self.testFrame3[y:y+h, x:x+w] = copy.copy(mask[0:h, 0:w])\n\n        # get initially proposed detections from contours\n        detections = self._get_contour_detections(mask, bbox_thresh)\n        if len(detections) == 0:\n            return None\n\n        # separate bboxes and scores\n        bboxes = detections[:, :4]\n        scores = detections[:, -1]\n\n        # perform Non-Maximal Supression on initial detections\n        return self._non_max_suppression(bboxes, scores, nms_thresh)\n\n    def _get_mask(self, frame1, frame2, kernel=np.array((9,9), dtype=np.uint8)):\n        \"\"\" Obtains image mask\n        \n            Inputs: \n                frame1 - Grayscale frame at time t\n                frame2 - Grayscale frame at time t + 1\n                kernel - (NxN) array for Morphological Operations\n            Outputs: \n                mask - Thresholded mask for moving pixels\n            \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectFrameDiff._get_mask\", get_ident())\n        frame_diff = cv2.subtract(frame2, frame1)\n\n        # blur the frame difference\n        frame_diff = cv2.medianBlur(frame_diff, 3)\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame2 = self._frameToStream(frame_diff)\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                #logger.debug(\"Thread %s: MotionDetectFrameDiff._get_mask . Preparing test frame 2 with ROI\", get_ident())\n                self.testFrame2[y:y+h, x:x+w] = copy.copy(frame_diff[0:h, 0:w])\n                #logger.debug(\"Thread %s: MotionDetectFrameDiff._get_mask . Done preparing test frame 2 with ROI\", get_ident())\n        \n        mask = cv2.adaptiveThreshold(frame_diff, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\\\n                cv2.THRESH_BINARY_INV, 11, 3)\n\n        mask = cv2.medianBlur(mask, 3)\n\n        # morphological operations\n        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)\n\n        return mask\n\nclass MotionDetectOpticalFlow(MotionDetectAlgoIB):\n    \"\"\" Motion detection by Optical Flow\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__()\n        \n        # Algorithn reference and testing\n        self.algoReferenceTit = \"Isaac Berrios - Introduction to Motion Detection: Part 2\"\n        self.algoReferenceURL = \"https://medium.com/@itberrios6/introduction-to-motion-detection-part-2-6ec3d6b385d4\"\n        self.testFrame1Title = \"Gray Scale blurred\"\n        self.testFrame2Title = \"Optical Flow\"\n        self.testFrame3Title = \"Motion Mask\"\n        self.testFrame4Title = \"Bounding Boxes after Non-Maximal Suppression\"\n\n        # Algorithm parameters\n        self.bbox_threshold = 400\n        self.nms_threshold = 0.001\n        self.motion_threshold = 1\n\n    def detectMotion(self, frame2, frame1, camInfo: str, rois: list, ronis: list):\n        \"\"\" Use frame differencing method to detect motion\n        \n            Inputs:\n                frame2 : frame at t+1\n                frame1 : frame at t\n            Returns:\n                motion : True/False if motion has been detected\n                trigger: Dict describing trigger\n        \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion\", get_ident())\n        motion = False\n        roiDetected = 0\n\n        triggerParams = {}\n        triggerParams[\"cam\"] = camInfo\n        roiDetected = 0\n\n        self.frame2 = copy.copy(frame2)\n        self.frame1 = copy.copy(frame1)\n\n        self.frame2o = frame2\n        self.rois = rois\n        self.ronis = ronis\n\n        if self.test == True and len(ronis) > 0:\n            self.testFrame1 = copy.copy(self.frame2)\n            self.testFrame2 = copy.copy(self.frame2)\n            self.testFrame3 = copy.copy(self.frame2)\n            self.testFrame3 = self.testFrame3[:, :, 0]\n            self.testFrame4 = copy.copy(self.frame2)\n\n        if self.test == True:\n            # convert to grayscale\n            self.testFrame1 = cv2.cvtColor(self.frame2, cv2.COLOR_RGB2GRAY)\n            # blurr image\n            self.testFrame1 = cv2.GaussianBlur(self.testFrame1, dst=None, ksize=(3,3), sigmaX=5)\n\n        for roni in ronis:\n            x = roni[0]\n            y = roni[1]\n            w = roni[2]\n            h = roni[3]\n            self.frame2[y:y+h, x:x+w] = (255,0,0)\n            self.frame1[y:y+h, x:x+w] = (255,0,0)\n\n        if len(rois) == 0:\n            self.currentRoI = None\n            self.detections = self._get_detections(self.frame1, self.frame2, motion_thresh=self.motion_threshold, bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold)\n            #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - got detections: %s\", get_ident(), self.detections)\n            if self.test == True:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        self._draw_bboxes()\n                        #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - done draw_bboxes\", get_ident())\n                self.testFrame1 = self._frameToStream(self.testFrame1)\n                self.testFrame4 = self._frameToStream(self.frame2)\n            else:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        triggerParams[\"Motion_thr\"] = self.motion_threshold\n                        triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                        triggerParams[\"IOU_thr\"] = self.nms_threshold\n                        motion = True\n        else:\n            idx = 0\n            for roi in rois:\n                idx += 1\n                self.currentRoI = roi\n                x = roi[0]\n                y = roi[1]\n                w = roi[2]\n                h = roi[3]\n                #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - checking ROI: %s\", get_ident(), self.currentRoI)\n                self.detections = self._get_detections(self.frame1[y:y+h, x:x+w], self.frame2[y:y+h, x:x+w], motion_thresh=self.motion_threshold, bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold)\n                #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - got detections: %s\", get_ident(), self.detections)\n                if self.test == True:\n                    color = (0,255,0)\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            color = (0,0,255)\n                            self._draw_bboxes()\n                            #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - done draw_bboxes\", get_ident())\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), color, 2)\n                else:\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            triggerParams[\"roi\"] = idx\n                            triggerParams[\"Motion_thr\"] = self.motion_threshold\n                            triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                            triggerParams[\"IOU_thr\"] = self.nms_threshold\n                            motion = True\n                            roiDetected = idx\n                            motion = True\n                            break\n            if self.test == True:\n                self.testFrame1 = self._frameToStream(self.testFrame1)\n                self.testFrame2 = self._frameToStream(self.testFrame2)\n                self.testFrame3 = self._frameToStream(self.testFrame3)\n                self.testFrame4 = self._frameToStream(self.frame2)\n\n        trigger = {\"trigger\":\"Motion Detection\", \"triggertype\":\"Optical Flow\", \"triggerparam\":triggerParams}    \n        #logger.debug(\"Thread %s: MotionDetectOpticalFlow.detectMotion - motion:%s\", get_ident(), motion)\n        return (motion, trigger, roiDetected)\n\n    def _get_detections(self, frame1, frame2, motion_thresh=1, bbox_thresh=400, nms_thresh=0.1, mask_kernel=np.ones((7,7), dtype=np.uint8)):\n        \"\"\" Main function to get detections via Frame Differencing\n            Inputs:\n                frame1 - Grayscale frame at time t\n                frame2 - Grayscale frame at time t + 1\n                motion_thresh - Minimum flow threshold for motion\n                bbox_thresh - Minimum threshold area for declaring a bounding box \n                nms_thresh - IOU threshold for computing Non-Maximal Supression\n                mask_kernel - kernel for morphological operations on motion mask\n            Outputs:\n                detections - list with bounding box locations of all detections\n                    bounding boxes are in the form of: (xmin, ymin, xmax, ymax)\n            \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectOpticalFlow._get_detections\", get_ident())\n        # get optical flow\n        flow = self._compute_flow(frame1, frame2)\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame2 = self._frameToStream(self._get_flow_viz(flow))\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                flow_viz = self._get_flow_viz(flow)\n                self.testFrame2[y:y+h, x:x+w] = copy.copy(flow_viz[0:h, 0:w])\n            #logger.debug(\"Thread %s: MotionDetectOpticalFlow._get_detections - staged testFrame2\", get_ident())\n\n        # separate into magntiude and angle\n        mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])\n\n        motion_mask = self._get_motion_mask(mag, motion_thresh=motion_thresh, kernel=mask_kernel)\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame3 = self._frameToStream(motion_mask)\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                self.testFrame3[y:y+h, x:x+w] = copy.copy(motion_mask[0:h, 0:w])\n            #logger.debug(\"Thread %s: MotionDetectOpticalFlow._get_detections - staged testFrame3\", get_ident())\n\n        # get initially proposed detections from contours\n        detections = self._get_contour_detections(motion_mask, thresh=bbox_thresh)\n        if len(detections) == 0:\n            return None\n\n        # separate bboxes and scores\n        bboxes = detections[:, :4]\n        scores = detections[:, -1]\n\n        # perform Non-Maximal Supression on initial detections\n        return self._non_max_suppression(bboxes, scores, threshold=nms_thresh)\n    \n    def _compute_flow(self, frame1, frame2):\n        #logger.debug(\"Thread %s: MotionDetectOpticalFlow._compute_flow\", get_ident())\n        # convert to grayscale\n        gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)\n        gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)\n        self.frame1g = gray1\n        self.frame2g = gray2\n\n        # blurr image\n        gray1 = cv2.GaussianBlur(gray1, dst=None, ksize=(3,3), sigmaX=5)\n        gray2 = cv2.GaussianBlur(gray2, dst=None, ksize=(3,3), sigmaX=5)\n\n        flow = cv2.calcOpticalFlowFarneback(gray1, gray2, None,\n                                            pyr_scale=0.75,\n                                            levels=3,\n                                            winsize=5,\n                                            iterations=3,\n                                            poly_n=10,\n                                            poly_sigma=1.2,\n                                            flags=0)\n        return flow\n\n    def _get_flow_viz(self, flow):\n        \"\"\" Obtains BGR image to Visualize the Optical Flow \n        \"\"\"\n        hsv = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8)\n        hsv[..., 1] = 255\n\n        mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])\n        hsv[..., 0] = ang*180/np.pi/2\n        hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)\n        rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)\n\n        return rgb\n\n    def _get_motion_mask(self, flow_mag, motion_thresh=1, kernel=np.ones((7,7))):\n        \"\"\" Obtains Detection Mask from Optical Flow Magnitude\n            Inputs:\n                flow_mag (array) Optical Flow magnitude\n                motion_thresh - thresold to determine motion\n                kernel - kernal for Morphological Operations\n            Outputs:\n                motion_mask - Binray Motion Mask\n            \"\"\"\n        motion_mask = np.uint8(flow_mag > motion_thresh)*255\n\n        motion_mask = cv2.erode(motion_mask, kernel, iterations=1)\n        motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_OPEN, kernel, iterations=1)\n        motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_CLOSE, kernel, iterations=3)\n        \n        return motion_mask\n\n    def _get_contour_detections_2(self, mask, ang, angle_thresh=2, thresh=400):\n        \"\"\" Obtains initial proposed detections from contours discoverd on the\n            mask. Scores are taken as the bbox area, larger is higher.\n            Inputs:\n                mask - thresholded image mask\n                angle_thresh - threshold for flow angle standard deviation\n                thresh - threshold for contour size\n            Outputs:\n                detectons - array of proposed detection bounding boxes and scores \n                            [[x1,y1,x2,y2,s]]\n            \"\"\"\n        # get mask contours\n        contours, _ = cv2.findContours(mask, \n                                        cv2.RETR_EXTERNAL, # cv2.RETR_TREE, \n                                        cv2.CHAIN_APPROX_TC89_L1)\n        temp_mask = np.zeros_like(mask) # used to get flow angle of contours\n        angle_thresh = angle_thresh*ang.std()\n        detections = []\n        for cnt in contours:\n            # get area of contour\n            x,y,w,h = cv2.boundingRect(cnt)\n            area = w*h\n\n            # get flow angle inside of contour\n            cv2.drawContours(temp_mask, [cnt], 0, (255,), -1)\n            flow_angle = ang[np.nonzero(temp_mask)]\n\n            if (area > thresh) and (flow_angle.std() < angle_thresh):\n                detections.append([x,y,x+w,y+h, area])\n\n        return np.array(detections)\n\nclass MotionDetectBgSubtract(MotionDetectAlgoIB):\n    \"\"\" Motion detection by Background Subtraction\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__()\n        \n        # Algorithn reference and testing\n        self.algoReferenceTit = \"Isaac Berrios - Introduction to Motion Detection: Part 3\"\n        self.algoReferenceURL = \"https://medium.com/@itberrios6/introduction-to-motion-detection-part-3-025271f66ef9\"\n        self.testFrame1Title = \"Normal Video\"\n        self.testFrame2Title = \"Current Background\"\n        self.testFrame3Title = \"Motion Mask\"\n        self.testFrame4Title = \"Bounding Boxes after Non-Maximal Suppression\"\n\n        # Algorithm parameters\n        self.bbox_threshold = 400\n        self.nms_threshold = 0.001\n        self._backSubModel = \"MOG2\"\n        self._backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n        self._backSub.setShadowThreshold(0.5)\n\n        # For ROIs, each ROI will have its own background model\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n        self._roiBackSubs = []\n        if tc.useRoI == True:\n            for roi in tc.regionOfInterest:\n                backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n                backSub.setShadowThreshold(0.5)\n                self._roiBackSubs.append(backSub)\n    \n    @property\n    def backSubModel(self):\n        return self._backSubModel\n\n    @backSubModel.setter\n    def backSubModel(self, value):\n        logger.debug(\"Thread %s: MotionDetectBgSubtract.backSubModel - value: %s\", get_ident(), value)\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n        if value == \"MOG2\":\n            self._backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n            self._backSub.setShadowThreshold(0.5)\n            self._roiBackSubs = []\n            if tc.useRoI == True:\n                for roi in tc.regionOfInterest:\n                    backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n                    backSub.setShadowThreshold(0.5)\n                    self._roiBackSubs.append(backSub)\n        elif value == \"KNN\":\n            self._backSub = cv2.createBackgroundSubtractorKNN(dist2Threshold=1000, detectShadows=True)\n            self._roiBackSubs = []\n            if tc.useRoI == True:\n                for roi in tc.regionOfInterest:\n                    backSub = cv2.createBackgroundSubtractorKNN(dist2Threshold=1000, detectShadows=True)\n                    backSub.setShadowThreshold(0.5)\n                    self._roiBackSubs.append(backSub)\n        else:\n            value = \"MOG2\"\n            self._backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n            self._backSub.setShadowThreshold(0.5)\n            self._roiBackSubs = []\n            if tc.useRoI == True:\n                for roi in tc.regionOfInterest:\n                    backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=16, detectShadows=True)\n                    backSub.setShadowThreshold(0.5)\n                    self._roiBackSubs.append(backSub)\n        self._backSubModel = value\n\n    def detectMotion(self, frame2, frame1, camInfo: str, rois: list, ronis: list):\n        \"\"\" Use frame differencing method to detect motion\n        \n            Inputs:\n                frame2 : frame at t+1\n                frame1 : frame at t\n            Returns:\n                motion : True/False if motion has been detected\n                trigger: Dict describing trigger\n        \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion\", get_ident())\n        motion = False\n        roiDetected = 0\n\n        triggerParams = {}\n        triggerParams[\"cam\"] = camInfo\n        roiDetected = 0\n\n        self.frame2 = copy.copy(frame2)\n        self.frame1 = copy.copy(frame1)\n\n        self.frame2o = frame2\n        self.rois = rois\n        self.ronis = ronis\n\n        if self.test == True:\n            self.testFrame1 = self._frameToStream(self.frame2)\n            self.testFrame2 = copy.copy(self.frame2)\n            self.testFrame3 = copy.copy(self.frame2)[ :, :, 0]\n            #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - staged frame_gray\", get_ident())\n\n        for roni in ronis:\n            x = roni[0]\n            y = roni[1]\n            w = roni[2]\n            h = roni[3]\n            self.frame2[y:y+h, x:x+w] = (255,0,0)\n            self.frame1[y:y+h, x:x+w] = (255,0,0)\n\n        if len(rois) == 0:\n            self.currentRoI = None\n            kernel=np.array((9,9), dtype=np.uint8)\n            self.detections = self._get_detections(self._backSub, self.frame2, bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold, kernel=kernel)\n            #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - got detections: %s\", get_ident(), self.detections)\n            if self.test == True:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        self._draw_bboxes()\n                        #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - done draw_bboxes\", get_ident())\n                self.testFrame4 = self._frameToStream(self.frame2)\n            else:\n                if not self.detections is None:\n                    if len(self.detections) > 0:\n                        triggerParams[\"Model\"] = self.backSubMod\n                        triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                        triggerParams[\"IOU_thr\"] = self.nms_threshold\n                        motion = True\n        else:\n            idx = 0\n            for roi in rois:\n                idx += 1\n                self.currentRoI = roi\n                self.currentRoiIdx = idx\n                x = roi[0]\n                y = roi[1]\n                w = roi[2]\n                h = roi[3]\n                kernel=np.array((9,9), dtype=np.uint8)\n                self.detections = self._get_detections(self._backSub, self.frame2[y:y+h, x:x+w], bbox_thresh=self.bbox_threshold, nms_thresh=self.nms_threshold, kernel=kernel)\n                #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - got detections: %s\", get_ident(), self.detections)\n                if self.test == True:\n                    color = (0,255,0)\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            color = (0,0,255)\n                            self._draw_bboxes()\n                            #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - done draw_bboxes\", get_ident())\n                    cv2.rectangle(self.frame2, (x,y), (x+w,y+h), color, 2)\n                else:\n                    if not self.detections is None:\n                        if len(self.detections) > 0:\n                            triggerParams[\"roi\"] = idx\n                            triggerParams[\"Model\"] = self.backSubMod\n                            triggerParams[\"BBox_thr\"] = self.bbox_threshold\n                            triggerParams[\"IOU_thr\"] = self.nms_threshold\n                            motion = True\n                            roiDetected = idx\n                            motion = True\n                            break\n            if self.test == True:\n                self.testFrame2 = self._frameToStream(self.testFrame2)\n                self.testFrame3 = self._frameToStream(self.testFrame3)\n                self.testFrame4 = self._frameToStream(self.frame2)\n\n        trigger = {\"trigger\":\"Motion Detection\", \"triggertype\":\"BG Subtraction\", \"triggerparam\":triggerParams}    \n        #logger.debug(\"Thread %s: MotionDetectBgSubtract.detectMotion - motion:%s\", get_ident(), motion)\n        return (motion, trigger, roiDetected)\n    \n    def _get_detections(self, backSub, frame, bbox_thresh=100, nms_thresh=0.1, kernel=np.array((9,9), dtype=np.uint8)):\n        \"\"\" Main function to get detections via Frame Differencing\n            Inputs:\n                backSub - Background Subtraction Model\n                frame - Current BGR Frame\n                bbox_thresh - Minimum threshold area for declaring a bounding box\n                nms_thresh - IOU threshold for computing Non-Maximal Supression\n                kernel - kernel for morphological operations on motion mask\n            Outputs:\n                detections - list with bounding box locations of all detections\n                    bounding boxes are in the form of: (xmin, ymin, xmax, ymax)\n            \"\"\"\n        #logger.debug(\"Thread %s: MotionDetectBgSubtract._get_detections - backSub %s\", get_ident(), backSub)\n        # Update Background Model and get foreground mask\n        if self.currentRoI is None:\n            fg_mask = backSub.apply(frame)\n        else:\n            backSub = self._roiBackSubs[self.currentRoiIdx - 1]\n            fg_mask = backSub.apply(frame)\n\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame2 = self._frameToStream(backSub.getBackgroundImage())\n                #logger.debug(\"Thread %s: MotionDetectBgSubtract._get_detections - staged background\", get_ident())\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                bgImage = backSub.getBackgroundImage()\n                self.testFrame2[y:y+h, x:x+w] = copy.copy(bgImage[0:h, 0:w])\n\n        # get clean motion mask\n        motion_mask = self._get_motion_mask(fg_mask, kernel=kernel)\n        if self.test == True:\n            if self.currentRoI is None:\n                self.testFrame3 = self._frameToStream(motion_mask)\n            else:\n                x = self.currentRoI[0]\n                y = self.currentRoI[1]\n                w = self.currentRoI[2]\n                h = self.currentRoI[3]\n                self.testFrame3[y:y+h, x:x+w] = copy.copy(motion_mask[0:h, 0:w])\n\n        # get initially proposed detections from contours\n        detections = self._get_contour_detections(motion_mask, bbox_thresh)\n        if len(detections) == 0:\n            return None\n\n        # separate bboxes and scores\n        bboxes = detections[:, :4]\n        scores = detections[:, -1]\n\n        # perform Non-Maximal Supression on initial detections\n        return self._non_max_suppression(bboxes, scores, nms_thresh)\n    \n    def _get_motion_mask(self, fg_mask, min_thresh=0, kernel=np.array((9,9), dtype=np.uint8)):\n        \"\"\" Obtains image mask\n            Inputs: \n                fg_mask - foreground mask\n                kernel - kernel for Morphological Operations\n            Outputs: \n                mask - Thresholded mask for moving pixels\n            \"\"\"\n        _, thresh = cv2.threshold(fg_mask,min_thresh,255,cv2.THRESH_BINARY)\n        motion_mask = cv2.medianBlur(thresh, 3)\n        \n        # morphological operations\n        motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_OPEN, kernel, iterations=1)\n        motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_CLOSE, kernel, iterations=1)\n\n        return motion_mask    \n    "
  },
  {
    "path": "raspiCamSrv/motionDetector.py",
    "content": "from raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg\nimport numpy as np\nfrom _thread import get_ident, allocate_lock\nimport threading\nimport time\nfrom datetime import datetime\nfrom datetime import timedelta\nimport logging\nfrom raspiCamSrv.dbx import get_dbx\nimport smtplib\nfrom email.message import EmailMessage\nimport mimetypes\nimport copy\n\nlogger = logging.getLogger(__name__)\n\nclass MotionEvent(object):\n    \"\"\"An Event-like class that signals all active clients when a new frame is\n    available.\n    \"\"\"\n    def __init__(self):\n        #logger.debug(\"Thread %s: CameraEvent.__init__\", get_ident())\n        self.events = {}\n\n    def wait(self):\n        \"\"\"Invoked from each client's thread to wait for the next frame.\"\"\"\n        #logger.debug(\"Thread %s: CameraEvent.wait\", get_ident())\n        ident = get_ident()\n        if ident not in self.events:\n            # this is a new client\n            # add an entry for it in the self.events dict\n            # each entry has two elements, a threading.Event() and a timestamp\n            self.events[ident] = [threading.Event(), time.time()]\n            #logger.debug(\"Thread %s: CameraEvent.wait - Event ident: %s added to events dict. time:%s\", get_ident(), ident, self.events[ident][1])\n        #for ident, event in self.events.items():\n            #logger.debug(\"Thread %s: CameraEvent.wait - Event ident: %s Flag: %s Time: %s (Flag False -> blocking)\", get_ident(), ident, self.events[ident][0].is_set(), event[1])\n            \n        return self.events[ident][0].wait()\n\n    def set(self):\n        \"\"\"Invoked by MotionDetector when a new frame is available.\"\"\"\n        #logger.debug(\"Thread %s: CameraEvent.set\", get_ident())\n        now = time.time()\n        remove = None\n        for ident, event in self.events.items():\n            if not event[0].isSet():\n                # if this client's event is not set, then set it\n                # also update the last set timestamp to now\n                event[0].set()\n                event[1] = now\n                #logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s Flag: False -> True (unblock/notify)\", get_ident(), ident)\n            else:\n                # if the client's event is already set, it means the client\n                # did not process a previous frame\n                # if the event stays set for more than 5 seconds, then assume\n                # the client is gone and remove it\n                #logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s Flag: True (Last image not processed).\", get_ident(), ident)\n                if now - event[1] > 5:\n                    #logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s  too old; marked for removal.\", get_ident(), ident)\n                    remove = ident\n        if remove:\n            del self.events[remove]\n            #logger.debug(\"Thread %s: CameraEvent.set  - Event ident: %s removed.\", get_ident(), ident)\n\n    def clear(self):\n        \"\"\"Invoked from each client's thread after a frame was processed.\"\"\"\n        ident = get_ident()\n        if ident in self.events:\n            self.events[get_ident()][0].clear()\n        #logger.debug(\"Thread %s: CameraEvent.clear - Flag set to False -> blocking.\", get_ident())\n\nclass MotionDetector():\n    \"\"\" Class for detection of motion and triggering actions\n    \n\n    \"\"\"\n    logger.debug(\"Thread %s: MotionDetector - setting class variables\", get_ident())\n    _instance = None\n    db = None\n    mThread = None\n    mThreadStop = False\n    camInfo = \"\"\n    rois = []\n    ronis = []\n    roiDetected = 0\n    eventKey = None\n    eventStart = None\n    nrPhotos = 0\n    lastPhoto = None\n    videoStart = None\n    videoKey = None\n    videoStop = None\n    videoEncoder = None\n    videoCircOutput = None\n    videoName = None\n    notificationDone = None\n    notifyMail = None\n    notifyBuffer = []\n    notifyBufferLock = allocate_lock()      # lock for making access to notifyBuffer thread-safe\n    mdAlgo = None\n    event = MotionEvent()\n\n    # Callbacks\n    when_motion_detected = None\n\n    def __new__(cls):\n        logger.debug(\"Thread %s: MotionDetector.__new__\", get_ident())\n        if cls._instance is None:\n            logger.debug(\"Thread %s: MotionDetector.__new__ - Instantiating Class\", get_ident())\n            cls._instance = super(MotionDetector, cls).__new__(cls)\n            cfg = CameraCfg()\n            tc = cfg.triggerConfig\n            if tc.motionDetectAlgo in range(2, 5):\n                if CameraCfg().serverConfig.supportsExtMotionDetection == True:\n                    import raspiCamSrv.motionAlgoIB as mda\n                    if tc.motionDetectAlgo == 2:\n                        cls.mdAlgo = mda.MotionDetectFrameDiff()\n                    elif tc.motionDetectAlgo == 3:\n                        cls.mdAlgo = mda.MotionDetectOpticalFlow()\n                    else:\n                        cls.mdAlgo = mda.MotionDetectBgSubtract()\n                else:\n                    tc.error = f\"Error while initializing MotionDetector: algorithm {tc.motionDetectAlgo} currently not supported.\"\n                    tc.errorSource = \"MotionDetector.__new__\"\n                    logger.error(\"Error while initializing MotionDetector: algorithm %s currently not supported\", tc.motionDetectAlg)\n\n        return cls._instance\n\n    @classmethod\n    def setAlgorithm(cls) -> bool:\n        \"\"\" Set the motion detection algorithm\n        \n            The currently active algorithm is taken from trigger configuration\n            \n            Return:\n                True : requested algorithm has been set\n                False: requested algorithm could not be set\n        \"\"\"\n        ret = False\n        cls.mdAlgo = None\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n        logger.debug(\"Thread %s: MotionDetector.setAlgorithm - set algorithm to %s\", get_ident(), tc.motionDetectAlgo)\n        if tc.motionDetectAlgo == 1:\n            ret = True\n        if tc.motionDetectAlgo in range(2, 5):\n            if CameraCfg().serverConfig.supportsExtMotionDetection == True:\n                import raspiCamSrv.motionAlgoIB as mda\n                if tc.motionDetectAlgo == 2:\n                    cls.mdAlgo = mda.MotionDetectFrameDiff()\n                elif tc.motionDetectAlgo == 3:\n                    cls.mdAlgo = mda.MotionDetectOpticalFlow()\n                else:\n                    cls.mdAlgo = mda.MotionDetectBgSubtract()\n                    cls.mdAlgo.backSubModel = tc.backSubModel\n                ret = True\n        return ret\n\n    def get_testFrame1(self):\n        if not MotionDetector.mdAlgo is None:\n            MotionDetector.event.wait()\n            MotionDetector.event.clear()\n            return MotionDetector.mdAlgo.testFrame1\n        else:\n            return None\n\n    def get_testFrame2(self):\n        if not MotionDetector.mdAlgo is None:\n            MotionDetector.event.wait()\n            MotionDetector.event.clear()\n            return MotionDetector.mdAlgo.testFrame2\n        else:\n            return None\n\n    def get_testFrame3(self):\n        if not MotionDetector.mdAlgo is None:\n            MotionDetector.event.wait()\n            MotionDetector.event.clear()\n            return MotionDetector.mdAlgo.testFrame3\n        else:\n            return None\n\n    def get_testFrame4(self):\n        if not MotionDetector.mdAlgo is None:\n            MotionDetector.event.wait()\n            MotionDetector.event.clear()\n            return MotionDetector.mdAlgo.testFrame4\n        else:\n            return None\n\n    @classmethod\n    def prepareRoIs(cls):\n        \"\"\" Prepare Regions of Interest for motion detection\n\n            In the following, capital letters refer to sensor coordinates,\n            while lowercase letters refer to image coordinates.\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector.prepareRoIs\", get_ident())\n\n        cls.rois = []\n        cls.ronis = []\n\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n\n        cls.camInfo = f\"{sc.activeCamera}-{sc.activeCameraModel[:8]}\"\n\n        if tc.useRoI == False:\n            return\n\n        # Sensor size\n        camProps = cfg.cameraProperties\n        Wsensor = camProps.pixelArraySize[0]\n        Hsensor = camProps.pixelArraySize[1]\n        logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Sensor Size: %sx%s\", get_ident(), Wsensor, Hsensor)\n\n        # Scaler Crop Live View\n        scalerCrop = sc.scalerCropLiveView\n        Xl = scalerCrop[0]\n        Yl = scalerCrop[1]\n        Wl = scalerCrop[2]\n        Hl = scalerCrop[3]\n        logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Scaler Crop Live View: (%s,%s,%s,%s)\", get_ident(), Xl, Yl, Wl, Hl)\n\n        # Live View size\n        lvCfg = cfg.liveViewConfig\n        streamSize = lvCfg.stream_size\n        wl = streamSize[0]\n        hl = streamSize[1]\n        logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Live View Size: %sx%s\", get_ident(), wl, hl)\n\n        # Regions of Interest\n        ROIs = tc.regionOfInterest\n        if len(ROIs) > 0:\n            logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Preparing rois\", get_ident())\n            for ROI in ROIs:\n                Xr = ROI[0]\n                Yr = ROI[1]\n                Wr = ROI[2]\n                Hr = ROI[3]\n                logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Defined RoI: (%s,%s,%s,%s)\", get_ident(), Xr, Yr, Wr, Hr)\n                # Convert to image coordinates\n                xr = int((Xr - Xl) * wl / Wl)\n                if xr < 0:\n                    xr = 0\n                if xr >= wl:\n                    xr = wl -1\n                yr = int((Yr - Yl) * hl / Hl)\n                if yr < 0:\n                    yr = 0\n                if yr >= hl:\n                    yr = hl -1\n                wr = int(Wr * wl / Wl)\n                if wr <= 0:\n                    wr = 1\n                hr = int(Hr * hl / Hl)\n                if hr <= 0:\n                    hr = 1\n                roi = (xr, yr, wr, hr)\n                logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Converted roi: (%s,%s,%s,%s)\", get_ident(), xr, yr, wr, hr)\n                cls.rois.append(roi)\n\n        # Regions of No Interest\n        RONIs = tc.regionOfNoInterest\n        if len(RONIs) > 0:\n            logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Preparing ronis\", get_ident())\n            for RONI in RONIs:\n                Xr = RONI[0]\n                Yr = RONI[1]\n                Wr = RONI[2]\n                Hr = RONI[3]\n                logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Defined RONI: (%s,%s,%s,%s)\", get_ident(), Xr, Yr, Wr, Hr)\n                # Convert to image coordinates\n                xr = int((Xr - Xl) * wl / Wl)\n                if xr < 0:\n                    xr = 0\n                if xr >= wl:\n                    xr = wl -1\n                yr = int((Yr - Yl) * hl / Hl)\n                if yr < 0:\n                    yr = 0\n                if yr >= hl:\n                    yr = hl -1\n                wr = int(Wr * wl / Wl)\n                if wr <= 0:\n                    wr = 1\n                hr = int(Hr * hl / Hl)\n                if hr <= 0:\n                    hr = 1\n                roni = (xr, yr, wr, hr)\n                logger.debug(\"Thread %s: MotionDetector.prepareRoIs - Converted roni: (%s,%s,%s,%s)\", get_ident(), xr, yr, wr, hr)\n                cls.ronis.append(roni)\n\n    @classmethod\n    def _motionDetected(cls, fCur, fPrv) -> tuple:\n        \"\"\" Analyze input frames to detect motion\n        \n        \"\"\"\n        tc = CameraCfg().triggerConfig\n        # logger.debug(\"Thread %s: MotionDetector._motionDetected - algo: %s\", get_ident(), tc.motionDetectAlgo)\n        motion = False\n        trigger = {}\n\n        if tc.motionDetectAlgo == 1:\n            (motion, trigger, roiDetected) = cls._motionAlgo_MeanSquare(fCur, fPrv, cls.camInfo, cls.rois, cls.ronis)\n            cls.roiDetected = roiDetected\n        if tc.motionDetectAlgo > 1:\n            (motion, trigger, roiDetected) = cls.mdAlgo.detectMotion(fCur, fPrv, cls.camInfo, cls.rois, cls.ronis)\n            cls.roiDetected = roiDetected\n            cls.event.set()\n\n        return (motion, trigger)\n\n    @staticmethod\n    def _motionAlgo_MeanSquare(fCur, fPrv, camInfo: str, rois: list, ronis: list) -> tuple:\n        \"\"\" Mean Square algorithm for motion detection\n        \n        \"\"\"\n        # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare\", get_ident())\n\n        triggerParams = {}\n        triggerParams[\"cam\"] = camInfo\n        roiDetected = 0\n\n        for roni in ronis:\n            x = roni[0]\n            y = roni[1]\n            w = roni[2]\n            h = roni[3]\n            fCur[y:y+h, x:x+w] = 0\n            fPrv[y:y+h, x:x+w] = 0\n        # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare - cleared %s ronis\", get_ident(), len(ronis))\n\n        motion = False\n        if len(rois) == 0:\n            msd = np.square(np.subtract(fCur, fPrv)).mean()\n            # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare msd: %s\", get_ident(), msd)\n            if msd > CameraCfg().triggerConfig.msdThreshold:\n                triggerParams[\"msd\"] = str(round(msd, 3))\n                motion = True\n        else:\n            idx = 0\n            for roi in rois:\n                idx += 1\n                x = roi[0]\n                y = roi[1]\n                w = roi[2]\n                h = roi[3]\n                rCur = fCur[y:y+h, x:x+w]\n                rPrv = fPrv[y:y+h, x:x+w]\n                msd = np.square(np.subtract(rCur, rPrv)).mean()\n                # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare shape(fCur)=%s\", get_ident(), fCur.shape)\n                # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare shape(rCur)=%s\", get_ident(), rCur.shape)\n                # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare roi (%s,%s,%s,%s) msd: %s\", get_ident(), x, y, w, h, msd)\n                if msd > CameraCfg().triggerConfig.msdThreshold:\n                    triggerParams[\"roi\"] = idx\n                    triggerParams[\"msd\"] = str(round(msd, 3))\n                    motion = True\n                    roiDetected = idx\n                    break\n        # logger.debug(\"Thread %s: MotionDetector._motionAlgo_MeanSquare - motion: %s\", get_ident(), motion)\n        return (motion, {\"trigger\":\"Motion Detection\", \"triggertype\":\"Mean Square Diff\", \"triggerparam\":triggerParams}, roiDetected)\n\n    @classmethod\n    def _doAction(cls, trigger: str):\n        \"\"\" Execute action\n        \n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._doAction\", get_ident())\n        tc = CameraCfg().triggerConfig\n\n        logEvent = False\n\n        now = datetime.now()\n        logger.debug(\"Thread %s: MotionDetector._doAction - now: %s\", get_ident(), now)\n        if cls.eventStart is None:\n            cls.eventKey = now.strftime(\"%Y-%m-%dT%H:%M:%S\")\n            cls.eventStart = now\n            logEvent = True\n            logger.debug(\"Thread %s: MotionDetector._doAction - New event started with key: %s\", get_ident(), cls.eventKey)\n\n        delta = now - cls.eventStart\n        deltaSec = delta.total_seconds()\n        logger.debug(\"Thread %s: MotionDetector._doAction - deltaSec: %s\", get_ident(), deltaSec)\n\n        if deltaSec > tc.detectionPauseSec:\n            # Difference to previous event is larger than pause -> new event\n            logger.debug(\"Thread %s: MotionDetector._doAction - Starting new event\", get_ident())\n            cls.eventKey = now.strftime(\"%Y-%m-%dT%H:%M:%S\")\n            cls.eventStart = now\n            cls.nrPhotos = 0\n            cls.lastPhoto = None\n            cls._stopAction(force=True)\n            cls.videoStart = None\n            cls.videoStop = None\n            if tc.actionVR == 1:\n                cls.videoEncoder = None\n            cls.videoName = None\n            deltaSec = 0\n            logEvent = True\n\n        startVideo = False\n        recordVideo = False\n        doPhoto = False\n        doNotify = False\n\n        if deltaSec >= tc.detectionDelaySec:\n            if tc.actionVideo == True:\n                if cls.videoStart is None:\n                    startVideo = True\n                    cls.videoStart = now\n                else:\n                    if cls.videoStop is None:\n                        recordVideo = True\n\n            if tc.actionPhoto == True:\n                if cls.nrPhotos == 0:\n                    doPhoto = True\n                    cls.lastPhoto = now\n                    cls.nrPhotos = 1\n                else:\n                    if cls.nrPhotos < tc.actionPhotoBurst:\n                        deltaP = now - cls.lastPhoto\n                        deltaPSec = deltaP.total_seconds()\n                        if deltaPSec >= tc.actionPhotoBurstDelaySec:\n                            doPhoto = True\n                            cls.nrPhotos += 1\n                            cls.lastPhoto = now\n\n            if tc.actionNotify == True:\n                if cls.notificationDone is None:\n                    doNotify = True\n                else:\n                    notifyDelta = cls.eventStart - cls.notificationDone\n                    notifyDeltaSec = notifyDelta.total_seconds()\n                    if notifyDeltaSec >= tc.notifyPause:\n                        if cls.notificationDone < cls.eventStart:\n                            doNotify = True\n            logger.debug(\"Thread %s: MotionDetector._doAction - delta>=delay - logEvent: %s startVideo: %s recordVideo: %s doPhoto: %s doNotify: %s\", get_ident(), logEvent, startVideo, recordVideo, doPhoto, doNotify)\n        else:\n            logger.debug(\"Thread %s: MotionDetector._doAction - delta<delay  - logEvent: %s startVideo: %s recordVideo: %s doPhoto: %s doNotify: %s\", get_ident(), logEvent, startVideo, recordVideo, doPhoto, doNotify)\n\n        fnRaw = now.strftime(\"%Y-%m-%dT%H-%M-%S\")\n        logTS = now.strftime(\"%Y-%m-%dT%H:%M:%S\")\n        fnPhoto = fnRaw + \".jpg\"\n        fnVideo = fnRaw + \".mp4\"\n\n        if logEvent:\n            if MotionDetector().when_motion_detected:\n                MotionDetector().when_motion_detected()\n\n        if logEvent:\n            with open(tc.logFilePath, \"a\") as f:\n                f.write(logTS + \" Event  detected       Trigger: \" + trigger[\"trigger\"] + \" - '\" + trigger[\"triggertype\"] + \"' \" + str(trigger[\"triggerparam\"]) + \"\\n\")\n            key = cls.eventKey\n            # logger.debug(\"Thread %s: MotionDetector._doAction - INSERT INTO events - timestamp: %s\", get_ident(), key)\n            cls.db.execute(\n                \"INSERT INTO events (timestamp, date, minute, time, type, trigger, triggertype, triggerparam) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                (key, key[:10], key[11:16], key[11:19], \"Motion\", trigger[\"trigger\"], trigger[\"triggertype\"], str(trigger[\"triggerparam\"]))\n            )\n            cls.db.commit()\n            # logger.debug(\"Thread %s: MotionDetector._doAction - DB committed\", get_ident())\n\n        if startVideo:\n            done = False\n            logger.debug(\"Thread %s: MotionDetector._doAction - Starting video\", get_ident())\n            if tc.motionDetectAlgo == 1 \\\n            or  (tc.videoBboxes == False and tc.photoRois == False):\n                if tc.actionVR == 1:\n                    (done, encoder, err) = Camera.quickVideoStart(tc.actionPath + \"/\" + fnVideo)\n                else:\n                    (done, err) = Camera.recordCircular(cls.videoCircOutput, tc.actionPath + \"/\" + fnVideo)\n                    encoder = cls.videoEncoder\n            if tc.motionDetectAlgo > 1 \\\n            and (tc.videoBboxes == True or tc.photoRois == True):\n                (done, fnVideo, err) = cls.mdAlgo.startRecordMotion(fnRaw, tc.photoRois)\n                encoder = None\n            if done == True:\n                if encoder:\n                    cls.videoEncoder = encoder\n                cls.videoName = fnVideo\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" started\" + \"\\n\")\n                cls.videoKey = logTS\n                # logger.debug(\"Thread %s: MotionDetector._doAction - INSERT INTO eventactions - Video\", get_ident())\n                cls.db.execute(\n                    \"INSERT INTO eventactions (event, timestamp, date, time, actiontype, actionduration, filename, fullpath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (cls.eventKey, logTS, logTS[:10], logTS[11:19], \"Video\", tc.actionVideoDuration, fnVideo, tc.actionPath + \"/\" + fnVideo)\n                )\n                cls.db.commit()\n                # logger.debug(\"Thread %s: MotionDetector._doAction - DB committed\", get_ident())\n            else:\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" Start   Error: \" + err + \"\\n\")\n\n        photoDone = False\n        if doPhoto:\n            fpPhoto = tc.actionPath + \"/\" + fnPhoto\n            (done, err, frame) = Camera.quickPhoto(fpPhoto, not tc.photoRois)\n            if tc.photoRois == True and done == False and err == \"\":\n                (done, err) = cls.savePhotoWithRois(frame, fpPhoto, cls.rois, cls.ronis, cls.roiDetected)\n            photoDone = done\n            if done:\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Photo: \" + fnPhoto + \"\\n\")\n                # logger.debug(\"Thread %s: MotionDetector._doAction - INSERT INTO eventactions - Photo\", get_ident())\n                cls.db.execute(\n                    \"INSERT INTO eventactions (event, timestamp, date, time, actiontype, actionduration, filename, fullpath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (cls.eventKey, logTS, logTS[:10], logTS[11:19], \"Photo\", 0, fnPhoto, tc.actionPath + \"/\" + fnPhoto)\n                )\n                cls.db.commit()\n                # logger.debug(\"Thread %s: MotionDetector._doAction - DB committed\", get_ident())\n                if not cls.notifyMail is None:\n                    if tc.notifyIncludePhoto == True:\n                        cls._attachToNotification(cls.notifyMail, fnPhoto)\n                        if cls.nrPhotos >= tc.actionPhotoBurst:\n                            if tc.notifyIncludeVideo == True:\n                                if not cls.videoStop is None:\n                                    cls._sendNotification()\n                            else:\n                                cls._sendNotification()\n            else:\n                if err != \"\":\n                    with open(tc.logFilePath, \"a\") as f:\n                        f.write(logTS + \" Photo: \" + fnPhoto + \" Error:  \" + err + \"\\n\")\n\n        if doNotify:\n            cls.notificationDone = now\n            logger.debug(\"Thread %s: MotionDetector._doAction - Notification time: %s\", get_ident(), cls.notificationDone)\n            cls.notifyMail = cls._initNotificationMessage(cls.eventKey, trigger)\n            if tc.notifyIncludePhoto:\n                if photoDone:\n                    cls._attachToNotification(cls.notifyMail, fnPhoto)\n            if (tc.notifyIncludeVideo == False or tc.actionVideo == False) \\\n            and ((tc.notifyIncludePhoto == False or tc.actionPhoto == False) \\\n            or  ((tc.notifyIncludePhoto == True and tc.actionPhoto == True) \\\n            and tc.actionPhotoBurst <= 1)):\n                logger.debug(\"Thread %s: MotionDetector._doAction - Notification to be sent now\", get_ident())\n                cls._sendNotification()\n            else:\n                logger.debug(\"Thread %s: MotionDetector._doAction - Notification to be sent later\", get_ident())\n\n    @staticmethod\n    def savePhotoWithRois(frame, fp, rois: list, ronis: list, roiDetected: int = 0) -> tuple:\n        \"\"\" Save a photo with ROIs drawn\n        \n        \"\"\"\n        done = False\n        err = \"\"\n        try:\n            import cv2\n            logger.debug(\"Thread %s: MotionDetector.savePhotoWithRois - saving photo with rois to %s\", get_ident(), fp)\n            for roni in ronis:\n                x = roni[0]\n                y = roni[1]\n                w = roni[2]\n                h = roni[3]\n                color = (255, 0, 0)\n                cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)\n\n            idx = 0\n            for roi in rois:\n                idx += 1\n                x = roi[0]\n                y = roi[1]\n                w = roi[2]\n                h = roi[3]\n                if idx == roiDetected:\n                    color = (0, 0, 255)\n                else:\n                    color = (0, 255, 0)\n                cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)\n\n            cv2.imwrite(fp, frame)\n            done = True\n            logger.debug(\"Thread %s: MotionDetector.savePhotoWithRois - done\", get_ident())\n        except Exception as e:\n            err = str(e)\n            logger.error(\"Thread %s: MotionDetector.savePhotoWithRois - Error saving photo with rois: %s\", get_ident(), err)\n        return (done, err)\n\n    @staticmethod            \n    def _initNotificationMessage(logTS, trigger) -> EmailMessage:\n        \"\"\" Set up an eMail Message for notification\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._initNotificationMessage\", get_ident())\n        msg = EmailMessage()\n        tc = CameraCfg().triggerConfig\n        msg[\"From\"] = tc.notifyFrom\n        msg[\"To\"] = tc.notifyTo\n        msg[\"Subject\"] = tc.notifySubject\n        msg.set_content(\n            \"Notification on an event\\n\\n\" \\\n            \"Time   : \" + logTS + \"\\n\" \\\n            \"Trigger: \" + trigger[\"trigger\"] + \"\\n\" \\\n            \"Type   : \" + trigger[\"triggertype\"] + \"\\n\" \\\n            \"MSD    : \" + str(trigger[\"triggerparam\"])\n        )\n        logger.debug(\"Thread %s: MotionDetector._initNotificationMessage - done\", get_ident())\n        return msg\n\n    @staticmethod            \n    def _attachToNotification(msg, fn):\n        \"\"\" Attach a photo to a notification mail\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._attachToNotification - fn: %s\", get_ident(), fn)\n        tc = CameraCfg().triggerConfig\n        img = tc.actionPath + \"/\" + fn\n        ctype, encoding = mimetypes.guess_type(img)\n        if ctype is None or encoding is not None:\n            ctype = 'application/octet-stream'\n        maintype, subtype = ctype.split('/', 1)\n        with open(img, 'rb') as fp:\n            msg.add_attachment(fp.read(),\n                                maintype=maintype,\n                                subtype=subtype,\n                                filename=fn)\n        logger.debug(\"Thread %s: MotionDetector._attachToNotification - done\", get_ident())\n\n    @classmethod\n    def _sendNotificationThread(cls):\n        \"\"\" Send notification mail in own thread\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._sendNotificationThread\", get_ident())\n        tc = CameraCfg().triggerConfig\n        scr =CameraCfg().secrets\n        with cls.notifyBufferLock:\n            msg = cls.notifyBuffer.pop(0)\n        try:\n            if tc.notifyUseSSL == True:\n                server = smtplib.SMTP_SSL(host=tc.notifyHost, port=tc.notifyPort)\n            else:\n                server = smtplib.SMTP(host=tc.notifyHost, port=tc.notifyPort)\n            server.connect(tc.notifyHost)\n\n            if tc.notifyAuthenticate == True:\n                logger.debug(\"Thread %s: MotionDetector._sendNotificationThread - Authentication with user/pwd\", get_ident())\n                server.login(scr.notifyUser, scr.notifyPwd)\n            else:\n                logger.debug(\"Thread %s: MotionDetector._sendNotificationThread - Authentication skipped\", get_ident())\n            server.ehlo()\n            server.send_message(msg)\n            server.quit()\n        except Exception as e:\n            tc.error = \"Error sending notification mail: \" + str(e)\n            tc.errorSource = \"MotionDetector._sendNotificationThread\"\n            logger.error(\"Error sending notification mail: %s\", e)\n        logger.debug(\"Thread %s: MotionDetector._sendNotificationThread - done\", get_ident())\n\n    @classmethod\n    def _sendNotification(cls):\n        \"\"\" Send notification mail\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._sendNotification\", get_ident())\n        msg = copy.copy(cls.notifyMail)\n        with cls.notifyBufferLock:\n            cls.notifyBuffer.append(msg)\n        thread = threading.Thread(target=cls._sendNotificationThread)\n        thread.start()\n        cls.notifyMail = None\n        logger.debug(\"Thread %s: MotionDetector._sendNotification - done\", get_ident())\n\n    @classmethod\n    def _cleanupEvent(cls):\n        \"\"\" Cleanup event data\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._cleanupEvent\", get_ident())\n        cls._stopAction(force=True)\n        cls.eventKey = None\n        cls.eventStart = None\n        cls.lastPhoto = None\n        cls.nrPhotos = 0\n        if CameraCfg().triggerConfig.actionVR == 1:\n            cls.videoEncoder = None\n        cls.videoKey = None\n        cls.videoName = None\n        cls.videoStart = None\n        cls.videoStop = None\n        if not cls.notifyMail is None:\n            cls._sendNotification()\n\n    @classmethod\n    def _stopAction(cls, force = False):\n        \"\"\" Stop an active action, if required\n        \"\"\"\n        # logger.debug(\"Thread %s: MotionDetector._stopAction\", get_ident())\n        tc = CameraCfg().triggerConfig\n        waitForVideo = False\n        if not cls.videoStart is None:\n            if cls.videoStop is None:\n                logger.debug(\"Thread %s: MotionDetector._stopAction - video is running\", get_ident())\n                if not cls.videoName is None:\n                    now = datetime.now()\n                    dur = now-cls.videoStart\n                    durSec = dur.total_seconds()\n                    if durSec > tc.actionVideoDuration or force:\n                        logger.debug(\"Thread %s: MotionDetector._stopAction - stopping video\", get_ident())\n                        done = False\n                        if tc._motionDetectAlgo == 1 \\\n                        or tc.videoBboxes == False:\n                            if tc.actionVR == 1:\n                                (done, err) = Camera.quickVideoStop(cls.videoEncoder)\n                            else:\n                                (done, err) = Camera.stopRecordingCircular(cls.videoCircOutput)\n                        if tc._motionDetectAlgo > 1 \\\n                        and tc.videoBboxes == True:\n                            cls.mdAlgo.stopRecordMotion()\n                            done = True\n                        logTS = now.strftime(\"%Y-%m-%d %H:%M:%S\")\n                        if done:\n                            cls.videoEncoder = None\n                            with open(tc.logFilePath, \"a\") as f:\n                                f.write(logTS + \" Video: \" + cls.videoName + \" stopped\" + \"\\n\")\n                            logger.debug(\"Thread %s: MotionDetector._stopAction - UPDATE eventactions\", get_ident())\n                            cls.db.execute(\n                                \"UPDATE eventactions set actionduration = ? WHERE event = ? AND timestamp = ? AND actiontype = ?\",\n                                (round(durSec,0), cls.eventKey, cls.videoKey, \"Video\")\n                            )\n                            cls.db.commit()\n                            logger.debug(\"Thread %s: MotionDetector._stopAction - DB committed\", get_ident())\n                            if not cls.notifyMail is None:\n                                if tc.notifyIncludeVideo == True:\n                                    cls._attachToNotification(cls.notifyMail, cls.videoName)\n                                now = datetime.now()\n                                dur = now -cls.notificationDone\n                                durSec = dur.total_seconds()\n                                noWait = durSec >= (tc.actionPhotoBurst + 1) * tc.actionPhotoBurstDelaySec\n                                if noWait:\n                                    cls._sendNotification()\n                        else:\n                            with open(tc.logFilePath, \"a\") as f:\n                                f.write(logTS + \" Video: \" + cls.videoName + \" Stop     Error\" + err + \"\\n\")\n                        cls.videoStop = now\n                    else:\n                        waitForVideo = True\n                        logger.debug(\"Thread %s: MotionDetector._stopAction - video still within duration\", get_ident())\n\n        # Send outstanding mails\n        if not cls.notifyMail is None:\n            # A mail has been initialized but not sent yet\n            # Check whether we need to wait for more photos\n            now = datetime.now()\n            dur = now-cls.notificationDone\n            durSec = dur.total_seconds()\n            noWait = durSec >= (tc.actionPhotoBurst + 1) * tc.actionPhotoBurstDelaySec\n            if waitForVideo:\n                noWait = False\n\n            if force or noWait:\n                logger.debug(\"Thread %s: MotionDetector._stopAction - outstanding mail to be sent\", get_ident())\n                cls._sendNotification()\n\n    @staticmethod\n    def _isActive() -> bool:\n        \"\"\" Check whether trigger is supposed to be active\n        \"\"\"\n        active = True\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n\n        now = datetime.now()\n        wd = str(now.isoweekday())\n        if tc.operationWeekdays[wd] == True:\n            h = now.hour\n            m = now.minute\n            dm = 60 * h + m\n            if dm >= tc.operationStartMinute \\\n            and dm <= tc.operationEndMinute:\n                active = True\n            else:\n                active = False\n        else:\n            active = False\n        if cfg.serverConfig.isTriggerTesting == True:\n            active = True\n        if active:\n            cfg.serverConfig.isTriggerWaiting = False\n        else:\n            cfg.serverConfig.isTriggerWaiting = True\n        return active\n\n    @classmethod\n    def _motionThread(cls):\n        \"\"\" Motion detection thread\n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector._motionThread\", get_ident())\n        cls.db = get_dbx()\n        logger.debug(\"Thread %s: MotionDetector._motionThread - got database\", get_ident())\n        cam = Camera()\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n        startTime = datetime.now()\n        count = 0\n        tc.motionTestFramerate = 0\n        prv = None\n        if cfg.triggerConfig.actionVR == 2:\n            (done, circ, encoder, err) = cam.startCircular()\n            if done:\n                logger.debug(\"Thread %s: MotionDetector._motionThread - Encoder for circular output started\", get_ident())\n                cls.videoCircOutput = circ\n                cls.videoEncoder = encoder\n            else:\n                logger.error(\"Circular output not started: %s\", err)\n                cfg.triggerConfig.actionVR = 1\n        stop = False\n        while not stop:\n            if cls._isActive():\n                if not cfg.serverConfig.isLiveStream:\n                    cam.startLiveStream()\n                try:\n                    # Just to keep the live stream running\n                    frame, frameRaw = cam.get_frame()\n                    cur = cam.getLiveViewImageForMotionDetection()\n                    # logger.debug(\"Thread %s: MotionDetector._motionThread - got live view buffer\", get_ident())\n                    if prv is not None:\n                        (motion, trigger) = cls._motionDetected(cur, prv)\n                        if not cls.mdAlgo is None:\n                            if cls.mdAlgo.test == True:\n                                count += 1\n                                timeDelta = datetime.now() - startTime\n                                timeDeltaSec = timeDelta.total_seconds()\n                                if timeDeltaSec > 0.5:\n                                    tc.motionTestFramerate = count / timeDeltaSec\n                        else:\n                            count += 1\n                            timeDelta = datetime.now() - startTime\n                            timeDeltaSec = timeDelta.total_seconds()\n                            if timeDeltaSec > 0.5:\n                                tc.motionTestFramerate = count / timeDeltaSec\n                            if timeDeltaSec > 3600:\n                                count = 0\n                                startTime = datetime.now()\n                        if motion:\n                            # logger.debug(\"Thread %s: MotionDetector._motionThread - motion detected\", get_ident())\n                            cls._doAction(trigger)\n                        cls._stopAction()\n                        # logger.debug(\"Thread %s: MotionDetector._motionThread - stopAction done\", get_ident())\n                        if tc.motionDetectAlgo > 1 \\\n                        and tc.videoBboxes == True:\n                            if not cls.videoStart is None \\\n                            and cls.videoStop is None:\n                                cls.mdAlgo.recordMotion()\n                    prv = cur\n                    if cls.mThreadStop:\n                        logger.debug(\"Thread %s: MotionDetector._motionThread - stop requested\", get_ident())\n                        cls._stopAction(force=True)\n                        stop = True\n                except Exception as e:\n                    cls._cleanupEvent()\n                    logger.error(\"Exception in _motionThread: %s\", e)\n                    cfg.triggerConfig.error = \"Error in motion detection: \" + str(e)\n                    cfg.triggerConfig.errorSource = \"motionDetector._motionThread\"\n                    stop = True\n            else:\n                cls._cleanupEvent()\n                time.sleep(2)\n                if cls.mThreadStop:\n                    stop = True\n\n        if cfg.triggerConfig.actionVR == 2:\n            (done, err) = cam.stopCircular(cls.videoEncoder)\n            if done:\n                logger.debug(\"Thread %s: MotionDetector._motionThread - Encoder for circular output stopped\", get_ident())\n                cls.videoCircOutput = None\n                cls.videoEncoder = None\n            else:\n                logger.error(\"Circular output not stopped: %s\", err)\n        cls.mThread = None\n\n    @classmethod\n    def startMotionDetection(cls):\n        \"\"\" Start motion detection\n        \n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector.startMotionDetection\", get_ident())\n        sc = CameraCfg().serverConfig\n        tc = CameraCfg().triggerConfig\n        if cls.mThread is None:\n            sc.error = None\n            tc.error = None\n            cls.prepareRoIs()\n            if tc.motionDetectAlgo == 2:\n                cls.mdAlgo.bbox_threshold = tc.bboxThreshold\n                cls.mdAlgo.nms_threshold = tc.nmsThreshold\n                cls.mdAlgo.frameSize = CameraCfg().liveViewConfig.stream_size\n                cls.mdAlgo.framerate = 15\n            if tc.motionDetectAlgo == 3:\n                cls.mdAlgo.motion_threshold = tc.motionThreshold\n                cls.mdAlgo.nms_threshold = tc.nmsThreshold\n                cls.mdAlgo.backSubMod = tc.backSubModel\n                cls.mdAlgo.frameSize = CameraCfg().liveViewConfig.stream_size\n                cls.mdAlgo.framerate = 5\n            if tc.motionDetectAlgo == 4:\n                cls.mdAlgo.bbox_threshold = tc.bboxThreshold\n                cls.mdAlgo.nms_threshold = tc.nmsThreshold\n                cls.mdAlgo.backSubMod = tc.backSubModel\n                cls.mdAlgo.frameSize = CameraCfg().liveViewConfig.stream_size\n                cls.mdAlgo.framerate = 15\n            if sc.isTriggerTesting == True:\n                logger.debug(\"Thread %s: MotionDetector.startMotionDetection - Activating test mode\", get_ident())\n                cls.mdAlgo.test = True\n                tc._motionTestFrame1Title = cls.mdAlgo.testFrame1Title\n                tc._motionTestFrame2Title = cls.mdAlgo.testFrame2Title\n                tc._motionTestFrame3Title = cls.mdAlgo.testFrame3Title\n                tc._motionTestFrame4Title = cls.mdAlgo.testFrame4Title\n                tc.motionRefTit = cls.mdAlgo.algoReferenceTit\n                tc.motionRefURL = cls.mdAlgo.algoReferenceURL\n            else:\n                logger.debug(\"Thread %s: MotionDetector.startMotionDetection - Activating normal mode\", get_ident())\n                if not cls.mdAlgo is None:\n                    cls.mdAlgo.test = False\n            if not CameraCfg().serverConfig.isLiveStream:\n                Camera().startLiveStream()\n            if not sc.error:\n                logger.debug(\"Thread %s: MotionDetector.startMotionDetection - starting new thread\", get_ident())\n                cls.mThread = threading.Thread(target=cls._motionThread, daemon=True)\n                cls.mThread.start()\n                logger.debug(\"Thread %s: MotionDetector.startMotionDetection - thread started\", get_ident())\n            else:\n                logger.debug(\"Thread %s: MotionDetector.startMotionDetection - not started\", get_ident())\n\n    @classmethod\n    def stopMotionDetection(cls):\n        \"\"\" Stop motion detection\n        \n        \"\"\"\n        logger.debug(\"Thread %s: MotionDetector.stopMotionDetection\", get_ident())\n        if cls.mThread is None:\n            logger.debug(\"Thread %s: MotionDetector.stopMotionDetection - thread was not active\", get_ident())\n        else:\n            logger.debug(\"Thread %s: MotionDetector.stopMotionDetection - stopping thread\", get_ident())\n            cls.mThreadStop = True\n            cnt = 0\n            while cls.mThread:\n                time.sleep(0.01)\n                cnt += 1\n                if cnt > 500:\n                    logger.error(\"Motion detection thread did not stop within 5 sec\")\n                    if cls.mThread.is_alive():\n                        cnt = 0\n                    else:\n                        cls.mThread = None\n                    # raise TimeoutError(\"Motion detection thread did not stop within 5 sec\")\n            cls.mThreadStop = False\n            cls._cleanupEvent()\n        logger.debug(\"Thread %s: MotionDetector.stopMotionDetection: Thread has stopped\", get_ident())\n"
  },
  {
    "path": "raspiCamSrv/photoseries.py",
    "content": "from flask import Blueprint, Response, flash, g, redirect, render_template, request, url_for\nfrom flask import send_file\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camCfg import CameraCfg\nfrom raspiCamSrv.photoseriesCfg import PhotoSeriesCfg\nfrom raspiCamSrv.photoseriesCfg import Series\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.sun import Sun\nfrom raspiCamSrv.version import version\nimport os\nimport copy\nfrom pathlib import Path\nfrom datetime import datetime\nfrom datetime import timedelta\nfrom zoneinfo import ZoneInfo\nfrom io import BytesIO\nfrom zipfile import ZipFile\nimport time\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\nbp = Blueprint(\"photoseries\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/photoseries\")\n@login_required\ndef main():\n    g.hostname = request.host\n    g.version = version\n    # Although not directly needed here, the camara needs to be initialized\n    # in order to load the camera-specific parameters into configuration\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if sc.lastPhotoSeriesTab == \"\":\n        sc.lastPhotoSeriesTab = \"series\"\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/new_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef new_series():\n    logger.debug(\"In new_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        seriesName = request.form[\"tlnewseries\"]\n        logger.debug(\"seriesName: %s\", seriesName)\n        if tl.nameExists(seriesName):\n            msg = \"Error: There is already a series with this name.\"\n            flash(msg)\n            serOK = False\n        else:\n            ser = Series()\n            ser.name = seriesName\n            ser.path = tl.rootPath + \"/\" + ser.name\n            serOK = True\n            logger.debug(\"ser.path: %s\", ser.path)\n            try:\n                os.makedirs(ser.path, exist_ok=False)\n                logger.debug(\"ser.path created: %s\", ser.path)\n                if sc.useHistograms:\n                    os.makedirs(ser.histogramPath, exist_ok=False)\n            except FileExistsError:\n                serOK = False\n                msg = \"A folder for this series name exists already: \" + ser.path\n                flash(msg)\n            except OSError:\n                serOK = False\n                msg = \"A folder with this name cannot be created: \" + ser.path + \" Choose a different name!\"\n                flash(msg)\n            except Exception:\n                serOK = False\n                msg = \"A folder with this name cannot be created: \" + ser.path + \" Choose a different name!\"\n                flash(msg)\n        if serOK:\n            ser.logFile = ser.path + \"/\" + ser.logFileName\n            ser.cfgFile = ser.path + \"/\" + ser.cfgFileName\n            ser.camFile = ser.path + \"/\" + ser.camFileName\n            try:\n                Path(ser.logFile).touch()\n                Path(ser.cfgFile).touch()\n                Path(ser.camFile).touch()\n                logger.debug(\"ser.logFile created: %s\", ser.logFile)\n                logger.debug(\"ser.cfgFile created: %s\", ser.cfgFile)\n                logger.debug(\"ser.camFile created: %s\", ser.camFile)\n            except Exception:\n                serOK = False\n                msg = \"Unable to create .log or .cfg or .cam File: \" + ser.logFile\n                flash(msg)\n        if serOK:\n            dt = datetime.now() + timedelta(minutes=1)\n            dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n            ser.start = dt\n            ser.end = ser.start\n            ser.interval = 5.0\n            ser.onDialMarks = False\n            ser.nrShots = 1\n            ser.nextStatus(\"create\")\n            ser.persist()\n            tl.appendSeries(ser)\n            logger.debug(\"Series appended: %s\", ser.name)\n            tl.curSeries = ser\n            logger.debug(\"Current series set to: %s\", ser.name)\n            sr=tl.curSeries\n            \n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/select_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef select_series():\n    logger.debug(\"In select_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        serName = request.form[\"selectseries\"]\n        logger.debug(\"selected series: %s\", serName)\n        for ser in tl.tlSeries:\n            if ser.name == serName:\n                tl.curSeries = ser\n                break\n        sr = tl.curSeries\n        logger.debug(\"current series set to: %s\", tl.curSeries.name)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/start_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef start_series():\n    logger.debug(\"In start_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        msg = None\n        sr.error = None\n        if sr.isExposureSeries \\\n        or sr.isFocusStackingSeries:\n            if sc.isTriggerRecording:\n                msg = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not msg:\n            if sr.status == \"READY\":\n                if sr.isExposureSeries or sr.isFocusStackingSeries:\n                    #Backup controls\n                    cfg.controlsBackup = copy.deepcopy(cfg.controls)\n                    logger.debug(\"Created backup for controls: %s\", cfg.controlsBackup.__dict__)\n                if sr.isExposureSeries:\n                    # For exposure series disable Auto and set fixed control parameter\n                    ctrl = cfg.controls\n                    ctrl.aeEnable = False\n                    ctrl.include_aeEnable = True\n                    ctrl.awbEnable = False\n                    ctrl.include_awbEnable = True\n                    if sr.isExpGainFix:\n                        ctrl.include_analogueGain = True\n                        ctrl.analogueGain = sr.expGainStart\n                    if sr.isExpExpTimeFix:\n                        ctrl.include_exposureTime = True\n                        ctrl.exposureTime = sr.expTimeStart\n                if sr.isFocusStackingSeries:\n                    # For focus series, set Autofocus to manual\n                    ctrl = cfg.controls\n                    ctrl.afMode = 0\n                # Ckeck whether series start is in the past\n                dt = datetime.now() + timedelta(seconds=5)\n                startnow = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n                startnow = startnow + timedelta(minutes=0)\n                logger.debug(\"now: %s  startnow: %s  sr.start: %s\", datetime.now(), startnow, sr.start)\n                if sr.isSunControlledSeries == False:\n                    if sr.start <= startnow:\n                        logger.debug(\"Start immediately\")\n                        sr.start = startnow\n                        timedifSec = int(sr.interval * sr.nrShots)\n                        delta = timedelta(seconds=timedifSec)\n                        serEndRaw = sr.start + delta\n                        serEnd = datetime(year=serEndRaw.year, month=serEndRaw.month, day=serEndRaw.day, hour=serEndRaw.hour, minute=serEndRaw.minute)\n                        serEnd = serEnd + timedelta(minutes=2)\n                        sr.end = serEnd\n                \n                tlOK = True\n                Camera.startPhotoSeries(sr)\n                time.sleep(2)\n                if sc.error:\n                    tlOK = False\n                    sr.nextStatus(\"pause\")\n                    msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n                    flash(msg)\n                    if sc.error2:\n                        flash(sc.error2)\n                    msg = None\n                if sr.error:\n                    tlOK = False\n                    sr.nextStatus(\"pause\")\n                    msg = \"Error in \" + sr.errorSource + \": \" + sr.error\n                    flash(msg)\n                    if sr.error2:\n                        flash(sr.error2)\n                    msg = None\n                if tlOK:\n                    sr.nextStatus(\"start\")\n                    sr.persist()\n            else:\n                logger.debug(\"Nothing to do sr.status is %s\", sr.status)\n        if msg:\n            flash(msg)\n    #return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n    return redirect(url_for(\"photoseries.main\"))\n\n@bp.route(\"/pause_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef pause_series():\n    logger.debug(\"In pause_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        sr.nextStatus(\"pause\")\n        sr.persist()\n        Camera.stopPhotoSeries()\n        if sr.isExposureSeries or sr.isFocusStackingSeries:\n            if cfg.controlsBackup:\n                #Restore controls\n                cfg.controls = copy.deepcopy(cfg.controlsBackup)\n                logger.debug(\"Restored controls from backup: %s\", cfg.controls.__dict__)\n                cfg.controlsBackup = None\n                wait = None\n                if sr.isExposureSeries:\n                    #For an exposure series wait for the longest exposure time\n                    if sr.isExpGainFix:\n                        wait = 0.2 + sr.expTimeStop / 1000000\n                Camera().applyControlsForLivestream(wait)\n                logger.debug(\"Restored controls backup\")\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/finish_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef finish_series():\n    logger.debug(\"In finish_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        Camera.stopPhotoSeries()\n        logger.debug(\"Stopped Photo Series\")\n        sr.nextStatus(\"finish\")\n        dt = datetime.now()\n        dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n        sr.ended = dt\n        sr.persist()\n        if sr.isExposureSeries or sr.isFocusStackingSeries:\n            if cfg.controlsBackup:\n                #Restore controls\n                cfg.controls = copy.deepcopy(cfg.controlsBackup)\n                cfg.controlsBackup = None\n                wait = None\n                if sr.isExposureSeries:\n                    #For an exposure series wait for the longest exposure time\n                    if sr.isExpGainFix:\n                        wait = 0.2 + sr.expTimeStop / 1000000\n                Camera().applyControlsForLivestream(wait)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/continue_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef continue_series():\n    logger.debug(\"In continue_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        msg = None\n        sr.error = None\n        if sr.isExposureSeries \\\n        or sr.isFocusStackingSeries:\n            if sc.isTriggerRecording:\n                msg = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if not msg:\n            if sr.status == \"PAUSED\":\n                if sr.isExposureSeries or sr.isFocusStackingSeries:\n                    #Backup controls\n                    cfg.controlsBackup = copy.deepcopy(cfg.controls)\n                    logger.debug(\"Created backup for controls: %s\", cfg.controlsBackup.__dict__)\n                if sr.isExposureSeries:\n                    # For exposure series disable Auto and set fixed control parameter\n                    ctrl = cfg.controls\n                    ctrl.aeEnable = False\n                    ctrl.include_aeEnable = True\n                    ctrl.awbEnable = False\n                    ctrl.include_awbEnable = True\n                    if sr.isExpGainFix:\n                        ctrl.include_analogueGain = True\n                        ctrl.analogueGain = sr.expGainStart\n                    if sr.isExpExpTimeFix:\n                        ctrl.include_exposureTime = True\n                        ctrl.exposureTime = sr.expTimeStart\n                if sr.isFocusStackingSeries:\n                    # For focus series, set Autofocus to manual\n                    ctrl = cfg.controls\n                    ctrl.afMode = 0\n\n                if sr.isSunControlledSeries == False:\n                    #Adjust end time of series\n                    logger.debug(\"Start immediately\")\n                    if sr.nrShots is None or sr.curShots is None:\n                        timedifSec = int(sr.interval)\n                    else:    \n                        timedifSec = int(sr.interval * (sr.nrShots - sr.curShots + 1))\n                    delta = timedelta(seconds=timedifSec)\n                    serEndRaw = datetime.now() + delta\n                    serEnd = datetime(year=serEndRaw.year, month=serEndRaw.month, day=serEndRaw.day, hour=serEndRaw.hour, minute=serEndRaw.minute)\n                    serEnd = serEnd + timedelta(minutes=2)\n                    sr.end = serEnd\n                    logger.debug(\"Adjusted series end time to %s\", sr.end)\n\n                tlOK = True\n                Camera.startPhotoSeries(sr)\n                time.sleep(2)\n                if sc.error:\n                    tlOK = False\n                    sr.nextStatus(\"pause\")\n                    msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n                    flash(msg)\n                    if sc.error2:\n                        flash(sc.error2)\n                    msg = None\n                if sr.error:\n                    tlOK = False\n                    sr.nextStatus(\"pause\")\n                    msg = \"Error in \" + sr.errorSource + \": \" + sr.error\n                    flash(msg)\n                    if sr.error2:\n                        flash(sr.error2)\n                    msg = None\n                if tlOK:\n                    sr.nextStatus(\"start\")\n                    sr.persist()\n            else:\n                logger.debug(\"Nothing to do sr.status is %s\", sr.status)\n        if msg:\n            flash(msg)\n    return redirect(url_for(\"photoseries.main\"))\n\n@bp.route(\"/remove_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remove_series():\n    logger.debug(\"In remove_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        nam = sr.name\n        path = sr.path\n        tl.removeCurrentSeries()\n        sr = tl.curSeries\n        msg = \"Photoseries \" + nam + \" removed. Path: \" + path\n        flash(msg)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/download_series\", methods=(\"GET\", \"POST\"))\n@login_required\ndef download_series():\n    logger.debug(\"In download_series\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        logger.debug(\"download_series - Preparing archive\")\n        sc.lastPhotoSeriesTab = \"series\"\n        nam = sr.name\n        path = sr.path\n        dt = datetime.now()\n        dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n        sr.downloaded = dt\n        sr.persist()\n        stream = BytesIO()\n        with ZipFile(stream, 'w') as zf:\n            for root, dirs, files in os.walk(path):\n                for file in files:\n                    zf.write(os.path.join(root, file), \n                            os.path.relpath(os.path.join(root, file), \n                                            os.path.join(path, '..')))\n        stream.seek(0)\n        logger.debug(\"download_series - archive done\")\n\n        now = datetime.now()\n        zipName = \"raspiCamSrvSeries_\" + nam + \"_\" + now.strftime(\"%Y%m%d_%H%M%S\") + \".zip\"\n        logger.debug(\"images/download_selected - downloading as %s\", zipName)\n        msg = f\"Downloading archive {zipName}.\"\n        flash(msg)\n        return send_file(\n            stream,\n            as_attachment=True,\n            download_name=zipName\n        )\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/series_properties\", methods=(\"GET\", \"POST\"))\n@login_required\ndef series_properties():\n    logger.debug(\"In series_properties\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        if sr.status != \"FINISHED\":\n            serOK = True\n            if sr.status == \"ACTIVE\" \\\n            or sr.status == \"PAUSED\":\n                sertype = sr.type\n            else:\n                sertype = request.form[\"imgtype\"]\n                serStartFormIso = request.form[\"serstart\"]\n                sr.start = datetime.fromisoformat(serStartFormIso)\n            serEndFormIso = request.form[\"serend\"]\n            serIntForm = float(request.form[\"serinterval\"])\n            if request.form.get(\"serondialmarks\") is None:\n                serOnDialMarks = False\n            else:\n                serOnDialMarks = True\n            serShtForm = int(request.form[\"sernrshots\"])\n            if request.form.get(\"isautocontinue\") is None:\n                continueOnServerStart = False\n            else:\n                continueOnServerStart = True\n            # Iso date from form does not include seconds, \n            # so we need to cut off the seconds from the stored series\n            serEndOldIso = sr.endIso\n            if len(serEndOldIso) > len(serEndFormIso):\n                serEndOldIso = serEndOldIso[:len(serEndFormIso)]\n            logger.debug(\"Series end Iso: old=%s form=%s\", serEndOldIso, serEndFormIso)\n            if serEndFormIso != serEndOldIso:\n                # End time has been changed\n                serEnd = datetime.fromisoformat(serEndFormIso)\n                timedif = serEnd - sr.start\n                timedifSec = timedif.total_seconds()\n                if timedifSec <= 0:\n                    msg = \"Series end must be later than series start!\"\n                    flash(msg)\n                    serOK = False\n                else:\n                    if serIntForm != sr.interval:\n                        # Interval has been changed -> calculate nrShots\n                        serInt = serIntForm\n                        serNrShots = int(timedifSec / serInt)\n                    elif serShtForm != sr.nrShots:\n                        # Nr shots has been changed -> calculate interval\n                        serNrShots = serShtForm\n                        serInt = int(10 * timedifSec / serNrShots) / 10\n                    else:\n                        # Only series end has been changed -> keep interval and calculate nr shots\n                        serInt = sr.interval\n                        serNrShots = int(timedifSec / serInt)\n            else:\n                # Series end not changed -> calculate it from other params\n                serInt = serIntForm\n                serNrShots = serShtForm\n                timedifSec = int(serInt * serNrShots)\n                delta = timedelta(seconds=timedifSec)\n                serEndRaw = sr.start + delta\n                serEnd = datetime(year=serEndRaw.year, month=serEndRaw.month, day=serEndRaw.day, hour=serEndRaw.hour, minute=serEndRaw.minute)\n                serEnd = serEnd + timedelta(minutes=1)\n            if serOK:\n                sr.type = sertype\n                if sr.isSunControlledSeries == False:\n                    sr.end = serEnd\n                sr.interval = serInt\n                sr.onDialMarks = serOnDialMarks\n                sr.nrShots = serNrShots\n                if sr.isExposureSeries == False \\\n                and sr.isFocusStackingSeries == False:\n                    sr.continueOnServerStart = continueOnServerStart\n                else:\n                    sr.continueOnServerStart = False\n                if sr.status == \"NEW\":\n                    sr.nextStatus(\"configure\")\n                sr.persist()\n        else:\n            msg = \"The series is already FINISHED\"\n            flash(msg)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/attach_camera_cfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef attach_camera_cfg():\n    logger.debug(\"In attach_camera_cfg\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        sr = tl.curSeries\n        if sr.type == \"jpg\":\n            sr.cameraConfig = copy.deepcopy(cfg.photoConfig)\n            msg = \"Current 'Photo' configuration and Controls attached to Photoseries.\"\n        else:\n            sr.cameraConfig = copy.deepcopy(cfg.rawConfig)\n            msg = \"Current 'Raw Photo' configuration and Controls attached to Photoseries.\"\n        sr.cameraControls = copy.deepcopy(cfg.controls)\n        sr.persist()\n        flash(msg)\n        \n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/activate_camera_cfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef activate_camera_cfg():\n    logger.debug(\"In activate_camera_cfg\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        sr = tl.curSeries\n        if sr.cameraConfig:\n            if sr.type == \"jpg\":\n                cfg.photoConfig = copy.deepcopy(sr.cameraConfig)\n                msg = \"'Photo' configuration and Controls replaced with settings from Photoseries.\"\n            else:\n                cfg.rawConfig = copy.deepcopy(sr.cameraConfig)\n                msg = \"'Raw Photo' configuration and Controls replaced with settings from Photoseries.\"\n            cfg.controls = copy.deepcopy(sr.cameraControls)\n            Camera().applyControlsForLivestream()\n            sr.persist()\n            flash(msg)\n        else:\n            msg=\"The Photoseries has no camera configuration attached.\"\n            flash(msg)\n        \n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/show_preview\", methods=(\"GET\", \"POST\"))\n@login_required\ndef show_preview():\n    logger.debug(\"In show_preview\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        sr.showPreview = True\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/hide_preview\", methods=(\"GET\", \"POST\"))\n@login_required\ndef hide_preview():\n    logger.debug(\"In hide_preview\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        sc.lastPhotoSeriesTab = \"series\"\n        sr.showPreview = False\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\ndef calcSunControlledSeries(sr: Series, sun: Sun):\n    \"\"\"Determine series end and # shots for sun-controlled series\n\n    Args:\n        - sr (Series): The series to be processed\n    \"\"\"\n    logger.debug(\"In calcSunControlledSeries\")\n    if sr.isSunControlledSeries == True \\\n    and sr.sunCtrlMode == 1:\n        serend = sr.end\n        dayStart = sr.start.astimezone(ZoneInfo(sun.sunTimezone()))\n        now = datetime.now(tz=ZoneInfo(sun.sunTimezone()))\n        if dayStart < now:\n            dayStart = now\n        logger.debug(\"Start at %s with interval %s\", dayStart, sr.interval)\n        day = 1\n        cnt = sr.curShots\n        if not cnt:\n            cnt = 0\n        while day <= sr.sunCtrlPeriods:\n            dat = dayStart.strftime(\"%Y-%m-%d\")\n            tim = datetime.fromisoformat(dat)\n            sunrise, sunset = sun.sunrise_sunset(tim)\n            if sr.sunCtrlStart1Trg == 1:\n                start1 = sunrise + timedelta(minutes=sr.sunCtrlStart1Shft)\n            if sr.sunCtrlStart1Trg == 2:\n                start1 = sunset + timedelta(minutes=sr.sunCtrlStart1Shft)\n            if sr.sunCtrlEnd1Trg == 1:\n                end1 = sunrise + timedelta(minutes=sr.sunCtrlEnd1Shft)\n            if sr.sunCtrlEnd1Trg == 2:\n                end1 = sunset + timedelta(minutes=sr.sunCtrlEnd1Shft)\n            serend = end1\n            start = dayStart\n            if start < start1:\n                start = start1\n            while start < end1:\n                cnt += 1\n                start += timedelta(seconds=sr.interval)\n            if start <= end1:\n                cnt += 1\n            logger.debug(\"Day: %s - Period 1: %s to %s - #shots: %s\", day, start1, end1, cnt)\n            if sr.sunCtrlStart2Trg > 0 and sr.sunCtrlEnd2Trg > 0:\n                if sr.sunCtrlStart2Trg == 1:\n                    start2 = sunrise + timedelta(minutes=sr.sunCtrlStart2Shft)\n                if sr.sunCtrlStart2Trg == 2:\n                    start2 = sunset + timedelta(minutes=sr.sunCtrlStart2Shft)\n                if sr.sunCtrlEnd2Trg == 1:\n                    end2 = sunrise + timedelta(minutes=sr.sunCtrlEnd2Shft)\n                if sr.sunCtrlEnd2Trg == 2:\n                    end2 = sunset + timedelta(minutes=sr.sunCtrlEnd2Shft)\n                if end2 > serend:\n                    serend = end2\n                if start < start2:\n                    start = start2\n                while start < end2:\n                    cnt += 1\n                    start += timedelta(seconds=sr.interval)\n                if start <= end2:\n                    cnt += 1\n                logger.debug(\"Day: %s - Period 2: %s to %s - #shots: %s\", day, start2, end2, cnt)\n            if cnt > 0:\n                day += 1\n            dayStart += timedelta(days=1)\n            dayStart = dayStart.strftime(\"%Y-%m-%d\")\n            dayStart = datetime.fromisoformat(dayStart)\n            dayStart = dayStart.astimezone(ZoneInfo(sun.sunTimezone()))\n        sr.end = serend\n        sr.nrShots = cnt\n        logger.debug(\"calcSunControlledSeries - sr.end=%s, sr.nrShots=%s\", sr.end, sr.nrShots)\n\ndef calcSunAzimuthSeries(sr: Series, sun: Sun):\n    \"\"\"Determine series end and # shots for sun-azimuth-controlled series\n\n    Args:\n        - sr (Series): The series to be processed\n    \"\"\"\n    logger.debug(\"In calcSunAzimuthSeries\")\n    err = \"\"\n    if sr.isSunControlledSeries == True \\\n    and sr.sunCtrlMode == 2:\n        start = sr.start.astimezone(ZoneInfo(sun.sunTimezone()))\n        now = datetime.now(tz=ZoneInfo(sun.sunTimezone()))\n        if start < now:\n            start = now\n        logger.debug(\"calcSunAzimuthSeries - Start at %s\", start)\n        serend = start\n        day = 1\n        cnt = sr.curShots\n        if not cnt:\n            cnt = 0\n        cur = start\n        while day <= sr.sunCtrlPeriods and err == \"\":\n            dat = cur.strftime(\"%Y-%m-%d\")\n            tim = datetime.fromisoformat(dat)\n            for a in sr.sunAzimuths:\n                times = sun.find_times_for_azimuth(tim, a)\n                if len(times) == 0:\n                    err = f\"Azimuth {a} is not reached at day {day} ({dat})\"\n                    break\n                else:\n                    if times[0][\"time\"] > start:\n                        cnt += 1\n                        serend = times[0][\"time\"]\n            day += 1\n            cur += timedelta(days=1)\n        if err == \"\":\n            sr.end = serend\n            sr.nrShots = cnt\n            logger.debug(\"calcSunAzimuthSeries - sr.end=%s, sr.nrShots=%s\", sr.end, sr.nrShots)\n        else:\n            logger.debug(\"calcSunAzimuthSeries - error: %s\", err)\n    return err\n\n@bp.route(\"/tlseries_properties\", methods=(\"GET\", \"POST\"))\n@login_required\ndef tlseries_properties():\n    logger.debug(\"In tlseries_properties\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        ok = True\n        msg = \"\"\n        sc.lastPhotoSeriesTab = \"tldetails\"\n        locked = True\n        if sr.status == \"NEW\" or sr.status == \"READY\":\n            locked = False\n\n        if locked == False:\n            if request.form.get(\"issuncontrolled\") is None:\n                sr.isSunControlledSeries = False\n                sr.resetSunCtrlData()\n            else:\n                if sr.isFocusStackingSeries or sr.isExposureSeries:\n                    ok = False\n                    if sr.isFocusStackingSeries:\n                        msg = \"The series is already marked as Focus Stack\"\n                    if sr.isExposureSeries:\n                        msg = \"The series is already marked as Exposure Series\"\n                else:\n                    sr.isSunControlledSeries = True\n                    logger.debug(\"tlseries_properties - Series marked as Sun-Controlled Series\")\n\n                    if sr.isSunControlledSeries == True:\n                        mode = int(request.form[\"sunctrlmode\"])\n                        logger.debug(\"tlseries_properties - Selected Sun-Control Mode: %s\", mode)\n                        sr.sunCtrlMode = mode\n\n        if sr.isSunControlledSeries == True:\n            if sc.locLatitude == 0.0 \\\n            and sc.locLongitude == 0.0 \\\n            and sc.locElevation == 0.0:\n                ok = False\n                msg = \"Please go to 'Settings' and set Latitude, Longitude, Elevation and Time Zone\"\n            if ok:\n                nrDays = int(request.form[\"sunctrlperiods\"])\n                sr.sunCtrlPeriods = nrDays\n\n                sun = Sun(sc.locLatitude, sc.locLongitude, sc.locElevation, sc.locTzKey)\n                now = datetime.now()\n                dat = now.strftime(\"%Y-%m-%d\")\n                tim = datetime.fromisoformat(dat)\n                if sr.sunCtrlMode == 1:\n                    sr.sunrise, sr.sunset = sun.sunrise_sunset(tim)\n                    sr.resetSunAzimuthata()\n                if sr.sunCtrlMode == 2:\n                    sr.resetSunSunriseData()\n                    if request.form.get(\"sunazimuthtime\") is None:\n                        sunAzimuthTime = None\n                    else:\n                        sunAzimuthTimeFormIso = request.form[\"sunazimuthtime\"]\n                        if sunAzimuthTimeFormIso == \"\":\n                            sunAzimuthTime = None\n                        else:\n                            sunAzimuthTime = datetime.fromisoformat(sunAzimuthTimeFormIso)\n                    logger.debug(\"tlseries_properties - sunAzimuthTime: %s\", sunAzimuthTime)\n                    if sunAzimuthTime is None:\n                        sunAzimuthTime = datetime.now()\n                    if sunAzimuthTime == sr.sunAzimuthTime:\n                        sunAzimuthTime = datetime.now()\n                    sr.sunAzimuthTime = sunAzimuthTime\n                    sr.sunAzimuth = sun.solar_position(sunAzimuthTime)[\"azimuth\"]\n                    sr.sunElevation = sun.solar_position(sunAzimuthTime)[\"elevation\"]\n\n                if locked == False:\n                    interval = float(request.form[\"serinterval2\"])\n                    sr.interval = interval\n                    if sr.sunCtrlMode == 1:\n                        p1StartRef = int(request.form[\"sunctrlstart1trg\"])\n                        p1StartShift = int(request.form[\"sunctrlstart1shft\"])\n                        p1EndRef = int(request.form[\"sunctrlend1trg\"])\n                        p1EndShift = int(request.form[\"sunctrlend1shft\"])\n                        p2StartRef = int(request.form[\"sunctrlstart2trg\"])\n                        p2StartShift = int(request.form[\"sunctrlstart2shft\"])\n                        p2EndRef = int(request.form[\"sunctrlend2trg\"])\n                        p2EndShift = int(request.form[\"sunctrlend2shft\"])\n                        if p1StartRef == 0 or p1EndRef == 0:\n                            ok = False\n                            msg = \"Please specify Reference for Start and End for Period 1!\"\n                        else:\n                            if p1StartRef == p1EndRef and p1StartShift >= p1EndShift:\n                                ok = False\n                                msg = \"The specification for Period 1 is invalid!\"\n                        if p2StartRef != 0 or p2EndRef != 0:\n                            if p2StartRef == 0 or p2EndRef == 0:\n                                ok = False\n                                msg = \"Please specify Reference for Start and End for Period 2 or set both to Unused!\"\n                            else:\n                                if p2StartRef == p2EndRef and p2StartShift >= p2EndShift:\n                                    ok = False\n                                    msg = \"The specification for Period 2 is invalid!\"\n                        if ok:\n                            if sr.sunCtrlMode == 1:\n                                sr.sunCtrlStart1Trg = p1StartRef\n                                sr.sunCtrlStart1Shft = p1StartShift\n                                if p1StartRef == 1:\n                                    sr.sunCtrlStart1 = sr.sunrise + timedelta(minutes=p1StartShift)\n                                if p1StartRef == 2:\n                                    sr.sunCtrlStart1 = sr.sunset + timedelta(minutes=p1StartShift)\n                                sr.sunCtrlEnd1Trg = p1EndRef\n                                sr.sunCtrlEnd1Shft = p1EndShift\n                                if p1EndRef == 1:\n                                    sr.sunCtrlEnd1 = sr.sunrise + timedelta(minutes=p1EndShift)\n                                if p1EndRef == 2:\n                                    sr.sunCtrlEnd1 = sr.sunset + timedelta(minutes=p1EndShift)\n                                sr.sunCtrlStart2Trg = p2StartRef\n                                sr.sunCtrlStart2Shft = p2StartShift\n                                if p2StartRef == 1:\n                                    sr.sunCtrlStart2 = sr.sunrise + timedelta(minutes=p2StartShift)\n                                if p2StartRef == 2:\n                                    sr.sunCtrlStart2 = sr.sunset + timedelta(minutes=p2StartShift)\n                                sr.sunCtrlEnd2Trg = p2EndRef\n                                sr.sunCtrlEnd2Shft = p2EndShift\n                                if p2EndRef == 1:\n                                    sr.sunCtrlEnd2 = sr.sunrise + timedelta(minutes=p2EndShift)\n                                if p2EndRef == 2:\n                                    sr.sunCtrlEnd2 = sr.sunset + timedelta(minutes=p2EndShift)\n                                    \n                                calcSunControlledSeries(sr, sun)\n\n                    if sr.sunCtrlMode == 2:\n                        sunAzimuths = {}\n                        errAzimuths = []\n                        if request.form.get(\"sunazimuth1\") is not None:\n                            sunAzimuthStr = request.form[\"sunazimuth1\"]\n                            if sunAzimuthStr != \"\":\n                                sunAzimuth = float(sunAzimuthStr)\n                                times = sun.find_times_for_azimuth(now, sunAzimuth)\n                                if len(times) > 0:\n                                    dt = times[0][\"time\"]\n                                    dts = dt.strftime(\"%Y-%m-%d %H:%M\")\n                                    sunAzimuths[dts] = sunAzimuth\n                                else:\n                                    errAzimuths.append(sunAzimuth)\n                        if request.form.get(\"sunazimuth2\") is not None:\n                            sunAzimuthStr = request.form[\"sunazimuth2\"]\n                            if sunAzimuthStr != \"\":\n                                sunAzimuth = float(sunAzimuthStr)\n                                times = sun.find_times_for_azimuth(now, sunAzimuth)\n                                if len(times) > 0:\n                                    dt = times[0][\"time\"]\n                                    dts = dt.strftime(\"%Y-%m-%d %H:%M\")\n                                    sunAzimuths[dts] = sunAzimuth\n                                else:\n                                    errAzimuths.append(sunAzimuth)\n                        if request.form.get(\"sunazimuth3\") is not None:\n                            sunAzimuthStr = request.form[\"sunazimuth3\"]\n                            if sunAzimuthStr != \"\":\n                                sunAzimuth = float(sunAzimuthStr)\n                                times = sun.find_times_for_azimuth(now, sunAzimuth)\n                                if len(times) > 0:\n                                    dt = times[0][\"time\"]\n                                    dts = dt.strftime(\"%Y-%m-%d %H:%M\")\n                                    sunAzimuths[dts] = sunAzimuth\n                                else:\n                                    errAzimuths.append(sunAzimuth)\n                        if request.form.get(\"sunazimuth4\") is not None:\n                            sunAzimuthStr = request.form[\"sunazimuth4\"]\n                            if sunAzimuthStr != \"\":\n                                sunAzimuth = float(sunAzimuthStr)\n                                times = sun.find_times_for_azimuth(now, sunAzimuth)\n                                if len(times) > 0:\n                                    dt = times[0][\"time\"]\n                                    dts = dt.strftime(\"%Y-%m-%d %H:%M\")\n                                    sunAzimuths[dts] = sunAzimuth\n                                else:\n                                    errAzimuths.append(sunAzimuth)\n                                    \n                        # Sort azimuths by time\n                        sunAzimuths = dict(sorted(sunAzimuths.items(), key=lambda item: item[0]))\n                        errAzimuths.sort()\n\n                        n = 0\n                        for azimithTime, azimuth in sunAzimuths.items():\n                            n += 1\n                            if n == 1:\n                                sr.sunAzimuth1 = azimuth\n                                sr.sunAzimuth1Time = datetime.strptime(azimithTime, \"%Y-%m-%d %H:%M\")\n                            elif n == 2:\n                                sr.sunAzimuth2 = azimuth\n                                sr.sunAzimuth2Time = datetime.strptime(azimithTime, \"%Y-%m-%d %H:%M\")\n                            elif n == 3:\n                                sr.sunAzimuth3 = azimuth\n                                sr.sunAzimuth3Time = datetime.strptime(azimithTime, \"%Y-%m-%d %H:%M\")\n                            elif n == 4:    \n                                sr.sunAzimuth4 = azimuth\n                                sr.sunAzimuth4Time = datetime.strptime(azimithTime, \"%Y-%m-%d %H:%M\")\n                        for azimuth in errAzimuths:\n                            n += 1\n                            if n == 1:\n                                sr.sunAzimuth1 = azimuth\n                                sr.sunAzimuth1Time = None\n                            elif n == 2:\n                                sr.sunAzimuth2 = azimuth\n                                sr.sunAzimuth2Time = None\n                            elif n == 3:\n                                sr.sunAzimuth3 = azimuth\n                                sr.sunAzimuth3Time = None\n                            elif n == 4:    \n                                sr.sunAzimuth4 = azimuth\n                                sr.sunAzimuth4Time = None\n                        for m in range(n+1, 5):\n                            if m == 1:\n                                sr.sunAzimuth1 = None\n                                sr.sunAzimuth1Time = None\n                            elif m == 2:\n                                sr.sunAzimuth2 = None\n                                sr.sunAzimuth2Time = None\n                            elif m == 3:\n                                sr.sunAzimuth3 = None\n                                sr.sunAzimuth3Time = None\n                            elif m == 4:    \n                                sr.sunAzimuth4 = None\n                                sr.sunAzimuth4Time = None\n\n                        if len(sunAzimuths) > 0 \\\n                        or len(errAzimuths) > 0:\n                            msg = calcSunAzimuthSeries(sr, sun)\n                            if msg != \"\":\n                                ok = False\n                        else:\n                            ok = False\n                            msg = \"Please specify at least one Azimuth\"\n        if ok:\n            if not locked:\n                sr.nextStatus(\"configure\")\n            sr.persist()\n            logger.debug(\"tlseries_properties - Series persisted\")\n        if msg != \"\":\n            flash(msg)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\ndef calcExpSeries(start, stop, int):\n    \"\"\" Iterate an Exposure Series and return number of shots and stop\n    \"\"\"\n    if int == 0:\n        fact = 2\n    elif int == 1:\n        fact = 2 ** (1.0 / 3)\n    elif int == 2:\n        fact = 4\n    else:\n        fact =  2\n    v = start\n    vv = v\n    nrShot = 0\n    while vv <= stop:\n        v = vv\n        nrShot += 1\n        vv = vv * fact\n    if v < stop:\n        nrShot += 1\n        v = v * fact\n    return nrShot, v\n\n@bp.route(\"/expseries_properties\", methods=(\"GET\", \"POST\"))\n@login_required\ndef expseries_properties():\n    logger.debug(\"In expseries_properties\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        ok = True\n        sc.lastPhotoSeriesTab = \"exposure\"\n        locked = True\n        if sr.status == \"NEW\" or sr.status == \"READY\":\n            locked = False\n        if not locked:\n            if request.form.get(\"isexposure\") is None:\n                sr.isExposureSeries = False\n            else:\n                msg = \"\"\n                if sr.isFocusStackingSeries or sr.isSunControlledSeries:\n                    ok = False\n                    if sr.isFocusStackingSeries:\n                        msg = \"The series is already marked as Focus Stack\"\n                    if sr.isSunControlledSeries:\n                        msg = \"The series is already marked as sun-controlled Timelapse Series\"\n                else:\n                    sr.isExposureSeries = True\n                    sr.continueOnServerStart = False\n                    if request.form.get(\"isexptimefix\") is None:\n                        sr.isExpExpTimeFix = False\n                        if request.form.get(\"isexpgainfix\") is None:\n                            msg = \"Select exactly one parameter as fix.\"\n                            ok = False\n                        else:\n                            sr.isExpGainFix = True\n                    else:\n                        sr.isExpExpTimeFix = True\n                        if request.form.get(\"isexpgainfix\") is None:\n                            sr.isExpGainFix = False\n                        else:\n                            msg = \"Select exactly one parameter as fix.\"\n                            ok = False\n                if ok:\n                    if sr.isExpGainFix:\n                        expTimeStart = int(request.form[\"exptimestart\"])\n                        expTimeStop = int(request.form[\"exptimestop\"])\n                        expTimeStep = int(request.form[\"exptimestep\"])\n                        nrShots, expTimeStop = calcExpSeries(expTimeStart, expTimeStop, expTimeStep)\n                        expGainFix = float(request.form[\"expgainstart\"])\n                        sr.nrShots = nrShots\n                        sr.expTimeStart = expTimeStart\n                        sr.expTimeStop = int(expTimeStop)\n                        sr.expTimeStep = expTimeStep\n                        sr.expGainStart = expGainFix\n                        sr.expGainStop = expGainFix\n                        sr.expGainStep = 0\n                    if sr.isExpExpTimeFix:\n                        expGainStart = float(request.form[\"expgainstart\"])\n                        expGainStop = float(request.form[\"expgainstop\"])\n                        expGainStep = int(request.form[\"expgainstep\"])\n                        nrShots, expGainStop = calcExpSeries(expGainStart, expGainStop, expGainStep)\n                        expTimeFix = int(request.form[\"exptimestart\"])\n                        sr.nrShots = nrShots\n                        sr.expGainStart = expGainStart\n                        sr.expGainStop = expGainStop\n                        sr.expGainStep = expGainStep\n                        sr.expTimeStart = expTimeFix\n                        sr.expTimeStop = expTimeFix\n                        sr.expGTimeStep = 0\n                else:\n                    flash(msg)\n            if ok:\n                sr.nextStatus(\"configure\")\n                sr.persist()\n        else:\n            msg = \"Series parameters can not be changed for a series in status \" + sr.status\n            flash(msg)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\ndef calcFocusSeries(start, stop, intv):\n    \"\"\" Iterate an Exposure Series and return number of shots and stop\n    \"\"\"\n    nrShot = int((stop - start) / intv) + 1\n    v = start + (nrShot - 1) * intv\n    if intv < 0:\n        if v > stop:\n            if v + intv > 0:\n                nrShot += 1\n    else:\n        if v < stop:\n            nrShot += 1\n    v = start + (nrShot - 1) * intv\n    v = round(v, 2)\n    return nrShot, v        \n\n@bp.route(\"/focusstack_properties\", methods=(\"GET\", \"POST\"))\n@login_required\ndef focusstack_properties():\n    logger.debug(\"In focusstack_properties\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera().cam\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tl = PhotoSeriesCfg()\n    sr = tl.curSeries\n    cp = cfg.cameraProperties\n    sc.curMenu = \"photoseries\"\n    if request.method == \"POST\":\n        ok = True\n        sc.lastPhotoSeriesTab = \"focusstack\"\n        locked = True\n        if sr.status == \"NEW\" or sr.status == \"READY\":\n            locked = False\n        if not locked:\n            if request.form.get(\"isfocusstack\") is None:\n                sr.isFocusStackingSeries = False\n            else:\n                msg = \"\"\n                if sr.isExposureSeries or sr.isSunControlledSeries:\n                    ok = False\n                    if sr.isExposureSeries:\n                        msg = \"The series is already marked as Exposure Series!\"\n                    if sr.isSunControlledSeries:\n                        msg = \"The series is already marked as sun-controlled Timelapse Series!\"\n                else:\n                    focusStart = float(request.form[\"focaldiststart\"])\n                    focusStop = float(request.form[\"focaldiststop\"])\n                    focusStep = float(request.form[\"focaldiststep\"])\n                    if focusStart <= 0.0:\n                        msg = \"The start value must be > 0!\"\n                        ok = False\n                    else:\n                        if focusStop > focusStart:\n                            if focusStep > 0.0:\n                                pass\n                            else:\n                                msg = \"If Stop > Start, Interval must be > 0!\"\n                                ok = False\n                        elif focusStop == 0.0:\n                            msg = \"Stop must not be 0!\"\n                            ok = False\n                        else:\n                            if focusStep < 0.0:\n                                pass\n                            else:\n                                msg = \"If Stop < Start, Interval must be < 0!\"\n                                ok = False\n                if ok:\n                    sr.isFocusStackingSeries = True\n                    sr.continueOnServerStart = False\n                    nrShots, focusStop = calcFocusSeries(focusStart, focusStop, focusStep)\n                    sr.focalDistStart = focusStart\n                    sr.focalDistStop = focusStop\n                    sr.focalDistStep = focusStep\n                    sr.nrShots = nrShots\n                else:\n                    flash(msg)\n            if ok:\n                sr.nextStatus(\"configure\")\n                sr.persist()\n        else:\n            msg = \"Series parameters can not be changed for a series in status \" + sr.status\n            flash(msg)\n    return render_template(\"photoseries/main.html\", sc=sc, tl=tl, sr=sr, cp=cp)\n\n@bp.route(\"/media-viewer\")\n@login_required\ndef media_viewer():\n    src = request.args.get(\"src\")\n    media_type = request.args.get(\"type\", \"image\")\n\n    filename = os.path.basename(src) if src else \"\"\n\n    return render_template(\n        \"media_viewer.html\",\n        src=src,\n        media_type=media_type,\n        filename=filename\n    )\n"
  },
  {
    "path": "raspiCamSrv/photoseriesCfg.py",
    "content": "from datetime import datetime\nfrom datetime import timedelta\nfrom raspiCamSrv.camCfg import CameraCfg, CameraConfig, CameraControls\nimport raspiCamSrv.camera_pi\nfrom raspiCamSrv.sun import Sun\nfrom _thread import get_ident\nimport os\nimport csv\nimport copy\nimport shutil\nfrom pathlib import Path\nimport json\nimport math\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nclass Series():\n    PHOTODIGITS = 6     # Number of digits for photo number in filename\n    HISTOGRAMFOLDER = \"hist\"\n    SUNCONTROLMODES = [\"Sunrise/Sunset\", \"Azimuth\"]\n    def __init__(self):\n        self._name = \"\"\n        self._status = \"NE\"\n        self._path = \"\"\n        self._start = None\n        self._started = None\n        self._end = None\n        self._ended = None\n        self._downloaded= None\n        self._interval = None\n        self._onDialMarks = None\n        self._nrShots = None\n        self._curShots = None\n        self._type = \"jpg\"\n        self._continueOnServerStart = False\n        self._showPreview = True\n        self._logFile = None\n        self._cfgFile = None\n        self._camFile = None\n        self._cameraConfig = None\n        self._cameraControls = None\n        self._logHeadlineReq = True\n        self._firstCamEntry = True\n        self._isExposureSeries = False\n        self._isExpExpTimeFix = False\n        self._isExpGainFix = True\n        self._expTimeStart = 125\n        self._expTimeStop = 1024000\n        self._expTimeStep = 0\n        self._expGainStart = 1\n        self._expGainStop = 16\n        self._expGainStep = 0\n        self._isFocusStackingSeries = False\n        self._focalDistStart = 0\n        self._focalDistStop = 0\n        self._focalDistStep = 0\n        self._isSunControlledSeries = False\n        self._sunCtrlMode = 1\n        self._sunCtrlPeriods = 1\n        self._sunrise = None\n        self._sunset = None\n        self._sunCtrlStart1Trg = 1\n        self._sunCtrlStart1Shft = 0\n        self._sunCtrlStart1 = None\n        self._sunCtrlEnd1Trg = 2\n        self._sunCtrlEnd1Shft = 0\n        self._sunCtrlEnd1 = None\n        self._sunCtrlStart2Trg = 0\n        self._sunCtrlStart2Shft = 0\n        self._sunCtrlStart2 = None\n        self._sunCtrlEnd2Trg = 0\n        self._sunCtrlEnd2Shft = 0\n        self._sunCtrlEnd2 = None\n        self._sunAzimuthTime = None\n        self._sunAzimuth = None\n        self._sunElevation = None\n        self._sunAzimuth1 = None\n        self._sunAzimuth2 = None\n        self._sunAzimuth3 = None\n        self._sunAzimuth4 = None\n        self._sunAzimuth1Time = None\n        self._sunAzimuth2Time = None\n        self._sunAzimuth3Time = None\n        self._sunAzimuth4Time = None\n        self._metaData = {}\n        self._error = None\n        self._error2 = None\n        self._errorSource = None\n    \n    @property\n    def name(self) -> str:\n        return self._name\n\n    @name.setter\n    def name(self, value: str):\n        self._name = value\n    \n    @property\n    def status(self) -> str:\n        return self._status\n\n    @status.setter\n    def status(self, value: str):\n        self._status = value\n    \n    @property\n    def nextActions(self) -> list:\n        \"\"\"Return the allowed lifecycle actions depending on current status\n        \"\"\"\n        if self._status == \"NE\":\n            return [\"create\",]\n        elif self._status == \"NEW\":\n            return [\"configure\", \"remove\"]\n        elif self._status == \"READY\":\n            return [\"start\", \"remove\"]\n        elif self._status == \"ACTIVE\":\n            return [\"pause\", \"finish\"]\n        elif self.status == \"PAUSED\":\n            return [\"continue\", \"finish\"]\n        elif self.status == \"FINISHED\":\n            return [\"remove\",]\n        else:\n            return []\n    \n    def nextStatus(self, action: str) -> str:\n        \"\"\"Update and return the lifecycle status depending on action\n        \"\"\"\n        if action == \"create\":\n            self._status = \"NEW\"\n        elif action == \"configure\":\n            self._status = \"READY\"\n        elif action == \"start\":\n            self._status = \"ACTIVE\"\n        elif action == \"pause\":\n            self._status = \"PAUSED\"\n        elif action == \"finish\":\n            self._status = \"FINISHED\"\n            self.logCamCfgCtrlClose()\n        elif action == \"continue\":\n            self._status = \"ACTIVE\"\n        else:\n            self._status = \"NONE\"\n        return self._status\n    \n    @property\n    def path(self) -> str:\n        return self._path\n\n    @path.setter\n    def path(self, value: str):\n        self._path = value\n    \n    @property\n    def histogramPath(self) -> str:\n        return self._path + \"/\" + Series.HISTOGRAMFOLDER\n    \n    @property\n    def start(self) -> datetime:\n        return self._start\n\n    @start.setter\n    def start(self, value: datetime):\n        if value is None:\n            self._start = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._start = dt\n    \n    @property\n    def startIso(self) -> str:\n        return self._start.isoformat()\n    \n    @property\n    def started(self) -> datetime:\n        return self._started\n\n    @started.setter\n    def started(self, value: datetime):\n        if value is None:\n            self._started = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._started = dt\n    \n    @property\n    def startedIso(self) -> str:\n        if self._started is None:\n            return None\n        else:\n            return self._started.isoformat()\n    \n    @property\n    def end(self) -> datetime:\n        return self._end\n\n    @end.setter\n    def end(self, value: datetime):\n        if value is None:\n            self._end = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._end = dt\n    \n    @property\n    def endIso(self) -> str:\n        return self._end.isoformat()\n    \n    @property\n    def ended(self) -> datetime:\n        return self._ended\n\n    @ended.setter\n    def ended(self, value: datetime):\n        if value is None:\n            self._ended = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._ended = dt\n    \n    @property\n    def endedIso(self) -> str:\n        if self.ended is None:\n            return None\n        else:\n            return self._ended.isoformat()\n    \n    @property\n    def downloaded(self) -> datetime:\n        return self._downloaded\n\n    @downloaded.setter\n    def downloaded(self, value: datetime):\n        if value is None:\n            self.downloaded = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._downloaded = dt\n    \n    @property\n    def downloadedIso(self) -> str:\n        if self._downloaded is None:\n            return None\n        else:\n            return self._downloaded.isoformat()\n    \n    @property\n    def interval(self) -> float:\n        return self._interval\n\n    @interval.setter\n    def interval(self, value: float):\n        self._interval = value\n    \n    @property\n    def onDialMarks(self) -> bool:\n        return self._onDialMarks\n\n    @onDialMarks.setter\n    def onDialMarks(self, value: bool):\n        self._onDialMarks = value\n    \n    @property\n    def nrShots(self) -> int:\n        return self._nrShots\n\n    @nrShots.setter\n    def nrShots(self, value: int):\n        self._nrShots = value\n    \n    @property\n    def curShots(self) -> int:\n        return self._curShots\n\n    @curShots.setter\n    def curShots(self, value: int):\n        self._curShots = value\n    \n    @property\n    def type(self) -> str:\n        return self._type\n\n    @type.setter\n    def type(self, value: str):\n        self._type = value\n    \n    @property\n    def continueOnServerStart(self) -> bool:\n        return self._continueOnServerStart\n\n    @continueOnServerStart.setter\n    def continueOnServerStart(self, value: bool):\n        self._continueOnServerStart = value\n    \n    @property\n    def showPreview(self) -> bool:\n        return self._showPreview\n\n    @showPreview.setter\n    def showPreview(self, value: bool):\n        self._showPreview = value\n    \n    @property\n    def logFileName(self) -> str:\n        return self.name + \"_log.csv\"\n    \n    @property\n    def logFileRelPath(self) -> str:\n        return \"photoseries/\" + self.name + \"/\" + self.logFileName\n    \n    @property\n    def logFile(self) -> str:\n        return self._logFile\n\n    @logFile.setter\n    def logFile(self, value: str):\n        self._logFile = value\n    \n    @property\n    def cfgFileName(self) -> str:\n        return self.name + \"_cfg.json\"\n    \n    @property\n    def cfgFileRelPath(self) -> str:\n        return  \"photoseries/\" + self.name + \"/\" + self.cfgFileName\n    \n    @property\n    def cfgFile(self) -> str:\n        return self._cfgFile\n\n    @cfgFile.setter\n    def cfgFile(self, value: str):\n        self._cfgFile = value\n    \n    @property\n    def camFileName(self) -> str:\n        return self.name + \"_cam.json\"\n    \n    @property\n    def camFileRelPath(self) -> str:\n        return  \"photoseries/\" + self.name + \"/\" + self.camFileName\n    \n    @property\n    def camFile(self) -> str:\n        return self._camFile\n\n    @camFile.setter\n    def camFile(self, value: str):\n        self._camFile = value\n    \n    @property\n    def isExposureSeries(self) -> bool:\n        return self._isExposureSeries\n\n    @isExposureSeries.setter\n    def isExposureSeries(self, value: bool):\n        self._isExposureSeries = value\n    \n    @property\n    def isExpExpTimeFix(self) -> bool:\n        return self._isExpExpTimeFix\n\n    @isExpExpTimeFix.setter\n    def isExpExpTimeFix(self, value: bool):\n        self._isExpExpTimeFix = value\n    \n    @property\n    def isExpGainFix(self) -> bool:\n        return self._isExpGainFix\n\n    @isExpGainFix.setter\n    def isExpGainFix(self, value: bool):\n        self._isExpGainFix = value\n    \n    @property\n    def expTimeStart(self) -> int:\n        return self._expTimeStart\n\n    @expTimeStart.setter\n    def expTimeStart(self, value: int):\n        self._expTimeStart = value\n    \n    @property\n    def expTimeStop(self) -> int:\n        return self._expTimeStop\n\n    @expTimeStop.setter\n    def expTimeStop(self, value: int):\n        self._expTimeStop = value\n    \n    @property\n    def expTimeStep(self) -> int:\n        return self._expTimeStep\n\n    @expTimeStep.setter\n    def expTimeStep(self, value: int):\n        \"\"\" Step for exposure time:\n            0: 1 EV\n            1: 1/3 EV\n            2: 2 EV\n        \"\"\"\n        if value == 0 \\\n        or value == 1 \\\n        or value == 2:\n            self._expTimeStep = value\n        else:\n            self._expTimeStep = 0\n\n    @property\n    def expGainStart(self) -> float:\n        return self._expGainStart\n\n    @expGainStart.setter\n    def expGainStart(self, value: float):\n        self._expGainStart = value\n    \n    @property\n    def expGainStop(self) -> float:\n        return self._expGainStop\n\n    @expGainStop.setter\n    def expGainStop(self, value: float):\n        self._expGainStop = value\n    \n    @property\n    def expGainStep(self) -> int:\n        return self._expGainStep\n\n    @expGainStep.setter\n    def expGainStep(self, value: int):\n        \"\"\" Step for analogue gain:\n            0: 1 EV\n            1: 1/3 EV\n            2: 2 EV\n        \"\"\"\n        if value == 0 \\\n        or value == 1 \\\n        or value == 2:\n            self._expGainStep = value\n        else:\n            self._expGainStep = 0\n    \n    @property\n    def isFocusStackingSeries(self) -> bool:\n        return self._isFocusStackingSeries\n\n    @isFocusStackingSeries.setter\n    def isFocusStackingSeries(self, value: bool):\n        self._isFocusStackingSeries = value\n    \n    @property\n    def focalDistStart(self) -> float:\n        return self._focalDistStart\n\n    @focalDistStart.setter\n    def focalDistStart(self, value: float):\n        self._focalDistStart = value\n    \n    @property\n    def focalDistStop(self) -> float:\n        return self._focalDistStop\n\n    @focalDistStop.setter\n    def focalDistStop(self, value: float):\n        self._focalDistStop = value\n    \n    @property\n    def focalDistStep(self) -> float:\n        return self._focalDistStep\n\n    @focalDistStep.setter\n    def focalDistStep(self, value: float):\n        self._focalDistStep = value\n    \n    @property\n    def isSunControlledSeries(self) -> bool:\n        return self._isSunControlledSeries\n\n    @isSunControlledSeries.setter\n    def isSunControlledSeries(self, value: bool):\n        self._isSunControlledSeries = value\n        if self._isSunControlledSeries == False:\n            if \"Azimuth\" in self.metaData:\n                del self.metaData[\"Azimuth\"]\n\n\n    @property\n    def sunCtrlMode(self) -> int:\n        return self._sunCtrlMode\n\n    @sunCtrlMode.setter\n    def sunCtrlMode(self, value: int):\n        if value == 1 or value == 2:\n            self._sunCtrlMode = value\n            if value == 2:\n                self.metaData[\"Azimuth\"] = None\n            if value == 1:\n                if \"Azimuth\" in self.metaData:\n                    del self.metaData[\"Azimuth\"]\n        else:\n            self._sunCtrlMode = 0\n\n    @property\n    def sunCtrlPeriods(self) -> int:\n        return self._sunCtrlPeriods\n\n    @sunCtrlPeriods.setter\n    def sunCtrlPeriods(self, value: int):\n        self._sunCtrlPeriods = value\n    \n    @property\n    def sunrise(self) -> datetime:\n        return self._sunrise\n\n    @sunrise.setter\n    def sunrise(self, value: datetime):\n        if value is None:\n            self._sunrise = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunrise = dt\n    \n    @property\n    def sunriseIso(self) -> str:\n        return self._sunrise.isoformat()\n    \n    @property\n    def sunset(self) -> datetime:\n        return self._sunset\n\n    @sunset.setter\n    def sunset(self, value: datetime):\n        if value is None:\n            self._sunset = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunset = dt\n    \n    @property\n    def sunsetIso(self) -> str:\n        return self._sunset.isoformat()\n    \n    @property\n    def sunCtrlStart1Trg(self) -> int:\n        return self._sunCtrlStart1Trg\n\n    @sunCtrlStart1Trg.setter\n    def sunCtrlStart1Trg(self, value: int):\n        self._sunCtrlStart1Trg = value\n    \n    @property\n    def sunCtrlStart1Shft(self) -> int:\n        return self._sunCtrlStart1Shft\n\n    @sunCtrlStart1Shft.setter\n    def sunCtrlStart1Shft(self, value: int):\n        self._sunCtrlStart1Shft = value\n    \n    @property\n    def sunCtrlStart1(self) -> datetime:\n        return self._sunCtrlStart1\n\n    @sunCtrlStart1.setter\n    def sunCtrlStart1(self, value: datetime):\n        if value is None:\n            self._sunset = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunCtrlStart1 = dt\n    \n    @property\n    def sunCtrlStart1Iso(self) -> str:\n        return self._sunCtrlStart1.isoformat()\n    \n    @property\n    def sunCtrlEnd1Trg(self) -> int:\n        return self._sunCtrlEnd1Trg\n\n    @sunCtrlEnd1Trg.setter\n    def sunCtrlEnd1Trg(self, value: int):\n        self._sunCtrlEnd1Trg = value\n    \n    @property\n    def sunCtrlEnd1Shft(self) -> int:\n        return self._sunCtrlEnd1Shft\n\n    @sunCtrlEnd1Shft.setter\n    def sunCtrlEnd1Shft(self, value: int):\n        self._sunCtrlEnd1Shft = value\n    \n    @property\n    def sunCtrlEnd1(self) -> datetime:\n        return self._sunCtrlEnd1\n\n    @sunCtrlEnd1.setter\n    def sunCtrlEnd1(self, value: datetime):\n        if value is None:\n            self._sunCtrlEnd1 = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunCtrlEnd1 = dt\n    \n    @property\n    def sunCtrlEnd1Iso(self) -> str:\n        return self._sunCtrlEnd1.isoformat()\n    \n    @property\n    def sunCtrlStart2Trg(self) -> int:\n        return self._sunCtrlStart2Trg\n\n    @sunCtrlStart2Trg.setter\n    def sunCtrlStart2Trg(self, value: int):\n        self._sunCtrlStart2Trg = value\n    \n    @property\n    def sunCtrlStart2Shft(self) -> int:\n        return self._sunCtrlStart2Shft\n\n    @sunCtrlStart2Shft.setter\n    def sunCtrlStart2Shft(self, value: int):\n        self._sunCtrlStart2Shft = value\n    \n    @property\n    def sunCtrlStart2(self) -> datetime:\n        return self._sunCtrlStart2\n\n    @sunCtrlStart2.setter\n    def sunCtrlStart2(self, value: datetime):\n        if value is None:\n            self._sunCtrlStart2 = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunCtrlStart2 = dt\n    \n    @property\n    def sunCtrlStart2Iso(self) -> str:\n        return self._sunCtrlStart2.isoformat()\n    \n    @property\n    def sunCtrlEnd2Trg(self) -> int:\n        return self._sunCtrlEnd2Trg\n\n    @sunCtrlEnd2Trg.setter\n    def sunCtrlEnd2Trg(self, value: int):\n        self._sunCtrlEnd2Trg = value\n    \n    @property\n    def sunCtrlEnd2Shft(self) -> int:\n        return self._sunCtrlEnd2Shft\n\n    @sunCtrlEnd2Shft.setter\n    def sunCtrlEnd2Shft(self, value: int):\n        self._sunCtrlEnd2Shft = value\n    \n    @property\n    def sunCtrlEnd2(self) -> datetime:\n        return self._sunCtrlEnd2\n\n    @sunCtrlEnd2.setter\n    def sunCtrlEnd2(self, value: datetime):\n        if value is None:\n            self._sunCtrlEnd2 = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunCtrlEnd2 = dt\n    \n    @property\n    def sunCtrlEnd2Iso(self) -> str:\n        return self._sunCtrlEnd2.isoformat()\n\n    @property\n    def cameraConfig(self) -> CameraConfig:\n        return self._cameraConfig\n\n    @cameraConfig.setter\n    def cameraConfig(self, value: CameraConfig):\n        self._cameraConfig = value\n    \n    @property\n    def cameraControls(self) -> CameraControls:\n        return self._cameraControls\n\n    @cameraControls.setter\n    def cameraControls(self, value: CameraControls):\n        self._cameraControls = value\n\n    @property\n    def sunAzimuthTime(self) -> datetime:\n        return self._sunAzimuthTime\n\n    @sunAzimuthTime.setter\n    def sunAzimuthTime(self, value: datetime):\n        if value is None:\n            self._sunAzimuthTime = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunAzimuthTime = dt\n\n    @property\n    def sunAzimuthTimeIso(self) -> str:\n        if self._sunAzimuthTime is None:\n            return None\n        else:\n            return self._sunAzimuthTime.isoformat()\n\n    @property\n    def sunAzimuth(self) -> float:\n        return self._sunAzimuth\n\n    @sunAzimuth.setter\n    def sunAzimuth(self, value: float):\n        self._sunAzimuth = value\n\n    @property\n    def sunElevation(self) -> float:\n        return self._sunElevation\n\n    @sunElevation.setter\n    def sunElevation(self, value: float):\n        self._sunElevation = value\n\n    @property\n    def sunAzimuth1(self) -> float:\n        return self._sunAzimuth1\n\n    @sunAzimuth1.setter\n    def sunAzimuth1(self, value: float):\n        self._sunAzimuth1 = value\n\n    @property\n    def sunAzimuth2(self) -> float:\n        return self._sunAzimuth2\n\n    @sunAzimuth2.setter\n    def sunAzimuth2(self, value: float):\n        self._sunAzimuth2 = value\n\n    @property\n    def sunAzimuth3(self) -> float:\n        return self._sunAzimuth3\n\n    @sunAzimuth3.setter\n    def sunAzimuth3(self, value: float):\n        self._sunAzimuth3 = value\n\n    @property\n    def sunAzimuth4(self) -> float:\n        return self._sunAzimuth4\n\n    @sunAzimuth4.setter\n    def sunAzimuth4(self, value: float):\n        self._sunAzimuth4 = value\n\n    @property\n    def sunAzimuth1Time(self) -> datetime:\n        return self._sunAzimuth1Time\n\n    @sunAzimuth1Time.setter\n    def sunAzimuth1Time(self, value: datetime):\n        if value is None:\n            self._sunAzimuth1Time = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunAzimuth1Time = dt\n\n    @property\n    def sunAzimuth1TimeIso(self) -> str:\n        if self._sunAzimuth1Time is None:\n            return None\n        else:\n            return self._sunAzimuth1Time.isoformat()\n\n    @property\n    def sunAzimuth2Time(self) -> datetime:\n        return self._sunAzimuth2Time\n\n    @sunAzimuth2Time.setter\n    def sunAzimuth2Time(self, value: datetime):\n        if value is None:\n            self._sunAzimuth2Time = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunAzimuth2Time = dt\n\n    @property\n    def sunAzimuth2TimeIso(self) -> str:\n        if self._sunAzimuth2Time is None:\n            return None\n        else:\n            return self._sunAzimuth2Time.isoformat()\n\n    @property\n    def sunAzimuth3Time(self) -> datetime:\n        return self._sunAzimuth3Time\n\n    @sunAzimuth3Time.setter\n    def sunAzimuth3Time(self, value: datetime):\n        if value is None:\n            self._sunAzimuth3Time = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunAzimuth3Time = dt\n\n    @property\n    def sunAzimuth3TimeIso(self) -> str:\n        if self._sunAzimuth3Time is None:\n            return None\n        else:\n            return self._sunAzimuth3Time.isoformat()\n\n    @property\n    def sunAzimuth4Time(self) -> datetime:\n        return self._sunAzimuth4Time\n\n    @sunAzimuth4Time.setter\n    def sunAzimuth4Time(self, value: datetime):\n        if value is None:\n            self._sunAzimuth4Time = None\n        else:\n            dt = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute)\n            self._sunAzimuth4Time = dt\n\n    @property\n    def sunAzimuth4TimeIso(self) -> str:\n        if self._sunAzimuth4Time is None:\n            return None\n        else:\n            return self._sunAzimuth4Time.isoformat()\n\n    @property\n    def sunAzimuths(self) -> list:\n        azimuths = []\n        if self._sunAzimuth1 is not None:\n            azimuths.append(self._sunAzimuth1)\n        if self._sunAzimuth2 is not None:\n            azimuths.append(self._sunAzimuth2)\n        if self._sunAzimuth3 is not None:\n            azimuths.append(self._sunAzimuth3)\n        if self._sunAzimuth4 is not None:\n            azimuths.append(self._sunAzimuth4)\n        return azimuths\n\n    @property\n    def sunAzimuthTimes(self) -> list:\n        azimuthTimes = []\n        if self._sunAzimuth1Time is not None:\n            azimuthTimes.append(self._sunAzimuth1Time)\n        if self._sunAzimuth2Time is not None:\n            azimuthTimes.append(self._sunAzimuth2Time)\n        if self._sunAzimuth3Time is not None:\n            azimuthTimes.append(self._sunAzimuth3Time)\n        if self._sunAzimuth4Time is not None:\n            azimuthTimes.append(self._sunAzimuth4Time)\n        return azimuthTimes\n\n    @property\n    def metaData(self) -> dict:\n        return self._metaData\n\n    @metaData.setter\n    def metaData(self, value: dict):\n        self._metaData = value\n\n    @property\n    def error(self) -> str:\n        return self._error\n\n    @error.setter\n    def error(self, value: str):\n        self._error = value\n        if value is None:\n            self._errorSource = None\n            self._error2 = None\n\n    @property\n    def error2(self) -> str:\n        return self._error2\n\n    @error2.setter\n    def error2(self, value: str):\n        self._error2 = value\n\n    @property\n    def errorSource(self) -> str:\n        return self._errorSource\n\n    @errorSource.setter\n    def errorSource(self, value: str):\n        self._errorSource = value\n\n    def resetSunCtrlData(self):\n        \"\"\"Reset sun control data\n        \"\"\"\n        self._sunCtrlMode = 1\n        self._sunCtrlPeriods = 1\n        self._sunrise = None\n        self._sunset = None\n        self._sunCtrlStart1Trg = 1\n        self._sunCtrlStart1Shft = 0\n        self._sunCtrlStart1 = None\n        self._sunCtrlEnd1Trg = 2\n        self._sunCtrlEnd1Shft = 0\n        self._sunCtrlEnd1 = None\n        self._sunCtrlStart2Trg = 0\n        self._sunCtrlStart2Shft = 0\n        self._sunCtrlStart2 = None\n        self._sunCtrlEnd2Trg = 0\n        self._sunCtrlEnd2Shft = 0\n        self._sunCtrlEnd2 = None\n        self._sunAzimuthTime = None\n        self._sunAzimuth = None\n        self._sunAzimuth1 = None\n        self._sunAzimuth2 = None\n        self._sunAzimuth3 = None\n        self._sunAzimuth4 = None\n        self._sunAzimuth1Time = None\n        self._sunAzimuth2Time = None\n        self._sunAzimuth3Time = None\n        self._sunAzimuth4Time = None\n\n    def resetSunSunriseData(self):\n        \"\"\"Reset sun control data for mode sunrise/sunset\n        \"\"\"\n        self._sunrise = None\n        self._sunset = None\n        self._sunCtrlStart1Trg = 1\n        self._sunCtrlStart1Shft = 0\n        self._sunCtrlStart1 = None\n        self._sunCtrlEnd1Trg = 2\n        self._sunCtrlEnd1Shft = 0\n        self._sunCtrlEnd1 = None\n        self._sunCtrlStart2Trg = 0\n        self._sunCtrlStart2Shft = 0\n        self._sunCtrlStart2 = None\n        self._sunCtrlEnd2Trg = 0\n        self._sunCtrlEnd2Shft = 0\n        self._sunCtrlEnd2 = None\n\n    def resetSunAzimuthata(self):\n        \"\"\"Reset sun control data for mode Azimuth\n        \"\"\"\n        self._sunAzimuthTime = None\n        self._sunAzimuth = None\n        self._sunAzimuth1 = None\n        self._sunAzimuth2 = None\n        self._sunAzimuth3 = None\n        self._sunAzimuth4 = None\n        self._sunAzimuth1Time = None\n        self._sunAzimuth2Time = None\n        self._sunAzimuth3Time = None\n        self._sunAzimuth4Time = None\n    \n    def nextPhoto(self) -> tuple[int, str, dict]:\n        \"\"\"Return number and name for the next photo of the series\n\n        Returns:\n            - tuple[int, str]: \n            -- number of next photo\n            -- name of next photo\n            -- series metadata of next photo\n        \"\"\"\n        logger.debug(\"Thread %s: Series.nextPhoto\", get_ident())\n        name = \"\"\n        serMetaData = self.metaData.copy()\n        if self.curShots is None:\n            self.curShots = 0\n        if self.curShots < self.nrShots:\n            curShots = self.curShots + 1\n            name = self.name + \"_\" + str(curShots).zfill(Series.PHOTODIGITS)\n        else:\n            curShots = self.curShots\n            if self.ended is None:\n                logger.debug(\"Thread %s: Series.nextPhoto - Finishing series\", get_ident())\n                dt = datetime.now()\n                dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n                self.ended = dt\n                self.nextStatus(\"finish\")\n                self.persist()\n                #Restore camera controls\n                if CameraCfg().controlsBackup:\n                    CameraCfg().controls = copy.deepcopy(CameraCfg().controlsBackup)\n                    CameraCfg().controlsBackup = None\n                    logger.debug(\"Thread %s: Series.nextPhoto - Restored controls backup: %s\", get_ident(), CameraCfg().controls.__dict__)\n                    wait = None\n                    if self.isExposureSeries:\n                        #For an exposure series wait for the longest exposure time\n                        if self.isExpGainFix:\n                            wait = 0.2 + self.expTimeStop / 1000000\n                    raspiCamSrv.camera_pi.Camera().applyControlsForLivestream(wait)\n        logger.debug(\"Thread %s: Series.nextPhoto - returning: %s, %s, %s\", get_ident(), curShots, name, serMetaData)\n        return curShots, name, serMetaData\n    \n    def nextTimeOnlyAsStr(self) -> str: \n        \"\"\" Returns just the time for the next shot\n        \"\"\"\n        t = str(self.nextTime(test=True))\n        return t[11:]\n    \n    def nextTimeIso(self) -> str: \n        \"\"\" Returns the time for the next shot in ISO format\n        \"\"\"\n        t = self.nextTime(test=True)\n        return t.isoformat()\n    \n    def calcSunCtrlData(self, dat: str):\n        \"\"\"Calulate data for sun control for the given date\n\n        Args:\n            - dat (str): Date in isoformat for which to to calculate sun-control data\n        \"\"\"\n        logger.debug(\"Series.calcSunCtrlData - dat: %s\", dat)\n        tim = datetime.fromisoformat(dat)\n        sc = CameraCfg().serverConfig\n        sun = Sun(sc.locLatitude, sc.locLongitude, sc.locElevation, sc.locTzKey)\n        if self.sunCtrlMode == 1:\n            # sunrise/sunset based control\n            self.sunrise, self.sunset = sun.sunrise_sunset(tim)\n            if self.sunCtrlStart1Trg == 1:\n                self.sunCtrlStart1 = self.sunrise + timedelta(minutes=self.sunCtrlStart1Shft)\n            if self.sunCtrlStart1Trg == 2:\n                self.sunCtrlStart1 = self.sunset + timedelta(minutes=self.sunCtrlStart1Shft)\n            if self.sunCtrlEnd1Trg == 1:\n                self.sunCtrlEnd1 = self.sunrise + timedelta(minutes=self.sunCtrlEnd1Shft)\n            if self.sunCtrlEnd1Trg == 2:\n                self.sunCtrlEnd1 = self.sunset + timedelta(minutes=self.sunCtrlEnd1Shft)\n\n            if self.sunCtrlStart2Trg > 0 and self.sunCtrlEnd2Trg > 0:\n                if self.sunCtrlStart2Trg == 1:\n                    self.sunCtrlStart2 = self.sunrise + timedelta(minutes=self.sunCtrlStart2Shft)\n                if self.sunCtrlStart2Trg == 2:\n                    self.sunCtrlStart2 = self.sunset + timedelta(minutes=self.sunCtrlStart2Shft)\n                if self.sunCtrlEnd2Trg == 1:\n                    self.sunCtrlEnd2 = self.sunrise + timedelta(minutes=self.sunCtrlEnd2Shft)\n                if self.sunCtrlEnd2Trg == 2:\n                    self.sunCtrlEnd2 = self.sunset + timedelta(minutes=self.sunCtrlEnd2Shft)\n            else:\n                self.sunCtrlStart2 = None\n                self.sunCtrlEnd2 = None\n        \n        if self.sunCtrlMode == 2:\n            # Sun azimuth based control\n            if self.sunAzimuth1 is not None:\n                times = sun.find_times_for_azimuth(tim, self.sunAzimuth1)\n                if len(times) > 0:\n                    self.sunAzimuth1Time = times[0][\"time\"]\n                else:\n                    self.sunAzimuth1Time = None\n            else:\n                self.sunAzimuth1Time = None\n\n            if self.sunAzimuth2 is not None:\n                times = sun.find_times_for_azimuth(tim, self.sunAzimuth2)\n                if len(times) > 0:\n                    self.sunAzimuth2Time = times[0][\"time\"]\n                else:\n                    self.sunAzimuth2Time = None\n            else:\n                self.sunAzimuth2Time = None\n\n            if self.sunAzimuth3 is not None:\n                times = sun.find_times_for_azimuth(tim, self.sunAzimuth3)\n                if len(times) > 0:\n                    self.sunAzimuth3Time = times[0][\"time\"]\n                else:\n                    self.sunAzimuth3Time = None\n            else:\n                self.sunAzimuth3Time = None\n\n            if self.sunAzimuth4 is not None:\n                times = sun.find_times_for_azimuth(tim, self.sunAzimuth4)\n                if len(times) > 0:\n                    self.sunAzimuth4Time = times[0][\"time\"]\n                else:\n                    self.sunAzimuth4Time = None\n            else:\n                self.sunAzimuth4Time = None\n    \n    def nextTimeSunCtrl(self) -> datetime:\n        \"\"\"Calculate the time for the next photo of a sun-controlled series\n\n        Returns:\n            datetime: Time for next photo\n        \"\"\"\n        logger.debug(\"Thread %s: Series.nextTimeSunCtrl - Mode: %s\", get_ident(), self.sunCtrlMode)\n        # Check whether sunrise/sunset needs to be calculated\n        next = None\n        if self.sunCtrlMode == 1:\n            # sunrise/sunset based control\n            now = datetime.now()\n            dat = now.strftime(\"%Y-%m-%d\")\n            if self.sunrise is None:\n                self.calcSunCtrlData(dat)\n            last = self.sunCtrlEnd1\n            if self.sunCtrlStart2Trg > 0 and self.sunCtrlEnd2Trg > 0:\n                last = self.sunCtrlEnd2\n            if now > last:\n                now += timedelta(days=1)\n                dat = now.strftime(\"%Y-%m-%d\")\n                self.calcSunCtrlData(dat)\n                if self.onDialMarks == True:\n                    next = self.nextDialMark(self.sunCtrlStart1)\n                else:\n                    next = self.sunCtrlStart1\n            else:\n                if now < self.sunCtrlStart1:\n                    if self.onDialMarks == True:\n                        next = self.nextDialMark(self.sunCtrlStart1)\n                    else:\n                        next = self.sunCtrlStart1\n                else:\n                    if self.onDialMarks == True:\n                        next = self.nextDialMark(now)\n                    else:\n                        timedif = now - self.sunCtrlStart1\n                        timedifSec = timedif.total_seconds()\n                        nrint = int(timedifSec / self._interval)\n                        next = self.sunCtrlStart1 + timedelta(seconds = (nrint + 1)*self.interval)\n                if next > self.sunCtrlEnd1:\n                    if self.sunCtrlStart2Trg > 0 and self.sunCtrlEnd2Trg > 0:\n                        if now < self.sunCtrlStart2:\n                            if self.onDialMarks == True:\n                                next = self.nextDialMark(self.sunCtrlStart2)\n                            else:\n                                next = self.sunCtrlStart2\n                        else:\n                            if self.onDialMarks == True:\n                                next = self.nextDialMark(now)\n                            else:\n                                timedif = now - self.sunCtrlStart2\n                                timedifSec = timedif.total_seconds()\n                                nrint = int(timedifSec / self._interval)\n                                next = self.sunCtrlStart2 + timedelta(seconds = (nrint + 1)*self.interval)\n                        if next > self.sunCtrlEnd2:\n                            now1 = now + timedelta(days=1)\n                            dat = now1.strftime(\"%Y-%m-%d\")\n                            self.calcSunCtrlData(dat)\n                            if self.onDialMarks == True:\n                                next = self.nextDialMark(self.sunCtrlStart1)\n                            else:\n                                next = self.sunCtrlStart1\n                    else:\n                        now1 = now + timedelta(days=1)\n                        dat = now1.strftime(\"%Y-%m-%d\")\n                        self.calcSunCtrlData(dat)\n                        if self.onDialMarks == True:\n                            next = self.nextDialMark(self.sunCtrlStart1)\n                        else:\n                            next = self.sunCtrlStart1\n        if self.sunCtrlMode == 2:\n            # Sun azimuth based control\n            now = datetime.now()\n            ref = datetime.now()\n            dat = ref.strftime(\"%Y-%m-%d\")\n            logger.debug(\"Thread %s: Series.nextTimeSunCtrl - Sun azimuths: %s\", get_ident(), self.sunAzimuths)\n            if len(self.sunAzimuths) > 0:\n                self.calcSunCtrlData(dat)\n                \n                logger.debug(\"Thread %s: Series.nextTimeSunCtrl - Sun azimuthTimes: %s, %s, %s, %s\", get_ident(), self.sunAzimuth1TimeIso, self.sunAzimuth2TimeIso, self.sunAzimuth3TimeIso, self.sunAzimuth4TimeIso)\n                if len(self.sunAzimuthTimes) > 0:\n                    done = False\n                    cnt = 0\n                    while not done:\n                        timeFound = False\n                        i = 0\n                        for t in self.sunAzimuthTimes:\n                            if t is not None:\n                                timeFound = True\n                                if t > now:\n                                    azimuth = self.sunAzimuths[i]\n                                    self.metaData[\"Azimuth\"] = azimuth\n                                    next = t\n                                    logger.debug(\"Thread %s: Series.nextTimeSunCtrl - Found next sun azimuth %s at time: %s\", get_ident(), azimuth, next)\n                                    done = True\n                                    break\n                            i += 1\n                        if timeFound == False:\n                            done = True\n                        if next is None:\n                            cnt += 1\n                            if cnt > 3:\n                                done = True\n                            else:\n                                ref += timedelta(days=1)\n                                logger.debug(\"Thread %s: Series.nextTimeSunCtrl - No sun azimuth time found for now. Trying next day: %s\", get_ident(), ref)\n                                dat = ref.strftime(\"%Y-%m-%d\")\n                                self.calcSunCtrlData(dat)\n                                logger.debug(\"Thread %s: Series.nextTimeSunCtrl - Sun azimuthTimes: %s, %s, %s, %s\", get_ident(), self.sunAzimuth1TimeIso, self.sunAzimuth2TimeIso, self.sunAzimuth3TimeIso, self.sunAzimuth4TimeIso)\n        if not next:\n            next = datetime.now()\n        logger.debug(\"Thread %s: Series.nextTimeSunCtrl - returning: %s\", get_ident(), next.strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3])\n        return next\n\n    def nextDialMark(self, t:datetime) -> datetime:\n        \"\"\" Calculate and return the next dial mark for the given time\n        \n            t: time for which next dial mark is to be calculated\n            Return: updated time\n        \"\"\"\n        logger.debug(\"Thread %s: Series.nextDialMark - t: %s\", get_ident(), t)\n        dm = t\n        if (\n            (self.interval % 60 == 0)\n            or (self.interval % 120 == 0)\n            or (self.interval % 240 == 0)\n            or (self.interval % 300 == 0)\n            or (self.interval % 360 == 0)\n            or (self.interval % 600 == 0)\n            or (self.interval % 720 == 0)\n            or (self.interval % 900 == 0)\n            or (self.interval % 1200 == 0)\n            or (self.interval % 1800 == 0)\n            or (self.interval % 3600 == 0)\n        ):\n            minutes = t.hour * 60 + t.minute\n            period = math.floor(60.0 * minutes / self.interval)\n            nextmin = (period + 1) * self.interval / 60\n            dm = datetime(t.year,t.month, t.day) + timedelta(minutes=nextmin)\n        elif (\n            (self.interval % 2 == 0)\n            or (self.interval % 4 == 0)\n            or (self.interval % 5 == 0)\n            or (self.interval % 6 == 0)\n            or (self.interval % 10 == 0)\n            or (self.interval % 12 == 0)\n            or (self.interval % 15 == 0)\n            or (self.interval % 20 == 0)\n            or (self.interval % 30 == 0)\n        ):\n            seconds = t.minute * 60 + t.second\n            period = math.floor(seconds / self.interval)\n            nextsec = (period + 1) * self.interval\n            dm = datetime(t.year,t.month, t.day, t.hour) + timedelta(seconds=nextsec)\n        return dm\n\n    def nextTime(self, lastTime=None, test=False) -> datetime:\n        \"\"\" Calculate and return the time when the next photo must be taken\n        \n            lastTime: time when the last photo has been taken\n        \"\"\"\n        logger.debug(\"Thread %s: Series.nextTime - lastTime: %s\", get_ident(), lastTime)\n        next = None\n        curTime = datetime.now()\n        if curTime <= self.end:\n            if self.isSunControlledSeries == True:\n                next = self.nextTimeSunCtrl()\n            else:\n                if curTime < self.start:\n                    if self.onDialMarks == True:\n                        next = self.nextDialMark(self.start)\n                    else:\n                        next = self.start\n                else:\n                    if self.onDialMarks == True:\n                        next = self.nextDialMark(curTime)\n                    else:\n                        timedif = curTime - self.start\n                        timedifSec = timedif.total_seconds()\n                        nrint = int(timedifSec / self._interval)\n                        next = self.start + timedelta(seconds = (nrint + 1)*self.interval)\n        else:\n            if self.ended is None and test == False:\n                logger.debug(\"Thread %s: Series.nextTime - Finishing series\", get_ident())\n                dt = datetime.now()\n                dt = datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute)\n                self.ended = dt\n                self.nextStatus(\"finish\")\n                self.persist()\n                #Restore camera controls\n                if CameraCfg().controlsBackup:\n                    CameraCfg().controls = copy.deepcopy(CameraCfg().controlsBackup)\n                    CameraCfg().controlsBackup = None\n                    logger.debug(\"Thread %s: Series.nextTime - Restored controls backup: %s\", get_ident(), CameraCfg().controls.__dict__)\n                    wait = None\n                    if self.isExposureSeries:\n                        #For an exposure series wait for the longest exposure time\n                        if self.isExpGainFix:\n                            wait = 0.2 + self.expTimeStop / 1000000\n                    raspiCamSrv.camera_pi.Camera().applyControlsForLivestream(wait)\n        if not next:\n            next = datetime.now()\n        logger.debug(\"Thread %s: Series.nextTime - returning: %s\", get_ident(), next.strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3])\n        return next\n    \n    def getPreviewList(self):\n        \"\"\" return a list with the last n photos of the series\n        \"\"\"\n        list = []\n        if self.curShots:\n            n = self.curShots + 1\n            cnt = 0\n            \n            while (cnt < 20 and n >= 0):\n                name = self.name + \"_\" + str(n).zfill(Series.PHOTODIGITS) + \".jpg\"\n                path = self.path + \"/\" + name\n                if os.path.exists(path):\n                    relPath = \"photoseries/\" + self.name + \"/\" + name\n                    set = {}\n                    set[\"name\"] = name\n                    set[\"relPath\"] = relPath\n                    list.append(set)\n                    cnt += 1\n                n = n - 1\n        return list\n\n    def _readLog(self, file: str) -> dict:\n        \"\"\" Read the log file and return as dict\n        \"\"\"\n        ret = {}\n        with open(file, newline=\"\") as csvFile:\n            reader = csv.DictReader(csvFile, delimiter = \";\", quotechar = \"'\")\n            for row in reader:\n                ret[row[\"Name\"]] = row\n        return ret\n\n    def _getParamsFromLog(self, log: dict, name: str) -> dict:\n        \"\"\" Get parameters for a specific name with float limited to n digits\n        \"\"\"\n        ret = {}\n        if name in log:\n            ret = log[name]\n        # Limit number of digits\n        if \"AnalogueGain\" in ret:\n            ret[\"AnalogueGain\"] = round(float(ret[\"AnalogueGain\"]),4)\n        if \"DigitalGain\" in ret:\n            ret[\"DigitalGain\"] = round(float(ret[\"DigitalGain\"]),4)\n        if \"Lux\" in ret:\n            ret[\"Lux\"] = round(float(ret[\"Lux\"]),4)\n        if \"LensPosition\" in ret:\n            lp = ret[\"LensPosition\"]\n            if len(lp) > 0:\n                if float(ret[\"LensPosition\"]) > 0:\n                    ret[\"FocalDistance\"] = round(1.0/float(ret[\"LensPosition\"]), 4)\n                else:\n                    ret[\"FocalDistance\"] = 999.999\n                ret[\"LensPosition\"] = round(float(ret[\"LensPosition\"]),4)\n            else:\n                ret[\"FocalDistance\"] = \"0\"\n                ret[\"LensPosition\"] = \"0\"\n                \n        if \"ExposureTime\" in ret:\n            ret[\"ExposureTime\"] = round(float(ret[\"ExposureTime\"]) / 1000000,4)\n        return ret\n    \n    def getPreviewListHistDetail(self):\n        \"\"\" return a list with the last n photos of the series\n            including histogram and details\n        \"\"\"\n        log = self._readLog(self.logFile)\n        list = []\n        if self.curShots:\n            n = self.curShots + 1\n            cnt = 0\n            \n            while (cnt < 20 and n >= 0):\n                pureName = self.name + \"_\" + str(n).zfill(Series.PHOTODIGITS)\n                name = pureName + \".jpg\"\n                nameRaw = pureName + \".dng\"\n                photoPath = self.path + \"/\" + name\n                histoPath = self.histogramPath + \"/\" + name\n                include = False\n                if os.path.exists(photoPath):\n                    relPhotoPath = \"photoseries/\" + self.name + \"/\" + name\n                    include = True\n                else:\n                    relPhotoPath = None\n                if os.path.exists(histoPath):\n                    relHisroPath = \"photoseries/\" + self.name + \"/\" + Series.HISTOGRAMFOLDER + \"/\" + name\n                    include = True\n                else:\n                    relHisroPath = None\n                if include:\n                    set = {}\n                    if self.type == \"raw+jpg\":\n                        set[\"name\"] = nameRaw\n                    else:\n                        set[\"name\"] = name\n                    set[\"relPhotoPath\"] = relPhotoPath\n                    set[\"relHistoPath\"] = relHisroPath\n                    set[\"params\"] = self._getParamsFromLog(log, pureName)\n                    list.append(set)\n                    cnt += 1\n                n = n - 1\n        return list\n\n    def logCamCfgCtrlClose(self):\n        \"\"\"Append camera _cam.json file with closing ]\n        \"\"\"\n        if self.camFile:\n            with open(self.camFile, mode='a', encoding='utf-8') as f:\n                f.write(\"\\n]\")\n\n    def logCamCfgCtrl(self, name: str, cfg: dict, ctrl: dict):\n        \"\"\"Append camera config & controls  used for a photo to the _cam.json file\n           name: Name of the photo\n           cfg : camera configuration\n           ctrl: camera controls\n        \"\"\"\n        if self.camFile:\n            if not os.path.exists(self.camFile):\n                os.makedirs(self.path, exist_ok=True)\n                Path(self.camFile).touch()\n            new = {}\n            new[\"name\"] = name\n            new[\"config\"] = cfg\n            new[\"controls\"] = ctrl\n            logger.debug(\"logCamCfgCtrl new: %s\", new)\n            newJson = json.dumps(new, default=lambda o: getattr(o, '__dict__', str(o)), indent=4)\n            if self._firstCamEntry:\n                newJson = \"[\\n\" + newJson\n                self._firstCamEntry = False\n            else:\n                newJson = \",\\n\" + newJson\n            with open(self.camFile, mode='a', encoding='utf-8') as f:\n                f.write(newJson)\n    \n    def logPhoto(self, name: str, ptime: datetime, metadata: dict, seriesMetaData: dict):\n        \"\"\"Append a log entry for the photo\n        \"\"\"\n        if self.started is None:\n            self.started = ptime\n            \n        if self._logHeadlineReq:\n            log = \"Name\" + \";\"\n            log = log + \"Time\" + \";\"\n            log = log + \"SensorTimestamp\" + \";\"\n            log = log + \"ExposureTime\" + \";\"\n            log = log + \"AnalogueGain\" + \";\"\n            log = log + \"DigitalGain\" + \";\"\n            log = log + \"Lux\" + \";\"\n            log = log + \"LensPosition\" + \";\"\n            log = log + \"FocusFoM\" + \";\"\n            log = log + \"FrameDuration\" + \";\"\n            log = log + \"SensorTemperature\" + \";\"\n            log = log + \"ColourTemperature\" + \";\"\n            log = log + \"AeLocked\" + \";\"\n            log = log + \"ScalerCrops\" + \";\"\n            if len(seriesMetaData) > 0:\n                for key in seriesMetaData:\n                    log = log + key + \";\"\n            f = open(self.logFile, \"a\")\n            f.write(log + \"\\n\")\n            f.close()\n            self._logHeadlineReq = False\n        \n        log = name + \";\"\n        log = log + ptime.isoformat() + \";\"\n        if \"SensorTimestamp\" in metadata:\n            log = log + str(metadata[\"SensorTimestamp\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"ExposureTime\" in metadata:\n            log = log + str(metadata[\"ExposureTime\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"AnalogueGain\" in metadata:\n            log = log + str(metadata[\"AnalogueGain\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"DigitalGain\" in metadata:\n            log = log + str(metadata[\"DigitalGain\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"Lux\" in metadata:\n            log = log + str(metadata[\"Lux\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"LensPosition\" in metadata:\n            log = log + str(metadata[\"LensPosition\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"FocusFoM\" in metadata:\n            log = log + str(metadata[\"FocusFoM\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"FrameDuration\" in metadata:\n            log = log + str(metadata[\"FrameDuration\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"SensorTemperature\" in metadata:\n            log = log + str(metadata[\"SensorTemperature\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"ColourTemperature\" in metadata:\n            log = log + str(metadata[\"ColourTemperature\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"AeLocked\" in metadata:\n            log = log + str(metadata[\"AeLocked\"]) + \";\"\n        else:\n            log = log + \";\"\n        if \"ScalerCrops\" in metadata:\n            log = log + str(metadata[\"ScalerCrops\"]) + \";\"\n        else:\n            log = log + \";\"\n        if len(seriesMetaData) > 0:\n            for val in seriesMetaData.values():\n                log = log + str(val) + \";\"\n        f = open(self.logFile, \"a\")\n        f.write(log + \"\\n\")\n        f.close()\n    \n    def persist(self):\n        \"\"\" Store class dictionary in the config file\n        \"\"\"\n        if self.cfgFile:\n            if not os.path.exists(self.cfgFile):\n                os.makedirs(self.path, exist_ok=True)\n                Path(self.cfgFile).touch()\n            f = open(self.cfgFile, \"w\")\n            #cj = json.loads(json.dumps(self.toJson(), indent=4))\n            cj = self.toJson()\n            f.write(str(cj))\n            f.close()\n            \n    def toJson(self):\n        #return json.dumps(self, default=lambda o: o.__dict__)\n        return json.dumps(self, default=lambda o: getattr(o, '__dict__', str(o)), indent=4)\n    \n    @classmethod\n    def checkPhotos(cls, path: str, name: str):\n        \"\"\" Analyze photos <name>nnnnnn\n            Return nrPhotos, maxNumber\n        \"\"\"\n        nrPhotos = 0\n        maxNumber = 0\n        fs = []\n        try:\n            fs = os.listdir(path)\n        except FileNotFoundError:\n            fs = []\n        fs.sort()\n        l = len(name) + 1\n        nl = l + Series.PHOTODIGITS\n        for f in fs:\n            if f.endswith(\".jpg\"):\n                fn = f[:len(f) - 4]\n                if len(fn) == nl:\n                    nums = fn[l:]\n                    if nums.isnumeric:\n                        nrPhotos += 1\n                        num = int(nums)\n                        if num > maxNumber:\n                            maxNumber = num\n        return nrPhotos, maxNumber\n            \n\n    @classmethod                \n    def initFromDict(cls, dict:dict):\n        ser = Series()\n        for key, value in dict.items():\n            if key == \"_start\" \\\n            or key == \"_started\" \\\n            or key == \"_end\" \\\n            or key == \"_ended\" \\\n            or key == \"_sunrise\" \\\n            or key == \"_sunset\" \\\n            or key == \"_sunCtrlStart1\" \\\n            or key == \"_sunCtrlEnd1\" \\\n            or key == \"_sunCtrlStart2\" \\\n            or key == \"_sunCtrlEnd2\" \\\n            or key == \"_sunAzimuthTime\" \\\n            or key == \"_sunAzimuth1Time\" \\\n            or key == \"_sunAzimuth2Time\" \\\n            or key == \"_sunAzimuth3Time\" \\\n            or key == \"_sunAzimuth4Time\":\n                if value is None:\n                    setattr(ser, key, value)\n                else:\n                    setattr(ser, key, datetime.strptime(value, \"%Y-%m-%d %H:%M:%S\"))\n            elif key == \"_cameraConfig\":\n                if value is None:\n                    setattr(ser, key, value)\n                else:\n                    ccfg = CameraConfig.initFromDict(value)\n                    ser.cameraConfig = ccfg\n            elif key == \"_cameraControls\":\n                if value is None:\n                    setattr(ser, key, value)\n                else:\n                    cctr = CameraControls.initFromDict(value)\n                    ser.cameraControls = cctr\n            else:\n                setattr(ser, key, value)\n                \n        nrPhotos, maxNumber = Series.checkPhotos(ser.path, ser.name)\n        ser.curShots = maxNumber\n        return ser\n\nclass PhotoSeriesCfg():\n    _instance = None\n    def __new__(cls):\n        if cls._instance is None:\n            cls._instance = super(PhotoSeriesCfg, cls).__new__(cls)\n            cls._rootPath = None\n            cls._tlSeries = []\n            cls._curSeries: Series = None\n        return cls._instance\n    \n    @property\n    def rootPath(self) -> str:\n        return self._rootPath\n\n    @rootPath.setter\n    def rootPath(self, value: str):\n        self._rootPath = value\n    \n    @property\n    def tlSeries(self) -> list:\n        return self._tlSeries\n\n    @tlSeries.setter\n    def tlSeries(self, value: list):\n        self._tlSeries = value\n    \n    @property\n    def seriesNames(self) -> list:\n        nl = []\n        for s in self._tlSeries:\n            nl.append(s.name)\n        return nl\n    \n    @property\n    def curSeries(self) -> Series:\n        return self._curSeries\n\n    @curSeries.setter\n    def curSeries(self, value: Series):\n        self._curSeries = value\n    \n    @property\n    def hasCurSeries(self) -> bool:\n        return self._curSeries is not None\n        \n    def appendSeries(self, s:Series):\n        self._tlSeries.append(s)\n\n    def nameExists(self, name: str) -> bool:\n        ne = False\n        for s in self._tlSeries:\n            if s.name == name:\n                ne = True\n                break\n        return ne\n    \n    def _initSeriesFromCfg(self, spath: str, name: str) -> Series:\n        \"\"\" Initialize a photoseries series from folder information\n            Returns True/False if series is OK/NOK\n        \"\"\"\n        logger.debug(\"_initSeriesFromFolder - path: %s name: %s\", spath, name)\n        ser = None\n        cfgFile = spath + \"/\" + name + \"_cfg.json\"\n        if os.path.exists(cfgFile):\n            with open(cfgFile) as f:\n                try:\n                    sdict = json.load(f)\n                    ser = Series.initFromDict(sdict)\n                except Exception:\n                    ser = Series()\n                    ser.name = name\n                    ser.path = spath\n                    ser.cfgFile = cfgFile\n                    ser.logFile = spath + \"/\" + ser.logFileName\n                    ser.camFile = spath + \"/\" + ser.camFileName\n        return ser\n    \n    def initFromTlFolder(self):\n        \"\"\" Initialize photoseries from file system information\n        \"\"\"\n        try:\n            tls = os.listdir(self.rootPath)\n        except FileNotFoundError:\n            tls = []\n        tls.sort()\n        logger.debug(\"initFromTlFolder - Found TL series: %s\", tls)\n        curSer = None\n        lastSer = None\n        for tl in tls:\n            spath = self.rootPath + \"/\" + tl\n            if os.path.isdir(spath):\n                ser = self._initSeriesFromCfg(spath, tl)\n                if ser:\n                    self.tlSeries.append(ser)\n                    lastSer = ser\n                    if ser.status == \"ACTIVE\":\n                        curSer = ser\n        if curSer:\n            self.curSeries = curSer\n        else:\n            self.curSeries = lastSer\n        logger.debug(\"initFromTlFolder - # series: %s\", len(self.tlSeries))\n        \n    def removeCurrentSeries(self):\n        \"\"\" Remove the current series and set current series to last one in list\n        \"\"\"\n        sp = self.curSeries.path\n        try:\n            if os.path.exists(sp):\n                if os.path.isdir(sp):\n                    shutil.rmtree(sp)\n        except Exception as e:\n            logger.error(\"Failed to delete folder %s. Reason: %s\", sp, e)\n            \n        self.tlSeries.remove(self.curSeries)\n        if len(self.tlSeries) > 0:\n            self.curSeries = self.tlSeries[0]\n        else:\n            self.curSeries = None"
  },
  {
    "path": "raspiCamSrv/schema.sql",
    "content": "DROP TABLE IF EXISTS user;\nDROP TABLE IF EXISTS config;\nDROP TABLE IF EXISTS events;\nDROP TABLE IF EXISTS eventactions;\n\nCREATE TABLE IF NOT EXISTS user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  username TEXT UNIQUE NOT NULL,\n  password TEXT NOT NULL,\n  issuperuser INTEGER DEFAULT 0 NOT NULL,\n  isinitial INTEGER DEFAULT 1 NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS config (\n  key TEXT NOT NULL,\n  type TEXT NOT NULL,\n  value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  timestamp TEXT NOT NULL,\n  date TEXT NOT NULL,\n  minute TEXT NOT NULL,\n  time TEXT NOT NULL,\n  type TEXT NOT NULL,\n  trigger TEXT NOT NULL,\n  triggertype TEXT NOT NULL,\n  triggerparam TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS eventactions (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  event TEXT NOT NULL,\n  timestamp TEXT NOT NULL,\n  actiontype TEXT NOT NULL,\n  date TEXT NOT NULL,\n  time TEXT NOT NULL,\n  actionduration INTEGER, \n  filename TEXT NOT NULL,\n  fullpath TEXT NOT NULL,\n  FOREIGN KEY(event) REFERENCES events(timestamp)\n);\n\nCREATE INDEX IF NOT EXISTS events_date_idx ON events(\n  date,\n  minute\n);\n\nCREATE INDEX IF NOT EXISTS eventactions_type_idx ON eventactions(\n  event,\n  actiontype\n);\n\n"
  },
  {
    "path": "raspiCamSrv/settings.py",
    "content": "from flask import Blueprint, Response, flash, g, render_template, request, current_app\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camCfg import CameraCfg, CameraControls, CameraProperties, CameraConfig, ServerConfig, TriggerConfig, TuningConfig, vButton, ActionButton, AiConfig, LiveButton\nfrom raspiCamSrv.camCfg import GPIODevice\nfrom raspiCamSrv.camera_pi import Camera, CameraEvent\nfrom raspiCamSrv.photoseriesCfg import PhotoSeriesCfg\nfrom raspiCamSrv.motionDetector import MotionDetector\nfrom raspiCamSrv.triggerHandler import TriggerHandler\nfrom raspiCamSrv.version import version\nfrom raspiCamSrv.db import get_db\nfrom gpiozero import Button, RotaryEncoder, MotionSensor, DistanceSensor, LightSensor, LineSensor, DigitalInputDevice\nfrom gpiozero import LED, PWMLED, RGBLED, Buzzer, TonalBuzzer, Servo, AngularServo, Motor, DigitalOutputDevice, OutputDevice\nfrom raspiCamSrv.gpioDevices import StepperMotor, ServoPWM\nimport os\nimport shutil\nimport ast\nimport time\nfrom pathlib import Path\nimport subprocess\nimport json\nimport copy\nfrom raspiCamSrv.auth import login_required\nimport logging\n\n# Try to import flask_jwt_extended to avoid errors when upgrading to V2.11 from earlier versions\ntry:\n    from flask_jwt_extended import create_access_token\nexcept ImportError:\n    pass\n\n\nbp = Blueprint(\"settings\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/settings\")\n@login_required\ndef main():\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/serverconfig\", methods=(\"GET\", \"POST\"))\n@login_required\ndef serverconfig():\n    logger.debug(\"serverconfig\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsparams\"\n    if request.method == \"POST\":\n        msg = None\n        restartLiveStream = False\n        if sc.isTriggerRecording:\n            msg = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n        if sc.isVideoRecording == True:\n            msg = \"Please stop video recording before changing the tuning configuration\"\n        if sc.isPhotoSeriesRecording:\n            msg = \"Please go to 'Photo Series' and stop the active process before changing the tuning configuration\"\n        if sc.noCamera == False:\n            activeCam = int(request.form[\"activecamera\"])\n            if not sc.secondCamera is None:\n                if activeCam == sc.secondCamera:\n                    msg = \"Active camera must be different from second camera. Use 'Switch Cameras in Cam/Multi-Cam' to swap the cameras.\"\n        if not msg:\n            if sc.noCamera == False:\n                photoType = request.form[\"phototype\"]\n                sc.photoType = photoType\n                rawPhotoType = request.form[\"rawphototype\"]\n                sc.rawPhotoType = rawPhotoType\n                videoType = request.form[\"videotype\"]\n                sc.videoType = videoType\n                recordAudio = not request.form.get(\"recordaudio\") is None\n                sc.recordAudio = recordAudio        \n                audioSync = request.form[\"audiosync\"]\n                sc.audioSync = audioSync\n                useStereo = not request.form.get(\"usestereo\") is None\n                sc.useStereo = useStereo\n                useCameraAi = not request.form.get(\"usecameraai\") is None\n                if useCameraAi == False:\n                    ai = cfg.aiConfig\n                    if ai.enable == True:\n                        restartLiveStream = True\n                    ai.enable = False\n                    for key, scfg in cfg.streamingCfg.items():\n                        if \"aiconfig\" in scfg:\n                            ai = scfg[\"aiconfig\"]\n                            ai.enable = False\n                sc.useCameraAi = useCameraAi\n                useHist = not request.form.get(\"showhistograms\") is None\n                if not useHist:\n                    sc.displayContent = \"meta\"\n                sc.useHistograms = useHist\n                sc.requireAuthForStreaming = not request.form.get(\"requireAuthForStreaming\") is None\n                # If active camera has changed reset stream size to force adaptation of sensor mode\n                if activeCam != sc.activeCamera:\n                    sc.activeCamera = activeCam\n                    cfg.liveViewConfig.stream_size = None\n                    cfg.photoConfig.stream_size = None\n                    cfg.rawConfig.stream_size = None\n                    cfg.videoConfig.stream_size = None\n                    for cm in cs:\n                        if activeCam == cm.num:\n                            sc.activeCameraInfo = \"Camera \" + str(cm.num) + \" (\" + cm.model + \")\"\n                            sc.activeCameraModel = cm.model\n                            sc.activeCameraIsUsb = cm.isUsb\n                            sc.activeCameraUsbDev = cm.usbDev\n                            sc.activeCameraHasAi = cm.hasAi\n                            break\n                    strCfg = cfg.streamingCfg\n                    newCamStr = str(activeCam)\n                    if newCamStr in strCfg:\n                        ncfg = strCfg[newCamStr]\n                        if \"tuningconfig\" in ncfg:\n                            cfg.tuningConfig = ncfg[\"tuningconfig\"]\n                        else:\n                            cfg.tuningConfig = TuningConfig()\n                        if \"aiconfig\" in ncfg:\n                            cfg.aiConfig = copy.deepcopy(ncfg[\"aiconfig\"])\n                        else:\n                            cfg.aiConfig = AiConfig()\n                    else:\n                        cfg.tuningConfig = TuningConfig()\n                        cfg.aiConfig = AiConfig()\n                    Camera.switchCamera()\n                    msg = \"Camera switched to \" + sc.activeCameraInfo\n                    logger.debug(\"serverconfig - active camera set to %s\", sc.activeCamera)\n            useUsbCameras = not request.form.get(\"useusbcameras\") is None\n\n            reloadCamInfoNeeded = False\n            if useUsbCameras != sc.useUsbCameras:\n                logger.debug(\"serverconfig - useUsbCameras changed to %s\", useUsbCameras)\n                if len(sc.piCameras) == 0 and sc.useUsbCameras == True:\n                    if sc.isLiveStream == True \\\n                    or sc.isLiveStream2 == True \\\n                    or sc.isVideoRecording == True \\\n                    or sc.isPhotoSeriesRecording == True \\\n                    or sc.isTriggerRecording == True \\\n                    or sc.isEventhandling == True:\n                        msg = \"Please stop all active camera processes before changing the USB camera configuration\"\n                if not msg:\n                    if len(sc.piCameras) == 0:\n                        reloadCamInfoNeeded = True\n                    else:\n                        if sc.activeCameraIsUsb == True:\n                            reloadCamInfoNeeded = True\n                        if not sc.secondCamera is None:\n                            if sc.secondCameraIsUsb == True:\n                                reloadCamInfoNeeded = True\n                    sc.useUsbCameras = useUsbCameras\n                    cfg.setSupportedCameras()\n\n            useAPI = not request.form.get(\"useapi\") is None\n            sc.useAPI = useAPI\n            sc.locLatitude = float(request.form[\"loclatitude\"])\n            sc.locLongitude = float(request.form[\"loclongitude\"])\n            sc.locElevation = float(request.form[\"locelevation\"])\n            sc.locTzKey = request.form[\"loctzkey\"]\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Settings/General Parameters changed\")\n            if reloadCamInfoNeeded:\n                reloadCameraSystem()\n            else:\n                if sc.isLiveStream == True and restartLiveStream:\n                    Camera().restartLiveStream()\n                if sc.isLiveStream2 == True and restartLiveStream:\n                    if sc.secondCameraHasAi == True:\n                        Camera().restartLiveStream2()\n        if msg:\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef reloadCameraSystem():\n    \"\"\"Reload the camera system in case of hot plug-in/-out\n    \"\"\"\n    logger.debug(\"reloadCameraSystem\")\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    Camera._instance = None\n    cfg.cameras = []\n    cfg.sensorModes = []\n    cfg.rawFormats = []\n    cfg.cameraProperties = CameraProperties()\n    sc.noCamera = False\n    sc.supportedCameras = []\n    sc.usbCamAvailable = False\n    sc.piCameras = []\n    sc.hasMicrophone = False\n    sc.defaultMic = \"\"\n    sc.isMicMuted = False\n    sc.recordAudio = False\n\n    cam = Camera()\n    cfg.setSupportedCameras()\n    cfg.setPiCameras()\n    logger.debug(\"reloadCameraSystem - done\")\n\n@bp.route(\"/reloadCameras\", methods=(\"GET\", \"POST\"))\n@login_required\ndef reloadCameras():\n    logger.debug(\"reloadCameras\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        if sc.isVideoRecording:\n            Camera().stopVideoRecording()\n        if sc.isPhotoSeriesRecording == True:\n            tl = PhotoSeriesCfg()\n            sr = tl.curSeries\n            sr.nextStatus(\"pause\")\n            sr.persist()\n            Camera().stopPhotoSeries()\n            logger.debug(\"In resetServer - photo series stopped\")\n        if sc.isTriggerRecording == True:\n            MotionDetector().stopMotionDetection()\n            sc.isTriggerRecording = False\n            logger.debug(\"In resetServer - Motion detection stopped\")\n        if sc.isEventhandling:\n            TriggerHandler().stop()\n            sc.isEventhandling = False\n        if sc.isLiveStream == True:\n            Camera().stopLiveStream()\n            logger.debug(\"In resetServer - Live stream stopped\")\n        if sc.isLiveStream2 == True:\n            Camera().stopLiveStream2()\n            logger.debug(\"In resetServer - Live stream 2 stopped\")\n        logger.debug(\"Stopping camera system\")\n        time.sleep(3)\n        Camera().stopCameraSystem()\n        Camera.liveViewDeactivated = False\n        Camera.thread = None\n        Camera.thread2 = None\n        Camera.videoThread = None\n        Camera.photoSeriesThread = None\n        reloadCameraSystem()\n\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Camera system reloaded\")\n        los = getLoadConfigOnStart(cfgPath)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/resetServer\", methods=(\"GET\", \"POST\"))\n@login_required\ndef resetServer():\n    logger.debug(\"resetServer\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        if sc.isVideoRecording:\n            Camera().stopVideoRecording()\n        if sc.isPhotoSeriesRecording == True:\n            tl = PhotoSeriesCfg()\n            sr = tl.curSeries\n            sr.nextStatus(\"pause\")\n            sr.persist()\n            Camera().stopPhotoSeries()\n            logger.debug(\"In resetServer - photo series stopped\")\n        if sc.isTriggerRecording == True:\n            MotionDetector().stopMotionDetection()\n            sc.isTriggerRecording = False\n            logger.debug(\"In resetServer - Motion detection stopped\")\n        if sc.isEventhandling:\n            TriggerHandler().stop()\n            sc.isEventhandling = False\n        if sc.isLiveStream == True:\n            Camera().stopLiveStream()\n            logger.debug(\"In resetServer - Live stream stopped\")\n        if sc.isLiveStream2 == True:\n            Camera().stopLiveStream2()\n            logger.debug(\"In resetServer - Live stream 2 stopped\")\n        logger.debug(\"Stopping camera system\")\n        time.sleep(3)\n        Camera().stopCameraSystem()\n        Camera.liveViewDeactivated = False\n        Camera.thread = None\n        Camera.thread2 = None\n        Camera.videoThread = None\n        Camera.photoSeriesThread = None\n        logger.debug(\"Resetting server configuration\")\n        setLoadConfigOnStart(cfgPath, False)\n        photoRoot = sc.photoRoot\n        backupPath = sc.cfgBackupPath\n        prgOutputPath = sc.prgOutputPath\n        database = sc.database\n        actionPath = tc.actionPath\n        cfg = CameraCfg()\n        cfg.cameras = []\n        cfg.sensorModes = []\n        cfg.rawFormats = []\n\n        cfg.resetActiveCameraSettings()\n        \n        cfg._cameraConfigs = []\n        cfg.triggerConfig = TriggerConfig()\n        cfg.serverConfig = ServerConfig()\n\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n        sc.photoRoot = photoRoot\n        sc.cfgBackupPath = backupPath\n        sc.prgOutputPath = prgOutputPath\n        sc.database = database\n        tc.actionPath = actionPath\n        cfg.streamingCfg = {}\n        \n        sc.isVideoRecording = False\n        sc.isAudioRecording = False\n        sc.isTriggerRecording = False\n        sc.isPhotoSeriesRecording = False\n        sc.isLiveStream = False\n        sc.isLiveStream2 = False\n        sc.checkMicrophone()\n        sc.checkEnvironment()\n        if sc.supportsExtMotionDetection == False:\n            cfg.triggerConfig.motionDetectAlgos = [\"Mean Square Diff\",]\n        sc.curMenu = \"settings\"\n        \n        Camera.cam = None\n        Camera.cam2 = None\n        Camera.camNum = -1\n        Camera.camNum2 = -1\n        Camera.ctrl = None\n        Camera.ctrl2 = None\n        Camera.videoOutput = None\n        Camera.prgVideoOutput = None\n        Camera.photoSeries = None\n        Camera.thread = None\n        Camera.thread2 = None\n        Camera.liveViewDeactivated = False\n        Camera.videoThread = None\n        Camera.photoSeriesThread = None\n        Camera.frame = None\n        Camera.frame2 = None\n        Camera.last_access = 0\n        Camera.last_access2 = 0\n        Camera.stopRequested = False\n        Camera.stopRequested2 = False\n        Camera.stopVideoRequested = False\n        Camera.videoDuration = 0\n        Camera.stopPhotoSeriesRequested = False\n        Camera.event = CameraEvent()\n        Camera.event2 = None\n        Camera._instance = None\n        \n        msg = \"Server configuration has been reset to default values\"\n        flash(msg)\n        sc.unsavedChanges = False\n        sc.clearChangeLog()\n        los = getLoadConfigOnStart(cfgPath)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/configBackup\", methods=(\"GET\", \"POST\"))\n@login_required\ndef configBackup():\n    logger.debug(\"configBackup\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsconfig\"\n\n    if request.method == \"POST\":\n        msg = \"\"\n        backupRoot = sc.cfgBackupPath\n        logger.debug(\"configBackup - backupRoot=%s\", backupRoot)\n        if not os.path.exists(backupRoot):\n            os.makedirs(backupRoot, exist_ok=True)\n        if request.form[\"configbackupname\"]:\n            backupName = request.form[\"configbackupname\"]\n            if backupName.strip() == \"\":\n                msg = \"Please enter a valid backup name\"\n        else:\n            msg = \"Please enter a valid backup name\"\n        if msg == \"\":\n            backupPath = backupRoot + \"/\" + backupName\n            logger.debug(\"configBackup - backupPath=%s\", backupPath)\n            if os.path.exists(backupPath):\n                msg = \"Backup name already exists. Please choose a different name.\"\n        if msg == \"\":\n            try:\n                os.makedirs(backupPath, exist_ok=True)\n                \n                # Backup calib_data\n                stc = cfg.stereoCfg\n                src = sc.photoRoot + \"/\" + stc.calibDataSubPath\n                dst = backupPath + \"/static/\" + stc.calibDataSubPath\n                copyDir(src, dst)\n\n                #Backup calib_photos\n                src = sc.photoRoot + \"/\" + stc.calibPhotosSubPath\n                dst = backupPath + \"/static/\" + stc.calibPhotosSubPath\n                copyDir(src, dst)\n\n                #Backup config\n                src = sc.cfgPath\n                dst = backupPath + \"/static/\" + \"config\"\n                copyDir(src, dst)\n\n                #Backup events\n                tc = cfg.triggerConfig\n                src = tc.actionPath\n                dst = backupPath + \"/static/\" + \"events\"\n                copyDir(src, dst)\n\n                #Backup photos\n                src = sc.photoRoot + \"/photos\"\n                dst = backupPath + \"/static/\" + \"photos\"\n                copyDir(src, dst)\n\n                #Backup photo series\n                ps = PhotoSeriesCfg()\n                src = ps.rootPath\n                dst = backupPath + \"/static/\" + \"photoseries\"\n                copyDir(src, dst)\n                \n                # Backup database\n                src = sc.database\n                dst = backupPath + \"/instance/\" + \"database\"\n                copyDir(src, dst)\n\n            except Exception as e:\n                msg = f\"Error creating configuration backup: {e}\"\n\n        if msg == \"\":\n            msg = \"Configuration backup created under \" +  backupPath\n        flash(msg)\n        los = getLoadConfigOnStart(cfgPath)\n        backups = getBackupsList()\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef copyDir(src: str, dst: str):\n    \"\"\"Recursively copy a directory from src to dst.\n\n    Args:\n        src (str): Source directory path.\n        dst (str): Destination directory path.\n    \"\"\"\n    logger.debug(\"copyDir - src: %s, dst: %s\", src, dst)\n    if not os.path.exists(src):\n        logger.debug(\"copyDir - Source not found: %s\", src)\n        return\n\n    if not os.path.exists(dst):\n        os.makedirs(dst, exist_ok=True)\n        logger.debug(\"copyDir - Destination directory created: %s\", dst)\n\n    if os.path.isdir(src) == True:\n        for item in os.listdir(src):\n            s = os.path.join(src, item)\n            d = os.path.join(dst, item)\n            if os.path.isdir(s):\n                copyDir(s, d)\n            else:\n                shutil.copy2(s, d)\n    if os.path.isfile(src) == True:\n        shutil.copy2(src, dst)\n\ndef restoreDir(src: str, dst: str):\n    \"\"\"Recursively restore a directory from src to dst.\n\n    Args:\n        src (str): Source directory path.\n        dst (str): Destination directory path.\n    \"\"\"\n    logger.debug(\"restoreDir - src: %s, dst: %s\", src, dst)\n    if os.path.exists(src):\n        if os.path.exists(dst):\n            shutil.rmtree(dst)\n        copyDir(src, dst)\n    else:\n        if os.path.exists(dst):\n            shutil.rmtree(dst)\n\ndef getBackupsList() -> list:\n    \"\"\"Get the list of available backups.\n\n    Returns:\n        list: List of backup names.\n    \"\"\"\n    logger.debug(\"getBackupsList\")\n    res = []\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    backupRoot = sc.cfgBackupPath\n    if os.path.exists(backupRoot):\n        for entry in os.listdir(backupRoot):\n            backupPath = backupRoot + \"/\" + entry\n            if os.path.isdir(backupPath):\n                res.append(entry)\n    logger.debug(\"getBackupsList - found %s backups\", len(res))\n    return res\n\n@bp.route(\"/configRestore\", methods=(\"GET\", \"POST\"))\n@login_required\ndef configRestore():\n    logger.debug(\"configRestore\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsconfig\"\n\n    if request.method == \"POST\":\n        msg = \"\"\n        if request.form[\"configrestorename\"]:\n            backupName = request.form[\"configrestorename\"]\n            if backupName.strip() == \"\":\n                msg = \"Please select a valid backup name\"\n        else:\n            msg = \"Please select a valid backup name\"\n\n        if msg == \"\":\n            backupRoot = sc.cfgBackupPath\n            backupPath = backupRoot + \"/\" + backupName\n            logger.debug(\"configBackup - backupPath=%s\", backupPath)\n            try:\n                os.makedirs(backupPath, exist_ok=True)\n                \n                # Restore calib_data\n                stc = cfg.stereoCfg\n                dst = sc.photoRoot + \"/\" + stc.calibDataSubPath\n                src = backupPath + \"/static/\" + stc.calibDataSubPath\n                restoreDir(src, dst)\n\n                #Restore calib_photos\n                dst = sc.photoRoot + \"/\" + stc.calibPhotosSubPath\n                src = backupPath + \"/static/\" + stc.calibPhotosSubPath\n                restoreDir(src, dst)\n\n                #Restore config\n                dst = sc.cfgPath\n                src = backupPath + \"/static/\" + \"config\"\n                restoreDir(src, dst)\n\n                #Restore events\n                tc = cfg.triggerConfig\n                dst = tc.actionPath\n                src = backupPath + \"/static/\" + \"events\"\n                restoreDir(src, dst)\n\n                #Restore photos\n                dst = sc.photoRoot + \"/photos\"\n                src = backupPath + \"/static/\" + \"photos\"\n                restoreDir(src, dst)\n\n                #Restore photo series\n                ps = PhotoSeriesCfg()\n                dst = ps.rootPath\n                src = backupPath + \"/static/\" + \"photoseries\"\n                restoreDir(src, dst)\n                \n                # Restore database\n                dst = sc.database\n                src = backupPath + \"/instance/\" + \"database\" + \"/raspiCamSrv.sqlite\"\n                shutil.copy2(src, dst)\n\n            except Exception as e:\n                msg = f\"Error restoring backup {backupName}: {e}\"\n\n        if msg == \"\":\n            msg = \"Backup restored from \" +  backupPath\n        flash(msg)\n        los = getLoadConfigOnStart(cfgPath)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/configRemove\", methods=(\"GET\", \"POST\"))\n@login_required\ndef configRemove():\n    logger.debug(\"configRemove\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if request.form[\"configremovename\"]:\n            backupName = request.form[\"configremovename\"]\n            if backupName.strip() == \"\":\n                msg = \"Please select a valid backup name\"\n        else:\n            msg = \"Please select a valid backup name\"\n        if msg == \"\":\n            backupRoot = sc.cfgBackupPath\n            backupPath = backupRoot + \"/\" + backupName\n            try:\n                shutil.rmtree(backupPath)\n            except Exception as e:\n                msg = f\"Error removing backup {backupName}: {e}\"\n\n        if msg == \"\":\n            msg = f\"Backup {backupName} was removed.\"\n        flash(msg)\n        los = getLoadConfigOnStart(cfgPath)\n        backups = getBackupsList()\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/serverRestart\", methods=(\"GET\", \"POST\"))\n@login_required\ndef serverRestart():\n    logger.debug(\"serverRestart\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        msg = \"\"\n        startup_source = sc.detect_startup_source()\n        logger.debug(\"Startup source detected: %s\", startup_source)\n\n        try:\n            if startup_source == 1:\n                runresult = subprocess.run(\n                    [\"sudo\", \"systemctl\", \"restart\", \"raspiCamSrv.service\"],\n                    capture_output=True, text=True\n                )\n            elif startup_source == 2:\n                runresult = subprocess.run(\n                    [\"systemctl\", \"--user\", \"restart\", \"raspiCamSrv.service\"],\n                    capture_output=True, text=True\n                )\n            elif startup_source == 3:\n                msg = \"Please restart the server from the command line.\"\n            \n            else:\n                msg = \"Unable to detect the server startup source. Please restart the server manually.\"\n\n        except CalledProcessError as e:\n            logger.error(\"serverRestart - CalledProcessError: %s\", e)\n            msg = \"Error restarting server: \" + str(e)\n        except Exception as e:\n            logger.error(\"serverRestart - Exception: %s\", e)\n            msg = \"Error restarting server: \" + str(e)\n\n        if msg != \"\":\n            flash(msg)\n        los = getLoadConfigOnStart(cfgPath)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/remove_users\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remove_users():\n    logger.debug(\"In remove_users\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsusers\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    if request.method == \"POST\":\n        cnt = 0\n        msg = None\n        if not msg:\n            for user in g.users:\n                if request.form.get(\"sel_\" + str(user[\"id\"])) is not None:\n                    if user[\"id\"] == g.user[\"id\"]:\n                        msg = \"The active user cannot be removed\"\n                        break\n                    else:\n                        cnt += 1\n        if not msg:\n            logger.debug(\"Request to remove %s users\", cnt)\n            if cnt > 0:\n                db = get_db()\n                if cnt < len(g.users):\n                    while cnt > 0:\n                        logger.debug(\"cnt: %s\", cnt)\n                        userDel = None\n                        for user in g.users:\n                            logger.debug(\"Trying user %s %s\", user[\"id\"], user[\"username\"])\n                            if request.form.get(\"sel_\" + str(user[\"id\"])) is not None:\n                                userDel =user[\"id\"]\n                                logger.debug(\"User selected\")\n                                break\n                            else:\n                                logger.debug(\"User not selected\")\n                        if userDel:\n                            logger.debug(\"Removing user with id %s\", userDel)\n                            db.execute(\"DELETE FROM user WHERE id = ?\", (userDel,)).fetchone\n                            db.commit()\n                            g.nrUsers = db.execute(\"SELECT count(*) FROM user\").fetchone()[0]\n                            logger.debug(\"Found %s users\", g.nrUsers)\n                            g.users = db.execute(\"SELECT * FROM user\").fetchall()\n                            for user in g.users:\n                                logger.debug(\"Found user: ID: %s, UserName: %s\", user[\"id\"], user[\"username\"])\n                            cnt -= 1\n                else:\n                    msg=\"At least one user must remain\"\n                    flash(msg)\n            else:\n                msg=\"No users were selected\"\n                flash(msg)\n        else:\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/register_user\", methods=(\"GET\", \"POST\"))\n@login_required\ndef register_user():\n    logger.debug(\"In register_user\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsusers\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    if request.method == \"POST\":\n        return render_template(\"auth/register.html\", sc=sc, cp=cp)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/store_config\", methods=(\"GET\", \"POST\"))\n@login_required\ndef store_config():\n    logger.debug(\"In store_config\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        cfgPath = current_app.static_folder + \"/config\"\n        # Initialize the Photo viewer list\n        sc = cfg.serverConfig\n        sc.pvList = []\n        sc.updateStreamingClients()\n        cfg.persist(cfgPath)\n        msg = \"Configuration stored under \" + cfgPath\n        sc.unsavedChanges = False\n        sc.clearChangeLog()\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/load_config\", methods=(\"GET\", \"POST\"))\n@login_required\ndef load_config():\n    logger.debug(\"In load_config\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        msg = \"\"\n        # Stop background threads\n        if sc.isVideoRecording:\n            msg = \"Please stop video recording before loading the configuration\"\n        if msg == \"\":\n            if sc.isPhotoSeriesRecording == True:\n                tl = PhotoSeriesCfg()\n                sr = tl.curSeries\n                #sr.nextStatus(\"pause\")\n                #sr.persist()\n                Camera().stopPhotoSeries()\n                logger.debug(\"In load_config - photo series stopped\")\n                restartPhotoSeries = True\n            else:\n                restartPhotoSeries = False\n            if sc.isTriggerRecording == True:\n                MotionDetector().stopMotionDetection()\n                sc.isTriggerRecording = False\n                logger.debug(\"In load_config - Motion detection stopped\")\n                restartTriggerRecording = True\n            else:\n                restartTriggerRecording = False\n            if sc.isEventhandling:\n                TriggerHandler().stop()\n                sc.isEventhandling = False\n                logger.debug(\"In load_config - Eventhandling stopped\")\n                restartEventhandling = True\n            else:\n                restartEventhandling = False\n            if sc.isLiveStream == True:\n                Camera().stopLiveStream()\n                logger.debug(\"In load_config - Live stream stopped\")\n                restartLiveStream = True\n            else:\n                restartLiveStream = False\n            if sc.isLiveStream2 == True:\n                Camera().stopLiveStream2()\n                logger.debug(\"In load_config - Live stream 2 stopped\")\n                restartLiveStream2 = True\n            else:\n                restartLiveStream2 = False\n                \n            # Load stored configuration\n            cfg.loadConfig(cfgPath)\n            msg = \"Configuration loaded from \" + cfgPath\n            cam = Camera()\n            cfg = CameraCfg()\n            cs = cfg.cameras\n            sc = cfg.serverConfig\n            sc.checkMicrophone()\n            cp = cfg.cameraProperties\n            sc.curMenu = \"settings\"\n            cfgPath = current_app.static_folder + \"/config\"\n            los = getLoadConfigOnStart(cfgPath)\n\n            # Restart threads\n            if restartLiveStream == True:\n                Camera().restartLiveStream()\n                sc.isLiveStream = True\n                logger.debug(\"In load_config - Live stream started\")\n            if restartLiveStream2 == True:\n                Camera().restartLiveStream2()\n                sc.isLiveStream2 = True\n                logger.debug(\"In load_config - Live stream 2 started\")\n            if restartPhotoSeries == True:\n                Camera().startPhotoSeries(sr)\n                sc.isPhotoSeriesRecording = True\n                logger.debug(\"In load_config - photo series started\")\n            if restartTriggerRecording == True:\n                MotionDetector().startMotionDetection()\n                sc.isTriggerRecording = True\n                logger.debug(\"In load_config - Motion detection started\")\n            if restartEventhandling == True:\n                TriggerHandler().start()\n                sc.isEventhandling = True\n                logger.debug(\"In load_config - Eventhandling started\")\n            sc.unsavedChanges = False\n            sc.clearChangeLog()\n        if msg != \"\":\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef getLoadConfigOnStart(cfgPath: str) -> bool:\n    logger.debug(\"getLoadConfigOnStart\")\n    res = False\n    if cfgPath:\n        if os.path.exists(cfgPath):\n            fp = cfgPath + \"/_loadConfigOnStart.txt\"\n            if os.path.exists(fp):\n                res = True\n    logger.debug(\"getLoadConfigOnStart: %s\", res)\n    return res\n\ndef setLoadConfigOnStart(cfgPath: str, value: bool):\n    logger.debug(\"setLoadConfigOnStart - value: %s\", value)\n    if cfgPath:\n        if not os.path.exists(cfgPath):\n            os.makedirs(cfgPath, exist_ok=True)\n    fp = cfgPath + \"/_loadConfigOnStart.txt\"\n    if value == True:\n        Path(fp).touch()\n    else:\n        if os.path.exists(fp):\n            os.remove(fp)\n\n@bp.route(\"/loadConfigOnStart\", methods=(\"GET\", \"POST\"))\n@login_required\ndef loadConfigOnStart():\n    logger.debug(\"In loadConfigOnStart\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsconfig\"\n    if request.method == \"POST\":\n        cb = not request.form.get(\"loadconfigonstartcb\") is None\n        setLoadConfigOnStart(cfgPath, cb)\n        los = getLoadConfigOnStart(cfgPath)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/api_config\", methods=(\"GET\", \"POST\"))\n@login_required\ndef api_config():\n    logger.debug(\"In api_config\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsapi\"\n    if request.method == \"POST\":\n        msg = \"\"\n        sc = cfg.serverConfig\n        jwtAccessTokenExpirationMin = sc.jwtAccessTokenExpirationMin\n        jwtRefreshTokenExpirationDays = sc.jwtRefreshTokenExpirationDays\n        jwtKeyStore = request.form[\"jwtkeystore\"]\n        logger.debug(\"api_config - jwtKeyStore=%s\", jwtKeyStore)\n        if jwtKeyStore != \"\":\n            if os.path.exists(jwtKeyStore):\n                if os.path.isfile(jwtKeyStore):\n                    with open(jwtKeyStore) as f:\n                        try:\n                            secrets = json.load(f)\n                            sc.jwtKeyStore = jwtKeyStore\n                            logger.debug(\"api_config - jwtKeyStore successfully accessed\")\n                        except Exception as e:\n                            msg = f\"Error when accessing JWT Secret Key File: {e}\"\n                else:\n                    sc.jwtKeyStore = jwtKeyStore\n            else:\n                sc.jwtKeyStore = jwtKeyStore\n        else:\n            sc.jwtKeyStore = \"\"\n        sc.jwtAccessTokenExpirationMin = int(request.form[\"jwtaccesstokenexpirationmin\"])\n        sc.jwtRefreshTokenExpirationDays = int(request.form[\"jwtrefreshtokenexpirationdays\"])\n        if msg == \"\":\n            (secretKey, err, msg) = sc.checkJwtSettings()\n            logger.debug(\"api_config - secrKey = %s, err = %s, msg = %s\", secretKey, err, msg)\n            if not err is None:\n                msg = \"ERROR: \" + err\n        if msg != \"\":\n            flash(msg)\n        if sc.API_active == True:\n            if sc.jwtAuthenticationActive == True:\n                if jwtAccessTokenExpirationMin != sc.jwtAccessTokenExpirationMin \\\n                or jwtRefreshTokenExpirationDays != sc.jwtRefreshTokenExpirationDays:\n                    sc.jwtAuthenticationActive = False\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/API Settings changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/generate_token\", methods=(\"GET\", \"POST\"))\n@login_required\ndef generate_token():\n    logger.debug(\"In generate_token\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsapi\"\n    if request.method == \"POST\":\n        access_token = create_access_token(identity=g.user['username'])\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, access_token=access_token)\n\n@bp.route('/vbutton_dimensions', methods=(\"GET\", \"POST\"))\n@login_required\ndef vbutton_dimensions():\n    logger.debug(\"In vbutton_dimensions\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsvbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if request.form[\"vbuttonsrows\"]:\n            vButtonsRows = int(request.form[\"vbuttonsrows\"])\n        else:\n            msg = \"Please enter a valid number of rows\"\n        if request.form[\"vbuttonscols\"]:\n            vButtonsCols = int(request.form[\"vbuttonscols\"])\n        else:\n            msg = \"Please enter a valid number of columns\"\n        if msg == \"\":\n            if vButtonsRows == 0 \\\n            or vButtonsCols == 0:\n                sc.vButtonsCols = vButtonsCols\n                sc.vButtonsRows = vButtonsRows\n                sc.vButtons = []\n            else:\n                vButtons = []\n                for r in range(0, vButtonsRows):\n                    row = []\n                    for c in range(0, vButtonsCols):\n                        if r < sc.vButtonsRows and c < sc.vButtonsCols:\n                            btn = sc.vButtons[r][c]\n                        else:\n                            btn = vButton()\n                        btn.row = r\n                        btn.col = c\n                        row.append(btn)\n                    vButtons.append(row)\n                sc.vButtonsCols = vButtonsCols\n                sc.vButtonsRows = vButtonsRows\n                sc.vButtons = vButtons\n                sc.vButtonHasCommandLine = not request.form.get(\"vbuttonhascommandline\") is None\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Versatile Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/vbutton_settings', methods=(\"GET\", \"POST\"))\n@login_required\ndef vbutton_settings():\n    logger.debug(\"In vbutton_settings\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsvbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        for r in range(0, sc.vButtonsRows):\n            for c in range(0, sc.vButtonsCols):\n                btn = sc.vButtons[r][c]\n                visibleId = f\"vbtn_{btn.row}{ btn.col }_visible\"\n                btn.isVisible = not request.form.get(visibleId) is None\n                buttonTextKey = f\"vbtn_{btn.row}{btn.col}_buttontext\"\n                btn.buttonText = request.form[buttonTextKey]\n                buttonExecKey = f\"vbtn_{btn.row}{btn.col}_buttonexec\"\n                btn.buttonExec = request.form[buttonExecKey]\n                buttonShapeKey = f\"vbtn_{btn.row}{btn.col}_shape\"\n                btn.buttonShape = request.form[buttonShapeKey]\n                buttonColorKey = f\"vbtn_{btn.row}{btn.col}_color\"\n                btn.buttonColor = request.form[buttonColorKey]\n                confirmId = f\"vbtn_{btn.row}{ btn.col }_confirm\"\n                btn.needsConfirm = not request.form.get(confirmId) is None\n                sc.vButtons[r][c] = btn\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Versatile Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/abutton_dimensions', methods=(\"GET\", \"POST\"))\n@login_required\ndef abutton_dimensions():\n    logger.debug(\"In vbutton_dimensions\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsabuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if request.form[\"abuttonsrows\"]:\n            aButtonsRows = int(request.form[\"abuttonsrows\"])\n        else:\n            msg = \"Please enter a valid number of rows\"\n        if request.form[\"abuttonscols\"]:\n            aButtonsCols = int(request.form[\"abuttonscols\"])\n        else:\n            msg = \"Please enter a valid number of columns\"\n        if msg == \"\":\n            if aButtonsRows == 0 \\\n            or aButtonsCols == 0:\n                sc.aButtonsCols = aButtonsCols\n                sc.aButtonsRows = aButtonsRows\n                sc.aButtons = []\n            else:\n                aButtons = []\n                for r in range(0, aButtonsRows):\n                    row = []\n                    for c in range(0, aButtonsCols):\n                        if r < sc.aButtonsRows and c < sc.aButtonsCols:\n                            btn = sc.aButtons[r][c]\n                        else:\n                            btn = ActionButton()\n                        btn.row = r\n                        btn.col = c\n                        row.append(btn)\n                    aButtons.append(row)\n                sc.aButtonsCols = aButtonsCols\n                sc.aButtonsRows = aButtonsRows\n                sc.aButtons = aButtons\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Action Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/abutton_settings', methods=(\"GET\", \"POST\"))\n@login_required\ndef abutton_settings():\n    logger.debug(\"In abutton_settings\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsabuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        for r in range(0, sc.aButtonsRows):\n            for c in range(0, sc.aButtonsCols):\n                btn = sc.aButtons[r][c]\n                visibleId = f\"abtn_{btn.row}{ btn.col }_visible\"\n                btn.isVisible = not request.form.get(visibleId) is None\n                buttonTextKey = f\"abtn_{btn.row}{btn.col}_buttontext\"\n                btn.buttonText = request.form[buttonTextKey]\n                buttonAction = f\"abtn_{btn.row}{btn.col}_action\"\n                btn.buttonAction = request.form[buttonAction]\n                buttonShapeKey = f\"abtn_{btn.row}{btn.col}_shape\"\n                btn.buttonShape = request.form[buttonShapeKey]\n                buttonColorKey = f\"abtn_{btn.row}{btn.col}_color\"\n                btn.buttonColor = request.form[buttonColorKey]\n                confirmId = f\"abtn_{btn.row}{ btn.col }_confirm\"\n                btn.needsConfirm = not request.form.get(confirmId) is None\n                sc.aButtons[r][c] = btn\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Action Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/lbutton_dimensions', methods=(\"GET\", \"POST\"))\n@login_required\ndef lbutton_dimensions():\n    logger.debug(\"In vbutton_dimensions\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingslbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if request.form[\"lbuttonsrows\"]:\n            lButtonsRows = int(request.form[\"lbuttonsrows\"])\n        else:\n            msg = \"Please enter a valid number of rows\"\n        if request.form[\"lbuttonscols\"]:\n            lButtonsCols = int(request.form[\"lbuttonscols\"])\n        else:\n            msg = \"Please enter a valid number of columns\"\n        if msg == \"\":\n            if lButtonsRows == 0 \\\n            or lButtonsCols == 0:\n                sc.lButtonsCols = lButtonsCols\n                sc.lButtonsRows = lButtonsRows\n                sc.lButtons = []\n            else:\n                lButtons = []\n                for r in range(0, lButtonsRows):\n                    row = []\n                    for c in range(0, lButtonsCols):\n                        if r < sc.lButtonsRows and c < sc.lButtonsCols:\n                            btn = sc.lButtons[r][c]\n                        else:\n                            btn = LiveButton()\n                        btn.row = r\n                        btn.col = c\n                        row.append(btn)\n                    lButtons.append(row)\n                sc.lButtonsCols = lButtonsCols\n                sc.lButtonsRows = lButtonsRows\n                sc.lButtons = lButtons\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Live Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/lbutton_settings', methods=(\"GET\", \"POST\"))\n@login_required\ndef lbutton_settings():\n    logger.debug(\"In lbutton_settings\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingslbuttons\"\n    if request.method == \"POST\":\n        msg = \"\"\n        for r in range(0, sc.lButtonsRows):\n            for c in range(0, sc.lButtonsCols):\n                btn = sc.lButtons[r][c]\n                visibleId = f\"lbtn_{btn.row}{ btn.col }_visible\"\n                btn.isVisible = not request.form.get(visibleId) is None\n                buttonTextKey = f\"lbtn_{btn.row}{btn.col}_buttontext\"\n                btn.buttonText = request.form[buttonTextKey]\n                buttonExecKey = f\"lbtn_{btn.row}{btn.col}_buttonexec\"\n                btn.buttonExec = request.form[buttonExecKey]\n                buttonActionKey = f\"lbtn_{btn.row}{btn.col}_action\"\n                buttonAction = request.form[buttonActionKey]\n                if buttonAction != \"\" \\\n                and btn.buttonExec != \"\":\n                    buttonAction = \"\"\n                    msg = f\"Live Button {btn.row + 1}/{btn.col + 1}: Please enter either a command or an action, not both.\"\n                btn.buttonAction = buttonAction\n                buttonShapeKey = f\"lbtn_{btn.row}{btn.col}_shape\"\n                btn.buttonShape = request.form[buttonShapeKey]\n                buttonColorKey = f\"lbtn_{btn.row}{btn.col}_color\"\n                btn.buttonColor = request.form[buttonColorKey]\n                confirmId = f\"lbtn_{btn.row}{ btn.col }_confirm\"\n                btn.needsConfirm = not request.form.get(confirmId) is None\n                btn.isAction = not btn.buttonAction == \"\"\n                sc.lButtons[r][c] = btn\n        if msg != \"\":\n            flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Live Buttons changed\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/new_device', methods=(\"GET\", \"POST\"))\n@login_required\ndef new_device():\n    logger.debug(\"In new_device\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = \"\"\n        deviceId = request.form[\"newdeviceid\"]\n        deviceTypeId = request.form[\"newdevicetype\"]\n        if deviceId.strip() == \"\":\n            msg = \"Device ID cannot be empty\"\n        else:\n            for dev in sc.gpioDevices:\n                if dev.id == deviceId:\n                    msg = f\"Device IDs must be unique! A device with ID {deviceId} exists already.\"\n                    break\n        if msg == \"\":\n            device = GPIODevice()\n            device.id = deviceId\n            device.type = deviceTypeId\n            for dt in sc.deviceTypes:\n                if dt[\"type\"] == deviceTypeId:\n                    sc.curDeviceType = dt\n                    device.usage = dt[\"usage\"]\n                    device.docUrl = dt[\"docUrl\"]\n                    device.isOk = False\n                    params = {}\n                    for key, value in dt[\"params\"].items():\n                        params[key] = value[\"value\"]\n                    device.params = params\n                    if \"calibration\" in dt:\n                        device.needsCalibration = True\n            sc.gpioDevices.append(device)\n            sc.curDeviceId = deviceId\n            sc.curDevice = device\n        \n        if msg != \"\":\n            flash(msg)\n        if not deviceId is None:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Settings/Devices - new device added: {deviceId}\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/select_device', methods=(\"GET\", \"POST\"))\n@login_required\ndef select_device():\n    logger.debug(\"In select_device\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = \"\"\n        deviceId = request.form[\"selectdevice\"]\n        for device in sc.gpioDevices:\n            if device.id == deviceId:\n                sc.curDeviceId = deviceId\n                sc.curDevice = device\n                type = device.type\n                for dt in sc.deviceTypes:\n                    if dt[\"type\"] == type:\n                        sc.curDeviceType = dt\n                        break\n                break\n        if msg != \"\":\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef checkDeviceDeletion(deviceId: str, tc:TriggerConfig) -> str:\n    \"\"\" Check whether a device can be deleted\n    \n    The device must not be used in either triggers or actions.\n\n    Args:\n        deviceId (str): Device ID to be deleted\n\n    Returns:\n        str: Empty stringif device can be deleted\n             Or message where device occurs\n    \"\"\"\n    msg = \"\"\n    inTrg = []\n    for trigger in tc.triggers:\n        if trigger.device == deviceId:\n            inTrg.append(trigger.id)\n    \n    inAction = []\n    for action in tc.actions:\n        if action.device == deviceId:\n            inAction.append(action.id)\n\n    if len(inTrg) > 0 or len(inAction) > 0:\n        msg = f\"Device {deviceId} cannot be deleted because it is used in\"\n        if len(inTrg) > 0:\n            msg = msg + \" Triggers \" + str(inTrg)\n        if len(inAction) > 0:\n            msg = msg + \" Actions \" + str(inAction)\n    return msg        \n\n@bp.route('/delete_device', methods=(\"GET\", \"POST\"))\n@login_required\ndef delete_device():\n    logger.debug(\"In delete_device\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = checkDeviceDeletion(sc.curDeviceId, tc)\n        deviceDel = None\n        if msg == \"\":\n            deviceDel = sc.curDeviceId\n            idxDel = -1\n            idx = 0\n            for device in sc.gpioDevices:\n                if device.id == sc.curDeviceId:\n                    idxDel = idx\n                    break\n                idx += 1\n            if idxDel >= 0:\n                dev = sc.curDevice\n                if dev.needsCalibration == True:\n                    if dev._deviceStateFile != \"\":\n                        if os.path.exists(dev._deviceStateFile):\n                            os.remove(dev._deviceStateFile)\n                del sc.gpioDevices[idxDel]\n\n            if len(sc.gpioDevices) > 0:\n                sc.curDevice = sc.gpioDevices[0]\n                sc.curDeviceId = sc.curDevice.id\n                for deviceType in sc.deviceTypes:\n                    if deviceType[\"type\"] == sc.curDevice.type:\n                        sc.curDeviceType = deviceType\n            if not deviceDel is None:\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(f\"Settings/Devices - device deleted: {deviceDel}\")\n        if msg != \"\":\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef parseTuple(stuple: str) -> tuple[str, tuple]:\n    \"\"\" Parse a string which is assumed to be a tuple\n\n    Args:\n        stuple (str): string to be tuplelized\n\n    Returns:\n        tuple[str, tuple]: \n            - error\n            - tuplelized string    \n    \"\"\"\n    rest = stuple\n    err = \"\"\n    try:\n        tpl = ast.literal_eval(str(stuple))\n        if type(tpl) is tuple:\n            rest = tpl\n        else:\n            err = f\"{stuple} could not be cast to type of tuple!\"\n    except Exception as e:\n        err = f\"Error parsing {stuple} to tuple: {type(e):{e}}\"\n    return err, rest\n\ndef castType(val:str, tpl:object) ->tuple[str, object]:\n    \"\"\" Cast the given value to the type of the given template\n\n    Args:\n        val (str)   : Value to be casted\n        tpl (object): template\n\n    Returns:\n        tuple[str, object]: \n            - Error message\n            - type-converted value\n    \"\"\"\n    err = \"\"\n    res = val\n    if type(val) is str:\n        try:\n            if type(tpl) is str:\n                pass\n            elif type(tpl) is int:\n                res = int(val)\n            elif type(tpl) is float:\n                res = float(val)\n            elif type(tpl) is bool:\n                if val == \"0\":\n                    res = False\n                elif val == \"1\":\n                    res = True\n                elif val.casefold() == \"false\":\n                    res = False\n                elif val.casefold == \"true\":\n                    res = True\n                else:\n                    err = \"String does not represent boolean.\"            \n            elif type(tpl) is tuple:\n                l = len(tpl)\n                err, valt = parseTuple(val)\n                if err == \"\":\n                    ll = len(valt)\n                    if ll != l:\n                        err = f\"{val} should be a tuple of length {l}\"\n                    else:\n                        for n in range(0, l):\n                            if type(valt[n]) != type(tpl[n]):\n                                err = f\"{val} : elements of tuple do not have the expected type\"\n                                break\n                        if err == \"\":\n                            res = valt\n        except TypeError as e:\n            err = f\"Type error for {val}: {e}\"\n        except Exception as e:\n            err = f\"{type(e)} error for {val}: {e}\"\n    else:\n        err = f\"{val} should be a string rather than {type(val)}\"\n    return err, res\n\ndef parseColorTuple(stuple: str) -> tuple:\n    rest = (0, 0, 0)\n    err = \"\"\n    if stuple.startswith(\"(\"):\n        tpl = stuple[1:]\n        if tpl.endswith(\")\"):\n            tpl = tpl[0: len(tpl) - 1]\n            res = tpl.rsplit(\",\")\n            if len(res) == 3:\n                for n in range(0, 3):\n                    c = res[n]\n                    c = c.strip()\n                    cnum = c.replace('.','',1).replace(',','',1)\n                    if cnum.isdigit() == False:\n                        err = \"Tuple color values must be numeric.\"\n                if err == \"\":\n                    rest = (float(res[0]), float(res[1]), float(res[2]))\n            else:\n                err = \"Tuple for color must include 3 numeric color values.\"\n        else:\n            err=\"Tuple does not end with ')'.\"\n    else:\n        err=\"Tuple does not start with '('.\"\n    return err, rest\n\n\n@bp.route('/device_properties', methods=(\"GET\", \"POST\"))\n@login_required\ndef device_properties():\n    logger.debug(\"In device_properties\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = \"\"\n        newParams={}\n        usedPins = \"\"\n        ok = True\n        try:\n            for key, value in sc.curDeviceType[\"params\"].items():\n                paramId = f\"param_{key}\"\n                if value[\"type\"] == \"str\":\n                    val = request.form[paramId]\n                elif value[\"type\"] == \"int\":\n                    vals = request.form[paramId]\n                    if vals != \"\":\n                        val = int(vals)\n                    else:\n                        val = vals\n                elif value[\"type\"] == \"float\":\n                    val = float(request.form[paramId])\n                elif value[\"type\"] == \"floatOrNone\":\n                    vals = request.form[paramId]\n                    if vals == \"None\":\n                        val = None\n                    else:\n                        vals = vals.strip()\n                        if vals.replace('.','',1).replace(',','',1).isdigit() == True:\n                            vals = vals.replace(',', '.', 1)\n                            val = float(vals)\n                        else:\n                            msg = f\"{key} must be None or float\"\n                elif value[\"type\"] == \"bool\":\n                    val = not request.form.get(paramId) is None\n                elif value[\"type\"] == \"boolOrNone\":\n                    vals = request.form[paramId]\n                    if vals == \"None\":\n                        val = None\n                    else:\n                        if vals == \"True\":\n                            val = True\n                        elif vals == \"False\":\n                            val = False\n                        else:\n                            msg = f\"{key} must be bool or None\"\n                elif value[\"type\"] == \"tuple(float)\":\n                    vals = request.form[paramId]\n                    msg, val = parseColorTuple(vals)\n                elif value[\"type\"] == \"tuple(int)\":\n                    vals = request.form[paramId]\n                    msg, val = parseTuple(vals)\n                else:\n                    val = request.form[paramId]\n                newParams[key] = val\n                if \"isPin\" in value:\n                    if value[\"isPin\"] == True:\n                        if usedPins == \"\":\n                            usedPins = f\"{val}\"\n                        else:\n                            usedPins += f\", {val}\"\n\n                if val == \"\":\n                    ok = False\n                    \n        except Exception as e:\n            msg = f\"{type(e)}: {e}\"\n            \n        if msg == \"\":\n            sc.curDevice.params = newParams\n            sc.curDevice.usedPins = usedPins\n            sc.curDevice.isOk = ok\n            if sc.isEventhandling == True:\n                msg = \"Please restart Event Handling in Trigger/Control for changes to take effect.\"\n        if msg != \"\":\n            flash(msg)\n        if not sc.curDeviceId is None:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Settings/Devices - device properties changed for {sc.curDeviceId}\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\ndef storeResult(result:dict, test:str, testResult:str) -> dict:\n    \"\"\" Store a test result in the results dict\n    \n    Since dict keys must be unique, test, which is used as key must be made unique,\n    in order to avoid that duplicate tests are not registered.\n\n    Args:\n        result (dict)   : Results dict\n        test (str)      : Test to be registered\n        testResult (str): Test result\n\n    Returns:\n        dict: Results dict with the test result included\n    \"\"\"\n    testu = test\n    n = 1\n    while testu in result:\n        testu = test + \" - \" + str(n)\n        n+= 1\n    result[testu] = testResult\n    return result\n\n@bp.route('/test_device', methods=(\"GET\", \"POST\"))\n@login_required\ndef test_device():\n    logger.debug(\"In test_device\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if sc.isEventhandling == True:\n            msg = \"Device test is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n        if msg == \"\":\n            dev = sc.curDevice\n            devType = sc.curDeviceType\n            devClass = f\"{dev.type}\"\n            devArgs = dev.params\n            logger.debug(\"settings.test_device - devClass=%s\", devClass)\n            logger.debug(\"settings.test_device - devArgs=%s\", devArgs)\n            if \"testMethods\" in devType:\n                devTests = devType[\"testMethods\"]\n                logger.debug(\"settings.test_device - devTests=%s\", devTests)\n                try:\n                    logger.debug(\"settings.test_device -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                    dev.setState(devObj)\n                except Exception as e:\n                    logger.debug(\"settings.test_device - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                    try:\n                        if devObj:\n                            devObj.close()\n                    except Exception as e:\n                        logger.debug(\"settings.test_device - Error closing %s:%s\", devClass, e)\n                if msg == \"\":\n                    for test in devTests:\n                        testMethod = test\n                        rawTest = test\n                        assignValue = None\n                        if type(test) == dict:\n                            for key,val in test.items():\n                                testMethod = key\n                                assignValue = val\n                                break\n                        elif test.find(\"=\") >= 0:\n                            testmethod, assign = test.split(\"=\")\n                            if assign[0] == \"(\":\n                                err, assignValue = parseColorTuple(assign)\n                            else:\n                                assignValue = assign\n                            assignValue = castType()\n                            \n                        logger.debug(\"settings.test_device - Starting test %s\", test)\n                        if hasattr(devObj, testMethod):\n                            try:\n                                attr = getattr(devObj, testMethod)\n                                if callable(attr) == True:\n                                    if assignValue is None:\n                                        dispTest = f\"{devClass}.{testMethod}()\"\n                                        logger.debug(\"settings.test_device - %s\", dispTest)\n                                        res = attr()\n                                        result = storeResult(result, dispTest, res)\n                                    else:\n                                        dispTest = f\"{devClass}.{testMethod}({assignValue})\"\n                                        logger.debug(\"settings.test_device - %s\", dispTest)\n                                        res = attr(assignValue)\n                                        result = storeResult(result, dispTest, res)\n                                else:\n                                    if assignValue:\n                                        dispTest = f\"{devClass}.{testMethod}={assignValue}\"\n                                        logger.debug(\"settings.test_device - %s.%s=%s\",devClass, testMethod, assignValue)\n                                        setattr(devObj, testMethod, assignValue)\n                                        result = storeResult(result, dispTest, \"OK\")\n                                    else:\n                                        dispTest = f\"{devClass}.{testMethod}\"\n                                        result = storeResult(result, dispTest, attr)\n                                    logger.debug(\"settings.test_device - %s.%s=%s\",devClass, testMethod, result[dispTest])\n                                dev.trackState(devObj)\n                            except Exception as e:\n                                result = storeResult(result, testMethod, f\"{type(e)} : {e}\")\n                                logger.debug(\"settings.test_device - Exception %s, %s\", type(e), e)\n                        else:\n                            result = storeResult(result, testMethod, f\"Class {devClass} has no method {testMethod}\")\n                        if \"testStepDuration\" in devType:\n                            dur = devType[\"testStepDuration\"]\n                            time.sleep(dur)\n                    if \"testDuration\" in devType:\n                        dur = devType[\"testDuration\"]\n                        time.sleep(dur)\n                    try:\n                        if devObj:\n                            devObj.close()\n                            msg = f\"Test completed, {devClass} closed.\"\n                    except Exception as e:\n                        logger.debug(\"settings.test_device - Error closing %s:%s\", devClass, e)\n            else:\n                msg = f\"No test methods specified for device type {dev.type}\"\n        if msg != \"\":\n            flash(msg)\n    logger.debug(\"settings.test_device - result %s\", result)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/calibrate_device', methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_device():\n    logger.debug(\"In calibrate_device\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if sc.isEventhandling == True:\n            msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n        if msg == \"\":\n            dev = sc.curDevice\n            if dev.needsCalibration == True:\n                dev.isCalibrating = True\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.calibrate_device -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                    dev.setState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        setattr(devObj,\"value\", 0.0)\n                        dev.trackState(devObj)\n                        #result[\"value\"] = getattr(devObj, \"value\")\n                        result = dev.getUncalibratedState()\n                    dev.isCalibrating = True\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(f\"Settings/Devices - device calibration started: {sc.curDeviceId}\")\n                except Exception as e:\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n            else:\n                msg = f\"Device {dev.id} does not need calibration.\"\n        if msg != \"\":\n            flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/calibrate_fbwd', methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_fbwd():\n    logger.debug(\"In calibrate_fbwd reqest.method=%s\", request.method)\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n\n    msg = \"\"\n    if sc.isEventhandling == True:\n        msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n    if msg == \"\":\n        dev = sc.curDevice\n        dev.isCalibrating = True        \n        devType = sc.curDeviceType\n        if \"calibration\" in devType:\n            logger.debug(\"settings.calibrate_fbwd - calibrating\")\n            calibration = devType[\"calibration\"]\n            method = \"\"\n            params = \"\"\n            if \"fbwd\" in calibration:\n                adjust = calibration[\"fbwd\"]\n                logger.debug(\"settings.calibrate_fbwd - calibrating method=%s\", adjust)\n                if \"method\" in adjust:\n                    method = adjust[\"method\"]\n                if \"params\" in adjust:\n                    params = adjust[\"params\"]\n            if method != \"\":\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.calibrate_fbwd -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                except Exception as e:\n                    logger.debug(\"settings.calibrate_fbwd - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                if msg == \"\":\n                    dev.setState(devObj)\n                    if hasattr(devObj, method):\n                        try:\n                            attr = getattr(devObj, method)\n                            if callable(attr) == True:\n                                logger.debug(\"settings.calibrate_fbwd - calling %s.%s(**%s)\", devClass, method, params)\n                                res = attr(**params)\n                            else:\n                                msg = f\"{devClass}.{method} is not callable.\"\n                        except Exception as e:\n                            msg = f\"Error calling {devClass}.{method}: {type(e)} : {e}\"\n                    dev.trackState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        try:\n                            #result[\"value\"] = getattr(devObj, \"value\")\n                            result = dev.getUncalibratedState()\n                        except Exception as e:\n                            msg = f\"Property Error {devClass}.value: {type(e)} : {e}\"\n    if msg != \"\":\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/calibrate_bwd', methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_bwd():\n    logger.debug(\"In calibrate_bwd reqest.method=%s\", request.method)\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n\n    msg = \"\"\n    if sc.isEventhandling == True:\n        msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n    if msg == \"\":\n        dev = sc.curDevice\n        dev.isCalibrating = True        \n        devType = sc.curDeviceType\n        if \"calibration\" in devType:\n            logger.debug(\"settings.calibrate_bwd - calibrating\")\n            calibration = devType[\"calibration\"]\n            method = \"\"\n            params = \"\"\n            if \"bwd\" in calibration:\n                adjust = calibration[\"bwd\"]\n                logger.debug(\"settings.calibrate_bwd - calibrating method=%s\", adjust)\n                if \"method\" in adjust:\n                    method = adjust[\"method\"]\n                if \"params\" in adjust:\n                    params = adjust[\"params\"]\n            if method != \"\":\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.calibrate_bwd -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                except Exception as e:\n                    logger.debug(\"settings.calibrate_bwd - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                if msg == \"\":\n                    dev.setState(devObj)\n                    if hasattr(devObj, method):\n                        try:\n                            attr = getattr(devObj, method)\n                            if callable(attr) == True:\n                                logger.debug(\"settings.calibrate_bwd - calling %s.%s(**%s)\", devClass, method, params)\n                                res = attr(**params)\n                            else:\n                                msg = f\"{devClass}.{method} is not callable.\"\n                        except Exception as e:\n                            msg = f\"Error calling {devClass}.{method}: {type(e)} : {e}\"\n                    dev.trackState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        try:\n                            #result[\"value\"] = getattr(devObj, \"value\")\n                            result = dev.getUncalibratedState()\n                        except Exception as e:\n                            msg = f\"Property Error {devClass}.value: {type(e)} : {e}\"\n    if msg != \"\":\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/docalibrate', methods=(\"GET\", \"POST\"))\n@login_required\ndef docalibrate():\n    logger.debug(\"In docalibrate\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n\n    msg = \"\"\n    if sc.isEventhandling == True:\n        msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n    if msg == \"\":\n        dev = sc.curDevice\n        dev.isCalibrating = True        \n        devType = sc.curDeviceType\n        if \"calibration\" in devType:\n            logger.debug(\"settings.docalibrate - calibrating\")\n            calibration = devType[\"calibration\"]\n            method = \"\"\n            params = \"\"\n            if \"calibrate\" in calibration:\n                adjust = calibration[\"calibrate\"]\n                logger.debug(\"settings.docalibrate - calibrating method=%s\", adjust)\n                if \"method\" in adjust:\n                    # Calibration by calling a method or setting an attribute\n                    method = adjust[\"method\"]\n                    if \"params\" in adjust:\n                        params = adjust[\"params\"]\n                if \"param\" in adjust:\n                    # Calibration by setting a parameter to a specific value\n                    param = adjust[\"param\"]\n                    logger.debug(\"settings.docalibrate - Setting parameter %s to current value\", param)\n                    newParams={}\n                    for key, value in sc.curDevice.params.items():\n                        logger.debug(\"settings.docalibrate - key:%s value:%s\", key, value)\n                        val = value\n                        if key == param:\n                           state = sc.curDevice.getUncalibratedState()\n                           if \"value\" in state:\n                               val = state[\"value\"]\n                        logger.debug(\"settings.docalibrate - value:%s\", val)\n                        newParams[key] = val\n                        logger.debug(\"settings.docalibrate - newParams:%s\", newParams)\n                    sc.curDevice.params = newParams\n                    dev.isCalibrating = False\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(f\"Settings/Devices - device calibrated: {sc.curDeviceId}\")\n                    method = \"value\"\n                    params = {}\n                    params[\"value\"] = 0\n            if method != \"\":\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.docalibrate -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                except Exception as e:\n                    logger.debug(\"settings.docalibrate - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                if msg == \"\":\n                    dev.setState(devObj)\n                    if hasattr(devObj, method):\n                        try:\n                            attr = getattr(devObj, method)\n                            if callable(attr) == True:\n                                logger.debug(\"settings.docalibrate - calling %s.%s(**%s)\", devClass, method, params)\n                                res = attr(**params)\n                            else:\n                                if \"value\" in params:\n                                    value = params[\"value\"]\n                                    logger.debug(\"settings.docalibrate - calling %s.%s\", devClass, method)\n                                    setattr(devObj,\"value\", value)\n                                else:\n                                    msg = f\"'value' not not in {params}.\"\n                        except Exception as e:\n                            msg = f\"Error calling {devClass}.{method}: {type(e)} : {e}\"\n                    dev.trackState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        try:\n                            result[\"value\"] = getattr(devObj, \"value\")\n                        except Exception as e:\n                            msg = f\"Property Error {devClass}.value: {type(e)} : {e}\"\n                    dev.isCalibrating = False\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(f\"Settings/Devices - device calibrated: {sc.curDeviceId}\")\n    if msg != \"\":\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/calibrate_fwd', methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_fwd():\n    logger.debug(\"In calibrate_fwd reqest.method=%s\", request.method)\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n\n    msg = \"\"\n    if sc.isEventhandling == True:\n        msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n    if msg == \"\":\n        dev = sc.curDevice\n        dev.isCalibrating = True        \n        devType = sc.curDeviceType\n        if \"calibration\" in devType:\n            logger.debug(\"settings.calibrate_fwd - calibrating\")\n            calibration = devType[\"calibration\"]\n            method = \"\"\n            params = \"\"\n            if \"fwd\" in calibration:\n                adjust = calibration[\"fwd\"]\n                logger.debug(\"settings.calibrate_fwd - calibrating method=%s\", adjust)\n                if \"method\" in adjust:\n                    method = adjust[\"method\"]\n                if \"params\" in adjust:\n                    params = adjust[\"params\"]\n            if method != \"\":\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.calibrate_fwd -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                except Exception as e:\n                    logger.debug(\"settings.calibrate_fwd - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                if msg == \"\":\n                    dev.setState(devObj)\n                    if hasattr(devObj, method):\n                        try:\n                            attr = getattr(devObj, method)\n                            if callable(attr) == True:\n                                logger.debug(\"settings.calibrate_fwd - calling %s.%s(**%s)\", devClass, method, params)\n                                res = attr(**params)\n                            else:\n                                msg = f\"{devClass}.{method} is not callable.\"\n                        except Exception as e:\n                            msg = f\"Error calling {devClass}.{method}: {type(e)} : {e}\"\n                    dev.trackState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        try:\n                            #result[\"value\"] = getattr(devObj, \"value\")\n                            result = dev.getUncalibratedState()\n                        except Exception as e:\n                            msg = f\"Property Error {devClass}.value: {type(e)} : {e}\"\n    if msg != \"\":\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route('/calibrate_ffwd', methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_ffwd():\n    logger.debug(\"In calibrate_ffwd reqest.method=%s\", request.method)\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    sc.lastSettingsTab = \"settingsdevices\"\n\n    msg = \"\"\n    if sc.isEventhandling == True:\n        msg = \"Device calibration is not possible while Event Handling is active. Go to Trigger/Control and press Stop.\"\n    if msg == \"\":\n        dev = sc.curDevice\n        dev.isCalibrating = True        \n        devType = sc.curDeviceType\n        if \"calibration\" in devType:\n            logger.debug(\"settings.calibrate_ffwd - calibrating\")\n            calibration = devType[\"calibration\"]\n            method = \"\"\n            params = \"\"\n            if \"ffwd\" in calibration:\n                adjust = calibration[\"ffwd\"]\n                logger.debug(\"settings.calibrate_ffwd - calibrating method=%s\", adjust)\n                if \"method\" in adjust:\n                    method = adjust[\"method\"]\n                if \"params\" in adjust:\n                    params = adjust[\"params\"]\n            if method != \"\":\n                devClass = f\"{dev.type}\"\n                devArgs = dev.params\n                try:\n                    logger.debug(\"settings.calibrate_ffwd -instantiating %s(**%s)\", devClass, devArgs)\n                    devObj = globals()[devClass](**devArgs)\n                except Exception as e:\n                    logger.debug(\"settings.calibrate_ffwd - Error while instantiating %s:%s, %s\", devClass, type(e), e)\n                    msg = f\"Error while instantiating class {devClass}: {type(e)} {e}\"\n                if msg == \"\":\n                    dev.setState(devObj)\n                    if hasattr(devObj, method):\n                        try:\n                            attr = getattr(devObj, method)\n                            if callable(attr) == True:\n                                logger.debug(\"settings.calibrate_ffwd - calling %s.%s(**%s)\", devClass, method, params)\n                                res = attr(**params)\n                            else:\n                                msg = f\"{devClass}.{method} is not callable.\"\n                        except Exception as e:\n                            msg = f\"Error calling {devClass}.{method}: {type(e)} : {e}\"\n                    dev.trackState(devObj)\n                    if hasattr(devObj, \"value\"):\n                        try:\n                            #result[\"value\"] = getattr(devObj, \"value\")\n                            result = dev.getUncalibratedState()\n                        except Exception as e:\n                            msg = f\"Property Error {devClass}.value: {type(e)} : {e}\"\n    if msg != \"\":\n        flash(msg)\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/versionCheckEnabled\", methods=(\"GET\", \"POST\"))\n@login_required\ndef versionCheckEnabled():\n    logger.debug(\"versionCheckEnabled\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n\n    sc.lastSettingsTab = \"settingsupdate\"\n    if request.method == \"POST\":\n        msg = \"\"\n        versionCheckEnabled = not request.form.get(\"versioncheckenabledcb\") is None\n        if sc.versionCheckEnabled != versionCheckEnabled:\n            sc.versionCheckEnabled = versionCheckEnabled\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Settings/Update: Check for Updates changed to {sc.versionCheckEnabled}\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/serverUpdate\", methods=(\"GET\", \"POST\"))\n@login_required\ndef serverUpdate():\n    logger.debug(\"serverUpdate\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsupdate\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if sc.versionCurrent == sc.versionLatest:\n            msg = \"You are already using the latest version.\"\n        else:\n            try:\n                result = subprocess.run(\n                    [\"git\", \"fetch\", \"origin\", \"main\", \"--depth=1\"],\n                    capture_output=True, text=True\n                )\n                result = subprocess.run(\n                    [\"git\", \"reset\", \"--hard\", \"origin/main\"],\n                    capture_output=True, text=True\n                )\n                sc.updateDone = True\n                msg = \"raspiCamSrv updated successfully. Please restart the server to apply the update.\"\n            except CalledProcessError as e:\n                logger.error(\"serverUpdate - CalledProcessError: %s\", e)\n                msg = \"Error updating server: \" + str(e)\n            except Exception as e:\n                logger.error(\"serverUpdate - Exception: %s\", e)\n                msg = \"Error updating server: \" + str(e)\n        if msg != \"\":\n            flash(msg)\n\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/updateIgnoreLatest\", methods=(\"GET\", \"POST\"))\n@login_required\ndef updateIgnoreLatest():\n    logger.debug(\"updateIgnoreLatest\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsupdate\"\n    if request.method == \"POST\":\n        msg = \"\"\n        sc.versionCheckFrom = sc.versionLatest\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Update: Ignored latest version {sc.versionLatest}\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/versionCheckIntervalHours\", methods=(\"GET\", \"POST\"))\n@login_required\ndef versionCheckIntervalHours():\n    logger.debug(\"versionCheckIntervalHours\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsupdate\"\n    if request.method == \"POST\":\n        msg = \"\"\n        intvl = int(request.form[\"versioncheckintervalhours\"])\n        sc.versionCheckIntervalHours = intvl\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Update: Version Check Interval changed to {sc.versionCheckIntervalHours} hours\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n\n@bp.route(\"/versionCheckNow\", methods=(\"GET\", \"POST\"))\n@login_required\ndef versionCheckNow():\n    logger.debug(\"versionCheckNow\")\n    g.hostname = request.host\n    g.version = version\n    cam = Camera()\n    cfg = CameraCfg()\n    cs = cfg.cameras\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    # Check connection and access of microphone\n    sc.checkMicrophone()\n    cp = cfg.cameraProperties\n    sc.curMenu = \"settings\"\n    cfgPath = current_app.static_folder + \"/config\"\n    los = getLoadConfigOnStart(cfgPath)\n    result = {}\n    backups = getBackupsList()\n    sc.lastSettingsTab = \"settingsupdate\"\n    if request.method == \"POST\":\n        msg = \"\"\n        sc.getLatestVersion(now=True)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Settings/Update: Version Check Interval changed to {sc.versionCheckIntervalHours} hours\")\n    return render_template(\"settings/main.html\", sc=sc, tc=tc, cp=cp, cs=cs, los=los, result=result, backups=backups)\n"
  },
  {
    "path": "raspiCamSrv/static/w3.css",
    "content": "﻿/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */\nhtml {\n    box-sizing: border-box\n}\n\n*,\n*:before,\n*:after {\n    box-sizing: inherit\n}\n\n/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */\nhtml {\n    -ms-text-size-adjust: 100%;\n    -webkit-text-size-adjust: 100%\n}\n\nbody {\n    margin: 0\n}\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nmain,\nmenu,\nnav,\nsection {\n    display: block\n}\n\nsummary {\n    display: list-item\n}\n\naudio,\ncanvas,\nprogress,\nvideo {\n    display: inline-block\n}\n\nprogress {\n    vertical-align: baseline\n}\n\naudio:not([controls]) {\n    display: none;\n    height: 0\n}\n\n[hidden],\ntemplate {\n    display: none\n}\n\na {\n    background-color: transparent\n}\n\na:active,\na:hover {\n    outline-width: 0\n}\n\nabbr[title] {\n    border-bottom: none;\n    text-decoration: underline;\n    text-decoration: underline dotted\n}\n\nb,\nstrong {\n    font-weight: bolder\n}\n\ndfn {\n    font-style: italic\n}\n\nmark {\n    background: #ff0;\n    color: #000\n}\n\nsmall {\n    font-size: 80%\n}\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline\n}\n\nsub {\n    bottom: -0.25em\n}\n\nsup {\n    top: -0.5em\n}\n\nfigure {\n    margin: 1em 40px\n}\n\nimg {\n    border-style: none\n}\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: monospace, monospace;\n    font-size: 1em\n}\n\nhr {\n    box-sizing: content-box;\n    height: 0;\n    overflow: visible\n}\n\nbutton,\ninput,\nselect,\ntextarea,\noptgroup {\n    font: inherit;\n    margin: 0\n}\n\noptgroup {\n    font-weight: bold\n}\n\nbutton,\ninput {\n    overflow: visible\n}\n\nbutton,\nselect {\n    text-transform: none\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n    -webkit-appearance: button\n}\n\nbutton::-moz-focus-inner,\n[type=button]::-moz-focus-inner,\n[type=reset]::-moz-focus-inner,\n[type=submit]::-moz-focus-inner {\n    border-style: none;\n    padding: 0\n}\n\nbutton:-moz-focusring,\n[type=button]:-moz-focusring,\n[type=reset]:-moz-focusring,\n[type=submit]:-moz-focusring {\n    outline: 1px dotted ButtonText\n}\n\nfieldset {\n    border: 1px solid #c0c0c0;\n    margin: 0 2px;\n    padding: .35em .625em .75em\n}\n\nlegend {\n    color: inherit;\n    display: table;\n    max-width: 100%;\n    padding: 0;\n    white-space: normal\n}\n\ntextarea {\n    overflow: auto\n}\n\n[type=checkbox],\n[type=radio] {\n    padding: 0\n}\n\n[type=number]::-webkit-inner-spin-button,\n[type=number]::-webkit-outer-spin-button {\n    height: auto\n}\n\n[type=search] {\n    -webkit-appearance: textfield;\n    outline-offset: -2px\n}\n\n[type=search]::-webkit-search-decoration {\n    -webkit-appearance: none\n}\n\n::-webkit-file-upload-button {\n    -webkit-appearance: button;\n    font: inherit\n}\n\n/* End extract */\nhtml,\nbody {\n    font-family: Verdana, sans-serif;\n    font-size: 15px;\n    line-height: 1.5\n}\n\nhtml {\n    overflow-x: hidden\n}\n\nh1 {\n    font-size: 36px\n}\n\nh2 {\n    font-size: 30px\n}\n\nh3 {\n    font-size: 24px\n}\n\nh4 {\n    font-size: 20px\n}\n\nh5 {\n    font-size: 18px\n}\n\nh6 {\n    font-size: 16px\n}\n\n.w3-serif {\n    font-family: serif\n}\n\n.w3-sans-serif {\n    font-family: sans-serif\n}\n\n.w3-cursive {\n    font-family: cursive\n}\n\n.w3-monospace {\n    font-family: monospace\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    font-family: \"Segoe UI\", Arial, sans-serif;\n    font-weight: 400;\n    margin: 10px 0\n}\n\n.w3-wide {\n    letter-spacing: 4px\n}\n\nhr {\n    border: 0;\n    border-top: 1px solid #eee;\n    margin: 20px 0\n}\n\n.w3-image {\n    max-width: 100%;\n    height: auto\n}\n\nimg {\n    vertical-align: middle\n}\n\na {\n    color: inherit\n}\n\n.w3-table,\n.w3-table-all {\n    border-collapse: collapse;\n    border-spacing: 0;\n    width: 100%;\n    display: table\n}\n\n.w3-table-all {\n    border: 1px solid #ccc\n}\n\n.w3-bordered tr,\n.w3-table-all tr {\n    border-bottom: 1px solid #ddd\n}\n\n.w3-striped tbody tr:nth-child(even) {\n    background-color: #f1f1f1\n}\n\n.w3-table-all tr:nth-child(odd) {\n    background-color: #fff\n}\n\n.w3-table-all tr:nth-child(even) {\n    background-color: #f1f1f1\n}\n\n.w3-hoverable tbody tr:hover,\n.w3-ul.w3-hoverable li:hover {\n    background-color: #ccc\n}\n\n.w3-centered tr th,\n.w3-centered tr td {\n    text-align: center\n}\n\n.w3-table td,\n.w3-table th,\n.w3-table-all td,\n.w3-table-all th {\n    padding: 8px 8px;\n    display: table-cell;\n    text-align: left;\n    vertical-align: top\n}\n\n.w3-table th:first-child,\n.w3-table td:first-child,\n.w3-table-all th:first-child,\n.w3-table-all td:first-child {\n    padding-left: 16px\n}\n\n.w3-btn,\n.w3-button {\n    border: none;\n    display: inline-block;\n    padding: 8px 16px;\n    vertical-align: middle;\n    overflow: hidden;\n    text-decoration: none;\n    color: inherit;\n    background-color: inherit;\n    text-align: center;\n    cursor: pointer;\n    white-space: nowrap\n}\n\n.w3-btn:hover {\n    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)\n}\n\n.w3-btn,\n.w3-button {\n    -webkit-touch-callout: none;\n    -webkit-user-select: none;\n    -khtml-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none\n}\n\n.w3-disabled,\n.w3-btn:disabled,\n.w3-button:disabled {\n    cursor: not-allowed;\n    opacity: 0.3\n}\n\n.w3-disabled *,\n:disabled * {\n    pointer-events: none\n}\n\n.w3-btn.w3-disabled:hover,\n.w3-btn:disabled:hover {\n    box-shadow: none\n}\n\n.w3-badge,\n.w3-tag {\n    background-color: #000;\n    color: #fff;\n    display: inline-block;\n    padding-left: 8px;\n    padding-right: 8px;\n    text-align: center\n}\n\n.w3-badge {\n    border-radius: 50%\n}\n\n.w3-ul {\n    list-style-type: none;\n    padding: 0;\n    margin: 0\n}\n\n.w3-ul li {\n    padding: 8px 16px;\n    border-bottom: 1px solid #ddd\n}\n\n.w3-ul li:last-child {\n    border-bottom: none\n}\n\n.w3-tooltip,\n.w3-display-container {\n    position: relative\n}\n\n.w3-tooltip .w3-text {\n    display: none\n}\n\n.w3-tooltip:hover .w3-text {\n    display: inline-block\n}\n\n.w3-ripple:active {\n    opacity: 0.5\n}\n\n.w3-ripple {\n    transition: opacity 0s\n}\n\n.w3-input {\n    padding: 8px;\n    display: block;\n    border: none;\n    border-bottom: 1px solid #ccc;\n    width: 100%\n}\n\n.w3-select {\n    padding: 9px 0;\n    width: 100%;\n    border: none;\n    border-bottom: 1px solid #ccc\n}\n\n.w3-dropdown-click,\n.w3-dropdown-hover {\n    position: relative;\n    display: inline-block;\n    cursor: pointer\n}\n\n.w3-dropdown-hover:hover .w3-dropdown-content {\n    display: block\n}\n\n.w3-dropdown-hover:first-child,\n.w3-dropdown-click:hover {\n    background-color: #ccc;\n    color: #000\n}\n\n.w3-dropdown-hover:hover>.w3-button:first-child,\n.w3-dropdown-click:hover>.w3-button:first-child {\n    background-color: #ccc;\n    color: #000\n}\n\n.w3-dropdown-content {\n    cursor: auto;\n    color: #000;\n    background-color: #fff;\n    display: none;\n    position: absolute;\n    min-width: 160px;\n    margin: 0;\n    padding: 0;\n    z-index: 1\n}\n\n.w3-check,\n.w3-radio {\n    width: 24px;\n    height: 24px;\n    position: relative;\n    top: 6px\n}\n\n.w3-sidebar {\n    height: 100%;\n    width: 200px;\n    background-color: #fff;\n    position: fixed !important;\n    z-index: 1;\n    overflow: auto\n}\n\n.w3-bar-block .w3-dropdown-hover,\n.w3-bar-block .w3-dropdown-click {\n    width: 100%\n}\n\n.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,\n.w3-bar-block .w3-dropdown-click .w3-dropdown-content {\n    min-width: 100%\n}\n\n.w3-bar-block .w3-dropdown-hover .w3-button,\n.w3-bar-block .w3-dropdown-click .w3-button {\n    width: 100%;\n    text-align: left;\n    padding: 8px 16px\n}\n\n.w3-main,\n#main {\n    transition: margin-left .4s\n}\n\n.w3-modal {\n    z-index: 3;\n    display: none;\n    padding-top: 100px;\n    position: fixed;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    overflow: auto;\n    background-color: rgb(0, 0, 0);\n    background-color: rgba(0, 0, 0, 0.4)\n}\n\n.w3-modal-content {\n    margin: auto;\n    background-color: #fff;\n    position: relative;\n    padding: 0;\n    outline: 0;\n    width: 600px\n}\n\n.w3-bar {\n    width: 100%;\n    overflow: hidden\n}\n\n.w3-center .w3-bar {\n    display: inline-block;\n    width: auto\n}\n\n.w3-bar .w3-bar-item {\n    padding: 8px 16px;\n    float: left;\n    width: auto;\n    border: none;\n    display: block;\n    outline: 0\n}\n\n.w3-bar .w3-dropdown-hover,\n.w3-bar .w3-dropdown-click {\n    position: static;\n    float: left\n}\n\n.w3-bar .w3-button {\n    white-space: normal\n}\n\n.w3-bar-block .w3-bar-item {\n    width: 100%;\n    display: block;\n    padding: 8px 16px;\n    text-align: left;\n    border: none;\n    white-space: normal;\n    float: none;\n    outline: 0\n}\n\n.w3-bar-block.w3-center .w3-bar-item {\n    text-align: center\n}\n\n.w3-block {\n    display: block;\n    width: 100%\n}\n\n.w3-responsive {\n    display: block;\n    overflow-x: auto\n}\n\n.w3-container:after,\n.w3-container:before,\n.w3-panel:after,\n.w3-panel:before,\n.w3-row:after,\n.w3-row:before,\n.w3-row-padding:after,\n.w3-row-padding:before,\n.w3-cell-row:before,\n.w3-cell-row:after,\n.w3-clear:after,\n.w3-clear:before,\n.w3-bar:before,\n.w3-bar:after {\n    content: \"\";\n    display: table;\n    clear: both\n}\n\n.w3-col,\n.w3-half,\n.w3-third,\n.w3-twothird,\n.w3-threequarter,\n.w3-quarter {\n    float: left;\n    width: 100%\n}\n\n.w3-col.s1 {\n    width: 8.33333%\n}\n\n.w3-col.s2 {\n    width: 16.66666%\n}\n\n.w3-col.s3 {\n    width: 24.99999%\n}\n\n.w3-col.s4 {\n    width: 33.33333%\n}\n\n.w3-col.s5 {\n    width: 41.66666%\n}\n\n.w3-col.s6 {\n    width: 49.99999%\n}\n\n.w3-col.s7 {\n    width: 58.33333%\n}\n\n.w3-col.s8 {\n    width: 66.66666%\n}\n\n.w3-col.s9 {\n    width: 74.99999%\n}\n\n.w3-col.s10 {\n    width: 83.33333%\n}\n\n.w3-col.s11 {\n    width: 91.66666%\n}\n\n.w3-col.s12 {\n    width: 99.99999%\n}\n\n@media (min-width:601px) {\n    .w3-col.m1 {\n        width: 8.33333%\n    }\n\n    .w3-col.m2 {\n        width: 16.66666%\n    }\n\n    .w3-col.m3,\n    .w3-quarter {\n        width: 24.99999%\n    }\n\n    .w3-col.m4,\n    .w3-third {\n        width: 33.33333%\n    }\n\n    .w3-col.m5 {\n        width: 41.66666%\n    }\n\n    .w3-col.m6,\n    .w3-half {\n        width: 49.99999%\n    }\n\n    .w3-col.m7 {\n        width: 58.33333%\n    }\n\n    .w3-col.m8,\n    .w3-twothird {\n        width: 66.66666%\n    }\n\n    .w3-col.m9,\n    .w3-threequarter {\n        width: 74.99999%\n    }\n\n    .w3-col.m10 {\n        width: 83.33333%\n    }\n\n    .w3-col.m11 {\n        width: 91.66666%\n    }\n\n    .w3-col.m12 {\n        width: 99.99999%\n    }\n}\n\n@media (min-width:993px) {\n    .w3-col.l1 {\n        width: 8.33333%\n    }\n\n    .w3-col.l2 {\n        width: 16.66666%\n    }\n\n    .w3-col.l3 {\n        width: 24.99999%\n    }\n\n    .w3-col.l4 {\n        width: 33.33333%\n    }\n\n    .w3-col.l5 {\n        width: 41.66666%\n    }\n\n    .w3-col.l6 {\n        width: 49.99999%\n    }\n\n    .w3-col.l7 {\n        width: 58.33333%\n    }\n\n    .w3-col.l8 {\n        width: 66.66666%\n    }\n\n    .w3-col.l9 {\n        width: 74.99999%\n    }\n\n    .w3-col.l10 {\n        width: 83.33333%\n    }\n\n    .w3-col.l11 {\n        width: 91.66666%\n    }\n\n    .w3-col.l12 {\n        width: 99.99999%\n    }\n}\n\n.w3-rest {\n    overflow: hidden\n}\n\n.w3-stretch {\n    margin-left: -16px;\n    margin-right: -16px\n}\n\n.w3-content,\n.w3-auto {\n    margin-left: auto;\n    margin-right: auto\n}\n\n.w3-content {\n    max-width: 980px\n}\n\n.w3-auto {\n    max-width: 1140px\n}\n\n.w3-cell-row {\n    display: table;\n    width: 100%\n}\n\n.w3-cell {\n    display: table-cell\n}\n\n.w3-cell-top {\n    vertical-align: top\n}\n\n.w3-cell-middle {\n    vertical-align: middle\n}\n\n.w3-cell-bottom {\n    vertical-align: bottom\n}\n\n.w3-hide {\n    display: none !important\n}\n\n.w3-show-block,\n.w3-show {\n    display: block !important\n}\n\n.w3-show-inline-block {\n    display: inline-block !important\n}\n\n@media (max-width:1205px) {\n    .w3-auto {\n        max-width: 95%\n    }\n}\n\n@media (max-width:600px) {\n    .w3-modal-content {\n        margin: 0 10px;\n        width: auto !important\n    }\n\n    .w3-modal {\n        padding-top: 30px\n    }\n\n    .w3-dropdown-hover.w3-mobile .w3-dropdown-content,\n    .w3-dropdown-click.w3-mobile .w3-dropdown-content {\n        position: relative\n    }\n\n    .w3-hide-small {\n        display: none !important\n    }\n\n    .w3-mobile {\n        display: block;\n        width: 100% !important\n    }\n\n    .w3-bar-item.w3-mobile,\n    .w3-dropdown-hover.w3-mobile,\n    .w3-dropdown-click.w3-mobile {\n        text-align: center\n    }\n\n    .w3-dropdown-hover.w3-mobile,\n    .w3-dropdown-hover.w3-mobile .w3-btn,\n    .w3-dropdown-hover.w3-mobile .w3-button,\n    .w3-dropdown-click.w3-mobile,\n    .w3-dropdown-click.w3-mobile .w3-btn,\n    .w3-dropdown-click.w3-mobile .w3-button {\n        width: 100%\n    }\n}\n\n@media (max-width:768px) {\n    .w3-modal-content {\n        width: 500px\n    }\n\n    .w3-modal {\n        padding-top: 50px\n    }\n}\n\n@media (min-width:993px) {\n    .w3-modal-content {\n        width: 900px\n    }\n\n    .w3-hide-large {\n        display: none !important\n    }\n\n    .w3-sidebar.w3-collapse {\n        display: block !important\n    }\n}\n\n@media (max-width:992px) and (min-width:601px) {\n    .w3-hide-medium {\n        display: none !important\n    }\n}\n\n@media (max-width:992px) {\n    .w3-sidebar.w3-collapse {\n        display: none\n    }\n\n    .w3-main {\n        margin-left: 0 !important;\n        margin-right: 0 !important\n    }\n\n    .w3-auto {\n        max-width: 100%\n    }\n}\n\n.w3-top,\n.w3-bottom {\n    position: fixed;\n    width: 100%;\n    z-index: 1\n}\n\n.w3-top {\n    top: 0\n}\n\n.w3-bottom {\n    bottom: 0\n}\n\n.w3-overlay {\n    position: fixed;\n    display: none;\n    width: 100%;\n    height: 100%;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: rgba(0, 0, 0, 0.5);\n    z-index: 2\n}\n\n.w3-display-topleft {\n    position: absolute;\n    left: 0;\n    top: 0\n}\n\n.w3-display-topright {\n    position: absolute;\n    right: 0;\n    top: 0\n}\n\n.w3-display-bottomleft {\n    position: absolute;\n    left: 0;\n    bottom: 0\n}\n\n.w3-display-bottomright {\n    position: absolute;\n    right: 0;\n    bottom: 0\n}\n\n.w3-display-middle {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    -ms-transform: translate(-50%, -50%)\n}\n\n.w3-display-left {\n    position: absolute;\n    top: 50%;\n    left: 0%;\n    transform: translate(0%, -50%);\n    -ms-transform: translate(-0%, -50%)\n}\n\n.w3-display-right {\n    position: absolute;\n    top: 50%;\n    right: 0%;\n    transform: translate(0%, -50%);\n    -ms-transform: translate(0%, -50%)\n}\n\n.w3-display-topmiddle {\n    position: absolute;\n    left: 50%;\n    top: 0;\n    transform: translate(-50%, 0%);\n    -ms-transform: translate(-50%, 0%)\n}\n\n.w3-display-bottommiddle {\n    position: absolute;\n    left: 50%;\n    bottom: 0;\n    transform: translate(-50%, 0%);\n    -ms-transform: translate(-50%, 0%)\n}\n\n.w3-display-container:hover .w3-display-hover {\n    display: block\n}\n\n.w3-display-container:hover span.w3-display-hover {\n    display: inline-block\n}\n\n.w3-display-hover {\n    display: none\n}\n\n.w3-display-position {\n    position: absolute\n}\n\n.w3-circle {\n    border-radius: 50%\n}\n\n.w3-round-small {\n    border-radius: 2px\n}\n\n.w3-round,\n.w3-round-medium {\n    border-radius: 4px\n}\n\n.w3-round-large {\n    border-radius: 8px\n}\n\n.w3-round-xlarge {\n    border-radius: 16px\n}\n\n.w3-round-xxlarge {\n    border-radius: 32px\n}\n\n.w3-row-padding,\n.w3-row-padding>.w3-half,\n.w3-row-padding>.w3-third,\n.w3-row-padding>.w3-twothird,\n.w3-row-padding>.w3-threequarter,\n.w3-row-padding>.w3-quarter,\n.w3-row-padding>.w3-col {\n    padding: 0 8px\n}\n\n.w3-container,\n.w3-panel {\n    padding: 0.01em 16px\n}\n\n.w3-panel {\n    margin-top: 16px;\n    margin-bottom: 16px\n}\n\n.w3-grid {\n    display: grid\n}\n\n.w3-grid-padding {\n    display: grid;\n    gap: 16px\n}\n\n.w3-flex {\n    display: flex\n}\n\n.w3-text-center {\n    text-align: center\n}\n\n.w3-text-bold,\n.w3-bold {\n    font-weight: bold\n}\n\n.w3-text-italic,\n.w3-italic {\n    font-style: italic\n}\n\n.w3-code,\n.w3-codespan {\n    font-family: Consolas, \"courier new\";\n    font-size: 16px\n}\n\n.w3-code {\n    width: auto;\n    background-color: #fff;\n    padding: 8px 12px;\n    border-left: 4px solid #4CAF50;\n    word-wrap: break-word\n}\n\n.w3-codespan {\n    color: crimson;\n    background-color: #f1f1f1;\n    padding-left: 4px;\n    padding-right: 4px;\n    font-size: 110%\n}\n\n.w3-card,\n.w3-card-2 {\n    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12)\n}\n\n.w3-card-4,\n.w3-hover-shadow:hover {\n    box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.19)\n}\n\n.w3-spin {\n    animation: w3-spin 2s infinite linear\n}\n\n@keyframes w3-spin {\n    0% {\n        transform: rotate(0deg)\n    }\n\n    100% {\n        transform: rotate(359deg)\n    }\n}\n\n.w3-animate-fading {\n    animation: fading 10s infinite\n}\n\n@keyframes fading {\n    0% {\n        opacity: 0\n    }\n\n    50% {\n        opacity: 1\n    }\n\n    100% {\n        opacity: 0\n    }\n}\n\n.w3-animate-opacity {\n    animation: opac 0.8s\n}\n\n@keyframes opac {\n    from {\n        opacity: 0\n    }\n\n    to {\n        opacity: 1\n    }\n}\n\n.w3-animate-top {\n    position: relative;\n    animation: animatetop 0.4s\n}\n\n@keyframes animatetop {\n    from {\n        top: -300px;\n        opacity: 0\n    }\n\n    to {\n        top: 0;\n        opacity: 1\n    }\n}\n\n.w3-animate-left {\n    position: relative;\n    animation: animateleft 0.4s\n}\n\n@keyframes animateleft {\n    from {\n        left: -300px;\n        opacity: 0\n    }\n\n    to {\n        left: 0;\n        opacity: 1\n    }\n}\n\n.w3-animate-right {\n    position: relative;\n    animation: animateright 0.4s\n}\n\n@keyframes animateright {\n    from {\n        right: -300px;\n        opacity: 0\n    }\n\n    to {\n        right: 0;\n        opacity: 1\n    }\n}\n\n.w3-animate-bottom {\n    position: relative;\n    animation: animatebottom 0.4s\n}\n\n@keyframes animatebottom {\n    from {\n        bottom: -300px;\n        opacity: 0\n    }\n\n    to {\n        bottom: 0;\n        opacity: 1\n    }\n}\n\n.w3-animate-zoom {\n    animation: animatezoom 0.6s\n}\n\n@keyframes animatezoom {\n    from {\n        transform: scale(0)\n    }\n\n    to {\n        transform: scale(1)\n    }\n}\n\n.w3-animate-input {\n    transition: width 0.4s ease-in-out\n}\n\n.w3-animate-input:focus {\n    width: 100% !important\n}\n\n.w3-opacity,\n.w3-hover-opacity:hover {\n    opacity: 0.60\n}\n\n.w3-opacity-off,\n.w3-hover-opacity-off:hover {\n    opacity: 1\n}\n\n.w3-opacity-max {\n    opacity: 0.25\n}\n\n.w3-opacity-min {\n    opacity: 0.75\n}\n\n.w3-greyscale-max,\n.w3-grayscale-max,\n.w3-hover-greyscale:hover,\n.w3-hover-grayscale:hover {\n    filter: grayscale(100%)\n}\n\n.w3-greyscale,\n.w3-grayscale {\n    filter: grayscale(75%)\n}\n\n.w3-greyscale-min,\n.w3-grayscale-min {\n    filter: grayscale(50%)\n}\n\n.w3-sepia {\n    filter: sepia(75%)\n}\n\n.w3-sepia-max,\n.w3-hover-sepia:hover {\n    filter: sepia(100%)\n}\n\n.w3-sepia-min {\n    filter: sepia(50%)\n}\n\n.w3-tiny {\n    font-size: 10px !important\n}\n\n.w3-small {\n    font-size: 12px !important\n}\n\n.w3-medium {\n    font-size: 15px !important\n}\n\n.w3-large {\n    font-size: 18px !important\n}\n\n.w3-xlarge {\n    font-size: 24px !important\n}\n\n.w3-xxlarge {\n    font-size: 36px !important\n}\n\n.w3-xxxlarge {\n    font-size: 48px !important\n}\n\n.w3-jumbo {\n    font-size: 64px !important\n}\n\n.w3-left-align {\n    text-align: left !important\n}\n\n.w3-right-align {\n    text-align: right !important\n}\n\n.w3-justify {\n    text-align: justify !important\n}\n\n.w3-center {\n    text-align: center !important\n}\n\n.w3-border-0 {\n    border: 0 !important\n}\n\n.w3-border {\n    border: 1px solid #ccc !important\n}\n\n.w3-border-top {\n    border-top: 1px solid #ccc !important\n}\n\n.w3-border-bottom {\n    border-bottom: 1px solid #ccc !important\n}\n\n.w3-border-left {\n    border-left: 1px solid #ccc !important\n}\n\n.w3-border-right {\n    border-right: 1px solid #ccc !important\n}\n\n.w3-topbar {\n    border-top: 6px solid #ccc !important\n}\n\n.w3-bottombar {\n    border-bottom: 6px solid #ccc !important\n}\n\n.w3-leftbar {\n    border-left: 6px solid #ccc !important\n}\n\n.w3-rightbar {\n    border-right: 6px solid #ccc !important\n}\n\n.w3-section,\n.w3-code {\n    margin-top: 16px !important;\n    margin-bottom: 16px !important\n}\n\n.w3-margin {\n    margin: 16px !important\n}\n\n.w3-margin-top {\n    margin-top: 16px !important\n}\n\n.w3-margin-bottom {\n    margin-bottom: 16px !important\n}\n\n.w3-margin-left {\n    margin-left: 16px !important\n}\n\n.w3-margin-right {\n    margin-right: 16px !important\n}\n\n.w3-padding-small {\n    padding: 4px 8px !important\n}\n\n.w3-padding {\n    padding: 8px 16px !important\n}\n\n.w3-padding-large {\n    padding: 12px 24px !important\n}\n\n.w3-padding-16 {\n    padding-top: 16px !important;\n    padding-bottom: 16px !important\n}\n\n.w3-padding-24 {\n    padding-top: 24px !important;\n    padding-bottom: 24px !important\n}\n\n.w3-padding-32 {\n    padding-top: 32px !important;\n    padding-bottom: 32px !important\n}\n\n.w3-padding-48 {\n    padding-top: 48px !important;\n    padding-bottom: 48px !important\n}\n\n.w3-padding-64 {\n    padding-top: 64px !important;\n    padding-bottom: 64px !important\n}\n\n.w3-padding-top-64 {\n    padding-top: 64px !important\n}\n\n.w3-padding-top-48 {\n    padding-top: 48px !important\n}\n\n.w3-padding-top-32 {\n    padding-top: 32px !important\n}\n\n.w3-padding-top-24 {\n    padding-top: 24px !important\n}\n\n.w3-left {\n    float: left !important\n}\n\n.w3-right {\n    float: right !important\n}\n\n.w3-button:hover {\n    color: #000 !important;\n    background-color: #ccc !important\n}\n\n.w3-transparent,\n.w3-hover-none:hover {\n    background-color: transparent !important\n}\n\n.w3-hover-none:hover {\n    box-shadow: none !important\n}\n\n.w3-rtl {\n    direction: rtl\n}\n\n.w3-ltr {\n    direction: ltr\n}\n\n/* Colors */\n.w3-amber,\n.w3-hover-amber:hover {\n    color: #000 !important;\n    background-color: #ffc107 !important\n}\n\n.w3-aqua,\n.w3-hover-aqua:hover {\n    color: #000 !important;\n    background-color: #00ffff !important\n}\n\n.w3-blue,\n.w3-hover-blue:hover {\n    color: #fff !important;\n    background-color: #2196F3 !important\n}\n\n.w3-light-blue,\n.w3-hover-light-blue:hover {\n    color: #000 !important;\n    background-color: #87CEEB !important\n}\n\n.w3-brown,\n.w3-hover-brown:hover {\n    color: #fff !important;\n    background-color: #795548 !important\n}\n\n.w3-cyan,\n.w3-hover-cyan:hover {\n    color: #000 !important;\n    background-color: #00bcd4 !important\n}\n\n.w3-blue-grey,\n.w3-hover-blue-grey:hover,\n.w3-blue-gray,\n.w3-hover-blue-gray:hover {\n    color: #fff !important;\n    background-color: #607d8b !important\n}\n\n.w3-green,\n.w3-hover-green:hover {\n    color: #fff !important;\n    background-color: #4CAF50 !important\n}\n\n.w3-light-green,\n.w3-hover-light-green:hover {\n    color: #000 !important;\n    background-color: #8bc34a !important\n}\n\n.w3-indigo,\n.w3-hover-indigo:hover {\n    color: #fff !important;\n    background-color: #3f51b5 !important\n}\n\n.w3-khaki,\n.w3-hover-khaki:hover {\n    color: #000 !important;\n    background-color: #f0e68c !important\n}\n\n.w3-lime,\n.w3-hover-lime:hover {\n    color: #000 !important;\n    background-color: #cddc39 !important\n}\n\n.w3-orange,\n.w3-hover-orange:hover {\n    color: #000 !important;\n    background-color: #ff9800 !important\n}\n\n.w3-deep-orange,\n.w3-hover-deep-orange:hover {\n    color: #fff !important;\n    background-color: #ff5722 !important\n}\n\n.w3-pink,\n.w3-hover-pink:hover {\n    color: #fff !important;\n    background-color: #e91e63 !important\n}\n\n.w3-purple,\n.w3-hover-purple:hover {\n    color: #fff !important;\n    background-color: #9c27b0 !important\n}\n\n.w3-deep-purple,\n.w3-hover-deep-purple:hover {\n    color: #fff !important;\n    background-color: #673ab7 !important\n}\n\n.w3-red,\n.w3-hover-red:hover {\n    color: #fff !important;\n    background-color: #f44336 !important\n}\n\n.w3-sand,\n.w3-hover-sand:hover {\n    color: #000 !important;\n    background-color: #fdf5e6 !important\n}\n\n.w3-teal,\n.w3-hover-teal:hover {\n    color: #fff !important;\n    background-color: #009688 !important\n}\n\n.w3-yellow,\n.w3-hover-yellow:hover {\n    color: #000 !important;\n    background-color: #ffeb3b !important\n}\n\n.w3-white,\n.w3-hover-white:hover {\n    color: #000 !important;\n    background-color: #fff !important\n}\n\n.w3-black,\n.w3-hover-black:hover {\n    color: #fff !important;\n    background-color: #000 !important\n}\n\n.w3-grey,\n.w3-hover-grey:hover,\n.w3-gray,\n.w3-hover-gray:hover {\n    color: #000 !important;\n    background-color: #9e9e9e !important\n}\n\n.w3-light-grey,\n.w3-hover-light-grey:hover,\n.w3-light-gray,\n.w3-hover-light-gray:hover {\n    color: #000 !important;\n    background-color: #f1f1f1 !important\n}\n\n.w3-dark-grey,\n.w3-hover-dark-grey:hover,\n.w3-dark-gray,\n.w3-hover-dark-gray:hover {\n    color: #fff !important;\n    background-color: #616161 !important\n}\n\n.w3-asphalt,\n.w3-hover-asphalt:hover {\n    color: #fff !important;\n    background-color: #343a40 !important\n}\n\n.w3-crimson,\n.w3-hover-crimson:hover {\n    color: #fff !important;\n    background-color: #a20025 !important\n}\n\n.w3-cobalt,\nw3-hover-cobalt:hover {\n    color: #fff !important;\n    background-color: #0050ef !important\n}\n\n.w3-emerald,\n.w3-hover-emerald:hover {\n    color: #fff !important;\n    background-color: #008a00 !important\n}\n\n.w3-olive,\n.w3-hover-olive:hover {\n    color: #fff !important;\n    background-color: #6d8764 !important\n}\n\n.w3-paper,\n.w3-hover-paper:hover {\n    color: #000 !important;\n    background-color: #f8f9fa !important\n}\n\n.w3-sienna,\n.w3-hover-sienna:hover {\n    color: #fff !important;\n    background-color: #a0522d !important\n}\n\n.w3-taupe,\n.w3-hover-taupe:hover {\n    color: #fff !important;\n    background-color: #87794e !important\n}\n\n.w3-danger {\n    color: #fff !important;\n    background-color: #dd0000 !important\n}\n\n.w3-note {\n    color: #000 !important;\n    background-color: #fff599 !important\n}\n\n.w3-info {\n    color: #fff !important;\n    background-color: #0a6fc2 !important\n}\n\n.w3-warning {\n    color: #000 !important;\n    background-color: #ffb305 !important\n}\n\n.w3-success {\n    color: #fff !important;\n    background-color: #008a00 !important\n}\n\n.w3-pale-red,\n.w3-hover-pale-red:hover {\n    color: #000 !important;\n    background-color: #ffdddd !important\n}\n\n.w3-pale-green,\n.w3-hover-pale-green:hover {\n    color: #000 !important;\n    background-color: #ddffdd !important\n}\n\n.w3-pale-yellow,\n.w3-hover-pale-yellow:hover {\n    color: #000 !important;\n    background-color: #ffffcc !important\n}\n\n.w3-pale-blue,\n.w3-hover-pale-blue:hover {\n    color: #000 !important;\n    background-color: #ddffff !important\n}\n\n.w3-text-amber,\n.w3-hover-text-amber:hover {\n    color: #ffc107 !important\n}\n\n.w3-text-aqua,\n.w3-hover-text-aqua:hover {\n    color: #00ffff !important\n}\n\n.w3-text-blue,\n.w3-hover-text-blue:hover {\n    color: #2196F3 !important\n}\n\n.w3-text-light-blue,\n.w3-hover-text-light-blue:hover {\n    color: #87CEEB !important\n}\n\n.w3-text-brown,\n.w3-hover-text-brown:hover {\n    color: #795548 !important\n}\n\n.w3-text-cyan,\n.w3-hover-text-cyan:hover {\n    color: #00bcd4 !important\n}\n\n.w3-text-blue-grey,\n.w3-hover-text-blue-grey:hover,\n.w3-text-blue-gray,\n.w3-hover-text-blue-gray:hover {\n    color: #607d8b !important\n}\n\n.w3-text-green,\n.w3-hover-text-green:hover {\n    color: #4CAF50 !important\n}\n\n.w3-text-light-green,\n.w3-hover-text-light-green:hover {\n    color: #8bc34a !important\n}\n\n.w3-text-indigo,\n.w3-hover-text-indigo:hover {\n    color: #3f51b5 !important\n}\n\n.w3-text-khaki,\n.w3-hover-text-khaki:hover {\n    color: #b4aa50 !important\n}\n\n.w3-text-lime,\n.w3-hover-text-lime:hover {\n    color: #cddc39 !important\n}\n\n.w3-text-orange,\n.w3-hover-text-orange:hover {\n    color: #ff9800 !important\n}\n\n.w3-text-deep-orange,\n.w3-hover-text-deep-orange:hover {\n    color: #ff5722 !important\n}\n\n.w3-text-pink,\n.w3-hover-text-pink:hover {\n    color: #e91e63 !important\n}\n\n.w3-text-purple,\n.w3-hover-text-purple:hover {\n    color: #9c27b0 !important\n}\n\n.w3-text-deep-purple,\n.w3-hover-text-deep-purple:hover {\n    color: #673ab7 !important\n}\n\n.w3-text-red,\n.w3-hover-text-red:hover {\n    color: #f44336 !important\n}\n\n.w3-text-sand,\n.w3-hover-text-sand:hover {\n    color: #fdf5e6 !important\n}\n\n.w3-text-teal,\n.w3-hover-text-teal:hover {\n    color: #009688 !important\n}\n\n.w3-text-yellow,\n.w3-hover-text-yellow:hover {\n    color: #d2be0e !important\n}\n\n.w3-text-white,\n.w3-hover-text-white:hover {\n    color: #fff !important\n}\n\n.w3-text-black,\n.w3-hover-text-black:hover {\n    color: #000 !important\n}\n\n.w3-text-grey,\n.w3-hover-text-grey:hover,\n.w3-text-gray,\n.w3-hover-text-gray:hover {\n    color: #757575 !important\n}\n\n.w3-text-light-grey,\n.w3-hover-text-light-grey:hover,\n.w3-text-light-gray,\n.w3-hover-text-light-gray:hover {\n    color: #f1f1f1 !important\n}\n\n.w3-text-dark-grey,\n.w3-hover-text-dark-grey:hover,\n.w3-text-dark-gray,\n.w3-hover-text-dark-gray:hover {\n    color: #3a3a3a !important\n}\n\n.w3-border-amber,\n.w3-hover-border-amber:hover {\n    border-color: #ffc107 !important\n}\n\n.w3-border-aqua,\n.w3-hover-border-aqua:hover {\n    border-color: #00ffff !important\n}\n\n.w3-border-blue,\n.w3-hover-border-blue:hover {\n    border-color: #2196F3 !important\n}\n\n.w3-border-light-blue,\n.w3-hover-border-light-blue:hover {\n    border-color: #87CEEB !important\n}\n\n.w3-border-brown,\n.w3-hover-border-brown:hover {\n    border-color: #795548 !important\n}\n\n.w3-border-cyan,\n.w3-hover-border-cyan:hover {\n    border-color: #00bcd4 !important\n}\n\n.w3-border-blue-grey,\n.w3-hover-border-blue-grey:hover,\n.w3-border-blue-gray,\n.w3-hover-border-blue-gray:hover {\n    border-color: #607d8b !important\n}\n\n.w3-border-green,\n.w3-hover-border-green:hover {\n    border-color: #4CAF50 !important\n}\n\n.w3-border-light-green,\n.w3-hover-border-light-green:hover {\n    border-color: #8bc34a !important\n}\n\n.w3-border-indigo,\n.w3-hover-border-indigo:hover {\n    border-color: #3f51b5 !important\n}\n\n.w3-border-khaki,\n.w3-hover-border-khaki:hover {\n    border-color: #f0e68c !important\n}\n\n.w3-border-lime,\n.w3-hover-border-lime:hover {\n    border-color: #cddc39 !important\n}\n\n.w3-border-orange,\n.w3-hover-border-orange:hover {\n    border-color: #ff9800 !important\n}\n\n.w3-border-deep-orange,\n.w3-hover-border-deep-orange:hover {\n    border-color: #ff5722 !important\n}\n\n.w3-border-pink,\n.w3-hover-border-pink:hover {\n    border-color: #e91e63 !important\n}\n\n.w3-border-purple,\n.w3-hover-border-purple:hover {\n    border-color: #9c27b0 !important\n}\n\n.w3-border-deep-purple,\n.w3-hover-border-deep-purple:hover {\n    border-color: #673ab7 !important\n}\n\n.w3-border-red,\n.w3-hover-border-red:hover {\n    border-color: #f44336 !important\n}\n\n.w3-border-sand,\n.w3-hover-border-sand:hover {\n    border-color: #fdf5e6 !important\n}\n\n.w3-border-teal,\n.w3-hover-border-teal:hover {\n    border-color: #009688 !important\n}\n\n.w3-border-yellow,\n.w3-hover-border-yellow:hover {\n    border-color: #ffeb3b !important\n}\n\n.w3-border-white,\n.w3-hover-border-white:hover {\n    border-color: #fff !important\n}\n\n.w3-border-black,\n.w3-hover-border-black:hover {\n    border-color: #000 !important\n}\n\n.w3-border-grey,\n.w3-hover-border-grey:hover,\n.w3-border-gray,\n.w3-hover-border-gray:hover {\n    border-color: #9e9e9e !important\n}\n\n.w3-border-light-grey,\n.w3-hover-border-light-grey:hover,\n.w3-border-light-gray,\n.w3-hover-border-light-gray:hover {\n    border-color: #f1f1f1 !important\n}\n\n.w3-border-dark-grey,\n.w3-hover-border-dark-grey:hover,\n.w3-border-dark-gray,\n.w3-hover-border-dark-gray:hover {\n    border-color: #616161 !important\n}\n\n.w3-border-pale-red,\n.w3-hover-border-pale-red:hover {\n    border-color: #ffe7e7 !important\n}\n\n.w3-border-pale-green,\n.w3-hover-border-pale-green:hover {\n    border-color: #e7ffe7 !important\n}\n\n.w3-border-pale-yellow,\n.w3-hover-border-pale-yellow:hover {\n    border-color: #ffffcc !important\n}\n\n.w3-border-pale-blue,\n.w3-hover-border-pale-blue:hover {\n    border-color: #e7ffff !important\n}"
  },
  {
    "path": "raspiCamSrv/stereoCam.py",
    "content": "from raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg\nfrom raspiCamSrv.camCfg import StereoConfig\nfrom _thread import get_ident\nimport time\nimport threading\nimport logging\nimport cv2\nimport numpy as np\nimport os\nfrom datetime import datetime\n\nlogger = logging.getLogger(__name__)\n\nclass StereoEvent(object):\n    \"\"\"An Event-like class that signals all active clients when a new stereo frame is\n    available.\n    \"\"\"\n    def __init__(self):\n        #logger.debug(\"Thread %s: StereoEvent.__init__\", get_ident())\n        self.events = {}\n\n    def wait(self):\n        \"\"\"Invoked from each client's thread to wait for the next frame.\"\"\"\n        #logger.debug(\"Thread %s: StereoEvent.wait\", get_ident())\n        ident = get_ident()\n        if ident not in self.events:\n            # this is a new client\n            # add an entry for it in the self.events dict\n            # each entry has two elements, a threading.Event() and a timestamp\n            self.events[ident] = [threading.Event(), time.time()]\n            #logger.debug(\"Thread %s: StereoEvent.wait - Event ident: %s added to events dict. time:%s\", get_ident(), ident, self.events[ident][1])\n        #for ident, event in self.events.items():\n            #logger.debug(\"Thread %s: StereoEvent.wait - Event ident: %s Flag: %s Time: %s (Flag False -> blocking)\", get_ident(), ident, self.events[ident][0].is_set(), event[1])\n            \n        return self.events[ident][0].wait()\n\n    def set(self):\n        \"\"\"Invoked by StereoCam when a new frame is available.\"\"\"\n        #logger.debug(\"Thread %s: StereoEvent.set\", get_ident())\n        now = time.time()\n        remove = None\n        for ident, event in self.events.items():\n            if not event[0].isSet():\n                # if this client's event is not set, then set it\n                # also update the last set timestamp to now\n                event[0].set()\n                event[1] = now\n                #logger.debug(\"Thread %s: StereoEvent.set  - Event ident: %s Flag: False -> True (unblock/notify)\", get_ident(), ident)\n            else:\n                # if the client's event is already set, it means the client\n                # did not process a previous frame\n                # if the event stays set for more than 5 seconds, then assume\n                # the client is gone and remove it\n                #logger.debug(\"Thread %s: StereoEvent.set  - Event ident: %s Flag: True (Last image not processed).\", get_ident(), ident)\n                if now - event[1] > 5:\n                    #logger.debug(\"Thread %s: StereoEvent.set  - Event ident: %s  too old; marked for removal.\", get_ident(), ident)\n                    remove = ident\n        if remove:\n            del self.events[remove]\n            #logger.debug(\"Thread %s: StereoEvent.set  - Event ident: %s removed.\", get_ident(), ident)\n\n    def clear(self):\n        \"\"\"Invoked from each client's thread after a frame was processed.\"\"\"\n        ident = get_ident()\n        if ident in self.events:\n            self.events[get_ident()][0].clear()\n        #logger.debug(\"Thread %s: StereoEvent.clear - Flag set to False -> blocking.\", get_ident())\n\nclass StereoCam():\n    \"\"\" Class for stereo camera handling.\n\n    \"\"\"\n    logger.debug(\"Thread %s: StereoCam - setting class variables\", get_ident())\n    _instance = None\n\n    def __new__(cls):\n        logger.debug(\"Thread %s: StereoCam.__new__\", get_ident())\n        if cls._instance is None:\n            logger.debug(\"Thread %s: StereoCam.__new__ - Instantiating Class\", get_ident())\n            cls._instance = super(StereoCam, cls).__new__(cls)\n            cls.sThread = None\n            cls.sThreadStop = False\n            cls.pThread = None\n            cls.pThreadStop = False\n            cls.stereoFrameA = None\n            cls.stereoFrame = None\n            cls.event = StereoEvent()\n            cls.camL = None\n            cls.camR = None\n            cls.leftStereoMap_x = None\n            cls.leftStereoMap_y = None\n            cls.rightStereoMap_x = None\n            cls.rightStereoMap_y = None\n            cls.last_access = 0                 # time of last client access to a stereo frame\n            # Variables for video generation\n            cls.recordFilename = None\n            cls.recordIdx = None\n            cls.frameSize = None\n            cls.framerate = 20\n            cls.recordingStart = None\n            cls.recordingActive = False\n            cls.video = None\n\n        return cls._instance\n\n    def get_stereoFrame(self):\n        # logger.debug(\"Thread %s: StereoCam.get_stereoFrame\", get_ident())\n        self.last_access = time.time()\n        self.event.wait()\n        # logger.debug(\"Thread %s: StereoCam.get_stereoFrame - waiting done\", get_ident())\n        self.event.clear()\n        return self.stereoFrame\n\n    def _frameToStream(self, frame):\n        \"\"\" Convert frame to bytestream\"\"\"\n        # logger.debug(\"Thread %s: StereoCam._frameToStream\", get_ident())\n        frameb = None\n        (stat, frame_jpg) = cv2.imencode(\".jpg\", frame)\n        if stat == True:\n            frame_jpg_arr = np.array(frame_jpg)\n            frameb = frame_jpg_arr.tobytes()\n        return frameb\n\n    def _stereoBM(self, stc:StereoConfig, left, right):\n        \"\"\"StereoBM algorithm for stereo image processing\n        \"\"\"\n        # Create a Stereo Block Matching (SBM) object\n        sbm = cv2.StereoBM_create(\n            numDisparities=stc.bm_numDisparitiesFactor * 16,\n            blockSize=stc.bm_blockSize)\n\n        # Compute the Disparity Map\n        disparity = sbm.compute(left, right)\n\n        # Normalize the Disparity Map for visualization\n        disp_norm = cv2.normalize(\n            disparity, \n            None, \n            alpha=0, \n            beta= 255, \n            norm_type=cv2.NORM_MINMAX, \n            dtype=cv2.CV_8U\n        )\n        return disp_norm\n\n    def _stereoSGBM(self, stc: StereoConfig, left, right):\n        \"\"\"StereoSGBM algorithm for stereo image processing\"\"\"\n        # Create a Stereo Block Matching (SBM) object\n        sgbm = cv2.StereoSGBM_create(\n            minDisparity=stc.sgbm_minDisparity,\n            numDisparities=stc.sgbm_numDisparitiesFactor * 16,\n            blockSize=stc.sgbm_blockSize,\n            P1=stc.sgbm_P1,\n            P2=stc.sgbm_P2,\n            disp12MaxDiff=stc.sgbm_disp12MaxDiff,\n            preFilterCap=stc.sgbm_preFilterCap,\n            uniquenessRatio=stc.sgbm_uniquenessRatio,\n            speckleWindowSize=stc.sgbm_speckleWindowSize,\n            speckleRange=stc.sgbm_speckleRange,\n            mode=stc.sgbm_mode,\n        )\n\n        # Compute the Disparity Map\n        disparity = sgbm.compute(left, right)\n\n        # Normalize the Disparity Map for visualization\n        disp_norm = cv2.normalize(\n            disparity,\n            None,\n            alpha=0,\n            beta=255,\n            norm_type=cv2.NORM_MINMAX,\n            dtype=cv2.CV_8U,\n        )\n        return disp_norm\n\n    def _3DVideo(self, stc: StereoConfig, left, right):\n        \"\"\"create 3D video from stereo images\"\"\"\n\n        if stc.applyCalibRectify == True:\n            left = cv2.remap(\n                left,\n                self.leftStereoMap_x,\n                self.leftStereoMap_y,\n                cv2.INTER_LANCZOS4,\n                cv2.BORDER_CONSTANT,\n                0,\n            )\n            right = cv2.remap(\n                right,\n                self.rightStereoMap_x,\n                self.rightStereoMap_y,\n                cv2.INTER_LANCZOS4,\n                cv2.BORDER_CONSTANT,\n                0,\n            )\n\n        v3d = right.copy()\n        v3d[:, :, 0] = right[:, :, 0]\n        v3d[:, :, 1] = right[:, :, 1]\n        v3d[:, :, 2] = left[:, :, 2]\n\n        # output = Left_nice+Right_nice\n        # v3d = cv2.resize(v3d, (700, 700))\n\n        return v3d\n\n    def _processStereoImage(self, left, right):\n        \"\"\" Process stereo image\n        \"\"\"\n        # logger.debug(\"Thread %s: StereoCam._processStereoImage\", get_ident())\n\n        cfg = CameraCfg()\n        stc = cfg.stereoCfg\n\n        if stc.intent == \"DepthMap\":\n            # Convert to grayscale\n            left_gray = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY)\n            # logger.debug(\"Thread %s: StereoCam._processStereoImage - left image converted to grayscale\", get_ident())\n            right_gray = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY)\n            # logger.debug(\"Thread %s: StereoCam._processStereoImage - right image converted to grayscale\", get_ident())\n\n            if stc.applyCalibRectify == True:\n                # logger.debug(\"Thread %s: StereoCam._processStereoImage - shape(leftStereoMap_x): %s shape(leftStereoMap_y): %s\", get_ident(), self.leftStereoMap_x.shape, self.leftStereoMap_y.shape)\n                left_rect = cv2.remap(\n                    left_gray,\n                    self.leftStereoMap_x,\n                    self.leftStereoMap_y,\n                    cv2.INTER_LANCZOS4,\n                    cv2.BORDER_CONSTANT,\n                    0,\n                )\n                # logger.debug(\"Thread %s: StereoCam._processStereoImage - done\", get_ident())\n                # logger.debug(\"Thread %s: StereoCam._processStereoImage - shape(rightStereoMap_x): %s shape(rightStereoMap_y): %s\", get_ident(), self.rightStereoMap_x.shape, self.rightStereoMap_y.shape)\n                right_rect = cv2.remap(\n                    right_gray,\n                    self.rightStereoMap_x,\n                    self.rightStereoMap_y,\n                    cv2.INTER_LANCZOS4,\n                    cv2.BORDER_CONSTANT,\n                    0,\n                )\n                # logger.debug(\"Thread %s: StereoCam._processStereoImage - done\", get_ident())\n            else:\n                left_rect = left_gray\n                right_rect = right_gray\n\n            if stc.intentAlgo == \"StereoBM\":\n                # Use StereoBM for depth map\n                disp = self._stereoBM(stc, left_rect, right_rect)\n\n            if stc.intentAlgo == \"StereoSGBM\":\n                # Use StereoSGBM for depth map\n                disp = self._stereoSGBM(stc, left_rect, right_rect)\n        elif stc.intent == \"3DVideo\":\n            # Create 3D video from stereo images\n            disp = self._3DVideo(stc, left, right)\n        else:\n            logger.error(\"Thread %s: StereoCam._processStereoImage - Unknown stereo intent: %s\", get_ident(), stc.intent)\n            return\n\n        # Convert to stream\n        self.stereoFrameA = disp\n        self.stereoFrame = self._frameToStream(disp)\n\n        # Signal that a new stereo frame is available\n        self.event.set()\n\n    def _stereoThread(self):\n        \"\"\" Stereo camera thread\n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam._stereoThread\", get_ident())\n        cam = Camera()\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        left = None\n        right = None\n        stop = False\n        while not stop:\n            if not cfg.serverConfig.isLiveStream:\n                cam.startLiveStream()\n            if not cfg.serverConfig.isLiveStream2:\n                cam.startLiveStream2()\n            try:\n                # Just to keep the live stream running\n                frame, frameRaw = cam.get_frame()\n                left = cam.getLeftImageForStereo()\n                # logger.debug(\"Thread %s: StereoCam._stereoThread - got left live view buffer\", get_ident())\n                frame2, frame2Raw = cam.get_frame2()\n                right = cam.getRightImageForStereo()\n                # logger.debug(\"Thread %s: StereoCam._stereoThread - got right live view buffer\", get_ident())\n\n                self._processStereoImage(left, right)\n\n                if self.recordingActive == True:\n                    self._recordStereo()\n\n                if self.sThreadStop:\n                    logger.debug(\"Thread %s: StereoCam._stereoThread - stop requested\", get_ident())\n                    stop = True\n\n                # if there hasn't been any clients asking for frames in\n                # the last 10 seconds then stop the thread\n                if time.time() - self.last_access > 10:\n                    stop = True\n                    logger.debug(\"Thread %s: StereoCam._stereoThread - Stopping camera thread due to inactivity.\", get_ident())\n                    break\n\n            except Exception as e:\n                logger.error(\"Exception in _stereoThread: %s\", e)\n                stop = True\n        self.sThread = None\n        sc.isStereoCamActive = False\n\n    def startStereoCam(self):\n        \"\"\" Start stereo camera processing\n        \n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.startStereoCam\", get_ident())\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        stc = cfg.stereoCfg\n\n        # Load calibration params\n        if stc.applyCalibRectify == True:\n            dataPath = sc.photoRoot + \"/\" + stc.calibDataSubPath\n            dataFile = dataPath + stc.calibDataFile\n            logger.debug(\"Thread %s: StereoCam.startStereoCam - Reading calibData from %s\", get_ident(), dataFile)\n            calibData = cv2.FileStorage(dataFile,cv2.FILE_STORAGE_READ,)\n            logger.debug(\"Thread %s: StereoCam.startStereoCam - calibData read from %s\", get_ident(), dataFile)\n\n            self.leftStereoMap_x = calibData.getNode(\"Left_Stereo_Map_x\").mat()\n            self.leftStereoMap_y = calibData.getNode(\"Left_Stereo_Map_y\").mat()\n            self.rightStereoMap_x = calibData.getNode(\"Right_Stereo_Map_x\").mat()\n            self.rightStereoMap_y = calibData.getNode(\"Right_Stereo_Map_y\").mat()\n            calibData.release()\n            logger.debug(\"Thread %s: StereoCam.startStereoCam - Stereo_Maps extracted\", get_ident())\n\n        sc = CameraCfg().serverConfig\n        self.last_access = time.time()\n        if self.sThread is None:\n            sc.error = None\n            if not CameraCfg().serverConfig.isLiveStream:\n                Camera().startLiveStream()\n            if not CameraCfg().serverConfig.isLiveStream2:\n                Camera().startLiveStream2()\n            if not sc.error:\n                logger.debug(\"Thread %s: StereoCam.startStereoCam - starting new thread\", get_ident())\n                self.sThread = threading.Thread(target=self._stereoThread, daemon=True)\n                self.sThread.start()\n                logger.debug(\"Thread %s: StereoCam.startStereoCam - thread started\", get_ident())\n            else:\n                logger.debug(\"Thread %s: StereoCam.startStereoCam - not started\", get_ident())\n\n    def stopStereoCam(self):\n        \"\"\" Stop stereo camera processing\n        \n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.stopStereoCam\", get_ident())\n        if self.sThread is None:\n            logger.debug(\"Thread %s: StereoCam.stopStereoCam - thread was not active\", get_ident())\n        else:\n            logger.debug(\"Thread %s: StereoCam.stopStereoCam - stopping thread\", get_ident())\n            self.sThreadStop = True\n            cnt = 0\n            while self.sThread:\n                time.sleep(0.01)\n                cnt += 1\n                if cnt > 500:\n                    logger.error(\"Stereo thread did not stop within 5 sec\")\n                    if self.sThread.is_alive():\n                        cnt = 0\n                    else:\n                        self.sThread = None\n                    # raise TimeoutError(\"Stereo thread did not stop within 5 sec\")\n            self.sThreadStop = False\n            self.leftStereoMap_x = None\n            self.leftStereoMap_y = None\n            self.rightStereoMap_x = None\n            self.rightStereoMap_y = None\n        logger.debug(\n            \"Thread %s: StereoCam.stopStereoCam: Thread has stopped\", get_ident()\n        )\n\n    def _takeCalibPhotoThread(self):\n        \"\"\" Taking photos for camera calibration\n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread camL: %s, camR: %s\", get_ident(), self.camL, self.camR)\n        cam = Camera()\n        cfg = CameraCfg()\n        stc = cfg.stereoCfg\n        sc = cfg.serverConfig\n        left = None\n        right = None\n        stop = False\n        found = 0\n        while not stop:\n            if not cfg.serverConfig.isLiveStream:\n                cam.startLiveStream()\n            if not cfg.serverConfig.isLiveStream2:\n                cam.startLiveStream2()\n            try:\n                # Get the left and right images\n                # Call get_frame, Just to keep the live stream running\n                frame, frameRaw = cam.get_frame()\n                imgL = cam.getLeftImageForStereo()\n                # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - got left image\", get_ident())\n\n                if self.camR is not None:\n                    frame2, frame2Raw = cam.get_frame2()\n                    imgR = cam.getRightImageForStereo()\n                    # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - got right image\", get_ident())\n\n                # Convert images to grayscale\n                grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY)\n                if self.camR is not None:\n                    grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY)\n                # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - converted to grayscale\", get_ident())\n\n                # Find the chess board corners\n                if stc.calibPatternIdx == 0:  # Chessboard pattern\n                    # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - looking for chessboard corners\", get_ident())\n                    retL, cornersL = cv2.findChessboardCorners(grayL, stc.calibPatternSize, None)\n                    # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - done chessboard corners - retL=%s\", get_ident(), retL)\n                    retR = True\n                    if self.camR is not None:\n                        retR, cornersR = cv2.findChessboardCorners(grayR, stc.calibPatternSize, None)\n                        # logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - done chessboard corners - retR=%s\", get_ident(), retR)\n                else:\n                    logger.error(\"Thread %s: StereoCam._takeCalibPhotoThread - unknown calibration pattern\", get_ident())\n                    raise ValueError(\"Unknown calibration pattern\")\n\n                # If corners are detected, refine them and save the images\n                if (retL == True) and (retR == True):\n                    found += 1\n                    count = stc.getNextPhotoIdx() + 1\n                    fn = \"img%03d.png\" % count\n                    fnC = \"img%03d_corners.png\" % count\n                    fpL = stc.calibPhotosPath + self.camL + \"/\" + fn\n                    fpLC = stc.calibPhotosPath + self.camL + \"/\" + fnC\n                    fsL = stc.calibPhotosSubPath + self.camL + \"/\" + fn\n                    fsLC = stc.calibPhotosSubPath + self.camL + \"/\" + fnC\n                    logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Saving image to %s\", get_ident(), fpL)\n                    cv2.imwrite(fpL, imgL)\n                    logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Image saved: %s\", get_ident(), fsL)\n\n                    # Refine the corner positions\n                    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)\n                    cv2.cornerSubPix(grayL, cornersL, (11, 11), (-1, -1), criteria)\n                    # Create overlay images\n                    cv2.drawChessboardCorners(imgL, stc.calibPatternSize, cornersL, retL)\n                    cv2.imwrite(fpLC, imgL)\n                    logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Image with corners saved: %s\", get_ident(), fsLC)\n\n                    if self.camL in stc.calibPhotos:\n                        stc.calibPhotos[self.camL].insert(count-1, fsL)\n                        stc.calibPhotosCrn[self.camL].insert(count - 1, fsLC)\n                    else:\n                        stc.calibPhotos[self.camL] = [fsL]\n                        stc.calibPhotosCrn[self.camL] = [fsLC]\n                    stc.calibPhotosIdx[self.camL] = count - 1\n                    stc.calibPhotosCount[self.camL] = len(stc.calibPhotos[self.camL])\n\n                    if self.camR is not None:\n                        fpR = stc.calibPhotosPath + self.camR + \"/\" + fn\n                        fsR = stc.calibPhotosSubPath + self.camR + \"/\" + fn\n                        fpRC = stc.calibPhotosPath + self.camR + \"/\" + fnC\n                        fsRC = stc.calibPhotosSubPath + self.camR + \"/\" + fnC\n                        logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Saving image to %s\", get_ident(), fpR)\n                        cv2.imwrite(fpR, imgR)\n                        logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Image saved: %s\", get_ident(), fsR)\n\n                        # Refine the corner positions\n                        cv2.cornerSubPix(grayR, cornersR, (11, 11), (-1, -1), criteria)\n                        # Create overlay images\n                        cv2.drawChessboardCorners(imgR, stc.calibPatternSize, cornersR, retR)\n                        cv2.imwrite(fpRC, imgR)\n                        logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Image with corners saved: %s\", get_ident(), fsRC)\n\n                        if self.camR in stc.calibPhotos:\n                            stc.calibPhotos[self.camR].insert(count - 1, fsR)\n                            stc.calibPhotosCrn[self.camR].insert(count - 1, fsRC)\n                        else:\n                            stc.calibPhotos[self.camR] = [fsR]\n                            stc.calibPhotosCrn[self.camR] = [fsRC]\n                        stc.calibPhotosIdx[self.camR] = count - 1\n                        stc.calibPhotosCount[self.camR] = len(stc.calibPhotos[self.camR])\n\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(f\"Calibration photo(s) added\")\n                    stc.calibPhotoRecordingMsg = f\"{len(stc.calibPhotos[self.camL])} of {stc.calibPhotosTarget} Calibration photo(s) taken: {fn}\"\n\n                    time.sleep(2)\n                else:\n                    if found > 0:\n                        if self.camR is None:\n                            stc.calibPhotoRecordingMsg = \"No or not all chessboard corners found.\"\n                        else:\n                            stc.calibPhotoRecordingMsg = \"No or not all chessboard corners found on both cameras.\"\n\n                if stc.calibPhotosCount[self.camL] >= stc.calibPhotosTarget:\n                    stc.calibPhotosOK[self.camL] = True\n                    logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Target number of calibration photos reached for camera %s\", get_ident(), self.camL)\n                    if self.camR is not None:\n                        if stc.calibPhotosCount[self.camR] >= stc.calibPhotosTarget:\n                            stc.calibPhotosOK[self.camR] = True\n                            logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Target number of calibration photos reached for camera %s\", get_ident(), self.camR)\n                    if stc.isCalibPhotosOK(self.camL, self.camR):\n                        logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - Target number of calibration photos reached for both cameras\", get_ident())\n                        stop = True\n                        stc.calibPhotoRecordingMsg = \"Target number of calibration photos reached.\"\n                        time.sleep(2)\n\n                if self.pThreadStop:\n                    logger.debug(\"Thread %s: StereoCam._takeCalibPhotoThread - stop requested\", get_ident())\n                    stop = True\n                    stc.calibPhotoRecordingMsg = \"\"\n            except Exception as e:\n                logger.error(\"Exception in _takeCalibPhotoThread: %s\", e)\n                stc.calibPhotoRecordingMsg = f\"Error while taking photos for calibration: {e}.\"\n                stop = True\n        self.pThread = None\n        stc.calibPhotoRecording = False\n        stc.calibPhotoRecordingMsg = \"\"\n\n    def takeCalibrationPhotos(self, camL: str, camR: str):\n        \"\"\" Take calibration photos for camera calibration\n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - camL=%s, camR=%s\", get_ident(), camL, camR)\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        stc = cfg.stereoCfg\n        if stc.isCalibPhotosOK(camL, camR) == False:\n            logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - isCalibPhotosOK= %s\", get_ident(), stc.isCalibPhotosOK(camL, camR))\n            if self.pThread is None:\n                self.camL = camL\n                self.camR = camR\n                sc.error = None\n                if not stc.calibPhotoRecording:\n                    Camera().startLiveStream()\n                if not CameraCfg().serverConfig.isLiveStream2:\n                    Camera().startLiveStream2()\n                if not sc.error:\n                    logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - starting new thread\", get_ident())\n                    self.pThread = threading.Thread(target=self._takeCalibPhotoThread, daemon=True)\n                    self.pThread.start()\n                    stc.calibPhotoRecording = True\n                    logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - thread started\", get_ident())\n                else:\n                    logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - not started\", get_ident())\n        else:\n            logger.debug(\"Thread %s: StereoCam.takeCalibrationPhotos - isCalibPhotosOK= %s\", get_ident(), stc.isCalibPhotosOK(camL, camR))\n\n    def stoptakeCalibrationPhotos(self):\n        \"\"\" Stop taking calibration photos\n        \n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.stoptakeCalibrationPhotos\", get_ident())\n\n        cfg = CameraCfg()\n        stc = cfg.stereoCfg\n        if self.pThread is None:\n            logger.debug(\"Thread %s: StereoCam.stoptakeCalibrationPhotos - thread was not active\", get_ident())\n        else:\n            logger.debug(\"Thread %s: StereoCam.stoptakeCalibrationPhotos - stopping thread\", get_ident())\n            self.pThreadStop = True\n            cnt = 0\n            while self.pThread:\n                time.sleep(0.01)\n                cnt += 1\n                if cnt > 500:\n                    logger.error(\"takeCalibPhotoThread did not stop within 5 sec\")\n                    if self.pThread.is_alive():\n                        cnt = 0\n                    else:\n                        self.pThread = None\n                    # raise TimeoutError(\"Stereo thread did not stop within 5 sec\")\n            self.pThreadStop = False\n        stc.calibPhotoRecording = False\n        logger.debug(\"Thread %s: StereoCam.stoptakeCalibrationPhotos: Thread has stopped\", get_ident())\n\n    def calibrateCameras(self, camL: str, camR: str):\n        \"\"\" Calibrate the stereo cameras\n\n            Source: https://learnopencv.com/camera-calibration-using-opencv/\n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - camL=%s, camR=%s\", get_ident(), camL, camR)\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        stc = cfg.stereoCfg\n\n        # Termination criteria for refining the detected corners\n        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Termination criteria set: %s\", get_ident(), criteria)\n\n        # Prepare 3D object points, like (0,0,0), (1,0,0), (2,0,0) ....,(9,6,0)\n        objp = np.zeros((stc.calibPatternSize[0] * stc.calibPatternSize[1], 3), np.float32)\n        objp[:, :2] = np.mgrid[0:stc.calibPatternSize[0], 0:stc.calibPatternSize[1]].T.reshape(-1, 2)\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - 3D Object points prepared: %s\", get_ident(), objp)\n\n        # Initialize lists for 2D image points and 3D object points\n        img_ptsL = []\n        img_ptsR = []\n        obj_pts = []\n\n        # Process all prepared calibration images\n        for i in range(0, len(stc.calibPhotos[camL])):\n            # Read images\n            logger.debug(\"Thread %s: StereoCam.calibrateCameras - Loading image %s/%s\", get_ident(), i + 1, len(stc.calibPhotos[camL]))\n            pathL = sc.photoRoot + \"/\" + stc.calibPhotos[camL][i]\n            imgL = cv2.imread(pathL)\n            pathR = sc.photoRoot + \"/\" + stc.calibPhotos[camR][i]\n            imgR = cv2.imread(pathR)\n            logger.debug(\"Thread %s: StereoCam.calibrateCameras - Left and right image loaded\", get_ident())\n\n            # Convert to grayscale\n            imgL_gray = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY)\n            imgR_gray = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY)\n            logger.debug(\"Thread %s: StereoCam.calibrateCameras - Images converted to grayscale\", get_ident())\n\n            outputL = imgL.copy()\n            outputR = imgR.copy()\n            logger.debug(\"Thread %s: StereoCam.calibrateCameras - Images copied\", get_ident())\n\n            # Find chessboard corners\n            retL, cornersL = cv2.findChessboardCorners(imgL_gray, stc.calibPatternSize, None)\n            retR, cornersR = cv2.findChessboardCorners(imgR_gray, stc.calibPatternSize, None)\n            logger.debug(\"Thread %s: StereoCam.calibrateCameras - Corners found: L=%s, R=%s\", get_ident(), retL, retR)\n            # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Corners Left: %s\", get_ident(), cornersL)\n            # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Corners Right: %s\", get_ident(), cornersR)\n\n            if retR and retL:\n                # If found, add object points, image points (after refining them)\n                obj_pts.append(objp)\n                logger.debug(\"Thread %s: StereoCam.calibrateCameras - object points appended\", get_ident())\n\n                cornersRefL = cv2.cornerSubPix(imgL_gray, cornersL, (11, 11), (-1, -1), criteria)\n                cornersRefR = cv2.cornerSubPix(imgR_gray, cornersR, (11, 11), (-1, -1), criteria)\n                # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Refined corners Left: %s\", get_ident(), cornersL)\n                # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Refined corners Right: %s\", get_ident(), cornersR)\n\n                img_ptsL.append(cornersRefL)\n                img_ptsR.append(cornersRefR)\n                logger.debug(\"Thread %s: StereoCam.calibrateCameras - image points appended\", get_ident())\n\n        # Calibrate left camera\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Calibrating left camera.\", get_ident())\n        retL, mtxL, distL, rvecsL, tvecsL = cv2.calibrateCamera(\n            obj_pts, img_ptsL, imgL_gray.shape[::-1], None, None\n        )\n        stc.calibRmsReproError[camL] = retL\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Camera matrix: \\n%s\", get_ident(), mtxL)\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Distortion Coeff: \\n%s\", get_ident(), distL)\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Rotation vectors: \\n%s\", get_ident(), rvecsL)\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - Translation vectors: \\n%s\", get_ident(), tvecsL)\n\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Optimizing camera matrix.\", get_ident())\n        hL, wL = imgL_gray.shape[:2]\n        new_mtxL, roiL = cv2.getOptimalNewCameraMatrix(mtxL, distL, (wL, hL), 1, (wL, hL))\n        stc._calibCameraOK[camL] = True\n        # logger.debug(\"Thread %s: StereoCam.calibrateCameras - OptimizedCamera matrix: \\n%s\", get_ident(), new_mtxL)\n\n        # Calibrate right camera\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Calibrating right camera.\", get_ident())\n        retR, mtxR, distR, rvecsR, tvecsR = cv2.calibrateCamera(\n            obj_pts, img_ptsR, imgR_gray.shape[::-1], None, None\n        )\n        stc.calibRmsReproError[camR] = retR\n        hR, wR = imgR_gray.shape[:2]\n        new_mtxR, roiR = cv2.getOptimalNewCameraMatrix(mtxR, distR, (wR, hR), 1, (wR, hR))\n        stc._calibCameraOK[camR] = True\n\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Stereo calibration started.\", get_ident())\n        flags = 0\n        flags |= cv2.CALIB_FIX_INTRINSIC\n        # Here we fix the intrinsic camara matrixes so that only Rot, Trns, Emat and Fmat are calculated.\n        # Hence intrinsic parameters are the same\n\n        criteria_stereo = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)\n\n        # This step is performed to transformation between the two cameras and calculate Essential and Fundamenatl matrix\n        retS, new_mtxL, distL, new_mtxR, distR, Rot, Trns, Emat, Fmat = cv2.stereoCalibrate(\n            obj_pts,\n            img_ptsL,\n            img_ptsR,\n            new_mtxL,\n            distL,\n            new_mtxR,\n            distR,\n            imgL_gray.shape[::-1],\n            criteria_stereo,\n            flags,\n        )\n        stc.calibStereoOK = True\n\n        # Once we know the transformation between the two cameras we can perform stereo rectification\n        # StereoRectify function\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Stereo rectification started.\", get_ident())\n        rectify_scale = stc.rectifyScale  # if 0 image croped, if 1 image not croped\n        rect_l, rect_r, proj_mat_l, proj_mat_r, Q, roiL, roiR = cv2.stereoRectify(\n            new_mtxL,\n            distL,\n            new_mtxR,\n            distR,\n            imgL_gray.shape[::-1],\n            Rot,\n            Trns,\n            rectify_scale,\n            (0, 0),\n        )\n\n        # Use the rotation matrixes for stereo rectification and camera intrinsics for undistorting the image\n        # Compute the rectification map (mapping between the original image pixels and\n        # their transformed values after applying rectification and undistortion) for left and right camera frames\n        Left_Stereo_Map = cv2.initUndistortRectifyMap(\n            new_mtxL, distL, rect_l, proj_mat_l, imgL_gray.shape[::-1], cv2.CV_16SC2\n        )\n        Right_Stereo_Map = cv2.initUndistortRectifyMap(\n            new_mtxR, distR, rect_r, proj_mat_r, imgR_gray.shape[::-1], cv2.CV_16SC2\n        )\n        stc.stereoRectifyOK = True\n\n        dataPath = sc.photoRoot + \"/\" + stc.calibDataSubPath\n        dataFile = dataPath + stc.calibDataFile\n        os.makedirs(dataPath, exist_ok=True)\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Saving parameters to %s\", get_ident(), dataFile)\n        cv_file = cv2.FileStorage(dataFile, cv2.FILE_STORAGE_WRITE)\n        cv_file.write(\"Left_Stereo_Map_x\", Left_Stereo_Map[0])\n        cv_file.write(\"Left_Stereo_Map_y\", Left_Stereo_Map[1])\n        cv_file.write(\"Right_Stereo_Map_x\", Right_Stereo_Map[0])\n        cv_file.write(\"Right_Stereo_Map_y\", Right_Stereo_Map[1])\n        cv_file.release()\n        stc.calibDataOK = True\n        stc.calibDate = datetime.now()\n        logger.debug(\"Thread %s: StereoCam.calibrateCameras - Success\", get_ident())\n\n    def startRecordStereo(self, fnRaw) -> str:\n        \"\"\" Start recording stereo video\n        \n            Input:\n                fnRaw: Filename without extension\n            Return\n                Filename for video file\n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.startRecordStereo\", get_ident())\n        done = False\n        err = \"\"\n        camCfg = CameraCfg()\n        sc = camCfg.serverConfig\n        try:\n            if self.recordingActive == False:\n                self.recordFilename = fnRaw + \".mp4\"\n                fp = sc.photoRoot + \"/\" + \"photos/\" + \"camera_S\"\n                os.makedirs(fp, exist_ok=True)\n                save_path = fp + \"/\" + self.recordFilename\n                self.frameSize = CameraCfg().liveViewConfig.stream_size\n                logger.debug(\"Thread %s: StereoCam.startRecordStereo - video path:%s\", get_ident(), save_path)\n                # fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n                fourcc = cv2.VideoWriter_fourcc(*'avc1') \n                logger.debug(\"Thread %s: StereoCam.startRecordStereo - fps:%s framesize:%s\", get_ident(), self.framerate, self.frameSize)\n                self.video = cv2.VideoWriter(save_path, fourcc, self.framerate, self.frameSize)\n                assert self.video.isOpened()\n                self.recordingActive = True\n                sc.isStereoCamRecording = True\n                self.recordIdx = 0\n\n                # Create placeholder image\n                imgFilename = fnRaw + \".jpg\"\n                img_path = fp + \"/\" + imgFilename\n                cv2.imwrite(img_path, self.stereoFrameA)\n\n            self._recordStereo()\n            done = True\n        except AssertionError as e:\n            logger.error(\"Thread %s: StereoCam - AssertionError when starting recording: %s\", get_ident(), e)\n            err = f\"AssertionError: {e}\"\n        except Exception as e:\n            logger.error(\"Thread %s: StereoCam - Exception when starting recording: %s\", get_ident(), e)\n            err = f\"Exception: {e}\"\n        return (done, self.recordFilename, err)\n\n    def stopRecordStereo(self):\n        \"\"\" Stop recording stereo\n        \n        \"\"\"\n        logger.debug(\"Thread %s: StereoCam.stopRecordStereo\", get_ident())\n        camCfg = CameraCfg()\n        sc = camCfg.serverConfig\n        if self.recordingActive == True:\n            self.video.release()\n            logger.debug(\"Thread %s: StereoCam.stopRecordStereo - video released with %s frames\", get_ident(), self.recordIdx)\n            self.recordingActive = False\n            sc.isStereoCamRecording = False\n\n    def _recordStereo(self):\n        \"\"\" Record stereo as series of png - add new frame\n        \n        \"\"\"\n        if self.recordingActive == True:\n            logger.debug(\"Thread %s: StereoCam._recordStereo - recordIdx:%s\", get_ident(), self.recordIdx)\n            if len(self.stereoFrameA.shape) == 2:\n                framergb = cv2.cvtColor(self.stereoFrameA, cv2.COLOR_YUV2RGB_I420)\n            elif len(self.stereoFrameA.shape) == 3:\n                if self.stereoFrameA.shape[2] == 4:\n                    framergb = cv2.cvtColor(self.stereoFrameA, cv2.COLOR_RGBA2RGB)\n                else:\n                    framergb = self.stereoFrameA\n            else:\n                framergb = self.stereoFrameA\n            self.video.write(framergb)\n            self.recordIdx += 1\n"
  },
  {
    "path": "raspiCamSrv/sun.py",
    "content": "\"\"\" Module for calculation of sun path properties\n\n    - Sunrise / Sunset times\n      (Based on code from https://en.wikipedia.org/wiki/Sunrise_equation)\n\n    - Solar position (azimuth, elevation) dependent on time of day\n    - Time(s) when sun has a specific azimuth (e.g. for controlling camera direction)\n    \n    All calculations are based on the local time of the configured timezone, \n    which is determined by the longitude and the timezone key. The timezone key is used to determine the UTC offset and daylight saving time rules for the location. The calculations take into account the elevation of the location, which affects the sunrise and sunset times. The solar position is calculated using standard astronomical formulas, and the times for specific azimuths are found using a numerical root-finding method (bisection).\n\n    \n\"\"\"\n\nimport logging\nfrom datetime import datetime, timedelta, timezone, tzinfo\nfrom math import acos, asin, ceil, cos, degrees, fmod, radians, sin, sqrt\nfrom time import time\nfrom zoneinfo import ZoneInfo\n\nlogger = logging.getLogger(__name__)\n\nclass Sun():\n    def __init__(self, latitude: float, longitude: float, elevation: float, timezone: str):\n        \"\"\"Constructor for sun class\n\n        Args:\n            - `latitude (float)`    : Latitude\n            - `longitude (float)`   : Longitude\n            - `elevation (float)`   : Elevation\n            - `timezone (str)`      : Time Zone\n        \"\"\"\n        logger.debug('Sun - Creating Sun object with latitude=%s, longitude=%s, elevation=%s, timezone=%s', latitude, longitude, elevation, timezone)\n        self._latitude = latitude\n        self._longitude = longitude\n        self._elevation = elevation\n        self._timezone = timezone\n        \n    def _ts2human(self, ts: float, debugtz: tzinfo) -> str:\n        return str(datetime.fromtimestamp(ts, debugtz))\n\n\n    def _j2ts(self, j: float) -> float:\n        return (j - 2440587.5) * 86400\n\n\n    def _ts2j(self, ts: float) -> float:\n        return ts / 86400.0 + 2440587.5\n\n\n    def _j2human(self, j: float, debugtz: tzinfo) -> str:\n        ts = self._j2ts(j)\n        return f'{ts} = {self._ts2human(ts, debugtz)}'\n\n\n    def _deg2human(self, deg: float) -> str:\n        x = int(deg * 3600.0)\n        num = f'∠{deg:.3f}°'\n        rad = f'∠{radians(deg):.3f}rad'\n        human = f'∠{x // 3600}°{x // 60 % 60}′{x % 60}″'\n        return f'{rad} = {human} = {num}'\n\n\n    def _calc(\n            self,\n            current_timestamp: float,\n            f: float,\n            l_w: float,\n            elevation: float = 0.0,\n            *,\n            debugtz: tzinfo = None,\n    ) -> tuple:\n        logger.debug(f'Latitude               f       = {self._deg2human(f)}')\n        logger.debug(f'Longitude              l_w     = {self._deg2human(l_w)}')\n        logger.debug(f'Now                    ts      = {self._ts2human(current_timestamp, debugtz)}')\n\n        J_date = self._ts2j(current_timestamp)\n        logger.debug(f'Julian date            j_date  = {J_date:.3f} days')\n\n        # Julian day\n        # TODO: ceil ?\n        n = ceil(J_date - (2451545.0 + 0.0009) + 69.184 / 86400.0)\n        logger.debug(f'Julian day             n       = {n:.3f} days')\n\n        # Mean solar time\n        J_ = n + 0.0009 - l_w / 360.0\n        logger.debug(f'Mean solar time        J_      = {J_:.9f} days')\n\n        # Solar mean anomaly\n        # M_degrees = 357.5291 + 0.98560028 * J_  # Same, but looks ugly\n        M_degrees = fmod(357.5291 + 0.98560028 * J_, 360)\n        M_radians = radians(M_degrees)\n        logger.debug(f'Solar mean anomaly     M       = {self._deg2human(M_degrees)}')\n\n        # Equation of the center\n        C_degrees = 1.9148 * sin(M_radians) + 0.02 * sin(2 * M_radians) + 0.0003 * sin(3 * M_radians)\n        # The difference for final program result is few milliseconds\n        # https://www.astrouw.edu.pl/~jskowron/pracownia/praca/sunspot_answerbook_expl/expl-4.html\n        # e = 0.01671\n        # C_degrees = \\\n        #     degrees(2 * e - (1 / 4) * e ** 3 + (5 / 96) * e ** 5) * sin(M_radians) \\\n        #     + degrees(5 / 4 * e ** 2 - (11 / 24) * e ** 4 + (17 / 192) * e ** 6) * sin(2 * M_radians) \\\n        #     + degrees(13 / 12 * e ** 3 - (43 / 64) * e ** 5) * sin(3 * M_radians) \\\n        #     + degrees((103 / 96) * e ** 4 - (451 / 480) * e ** 6) * sin(4 * M_radians) \\\n        #     + degrees((1097 / 960) * e ** 5) * sin(5 * M_radians) \\\n        #     + degrees((1223 / 960) * e ** 6) * sin(6 * M_radians)\n\n        logger.debug(f'Equation of the center C       = {self._deg2human(C_degrees)}')\n\n        # Ecliptic longitude\n        # L_degrees = M_degrees + C_degrees + 180.0 + 102.9372  # Same, but looks ugly\n        L_degrees = fmod(M_degrees + C_degrees + 180.0 + 102.9372, 360)\n        logger.debug(f'Ecliptic longitude     L       = {self._deg2human(L_degrees)}')\n\n        Lambda_radians = radians(L_degrees)\n\n        # Solar transit (julian date)\n        J_transit = 2451545.0 + J_ + 0.0053 * sin(M_radians) - 0.0069 * sin(2 * Lambda_radians)\n        logger.debug(f'Solar transit time     J_trans = {self._j2human(J_transit, debugtz)}')\n\n        # Declination of the Sun\n        sin_d = sin(Lambda_radians) * sin(radians(23.4397))\n        # cos_d = sqrt(1-sin_d**2) # exactly the same precision, but 1.5 times slower\n        cos_d = cos(asin(sin_d))\n\n        # Hour angle\n        some_cos = (sin(radians(-0.833 - 2.076 * sqrt(elevation) / 60.0)) - sin(radians(f)) * sin_d) / (cos(radians(f)) * cos_d)\n        try:\n            w0_radians = acos(some_cos)\n        except ValueError:\n            return None, None, some_cos > 0.0\n        w0_degrees = degrees(w0_radians)  # 0...180\n\n        logger.debug(f'Hour angle             w0      = {self._deg2human(w0_degrees)}')\n\n        j_rise = J_transit - w0_degrees / 360\n        j_set = J_transit + w0_degrees / 360\n\n        logger.debug(f'Sunrise                j_rise  = {self._j2human(j_rise, debugtz)}')\n        logger.debug(f'Sunset                 j_set   = {self._j2human(j_set, debugtz)}')\n        logger.debug(f'Day length                       {w0_degrees / (180 / 24):.3f} hours')\n\n        return self._j2ts(j_rise), self._j2ts(j_set), None\n    \n    def sunTimezone(self) -> str:\n        \"\"\"## Return the timezone key used for sun calculations\n\n        ### Returns:\n            - `str`: Timezone key\n        \"\"\"\n        return self._timezone\n    \n    def sunrise_sunset(self, time: datetime) -> tuple[datetime, datetime]:\n        \"\"\"Determine sunrise and sunset for a specific date\n\n        Args:\n            - `time (datetime)`: Date for which to determine sunrise\n\n        Returns:\n            - `datetime`: time of sunrise\n        \"\"\"\n        timeTS = datetime.timestamp(time)\n        sunriseTS, sunsetTS, err = self._calc(\n                timeTS, \n                self._latitude, \n                self._longitude,\n                self._elevation,\n                debugtz=ZoneInfo(self._timezone)\n        )\n        sunrise = datetime.fromtimestamp(sunriseTS, ZoneInfo(self._timezone))\n        sunset = datetime.fromtimestamp(sunsetTS, ZoneInfo(self._timezone))\n        return sunrise, sunset\n\n    def _day_of_year(self, dt: datetime) -> int:\n        \"\"\"Day number in the year (1. January = 1).\"\"\"\n        return dt.timetuple().tm_yday\n\n    def _equation_of_time(self, N: int) -> float:\n        \"\"\"Equation of time in minutes for day number N.\n\n        Approximation formula according to Spencer (1971).\n        \"\"\"\n        B = radians(360 / 365 * (N - 81))\n        E = 9.87 * sin(2 * B) - 7.53 * cos(B) - 1.5 * sin(B)\n        return E  # Minuten\n\n    def _declination(self, N: int) -> float:\n        \"\"\"Solar declination in degrees for day number N.\"\"\"\n        return -23.45 * cos(radians(360 / 365 * (N + 10)))\n\n    def solar_position(\n        self,\n        dt: datetime,\n        log: bool = True,\n    ) -> dict:\n        \"\"\"Calculate solar azimuth and elevation.\n\n        Parameters\n        ----------\n        dt          : datetime  - Local date/time\n\n        Returns\n        -------\n        dict with:\n            azimuth       - Azimuth in degrees (0 = North, 90 = East, 180 = South, 270 = West)\n            elevation     - Sun elevation in degrees (negative = below horizon)\n            hour_angle    - Hour angle in degrees\n            declination   - Declination in degrees\n            solar_time    - True solar time as a string\n        \"\"\"\n        if log:\n            logger.debug(\"sun.solar_position - Calculating solar position for datetime: %s\", dt)\n        dt = dt.astimezone(ZoneInfo(self._timezone))\n        N = self._day_of_year(dt)\n        local_time_hours = dt.hour + dt.minute / 60 + dt.second / 3600\n        utc_offset = dt.utcoffset().total_seconds() / 3600\n\n        # Time correction: longitude correction + equation of time\n        ref_longitude = utc_offset * 15  # Reference meridian of the timezone\n        longitude_correction = (self._longitude - ref_longitude) * 4 / 60  # Hours\n        E_hours = self._equation_of_time(N) / 60\n\n        # True solar time (TST)\n        solar_time = local_time_hours + longitude_correction + E_hours\n\n        # Hour angle H (0 = noon, negative = morning, positive = afternoon)\n        H = (solar_time - 12) * 15  # Degrees\n\n        # Declination\n        delta = self._declination(N)\n\n        # Auxiliary values in radians\n        phi = radians(self._latitude)\n        delta_r = radians(delta)\n        H_r = radians(H)\n\n        # Solar elevation\n        sin_alpha = (\n            sin(phi) * sin(delta_r)\n            + cos(phi) * cos(delta_r) * cos(H_r)\n        )\n        sin_alpha = max(-1.0, min(1.0, sin_alpha))\n        alpha = degrees(asin(sin_alpha))\n\n        # Azimuth\n        cos_alpha = cos(radians(alpha))\n        if cos_alpha < 1e-10:\n            azimuth = 0.0\n        else:\n            cos_A = (sin(delta_r) - sin(phi) * sin_alpha) / (\n                cos(phi) * cos_alpha\n            )\n            cos_A = max(-1.0, min(1.0, cos_A))\n            A = degrees(acos(cos_A))\n            # Afternoon: Azimuth > 180\n            azimuth = 360 - A if H > 0 else A\n\n        # True solar time as a readable string\n        solar_h = int(solar_time) % 24\n        solar_m = int((solar_time - int(solar_time)) * 60)\n        solar_s = int(((solar_time - int(solar_time)) * 60 - solar_m) * 60)\n        solar_time_str = f\"{solar_h:02d}:{solar_m:02d}:{solar_s:02d}\"\n\n        if log:\n            logger.debug(\"sun.solar_position - Calculated solar position: azimuth=%.2f°, elevation=%.2f°, hour_angle=%.2f°, declination=%.2f°, solar_time=%s\", azimuth, alpha, H, delta, solar_time_str)\n        return {\n            \"azimuth\": round(azimuth, 2),\n            \"elevation\": round(alpha, 2),\n            \"hour_angle\": round(H, 2),\n            \"declination\": round(delta, 2),\n            \"solar_time\": solar_time_str,\n        }\n\n\n    def _get_az(self, base: datetime, minutes_from_midnight: float) -> float:\n        \"\"\"Azimuth at a specific minute of the day.\"\"\"\n        dt = base + timedelta(minutes=minutes_from_midnight)\n        return self.solar_position(dt, log=False)[\"azimuth\"]\n\n    def _az_diff(self, base: datetime, minutes: float, target_azimuth: float) -> float:\n        \"\"\"Differenz zwischen aktuellem und Ziel-Azimut, zirkulaer normiert.\"\"\"\n        diff = self._get_az(base, minutes) - target_azimuth\n        # Zirkulaere Normierung: -180 bis +180\n        while diff > 180:\n            diff -= 360\n        while diff < -180:\n            diff += 360\n        return diff\n\n    def _bisect(self, base: datetime, t_lo: float, t_hi: float, target_azimuth: float, tol: float = 0.5) -> float | None:\n        \"\"\"Bisektionsverfahren zur Nullstellensuche.\"\"\"\n        f_lo = self._az_diff(base, t_lo, target_azimuth)\n        f_hi = self._az_diff(base, t_hi, target_azimuth)\n        if f_lo * f_hi > 0:\n            return None\n        for _ in range(40):  # max. 40 Iterations -> about 0.001 Min. Precision\n            t_mid = (t_lo + t_hi) / 2\n            f_mid = self._az_diff(base, t_mid, target_azimuth)\n            if abs(t_hi - t_lo) < tol / 60: \n                return t_mid\n            if f_lo * f_mid <= 0:\n                t_hi, f_hi = t_mid, f_mid\n            else:\n                t_lo, f_lo = t_mid, f_mid\n        return (t_lo + t_hi) / 2\n\n    def find_times_for_azimuth(\n        self,\n        date: datetime,\n        target_azimuth: float,\n        min_elevation: float = 0.0,\n    ) -> list[dict]:\n        \"\"\" Calculate the time(s) when the sun has a specific azimuth.\n        This method uses a numerical root-finding approach (bisection method)\n        to find the time(s) when the sun's azimuth matches the target value.\n        Method: Numerical root-finding (bisection method)\n        The sign behavior of (azimuth(t) - target) is used to narrow down\n        the roots to hour- or minute-level accuracy.\n\n        Parameters\n        ----------\n        date           : datetime  - Date (time is ignored)\n        target_azimuth : float     - Target azimuth in degrees (0-360)\n        min_elevation  : float     - Minimum sun elevation (default: 0 = above horizon)\n\n        Returns\n        -------\n        List of dicts with:\n            time       - datetime object\n            time_str   - Time as HH:MM:SS\n            azimuth    - Actual azimuth at the time\n            elevation  - Sun elevation in degrees\n            side       - \"Morning\" or \"Afternoon\"\n        \"\"\"\n        logger.debug(\"sun.find_times_for_azimuth - Finding times for target azimuth %s° on date %s with min elevation %s°\", target_azimuth, date, min_elevation)    \n        results = []\n        date = date.astimezone(ZoneInfo(self._timezone))\n        base = date.replace(hour=0, minute=0, second=0, microsecond=0)\n        utc_offset = date.utcoffset().total_seconds() / 3600\n\n        # Sampling every 15 minutes for the entire day\n        step = 15  # Minutes\n        samples = [(t, self._az_diff(base, t, target_azimuth)) for t in range(0, 1440 + step, step)]\n\n        # Look for sign changes -> potential roots\n        found_times = set()\n        for i in range(len(samples) - 1):\n            t0, d0 = samples[i]\n            t1, d1 = samples[i + 1]\n            if d0 == 0.0:\n                # Exact match at t0\n                found_times.add(round(t0 * 60))  # Round to nearest second\n            elif d1 == 0.0:\n                # Exact match at t1\n                found_times.add(round(t1 * 60))  # Round to nearest second\n            else:\n                if d0 * d1 < 0:\n                    t_exact = self._bisect(base, t0, t1, target_azimuth)\n                    if t_exact is not None:\n                        # Round to the nearest second\n                        t_rounded_sec = round(t_exact * 60)\n                        if t_rounded_sec not in found_times:\n                            found_times.add(t_rounded_sec)\n\n        # Prepare results\n        for t_sec in sorted(found_times):\n            dt_result = base + timedelta(seconds=t_sec)\n            pos = self.solar_position(dt_result, log=False)\n            logger.debug(\"sun.find_times_for_azimuth - Found potential time: %s with azimuth %.2f° and elevation %.2f°\", dt_result, pos[\"azimuth\"], pos[\"elevation\"])\n\n            if pos[\"elevation\"] < min_elevation:\n                continue\n\n            if abs(pos[\"azimuth\"] - target_azimuth) > 0.5:\n                continue\n\n            h = dt_result.hour\n            m = dt_result.minute\n            s = dt_result.second\n\n            results.append({\n                \"time\": dt_result,\n                \"time_str\": f\"{h:02d}:{m:02d}:{s:02d}\",\n                \"azimuth\": round(pos[\"azimuth\"], 2),\n                \"elevation\": round(pos[\"elevation\"], 2),\n                \"side\": \"Morning\" if pos[\"hour_angle\"] < 0 else \"Afternoon\",\n            })\n        logger.debug(\"sun.find_times_for_azimuth - Found %s\", results)\n        return results\n\nif __name__ == \"__main__\":\n    # Example usage\n    latitude = 48.85827\n    longitude = 2.29451\n    elevation = 52.0\n    tz = \"Europe/Paris\"\n    dt_str = \"2026-03-24 13:05\"\n    dt = datetime.strptime(dt_str, \"%Y-%m-%d %H:%M\")\n    print(f\"latitude: {latitude}, longitude: {longitude}, elevation: {elevation}m\")\n    print(f\"Timezone: {tz}\")\n    print(f\"Time: {dt}\")\n    sun = Sun(latitude=latitude, longitude=longitude, elevation=elevation, timezone=tz)\n    sunrise, sunset = sun.sunrise_sunset(dt)\n    print(f\"Sunrise: {sunrise}, Sunset: {sunset}\")\n    print(\"\\n  Solar position:\")\n    pos = sun.solar_position(dt)\n    print(pos)\n    azimuth = pos[\"azimuth\"]\n    print(f\"\\n  Times when sun has azimuth {azimuth}°:\")\n    times = sun.find_times_for_azimuth(date=dt, target_azimuth=azimuth, min_elevation=0.0)\n    for t in times:\n        print(t)"
  },
  {
    "path": "raspiCamSrv/templates/auth/login.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Log In{% endblock %}\n{% endblock %}\n\n{% block content %}\n<h3>Log In</h3>\n<form class=\"w3-container\" method=\"post\">\n    <label for=\"username\">Username</label>\n    <input class=\"w3-input\" name=\"username\" id=\"username\" required autofocus>\n    <label for=\"password\">Password</label>\n    <input class=\"w3-input\" type=\"password\" name=\"password\" id=\"password\" required>\n    <p> </p>\n    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Log In\">\n</form>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/auth/password.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Change Password{% endblock %}\n{% endblock %}\n\n{% block content %}\n<h3>Change Password</h3>\n<form class=\"w3-container\" method=\"post\">\n    <label for=\"username\">Username</label>\n    <input class=\"w3-input\" name=\"username\" id=\"username\" required autofocus>\n    <label for=\"oldpassword\">Old Password</label>\n    <input class=\"w3-input\" type=\"password\" name=\"oldpassword\" id=\"oldpassword\" required>\n    <p> </p>\n    <label for=\"newpassword\">New Password</label>\n    <input class=\"w3-input\" type=\"password\" name=\"newpassword\" id=\"newpassword\" required>\n    <label for=\"newpassword2\">Repeat New Password</label>\n    <input class=\"w3-input\" type=\"password\" name=\"newpassword2\" id=\"newpassword2\" required>\n    <p> </p>\n    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Change Password\">\n</form>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/auth/register.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Register{% endblock %}\n{% endblock %}\n\n{% block content %}\n<h3>Register</h3>\n<form class=\"w3-container\" method=\"post\" action=\"{{ url_for('auth.register') }}\">\n    <label for=\"username\">Username</label>\n    <input class=\"w3-input\" name=\"username\" id=\"username\" required autofocus>\n    <label for=\"password\">Password</label>\n    <input class=\"w3-input\" type=\"password\" name=\"password\" id=\"password\" required>\n    <p> </p>\n    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Register\">\n</form>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <meta charset=\"UTF-8\">\n    <title>{% block title %}{% endblock %} - raspiCamSrv</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='w3.css') }}\">\n    <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\">\n    {% if sc.curMenu == \"live\" %}\n    <body \n        onload=\"showAfWindows('{{ sc.lastLiveTab }}', '{{ sc.isVideoRecording }}', 'include_afwindows', '{{ sc.isZoomModeDraw }}', '{{ sc.scalerCropLiveView }}')\"\n        onresize=\"showAfWindows('{{ sc.lastLiveTab }}', '{{ sc.isVideoRecording }}', 'include_afwindows', '{{ sc.isZoomModeDraw }}', '{{ sc.scalerCropLiveView }}')\"\n    >\n    {% elif sc.curMenu == \"trigger\" %}\n    <body \n        onload=\"showRoiWindows('{{ sc.lastTriggerTab }}', '{{ sc.isLiveStream }}', '{{ sc.scalerCropLiveView }}')\"\n        onresize=\"showRoiWindows('{{ sc.lastTriggerTab }}', '{{ sc.isLiveStream }}', '{{ sc.scalerCropLiveView }}')\"\n    >\n    {% else %}\n    <body>\n    {% endif %}\n        <div class=\"w3-container\">\n            <div class=\"w3-bar w3-blue w3-large\">\n                <div class=\"w3-bar-item w3-left\">\n                    raspiCamSrv\n                    {% if sc.canUpdate == True %}\n                    <span class=\"w3-text-yellow\">\n                    {% else %}\n                    <span>\n                    {% endif %}\n                        {{ g.version }}\n                    </span>\n                </div>\n                <div class=\"w3-bar-item w3-center\">{% block header %}{% endblock %}</div>\n                {% if g.user %}\n                <div class=\"w3-bar-item w3-right\" style=\"padding-top:0;padding-bottom:0\">\n                    {% if sc.unsavedChanges %}\n                    <img src=\"{{ url_for('static', filename='save_changes.png') }}\" class=\"w3-image\" id=\"save_changes\"\n                        alt=\"Save changes\" onclick=\"fetch(`/store_config`,{method: `POST`}).then(location.replace(location.pathname))\" style=\"cursor: pointer; height:34px; width:34px; margin-top:4px\">\n                    {% endif %}\n                    <div class=\"w3-bar-item w3-right\" style=\"padding-top:8px;padding-bottom:8px\">\n                        {{ g.user['username'] }}\n                        | \n                        {% if sc.noCamera == False %}\n                        {{ sc.activeCameraInfo}}\n                        {% else %}\n                        No Camera available\n                        {% endif %}\n                        |\n                        {{ g.hostname }}\n                    </div>\n                </div>\n                {% else %}\n                <div class=\"w3-bar-item w3-right\">\n                    {{ sc.activeCameraInfo}}\n                    |\n                    {{ g.hostname }}\n                </div>\n                {% endif %}\n            </div>\n            <div class=\"w3-bar w3-black\">\n                {% if g.user %}\n                {% if sc.noCamera == False %}\n                {% if sc.curMenu == \"live\" %}\n                <a href=\"{{ url_for('home.index') }}\" class=\"w3-bar-item w3-button menubtns w3-dark-grey\" id='livebtn' onclick=\"openMenu('livebtn')\">Live</a>\n                {% else %}\n                <a href=\"{{ url_for('home.index') }}\" class=\"w3-bar-item w3-button menubtns\" id='livebtn' onclick=\"openMenu('livebtn')\">Live</a>\n                {% endif %}\n                {% if sc.curMenu == \"config\" %}\n                <a href=\"{{ url_for('config.main') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='cfgbtn' onclick=\"openMenu('cfgbtn')\">Config</a>\n                {% else %}\n                <a href=\"{{ url_for('config.main') }}\" class=\"w3-bar-item w3-button\" id='cfgbtn' onclick=\"openMenu('cfgbtn')\">Config</a>\n                {% endif %}\n                {% endif %}\n                {% if sc.curMenu == \"info\" %}\n                <a href=\"{{ url_for('info.main') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='infobtn' onclick=\"openMenu('infobtn')\">Info</a>\n                {% else %}\n                <a href=\"{{ url_for('info.main') }}\" class=\"w3-bar-item w3-button\" id='infobtn' onclick=\"openMenu('infobtn')\">Info</a>\n                {% endif %}\n                {% if sc.noCamera == False %}\n                {% if sc.curMenu == \"photos\" %}\n                <a href=\"{{ url_for('images.main') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='imagesbtn' onclick=\"openMenu('imagesbtn')\">Photos</a>\n                {% else %}\n                <a href=\"{{ url_for('images.main') }}\" class=\"w3-bar-item w3-button\" id='imagesbtn' onclick=\"openMenu('imagesbtn')\">Photos</a>\n                {% endif %}\n                {% if sc.curMenu == \"photoseries\" %}\n                <a href=\"{{ url_for('photoseries.main') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='photoseriesbtn' onclick=\"openMenu('photoseriesbtn')\">Photo Series</a>\n                {% else %}\n                <a href=\"{{ url_for('photoseries.main') }}\" class=\"w3-bar-item w3-button\" id='photoseriesbtn' onclick=\"openMenu('photoseriesbtn')\">Photo Series</a>\n                {% endif %}\n                {% endif %}\n                {% if sc.curMenu == \"trigger\" %}\n                <a href=\"{{ url_for('trigger.trigger') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='triggerbtn' onclick=\"openMenu('triggerbtn')\">Trigger</a>\n                {% else %}\n                <a href=\"{{ url_for('trigger.trigger') }}\" class=\"w3-bar-item w3-button\" id='triggerbtn' onclick=\"openMenu('triggerbtn')\">Trigger</a>\n                {% endif %}\n                {% if sc.noCamera == False %}\n                {% if sc.curMenu == \"webcam\" %}\n                <a href=\"{{ url_for('webcam.webcam') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='cambtn' onclick=\"openMenu('cambtn')\">Cam</a>\n                {% else %}\n                <a href=\"{{ url_for('webcam.webcam') }}\" class=\"w3-bar-item w3-button\" id='cambtn' onclick=\"openMenu('cambtn')\">Cam</a>\n                {% endif %}\n                {% endif %}\n                {% if sc.curMenu == \"console\" %}\n                <a href=\"{{ url_for('console.console') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='consolebtn' onclick=\"openMenu('consolebtn')\">Console</a>\n                {% else %}\n                <a href=\"{{ url_for('console.console') }}\" class=\"w3-bar-item w3-button\" id='consolebtn' onclick=\"openMenu('consolebtn')\">Console</a>\n                {% endif %}\n                {% if sc.curMenu == \"settings\" %}\n                <a href=\"{{ url_for('settings.main') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='settingsbtn' onclick=\"openMenu('settingsbtn')\">Settings</a>\n                {% else %}\n                <a href=\"{{ url_for('settings.main') }}\" class=\"w3-bar-item w3-button\" id='settingsbtn' onclick=\"openMenu('settingsbtn')\">Settings</a>\n                {% endif %}\n                {% if sc.curMenu == \"live\" %}\n                <a href=\"{{ url_for('auth.logout') }}\" class=\"w3-bar-item w3-button\" id='logoutbtn' onclick=\"openMenu('logoutbtn')\">Log Out</a>\n                {% else %}\n                <a href=\"{{ url_for('auth.logout') }}\" class=\"w3-bar-item w3-button\" id='logoutbtn' onclick=\"openMenu('logoutbtn')\">Log Out</a>\n                {% endif %}\n                <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n                    {% if sc.noCamera == False %}\n                    {% if sc.supportedCameras|length() > 1 %}\n                    {% if sc.isVideoRecording2 %}\n                    <img src=\"{{ url_for('static', filename='recording_video2_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_video2_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isLiveStream2 == True %}\n                    {% if sc.isStereoCamActive == True %}\n                    <img src=\"{{ url_for('static', filename='recording_live2_stereo.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_live2_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% endif %}\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_live2_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% endif %}\n                    {% endif %}\n                    {% if sc.isEventhandling %}\n                    {% if sc.isEventsWaiting %}\n                    <img src=\"{{ url_for('static', filename='recording_events_wait.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_events_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_events_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isTriggerRecording %}\n                    {% if sc.isTriggerWaiting %}\n                    <img src=\"{{ url_for('static', filename='recording_trigger_wait.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording paused\" style=\"height:34px; width:34px\">\n                    {% elif sc.isTriggerTesting %}\n                    <img src=\"{{ url_for('static', filename='recording_trigger_test.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording testing\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_trigger_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_trigger_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isPhotoSeriesRecording %}\n                    <img src=\"{{ url_for('static', filename='recording_series_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_series_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isAudioRecording %}\n                    <img src=\"{{ url_for('static', filename='recording_audio_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_audio_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isVideoRecording %}\n                    <img src=\"{{ url_for('static', filename='recording_video_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_video_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% if sc.isLiveStream %}\n                    {% if sc.isStereoCamActive == True %}\n                    <img src=\"{{ url_for('static', filename='recording_live_stereo.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_live_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% endif %}\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_live_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\"> \n                    {% endif %}\n                    {% else %}\n                    {% if sc.isEventhandling %}\n                    {% if sc.isEventsWaiting %}\n                    <img src=\"{{ url_for('static', filename='recording_events_wait.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_events_active.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_events_inactive.png') }}\" class=\"w3-image\" id=\"recording_active\" alt=\"Recording active\" style=\"height:34px; width:34px\">\n                    {% endif %}\n                    {% endif %}\n                </div>\n                {% else %}\n                <div>\n                    {% if g.nrUsers == 0 %}\n                    {% if sc.curMenu == \"register\" %}\n                    <a href=\"{{ url_for('auth.register') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='registerbtn' onclick=\"openMenu('registerbtn')\">Register</a>\n                    {% else %}\n                    <a href=\"{{ url_for('auth.register') }}\" class=\"w3-bar-item w3-button\" id='registerbtn' onclick=\"openMenu('registerbtn')\">Register</a>\n                    {% endif %}\n                    {% endif %}\n                    {% if sc.curMenu == \"login\" %}\n                    <a href=\"{{ url_for('auth.login') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='loginbtn' onclick=\"openMenu('loginbtn')\">Log In</a>\n                    {% else %}\n                    <a href=\"{{ url_for('auth.login') }}\" class=\"w3-bar-item w3-button\" id='loginbtn' onclick=\"openMenu('loginbtn')\">Log In</a>\n                    {% endif %}\n                    {% if sc.curMenu == \"password\" %}\n                    <a href=\"{{ url_for('auth.password') }}\" class=\"w3-bar-item w3-button w3-dark-grey\" id='passwdbtn' onclick=\"openMenu('passwdbtn')\">Change Password</a>\n                    {% else %}\n                    <a href=\"{{ url_for('auth.password') }}\" class=\"w3-bar-item w3-button\" id='passwdbtn' onclick=\"openMenu('passwdbtn')\">Change Password</a>\n                    {% endif %}\n                </div>\n                {% endif %}\n            </div>\n            {% block content %}{% endblock %}\n            <div class=\"w3-bar w3-bottom w3-dark-grey\">\n                {% for message in get_flashed_messages() %}\n                <div class=\"flash\">{{ message }}</div>\n                {% endfor %}\n            </div>\n        </div>\n        <script>\n            function openMenu(menuButton) {\n                var b = document.getElementsByClassName(\"menubtns\");\n                for (i = 0; i < b.length; i++) {\n                    b[i].classList = \"w3-bar-item w3-button menubtns\";\n                }\n                document.getElementById(menuButton).classList = \"w3-bar-item w3-button menubtns w3-dark-grey\";\n            }\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "raspiCamSrv/templates/config/main.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Camera Configurations{% endblock %}\n    <style>\n        .inputfile {\n            width: 0.1px;\n            height: 0.1px;\n            opacity: 0;\n            overflow: hidden;\n            position: absolute;\n            z-index: -1;\n        }\n        .inputfile:focus + label {\n            outline: 1px dotted #000;\n            outline: -webkit-focus-ring-color auto 5px;\n        }        \n    </style>\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Config menue -->\n        {% set ignoreLastConfigTab = False %}\n        {% if sc.activeCameraIsUsb == False %}\n        {% if sc.lastConfigTab == \"cfgtuning\" %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfgtuningbtn\"\n            onclick=\"openCfgTab('cfgtuning', 'cfgtuningbtn')\">Tuning</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfgtuningbtn\" \n            onclick=\"openCfgTab('cfgtuning', 'cfgtuningbtn')\">Tuning</button>\n        {% endif %}\n        {% if sc.activeCameraHasAi == True and sc.useCameraAi == True %}\n        {% if sc.lastConfigTab == \"cfgai\" %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfgaibtn\"\n            onclick=\"openCfgTab('cfgai', 'cfgaibtn')\">AI Configuration</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfgaibtn\"\n            onclick=\"openCfgTab('cfgai', 'cfgaibtn')\">AI Configuration</button>\n        {% endif %}\n        {% else %}\n        {% if sc.lastConfigTab == \"cfgai\" %}\n        {% set ignoreLastConfigTab = True %}\n        {% endif %}\n        {% endif %}\n        {% else %}\n        {% if sc.lastConfigTab == \"cfgai\" or sc.lastConfigTab == \"cfgtuning\" %}\n        {% set ignoreLastConfigTab = True %}\n        {% endif %}\n        {% endif %}\n        {% if (sc.lastConfigTab == \"cfglive\") or (ignoreLastConfigTab == True) %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfglivebtn\" \n            onclick=\"openCfgTab('cfglive', 'cfglivebtn')\">Live View</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfglivebtn\" \n            onclick=\"openCfgTab('cfglive', 'cfglivebtn')\">Live View</button>\n        {% endif %}\n        {% if sc.lastConfigTab == \"cfgphoto\" %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfgphotobtn\" \n            onclick=\"openCfgTab('cfgphoto', 'cfgphotobtn')\">Photo</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfgphotobtn\" \n            onclick=\"openCfgTab('cfgphoto', 'cfgphotobtn')\">Photo</button>\n        {% endif %}\n        {% if sc.lastConfigTab == \"cfgraw\" %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfgrawbtn\"\n            onclick=\"openCfgTab('cfgraw', 'cfgrawbtn')\">Raw Photo</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfgrawbtn\"\n            onclick=\"openCfgTab('cfgraw', 'cfgrawbtn')\">Raw Photo</button>\n        {% endif %}\n        {% if sc.lastConfigTab == \"cfgvideo\" %}\n        <button class=\"w3-bar-item w3-button configmenu w3-light-green\" id=\"cfgvideobtn\"\n            onclick=\"openCfgTab('cfgvideo', 'cfgvideobtn')\">Video</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button configmenu\" id=\"cfgvideobtn\" \n            onclick=\"openCfgTab('cfgvideo', 'cfgvideobtn')\">Video</button>\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <form method=\"post\" id=\"syncaspectratiofrm\" action=\"{{ url_for('config.syncAspectRatio') }}\">\n                <table style=\"position:absolute;right:100px\">\n                    <tr>\n                        <td>\n                            <input id=\"activecfgtab\" name=\"activecfgtab\" value=\"-\" style=\"display: none\">\n                        </td>\n                        <td class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                               If checked, Stream Size for all other configurations will be adjusted \n                               so that the aspect ratio is identical with the current configuration.\n                            </span>\n                            <label for=\"syncaspectratio\">Sync Aspect Ratio:</label>\n                        </td>\n                        <td >\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% if sc.syncAspectRatio == true %}\n                            <input type=\"checkbox\" id=\"syncaspectratio\" name=\"syncaspectratio\" aria-label=\"SyncAspectRatio\" onchange=\"syncAspectRatio('syncaspectratiofrm')\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"syncaspectratio\" name=\"syncaspectratio\" aria-label=\"SyncAspectRatio\" onchange=\"syncAspectRatio('syncaspectratiofrm')\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            {% if sc.syncAspectRatio == true %}\n                            <input type=\"checkbox\" id=\"syncaspectratio\" name=\"syncaspectratio\" aria-label=\"SyncAspectRatio\" onchange=\"syncAspectRatio('syncaspectratiofrm')\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"syncaspectratio\" name=\"syncaspectratio\" aria-label=\"SyncAspectRatio\" onchange=\"syncAspectRatio('syncaspectratiofrm')\" value=\"0\" disabled>\n                            {% endif %}\n                            {% endif %}\n                        </td>\n                    </tr>\n                </table>\n            </form>\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from\n                    GitHub\n                </span>\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                    style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n            </div>\n        </div>\n    </div>\n    <!-- Tuning configuration -->\n    {% if sc.lastConfigTab == \"cfgtuning\" %}\n    <div id=\"cfgtuning\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfgtuning\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for Tuning</h4>\n        <div>\n            <form method=\"post\" action=\"{{ url_for('config.tuningCfg') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                When activated, the specified tuning file will be loaded when the camera is initialized.\n                            </span>\n                            <label for=\"loadtuningfile\">Load Tuning File:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            {% if tc.loadTuningFile == True %}\n                            <input type=\"checkbox\" id=\"loadtuningfile\" name=\"loadtuningfile\" onchange=\"blockTuningFileDeletion()\" aria-label=\"loadTuningFile\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"loadtuningfile\" name=\"loadtuningfile\" onchange=\"blockTuningFileDeletion()\" aria-label=\"loadTuningFile\" value=\"0\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Path to folder where tuning files are located.<br>\n                                Leave empty for default.\n                            </span>\n                            <label for=\"tuningfolder\">Full Path to Folder with Tuning Files:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            {% if tc.tuningFolder == None %}\n                            <input style=\"width:100%\" id=\"tuningfolder\" name=\"tuningfolder\" onchange=\"blockTuningFileDeletion()\" value=\"\">\n                            {% else %}\n                            <input style=\"width:100%\" id=\"tuningfolder\" name=\"tuningfolder\" onchange=\"blockTuningFileDeletion()\" value=\"{{ tc.tuningFolder }}\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                File name of tuning file to be loaded.\n                            </span>\n                            <label for=\"tuningfile\">Name of Tuning File:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            <select style=\"width:100%\" name=\"tuningfile\" id=\"tuningfile\" onchange=\"blockTuningFileDeletion()\">\n                                {% for fil in tfl %}\n                                {% if tc.tuningFile == fil %}\n                                <option value=\"{{ fil }}\" selected>{{ fil }}</option>\n                                {% else %}\n                                <option value=\"{{ fil }}\">{{ fil }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\"></td>\n                        <td style=\"width:30%\"></td>\n                        <td style=\"width:65%\">\n                            For tweaking the tuning file, see <a href=\"https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf\" target=\"_blank\">Tuning Guide</a>\n                            chapter 6.8\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Submit & Apply\">\n                <p style=\"margin-bottom: 0\"></p>\n                <hr>\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            {% if tc.isDefaultFolder == True %}\n            <form method=\"post\" action=\"{{ url_for('config.customTuning') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Custom Folder\">\n            </form>\n            {% else %}\n            <form method=\"post\" action=\"{{ url_for('config.defaultTuning') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Default Folder\">\n            </form>\n            {% endif %}\n            <p style=\"margin-bottom: 0\"></p>\n            {% if tc.isDefaultFolder == True or tc.loadTuningFile == True %}\n            <form id=\"deletetuningfile\" method=\"post\" action=\"{{ url_for('config.deleteTuningFile') }}\">\n                <input id=\"deletetuningfilebtn\" class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\"\n                    onclick=\"confirmDeleteTuningFile('deletetuningfile')\" value=\"Delete Tuning File\" disabled>\n            </form>\n            {% else %}\n            <form id=\"deletetuningfile\" method=\"post\" action=\"{{ url_for('config.deleteTuningFile') }}\">\n                <input id=\"deletetuningfilebtn\" class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\"\n                    onclick=\"confirmDeleteTuningFile('deletetuningfile')\" value=\"Delete Tuning File\">\n            </form>\n            {% endif %}\n            <p style=\"margin-bottom: 0\"></p>\n            <form method=\"post\" action=\"{{ url_for('config.downloadTuningFile') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Download Tuning File\">\n            </form>\n            <p style=\"margin-bottom: 0\"></p>\n            <!-- \n                Solution for styling file upload from\n                https://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/\n            -->\n            {% if tc.isDefaultFolder == True %}\n            <form action=\"{{ url_for('config.uploadTuningFile') }}\" method=\"post\" enctype=\"multipart/form-data\">\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Select Tuning File for Upload\" disabled>\n                <br>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Upload selected File\" disabled>\n            </form>\n            {% else %}\n            <form action=\"{{ url_for('config.uploadTuningFile') }}\" method=\"post\" enctype=\"multipart/form-data\">\n                <input class=\"inputfile\" id=\"uploadtuningfile\" style=\"width: 40ch\" type=\"file\" accept=\".json\"\n                    data-multiple-caption=\"{count}\" name=\"tuningfile\" multiple />\n                <label id=\"uploadtuningfilelbl\" class=\"w3-button w3-black\" style=\"width: 30ch\" for=\"uploadtuningfile\">\n                    Select Tuning File for Upload\n                </label>\n                <br>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 30ch\"  type=\"submit\" value=\"Upload selected File\">\n            </form>\n            {% endif %}\n        </div>\n    </div>\n\n    <!-- AI configuration -->\n    {% if sc.lastConfigTab == \"cfgai\" and sc.activeCameraHasAi == True and sc.useCameraAi == True %}\n    <div id=\"cfgai\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfgai\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for AI</h4>\n        <p>To change the model, wait until Active Camera is no longer streaming!</p>\n        <div>\n            <table class=\"w3-table-all\">\n                <form id=\"getaimodelfiles\" method=\"post\" action=\"{{ url_for('config.getAiModelFiles') }}\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Path to folder where model files are located.<br>\n                                Leave empty for default.\n                            </span>\n                            <label for=\"modelfolder\">Full Path to Folder with model Files:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            {% if ai.modelFolder == None %}\n                            <input style=\"width:100%\" id=\"modelfolder\" name=\"modelfolder\" onchange=\"confirmModelBase('getaimodelfiles')\" value=\"\"{% if sc. isLiveStream == True %} disabled{% endif %}>\n                            {% else %}\n                            <input style=\"width:100%\" id=\"modelfolder\" name=\"modelfolder\" onchange=\"confirmModelBase('getaimodelfiles')\" value=\"{{ ai.modelFolder }}\"{% if sc. isLiveStream == True %} disabled{% endif %}>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Intended task for the AI model.\n                            </span>\n                            <label for=\"aitask\">Task:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"aitask\" id=\"aitask\" onchange=\"confirmModelBase('getaimodelfiles')\"{% if sc. isLiveStream == True %} disabled{% endif %}>\n                                {% if ai.task is none %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for task in ai.tasks %}\n                                {% if ai.task == task|lower() %}\n                                <option value=\"{{ task }}\" selected>{{ task }}</option>\n                                {% else %}\n                                <option value=\"{{ task }}\">{{ task }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                </form>\n                <form id=\"setaimodelfile\" method=\"post\" action=\"{{ url_for('config.setAiModelFile') }}\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                AI Model File to be uploaded to the camera.\n                            </span>\n                            <label for=\"aimodelfile\">AI Model File:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"aimodelfile\" id=\"aimodelfile\" onchange=\"doSubmit('setaimodelfile')\"{% if sc. isLiveStream == True %} disabled{% endif %}>\n                                {% if ai.modelFile == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for modelfile in ai.modelFiles %}\n                                {% if ai.modelFile == modelfile %}\n                                <option value=\"{{ modelfile }}\" selected>{{ modelfile }}</option>\n                                {% else %}\n                                <option value=\"{{ modelfile }}\">{{ modelfile }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                </form>\n                <tr>\n                    <td style=\"width:5%\">\n                    </td>\n                    <td style=\"width:30%\">\n                        Intrinsics:\n                    </td>\n                    <td style=\"width:65%\">\n                    </td>\n                </tr>\n                {% if ai.modelIntrinsics is not none %}\n                {% for key, value in ai.modelIntrinsics.items() %}\n                <tr>\n                    <td style=\"width:5%\">\n                    </td>\n                    <td style=\"width:30%\">\n                        &nbsp;&nbsp;&nbsp;{{ key }}: \n                    </td>\n                    <td style=\"width:65%\">\n                        {{ value }}\n                    </td>\n                </tr>\n                {% endfor %}\n                {% endif %}\n                <tr>\n                    <td style=\"width:5%\">\n                    </td>\n                    <td style=\"width:30%\">\n                        <p>&nbsp;</p>\n                    </td>\n                    <td style=\"width:65%\">\n                    </td>\n                </tr>\n                <tr>\n                    <td style=\"width:35%\" colspan=\"2\">\n                        <h4 style=\"margin-top: 0; margin-bottom: 0\">Settings</h4>\n                    </td>\n                    <td style=\"width:65%\">\n                    </td>\n                </tr>\n                <form id=\"aisettingsfrm\" method=\"post\" action=\"{{ url_for('config.ai_settings') }}\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Defines the number of top-scored indices to be returned by the AI model.\n                            </span>\n                            <label for=\"topk\">Top K Indices:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            <input type=\"number\" id=\"topk\" name=\"topk\" aria-label=\"topk\" min=1 max=99 step=1 value=\"{{ ai.topK }}\"\n                            {% if ai.task != \"classification\" %}\n                                disabled\n                            {% endif %}\n                            >\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Defines the detection threshold for AI Object Detection models.\n                            </span>\n                            <label for=\"detectionthreshold\">Detection Threshold:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            <input type=\"number\" id=\"detectionthreshold\" name=\"detectionthreshold\" aria-label=\"detectionThreshold\" min=0.1 max=1.0 step=0.1 value=\"{{ ai.detectionThreshold }}\"\n                            {% if (ai.task != \"object detection\") and (ai.task != \"pose estimation\") %}\n                                disabled\n                            {% endif %}\n                            >\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Defines the IOU (Intersection over Union) threshold for AI Object detection models.\n                            </span>\n                            <label for=\"iouthreshold\">IOU Threshold:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            <input type=\"number\" id=\"iouthreshold\" name=\"iouthreshold\" aria-label=\"iouThreshold\" min=0.01 max=1.0 step=0.01 value=\"{{ ai.iouThreshold }}\"\n                            {% if ai.task != \"object detection\" %}\n                                disabled\n                            {% endif %}\n                            >\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Defines the maximum number of detections for AI Object detection models.\n                            </span>\n                            <label for=\"maxdetections\">Max Detections:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            <input type=\"number\" id=\"maxdetections\" name=\"maxdetections\" aria-label=\"maxDetections\" min=1 max=100 step=1 value=\"{{ ai.maxDetections }}\"\n                            {% if ai.task != \"object detection\" %}\n                                disabled\n                            {% endif %}\n                            >\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Draw AI Results on each frame of the lores stream, typically used for Live View.\n                            </span>\n                            <label for=\"drawonlores\">Draw Results on Stream <i>lores</i>:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            {% if ai.drawOnLores == True %}\n                            <input type=\"checkbox\" id=\"drawonlores\" name=\"drawonlores\" aria-label=\"drawonlores\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"drawonlores\" name=\"drawonlores\" aria-label=\"drawonlores\" value=\"0\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Draw AI Results on each frame of the main stream, typically used for Photo and Video.\n                            </span>\n                            <label for=\"drawonmain\">Draw Results on Stream <i>main</i>:</label>\n                        </td>\n                        <td style=\"width:65%\">\n                            {% if ai.drawOnMain == True %}\n                            <input type=\"checkbox\" id=\"drawonmain\" name=\"drawonmain\" aria-label=\"drawonmain\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"drawonmain\" name=\"drawonmain\" aria-label=\"drawonmain\" value=\"0\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:35%\" colspan=\"2\">\n                            <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:65%\">\n                        </td>\n                    </tr>\n                </form>\n                <tr>\n                    <td style=\"width:5%\">\n                    </td>\n                    <td style=\"width:30%\">\n                        <p>&nbsp;</p>\n                    </td>\n                    <td style=\"width:65%\">\n                    </td>\n                </tr>\n                <tr>\n                    <td style=\"width:5%\">\n                    </td>\n                    <td style=\"width:30%\" class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Check to enable AI for the active camera\n                        </span>\n                        <label for=\"enableai\">Enable AI:</label>\n                    </td>\n                    <td style=\"width:65%\">\n                        <form id=\"enableaifrm\" method=\"post\" action=\"{{ url_for('config.enableAi') }}\">\n                            {% if ai.enable == True %}\n                            <input type=\"checkbox\" id=\"enableai\" name=\"enableai\" onchange=\"enableAi('enableai', 'enableaifrm')\" aria-label=\"enableai\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"enableai\" name=\"enableai\" onchange=\"enableAi('enableai', 'enableaifrm')\" aria-label=\"enableai\" value=\"0\">\n                            {% endif %}\n                        </form>\n                    </td>\n                </tr>\n            </table>\n        </div>\n    </div>\n\n\n    <!-- Live View configuration -->\n    {% if sc.lastConfigTab == \"cfglive\" or ignoreLastConfigTab == True %}\n    <div id=\"cfglive\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfglive\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for Live View</h4>\n        {% set cfg = cfglive %}\n        <div>\n            <form method=\"post\" action=\"{{ url_for('config.liveViewCfg') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This is the unique ID of the configuration. It is only used to distinguish\n                                different configuration sets within this application. \n                                It is not relevant for the camera system.\n                            </span>\n                            <label for=\"LIVE_id\">ID:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"LIVE_id\" name=\"LIVE_id\" value=\"{{ cfg.id }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Usecase for this configuration.\n                            </span>\n                            <label for=\"LIVE_use_case\">Use Case:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"LIVE_use_case\" name=\"LIVE_use_case\" value=\"{{ cfg.use_case }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether camera images are horizontally or vertically mirrored, \n                                or both (giving a 180 degree rotation).\n                            </span>\n                            <label for=\"LIVE_transform_hflip\">Transform (flip ↔ or ↕ ):</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            ↔\n                            {% if cfg.transform_hflip == true %}\n                            <input type=\"checkbox\" id=\"LIVE_transform_hflip\" name=\"LIVE_transform_hflip\" aria-label=\"LIVE_transform_hflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_transform_hflip\" name=\"LIVE_transform_hflip\" aria-label=\"LIVE_transform_hflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            ↕\n                            {% if cfg.transform_vflip == true %}\n                            <input type=\"checkbox\" id=\"LIVE_transform_vflip\" name=\"LIVE_transform_vflip\" aria-label=\"LIVE_transform_vflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_transform_vflip\" name=\"LIVE_transform_vflip\" aria-label=\"LIVE_transform_vflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The colour space of the output images. \n                                The main and lores streams must always share the same colour space.\n                                The raw stream is always in a camera-specific colour space.\n                            </span>\n                            <label for=\"LIVE_colour_space\">Colour Space:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"LIVE_colour_space\" id=\"LIVE_colour_space\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.colour_space == \"sYCC\" %}\n                                <option value=\"sYCC\" selected>sYCC</option>\n                                {% else %}\n                                <option value=\"sYCC\">sYCC</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Smpte170m\" %}\n                                <option value=\"Smpte170m\" selected>Smpte170m</option>\n                                {% else %}\n                                <option value=\"Smpte170m\">Smpte170m</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Rec709\" %}\n                                <option value=\"Rec709\" selected>Rec709</option>\n                                {% else %}\n                                <option value=\"Rec709\">Rec709</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"{{ cfg.colour_space }}\" selected>{{ cfg.colour_space }}</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                the number of sets of buffers to allocate for the camera system. \n                                A single set of buffers represents\n                                one buffer for each of the streams that have been requested.\n                            </span>\n                            <label for=\"LIVE_buffer_count\">Buffer Count:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input type=\"number\" id=\"LIVE_buffer_count\" name=\"LIVE_buffer_count\" min=\"1\" max=\"12\" value=\"{{ cfg.buffer_count }}\">\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is allowed to queue up a frame \n                                ready for a capture request.\n                            </span>\n                            <label for=\"LIVE_queue\">Queue:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if cfg.queue == True %}\n                            <input type=\"checkbox\" id=\"LIVE_queue\" name=\"LIVE_queue\" aria-label=\"LIVE_queue\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_queue\" name=\"LIVE_queue\" aria-label=\"LIVE_queue\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                            <!-- This cell is used to store an invisible list of sensor mode data -->\n                            {% for mode in sm %}\n                            <p id=\"sensormode_ref{{ mode.id }}\" style=\"display:none\">Size: {{ mode.size }} FPS: {{ mode.fps }} Bit Depth: {{ mode.bit_depth }}</p>\n                            {% endfor%}\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Sensor Mode to be used in this configuration<br>\n                                For details, see the available Sensor Modes under 'Info'\n                            </span>\n                            <label for=\"LIVE_sensor_mode\">Sensor Mode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"LIVE_sensor_mode\" id=\"LIVE_sensor_mode\" \n                                onchange=\"sensorModeChanged('LIVE_sensor_mode', 'LIVE_sensor_mode_data', 'LIVE_stream_size_width', 'LIVE_stream_size_height', 'LIVE_stream_size_align')\">\n                                {% for mode in sm %}\n                                {% if cfg.sensor_mode == mode.id %}\n                                <option value=\"{{ mode.id }}\" selected>Sensor Mode {{ mode.id }}</option>\n                                {% else %}\n                                <option value=\"{{ mode.id }}\">Sensor Mode {{ mode.id }}</option>\n                                {% endif %}\n                                {% endfor%}\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.sensor_mode == \"custom\" %}\n                                <option value=\"custom\" selected>Custom</option>\n                                {% else %}\n                                <option value=\"custom\">Custom</option>\n                                {% endif %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\" id=\"LIVE_sensor_mode_data\">\n                            {% for mode in sm %}\n                            {% if cfg.sensor_mode == mode.id %}\n                            Size: {{ mode.size }} FPS: {{ mode.fps }} Bit Depth: {{ mode.bit_depth }}\n                            {% endif %}\n                            {% endfor%}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Camera stream to use\n                            </span>\n                            <label for=\"LIVE_stream\">Stream:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"LIVE_stream\" id=\"LIVE_stream\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.stream == \"main\" %}\n                                <option value=\"main\" selected>main</option>\n                                {% else %}\n                                <option value=\"main\">main</option>\n                                {% endif %}\n                                {% if cfg.stream == \"lores\" %}\n                                <option value=\"lores\" selected>lores</option>\n                                {% else %}\n                                <option value=\"lores\">lores</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"main\" selected>main</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Output size of the stream. Required if 'Custom' has been chosen for Sensor Mode\n                            </span>\n                            <label for=\"LIVE_stream_size_width\">Stream Size (width, height) </label>\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"LIVE_stream_size_width\" name=\"LIVE_stream_size_width\" min=\"1\" max={{ cp.pixelArraySize[0] }} \n                                value=\"{{ cfg.stream_size[0] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"LIVE_stream_size_width\" name=\"LIVE_stream_size_width\" min=\"1\" max={{ cp.pixelArraySize[0] }} \n                                value=\"{{ cfg.stream_size[0] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"LIVE_stream_size_height\" name=\"LIVE_stream_size_height\" aria-label=\"LIVE_stream_size_height\" min=\"1\" max={{ cp.pixelArraySize[1] }} \n                                value=\"{{ cfg.stream_size[1] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"LIVE_stream_size_height\" name=\"LIVE_stream_size_height\" aria-label=\"LIVE_stream_size_height\" min=\"1\" max={{ cp.pixelArraySize[1] }}\n                                value=\"{{ cfg.stream_size[1] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is requested to align the custom output size\n                                with available Sensor Modes.\n                            </span>\n                            <label for=\"LIVE_stream_size_align\">Stream size aligned with Sensor Modes:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"LIVE_stream_size_align\" name=\"LIVE_stream_size_align\" aria-label=\"LIVE_stream_size_align\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_stream_size_align\" name=\"LIVE_stream_size_align\" aria-label=\"LIVE_stream_size_align\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"LIVE_stream_size_align\" name=\"LIVE_stream_size_align\" aria-label=\"LIVE_stream_size_align\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_stream_size_align\" name=\"LIVE_stream_size_align\" aria-label=\"LIVE_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"LIVE_stream_size_align\" name=\"LIVE_stream_size_align\" aria-label=\"LIVE_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Image format to be used\n                            </span>\n                            <label for=\"LIVE_format\">Stream Format:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"LIVE_format\" id=\"LIVE_format\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.format == \"XBGR8888\" %}\n                                <option value=\"XBGR8888\" selected>XBGR8888</option>\n                                {% else %}\n                                <option value=\"XBGR8888\">XBGR8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"XRGB8888\" %}\n                                <option value=\"XRGB8888\" selected>XRGB8888</option>\n                                {% else %}\n                                <option value=\"XRGB8888\">XRGB8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"RGB888\" %}\n                                <option value=\"RGB888\" selected>RGB888</option>\n                                {% else %}\n                                <option value=\"RGB888\">RGB888</option>\n                                {% endif %}\n                                {% if cfg.format == \"BGR888\" %}\n                                <option value=\"BGR888\" selected>BGR888</option>\n                                {% else %}\n                                <option value=\"BGR888\">BGR888</option>\n                                {% endif %}\n                                {% if cfg.format == \"YUV420\" %}\n                                <option value=\"YUV420\" selected>YUV420</option>\n                                {% else %}\n                                <option value=\"YUV420\">YUV420</option>\n                                {% endif %}\n                                {% else %}\n                                {% for fmt in cfgrf %}\n                                {% if cfg.format == fmt %}\n                                <option value=\"{{ fmt }}\" selected>{{ fmt }}</option>\n                                {% else %}\n                                <option value=\"{{ fmt }}\">{{ fmt }}</option>\n                                {% endif %}\n                                {% endfor %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the camera streams are to be shown in the preview window. <br>\n                                Note: Preview view is not used within this applacation.\n                            </span>\n                            <label for=\"LIVE_display\">Display:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"LIVE_display\" name=\"LIVE_display\" value=\"{{ cfg.display }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the streams are to be encoded if a video recording is started. \n                            </span>\n                            <label for=\"LIVE_encode\">Encode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"LIVE_encode\" name=\"LIVE_encode\" value=\"{{ cfg.encode }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Submit & Apply\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            {% if sc.activeCameraIsUsb == False %}\n            <h5>Controls included in Configuration</h5>\n            <form method=\"post\" action=\"{{ url_for('config.remLiveViewControls') }}\">\n                <table class=\"w3-table-all\">\n                    {% for ctrl_key, ctrl_value in cfg.controls.items() %}\n                    <tr>\n                        <td style=\"width:5%\">\n                            <input type=\"checkbox\" id=\"sel_LIVE_{{ ctrl_key }}\" name=\"sel_LIVE_{{ ctrl_key }}\" aria-label=\"sel_LIVE_{{ ctrl_key }}\" value=\"0\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <label for=\"LIVE_control\">{{ ctrl_key }}:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input id=\"LIVE_control\" name=\"LIVE_control\" value=\"{{ ctrl_value }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    {% endfor%}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Remove Selected Ctrls\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            <form method=\"post\" action=\"{{ url_for('config.addLiveViewControls') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Add Active Ctrls\">\n            </form>\n            {% endif %}\n        </div>\n    </div>\n\n    <!-- Photo configuration -->\n    {% if sc.lastConfigTab == \"cfgphoto\" %}\n    <div id=\"cfgphoto\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfgphoto\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for Photo Taking</h4>\n        {% set cfg = cfgphoto %}\n        {% set cfgId = \"FOTO\" %}\n        <div>\n            <form method=\"post\" action=\"{{ url_for('config.photoCfg') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This is the unique ID of the configuration. It is only used to distinguish\n                                different configuration sets within this application.\n                                It is not relevant for the camera system.\n                            </span>\n                            <label for=\"{{ cfgId }}_id\">ID:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_id\" name=\"{{ cfgId }}_id\" value=\"{{ cfg.id }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Usecase for this configuration.\n                            </span>\n                            <label for=\"{{ cfgId }}_use_case\">Use Case:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_use_case\" name=\"{{ cfgId }}_use_case\" value=\"{{ cfg.use_case }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether camera images are horizontally or vertically mirrored,\n                                or both (giving a 180 degree rotation).\n                            </span>\n                            <label for=\"{{ cfgId }}_transform_hflip\">Transform (flip ↔ or ↕ ):</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            ↔\n                            {% if cfg.transform_hflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            ↕\n                            {% if cfg.transform_vflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The colour space of the output images.\n                                The main and lores streams must always share the same colour space.\n                                The raw stream is always in a camera-specific colour space.\n                            </span>\n                            <label for=\"{{ cfgId }}_colour_space\">Colour Space:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_colour_space\" id=\"{{ cfgId }}_colour_space\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.colour_space == \"sYCC\" %}\n                                <option value=\"sYCC\" selected>sYCC</option>\n                                {% else %}\n                                <option value=\"sYCC\">sYCC</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Smpte170m\" %}\n                                <option value=\"Smpte170m\" selected>Smpte170m</option>\n                                {% else %}\n                                <option value=\"Smpte170m\">Smpte170m</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Rec709\" %}\n                                <option value=\"Rec709\" selected>Rec709</option>\n                                {% else %}\n                                <option value=\"Rec709\">Rec709</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"{{ cfg.colour_space }}\" selected>{{ cfg.colour_space }}</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                the number of sets of buffers to allocate for the camera system.\n                                A single set of buffers represents\n                                one buffer for each of the streams that have been requested.\n                            </span>\n                            <label for=\"{{ cfgId }}_buffer_count\">Buffer Count:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input type=\"number\" id=\"{{ cfgId }}_buffer_count\" name=\"{{ cfgId }}_buffer_count\" min=\"1\" max=\"12\"\n                                value=\"{{ cfg.buffer_count }}\">\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is allowed to queue up a frame\n                                ready for a capture request.\n                            </span>\n                            <label for=\"{{ cfgId }}_queue\">Queue:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if cfg.queue == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                            <!-- This cell is used to store an invisible list of sensor mode data -->\n                            {% for mode in sm %}\n                            <p id=\"sensormode_ref{{ mode.id }}\" style=\"display:none\">Size: {{ mode.size }} FPS: {{ mode.fps }} Bit\n                                Depth: {{ mode.bit_depth }}</p>\n                            {% endfor%}\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Sensor Mode to be used in this configuration<br>\n                                For details, see the available Sensor Modes under 'Info'\n                            </span>\n                            <label for=\"{{ cfgId }}_sensor_mode\">Sensor Mode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_sensor_mode\" id=\"{{ cfgId }}_sensor_mode\"\n                                onchange=\"sensorModeChanged('{{ cfgId }}_sensor_mode', '{{ cfgId }}_sensor_mode_data', '{{ cfgId }}_stream_size_width', '{{ cfgId }}_stream_size_height', '{{ cfgId }}_stream_size_align')\">\n                                {% for mode in sm %}\n                                {% if cfg.sensor_mode == mode.id %}\n                                <option value=\"{{ mode.id }}\" selected>Sensor Mode {{ mode.id }}</option>\n                                {% else %}\n                                <option value=\"{{ mode.id }}\">Sensor Mode {{ mode.id }}</option>\n                                {% endif %}\n                                {% endfor%}\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.sensor_mode == \"custom\" %}\n                                <option value=\"custom\" selected>Custom</option>\n                                {% else %}\n                                <option value=\"custom\">Custom</option>\n                                {% endif %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\" id=\"{{ cfgId }}_sensor_mode_data\">\n                            {% for mode in sm %}\n                            {% if cfg.sensor_mode == mode.id %}\n                            Size: {{ mode.size }} FPS: {{ mode.fps }} Bit Depth: {{ mode.bit_depth }}\n                            {% endif %}\n                            {% endfor%}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Camera stream to use\n                            </span>\n                            <label for=\"{{ cfgId }}_stream\">Stream:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_stream\" id=\"{{ cfgId }}_stream\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.stream == \"main\" %}\n                                <option value=\"main\" selected>main</option>\n                                {% else %}\n                                <option value=\"main\">main</option>\n                                {% endif %}\n                                {% if cfg.stream == \"lores\" %}\n                                <option value=\"lores\" selected>lores</option>\n                                {% else %}\n                                <option value=\"lores\">lores</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"main\" selected>main</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Output size of the stream. Required if 'Custom' has been chosen for Sensor Mode\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_width\">Stream Size (width, height) </label>\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is requested to align the custom output size\n                                with available Sensor Modes.\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_align\">Stream size aligned with Sensor Modes:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Image format to be used\n                            </span>\n                            <label for=\"{{ cfgId }}_format\">Stream Format:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_format\" id=\"{{ cfgId }}_format\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.format == \"XBGR8888\" %}\n                                <option value=\"XBGR8888\" selected>XBGR8888</option>\n                                {% else %}\n                                <option value=\"XBGR8888\">XBGR8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"XRGB8888\" %}\n                                <option value=\"XRGB8888\" selected>XRGB8888</option>\n                                {% else %}\n                                <option value=\"XRGB8888\">XRGB8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"RGB888\" %}\n                                <option value=\"RGB888\" selected>RGB888</option>\n                                {% else %}\n                                <option value=\"RGB888\">RGB888</option>\n                                {% endif %}\n                                {% if cfg.format == \"BGR888\" %}\n                                <option value=\"BGR888\" selected>BGR888</option>\n                                {% else %}\n                                <option value=\"BGR888\">BGR888</option>\n                                {% endif %}\n                                {% if cfg.format == \"YUV420\" %}\n                                <option value=\"YUV420\" selected>YUV420</option>\n                                {% else %}\n                                <option value=\"YUV420\">YUV420</option>\n                                {% endif %}\n                                {% else %}\n                                {% for fmt in cfgrf %}\n                                {% if cfg.format == fmt %}\n                                <option value=\"{{ fmt }}\" selected>{{ fmt }}</option>\n                                {% else %}\n                                <option value=\"{{ fmt }}\">{{ fmt }}</option>\n                                {% endif %}\n                                {% endfor %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the camera streams are to be shown in the preview window. <br>\n                                Note: Preview view is not used within this applacation.\n                            </span>\n                            <label for=\"{{ cfgId }}_display\">Display:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_display\" name=\"{{ cfgId }}_display\" value=\"{{ cfg.display }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the streams are to be encoded if a video recording is started.\n                            </span>\n                            <label for=\"{{ cfgId }}_encode\">Encode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_encode\" name=\"{{ cfgId }}_encode\" value=\"{{ cfg.encode }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Submit\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            {% if sc.activeCameraIsUsb == False %}\n            <h5>Controls included in Configuration</h5>\n            <form method=\"post\" action=\"{{ url_for('config.remPhotoControls') }}\">\n                <table class=\"w3-table-all\">\n                    {% for ctrl_key, ctrl_value in cfg.controls.items() %}\n                    <tr>\n                        <td style=\"width:5%\">\n                            <input type=\"checkbox\" id=\"sel_{{ cfgId }}_{{ ctrl_key }}\" name=\"sel_{{ cfgId }}_{{ ctrl_key }}\" aria-label=\"sel_{{ cfgId }}_{{ ctrl_key }}\" value=\"0\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <label for=\"{{ cfgId }}_control\">{{ ctrl_key }}:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input id=\"{{ cfgId }}_control\" name=\"{{ cfgId }}_control\" value=\"{{ ctrl_value }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    {% endfor%}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Remove Selected Ctrls\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            <form method=\"post\" action=\"{{ url_for('config.addPhotoControls') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Add Active Ctrls\">\n            </form>\n            {% endif %}\n        </div>    \n    </div>\n\n    <!-- Raw Photo configuration -->\n    {% if sc.lastConfigTab == \"cfgraw\" %}\n    <div id=\"cfgraw\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfgraw\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for Raw Photo Taking</h4>\n        {% set cfg = cfgraw %}\n        {% set cfgId = \"PRAW\" %}\n        <div>\n            <form method=\"post\" action=\"{{ url_for('config.rawCfg') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This is the unique ID of the configuration. It is only used to distinguish\n                                different configuration sets within this application.\n                                It is not relevant for the camera system.\n                            </span>\n                            <label for=\"{{ cfgId }}_id\">ID:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_id\" name=\"{{ cfgId }}_id\" value=\"{{ cfg.id }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Usecase for this configuration.\n                            </span>\n                            <label for=\"{{ cfgId }}_use_case\">Use Case:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_use_case\" name=\"{{ cfgId }}_use_case\" value=\"{{ cfg.use_case }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether camera images are horizontally or vertically mirrored,\n                                or both (giving a 180 degree rotation).\n                            </span>\n                            <label for=\"{{ cfgId }}_transform_hflip\">Transform (flip ↔ or ↕ ):</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            ↔\n                            {% if cfg.transform_hflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            ↕\n                            {% if cfg.transform_vflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The colour space of the output images.\n                                The main and lores streams must always share the same colour space.\n                                The raw stream is always in a camera-specific colour space.\n                            </span>\n                            <label for=\"{{ cfgId }}_colour_space\">Colour Space:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_colour_space\" id=\"{{ cfgId }}_colour_space\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.colour_space == \"sYCC\" %}\n                                <option value=\"sYCC\" selected>sYCC</option>\n                                {% else %}\n                                <option value=\"sYCC\">sYCC</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Smpte170m\" %}\n                                <option value=\"Smpte170m\" selected>Smpte170m</option>\n                                {% else %}\n                                <option value=\"Smpte170m\">Smpte170m</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Rec709\" %}\n                                <option value=\"Rec709\" selected>Rec709</option>\n                                {% else %}\n                                <option value=\"Rec709\">Rec709</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"{{ cfg.colour_space }}\" selected>{{ cfg.colour_space }}</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                the number of sets of buffers to allocate for the camera system.\n                                A single set of buffers represents\n                                one buffer for each of the streams that have been requested.\n                            </span>\n                            <label for=\"{{ cfgId }}_buffer_count\">Buffer Count:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input type=\"number\" id=\"{{ cfgId }}_buffer_count\" name=\"{{ cfgId }}_buffer_count\" min=\"1\" max=\"12\"\n                                value=\"{{ cfg.buffer_count }}\">\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is allowed to queue up a frame\n                                ready for a capture request.\n                            </span>\n                            <label for=\"{{ cfgId }}_queue\">Queue:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if cfg.queue == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                            <!-- This cell is used to store an invisible list of sensor mode data -->\n                            {% for mode in sm %}\n                            <p id=\"sensormode_ref{{ mode.id }}\" style=\"display:none\">Size: {{ mode.size }} FPS: {{ mode.fps }} Bit\n                                Depth: {{ mode.bit_depth }}</p>\n                            {% endfor%}\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Sensor Mode to be used in this configuration<br>\n                                For details, see the available Sensor Modes under 'Info'\n                            </span>\n                            <label for=\"{{ cfgId }}_sensor_mode\">Sensor Mode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_sensor_mode\" id=\"{{ cfgId }}_sensor_mode\"\n                                onchange=\"sensorModeChanged('{{ cfgId }}_sensor_mode', '{{ cfgId }}_sensor_mode_data', '{{ cfgId }}_stream_size_width', '{{ cfgId }}_stream_size_height', '{{ cfgId }}_stream_size_align')\">\n                                {% for mode in sm %}\n                                {% if cfg.sensor_mode == mode.id %}\n                                <option value=\"{{ mode.id }}\" selected>Sensor Mode {{ mode.id }}</option>\n                                {% else %}\n                                <option value=\"{{ mode.id }}\">Sensor Mode {{ mode.id }}</option>\n                                {% endif %}\n                                {% endfor%}\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.sensor_mode == \"custom\" %}\n                                <option value=\"custom\" selected>Custom</option>\n                                {% else %}\n                                <option value=\"custom\">Custom</option>\n                                {% endif %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\" id=\"{{ cfgId }}_sensor_mode_data\">\n                            {% for mode in sm %}\n                            {% if cfg.sensor_mode == mode.id %}\n                            Size: {{ mode.size }} FPS: {{ mode.fps }} Bit Depth: {{ mode.bit_depth }}\n                            {% endif %}\n                            {% endfor%}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Camera stream to use\n                            </span>\n                            <label for=\"{{ cfgId }}_stream\">Stream:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <!-- A T T E N T I O N : Specific for raw -->\n                            <select name=\"{{ cfgId }}_stream\" id=\"{{ cfgId }}_stream\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.stream == \"raw\" %}\n                                <option value=\"raw\" selected>raw</option>\n                                {% else %}\n                                <option value=\"raw\">raw</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"main\" selected>main</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Output size of the stream. Required if 'Custom' has been chosen for Sensor Mode\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_width\">Stream Size (width, height) </label>\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is requested to align the custom output size\n                                with available Sensor Modes.\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_align\">Stream size aligned with Sensor Modes:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Image format to be used\n                            </span>\n                            <label for=\"{{ cfgId }}_format\">Stream Format:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_format\" id=\"{{ cfgId }}_format\">\n                                <!-- A T T E N T I O N : Specific for raw -->\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% for fmt in cfgrf %}\n                                {% if cfg.format == fmt %}\n                                <option value=\"{{ fmt }}\" selected>{{ fmt }}</option>\n                                {% else %}\n                                <option value=\"{{ fmt }}\">{{ fmt }}</option>\n                                {% endif %}\n                                {% endfor %}\n                                {% else %}\n                                <option value=\"tiff\" selected>tiff</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the camera streams are to be shown in the preview window. <br>\n                                Note: Preview view is not used within this applacation.\n                            </span>\n                            <label for=\"{{ cfgId }}_display\">Display:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_display\" name=\"{{ cfgId }}_display\" value=\"{{ cfg.display }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the streams are to be encoded if a video recording is started.\n                            </span>\n                            <label for=\"{{ cfgId }}_encode\">Encode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_encode\" name=\"{{ cfgId }}_encode\" value=\"{{ cfg.encode }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Submit\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            {% if sc.activeCameraIsUsb == False %}\n            <h5>Controls included in Configuration</h5>\n            <form method=\"post\" action=\"{{ url_for('config.remRawControls') }}\">\n                <table class=\"w3-table-all\">\n                    {% for ctrl_key, ctrl_value in cfg.controls.items() %}\n                    <tr>\n                        <td style=\"width:5%\">\n                            <input type=\"checkbox\" id=\"sel_{{ cfgId }}_{{ ctrl_key }}\" name=\"sel_{{ cfgId }}_{{ ctrl_key }}\" aria-label=\"sel_{{ cfgId }}_{{ ctrl_key }}\" value=\"0\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <label for=\"{{ cfgId }}_control\">{{ ctrl_key }}:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input id=\"{{ cfgId }}_control\" name=\"{{ cfgId }}_control\" value=\"{{ ctrl_value }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    {% endfor%}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Remove Selected Ctrls\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            <form method=\"post\" action=\"{{ url_for('config.addRawControls') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Add Active Ctrls\">\n            </form>\n            {% endif %}\n        </div>\n    </div>\n\n    <!-- Raw Photo configuration -->\n    {% if sc.lastConfigTab == \"cfgvideo\" %}\n    <div id=\"cfgvideo\" class=\"cfggroup\">\n    {% else %}\n    <div id=\"cfgvideo\" class=\"cfggroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Configuration for Video Recording</h4>\n        {% set cfg = cfgvideo %}\n        {% set cfgId = \"VIDO\" %}\n        <div>\n            <form method=\"post\" action=\"{{ url_for('config.videoCfg') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This is the unique ID of the configuration. It is only used to distinguish\n                                different configuration sets within this application.\n                                It is not relevant for the camera system.\n                            </span>\n                            <label for=\"{{ cfgId }}_id\">ID:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_id\" name=\"{{ cfgId }}_id\" value=\"{{ cfg.id }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Usecase for this configuration.\n                            </span>\n                            <label for=\"{{ cfgId }}_use_case\">Use Case:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_use_case\" name=\"{{ cfgId }}_use_case\" value=\"{{ cfg.use_case }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether camera images are horizontally or vertically mirrored,\n                                or both (giving a 180 degree rotation).\n                            </span>\n                            <label for=\"{{ cfgId }}_transform_hflip\">Transform (flip ↔ or ↕ ):</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            ↔\n                            {% if cfg.transform_hflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_hflip\" name=\"{{ cfgId }}_transform_hflip\" aria-label=\"{{ cfgId }}_transform_hflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            ↕\n                            {% if cfg.transform_vflip == true %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_transform_vflip\" name=\"{{ cfgId }}_transform_vflip\" aria-label=\"{{ cfgId }}_transform_vflip\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The colour space of the output images.\n                                The main and lores streams must always share the same colour space.\n                                The raw stream is always in a camera-specific colour space.\n                            </span>\n                            <label for=\"{{ cfgId }}_colour_space\">Colour Space:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_colour_space\" id=\"{{ cfgId }}_colour_space\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.colour_space == \"sYCC\" %}\n                                <option value=\"sYCC\" selected>sYCC</option>\n                                {% else %}\n                                <option value=\"sYCC\">sYCC</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Smpte170m\" %}\n                                <option value=\"Smpte170m\" selected>Smpte170m</option>\n                                {% else %}\n                                <option value=\"Smpte170m\">Smpte170m</option>\n                                {% endif %}\n                                {% if cfg.colour_space == \"Rec709\" %}\n                                <option value=\"Rec709\" selected>Rec709</option>\n                                {% else %}\n                                <option value=\"Rec709\">Rec709</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"{{ cfg.colour_space }}\" selected>{{ cfg.colour_space }}</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                the number of sets of buffers to allocate for the camera system.\n                                A single set of buffers represents\n                                one buffer for each of the streams that have been requested.\n                            </span>\n                            <label for=\"{{ cfgId }}_buffer_count\">Buffer Count:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input type=\"number\" id=\"{{ cfgId }}_buffer_count\" name=\"{{ cfgId }}_buffer_count\" min=\"1\" max=\"12\"\n                                value=\"{{ cfg.buffer_count }}\">\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is allowed to queue up a frame\n                                ready for a capture request.\n                            </span>\n                            <label for=\"{{ cfgId }}_queue\">Queue:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if cfg.queue == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_queue\" name=\"{{ cfgId }}_queue\" aria-label=\"{{ cfgId }}_queue\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                            <!-- This cell is used to store an invisible list of sensor mode data -->\n                            {% for mode in sm %}\n                            <p id=\"sensormode_ref{{ mode.id }}\" style=\"display:none\">Size: {{ mode.size }} FPS: {{ mode.fps }} Bit\n                                Depth: {{ mode.bit_depth }}</p>\n                            {% endfor%}\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Sensor Mode to be used in this configuration<br>\n                                For details, see the available Sensor Modes under 'Info'\n                            </span>\n                            <label for=\"{{ cfgId }}_sensor_mode\">Sensor Mode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_sensor_mode\" id=\"{{ cfgId }}_sensor_mode\"\n                                onchange=\"sensorModeChanged('{{ cfgId }}_sensor_mode', '{{ cfgId }}_sensor_mode_data', '{{ cfgId }}_stream_size_width', '{{ cfgId }}_stream_size_height', '{{ cfgId }}_stream_size_align')\">\n                                {% for mode in sm %}\n                                {% if cfg.sensor_mode == mode.id %}\n                                <option value=\"{{ mode.id }}\" selected>Sensor Mode {{ mode.id }}</option>\n                                {% else %}\n                                <option value=\"{{ mode.id }}\">Sensor Mode {{ mode.id }}</option>\n                                {% endif %}\n                                {% endfor%}\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.sensor_mode == \"custom\" %}\n                                <option value=\"custom\" selected>Custom</option>\n                                {% else %}\n                                <option value=\"custom\">Custom</option>\n                                {% endif %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\" id=\"{{ cfgId }}_sensor_mode_data\">\n                            {% for mode in sm %}\n                            {% if cfg.sensor_mode == mode.id %}\n                            Size: {{ mode.size }} FPS: {{ mode.fps }} Bit Depth: {{ mode.bit_depth }}\n                            {% endif %}\n                            {% endfor%}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Camera stream to use\n                            </span>\n                            <label for=\"{{ cfgId }}_stream\">Stream:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_stream\" id=\"{{ cfgId }}_stream\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.stream == \"main\" %}\n                                <option value=\"main\" selected>main</option>\n                                {% else %}\n                                <option value=\"main\">main</option>\n                                {% endif %}\n                                {% if cfg.stream == \"lores\" %}\n                                <option value=\"lores\" selected>lores</option>\n                                {% else %}\n                                <option value=\"lores\">lores</option>\n                                {% endif %}\n                                {% else %}\n                                <option value=\"main\">main</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Output size of the stream. Required if 'Custom' has been chosen for Sensor Mode\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_width\">Stream Size (width, height) </label>\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_width\" name=\"{{ cfgId }}_stream_size_width\" \n                                min=\"1\" max={{ cp.pixelArraySize[0] }} value=\"{{ cfg.stream_size[0] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"{{ cfgId }}_stream_size_height\" name=\"{{ cfgId }}_stream_size_height\" aria-label=\"{{ cfgId }}_stream_size_height\" \n                                min=\"1\" max={{ cp.pixelArraySize[1] }} value=\"{{ cfg.stream_size[1] }}\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Whether the system is requested to align the custom output size\n                                with available Sensor Modes.\n                            </span>\n                            <label for=\"{{ cfgId }}_stream_size_align\">Stream size aligned with Sensor Modes:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% if cfg.sensor_mode == \"custom\" %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            {% if cfg.stream_size_align == True %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"{{ cfgId }}_stream_size_align\" name=\"{{ cfgId }}_stream_size_align\" aria-label=\"{{ cfgId }}_stream_size_align\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Image format to be used\n                            </span>\n                            <label for=\"{{ cfgId }}_format\">Stream Format:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <select name=\"{{ cfgId }}_format\" id=\"{{ cfgId }}_format\">\n                                {% if sc.activeCameraIsUsb == False %}\n                                {% if cfg.format == \"XBGR8888\" %}\n                                <option value=\"XBGR8888\" selected>XBGR8888</option>\n                                {% else %}\n                                <option value=\"XBGR8888\">XBGR8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"XRGB8888\" %}\n                                <option value=\"XRGB8888\" selected>XRGB8888</option>\n                                {% else %}\n                                <option value=\"XRGB8888\">XRGB8888</option>\n                                {% endif %}\n                                {% if cfg.format == \"RGB888\" %}\n                                <option value=\"RGB888\" selected>RGB888</option>\n                                {% else %}\n                                <option value=\"RGB888\">RGB888</option>\n                                {% endif %}\n                                {% if cfg.format == \"BGR888\" %}\n                                <option value=\"BGR888\" selected>BGR888</option>\n                                {% else %}\n                                <option value=\"BGR888\">BGR888</option>\n                                {% endif %}\n                                {% if cfg.format == \"YUV420\" %}\n                                <option value=\"YUV420\" selected>YUV420</option>\n                                {% else %}\n                                <option value=\"YUV420\">YUV420</option>\n                                {% endif %}\n                                {% else %}\n                                {% for fmt in cfgrf %}\n                                {% if cfg.format == fmt %}\n                                <option value=\"{{ fmt }}\" selected>{{ fmt }}</option>\n                                {% else %}\n                                <option value=\"{{ fmt }}\">{{ fmt }}</option>\n                                {% endif %}\n                                {% endfor %}\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the camera streams are to be shown in the preview window. <br>\n                                Note: Preview view is not used within this applacation.\n                            </span>\n                            <label for=\"{{ cfgId }}_display\">Display:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_display\" name=\"{{ cfgId }}_display\" value=\"{{ cfg.display }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This names which (if any) of the streams are to be encoded if a video recording is started.\n                            </span>\n                            <label for=\"{{ cfgId }}_encode\">Encode:</label>\n                        </td>\n                        <td style=\"width:20%\" colspan=\"2\">\n                            <input id=\"{{ cfgId }}_encode\" name=\"{{ cfgId }}_encode\" value=\"{{ cfg.encode }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Submit\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            {% if sc.activeCameraIsUsb == False %}\n            <h5>Controls included in Configuration</h5>\n            <form method=\"post\" action=\"{{ url_for('config.remVideoControls') }}\">\n                <table class=\"w3-table-all\">\n                    {% for ctrl_key, ctrl_value in cfg.controls.items() %}\n                    <tr>\n                        <td style=\"width:5%\">\n                            <input type=\"checkbox\" id=\"sel_{{ cfgId }}_{{ ctrl_key }}\" name=\"sel_{{ cfgId }}_{{ ctrl_key }}\" aria-label=\"sel_{{ cfgId }}_{{ ctrl_key }}\" value=\"0\">\n                        </td>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <label for=\"{{ cfgId }}_control\">{{ ctrl_key }}:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input id=\"{{ cfgId }}_control\" name=\"{{ cfgId }}_control\" value=\"{{ ctrl_value }}\" disabled>\n                        </td>\n                        <td style=\"width:45%\">\n                        </td>\n                    </tr>\n                    {% endfor%}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Remove Selected Ctrls\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            <form method=\"post\" action=\"{{ url_for('config.addVideoControls') }}\">\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Add Active Ctrls\">\n            </form>\n            {% endif %}\n        </div>\n    </div>\n\n    <script>\n        function openCfgTab(cfgTabName, cfgTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"cfggroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(cfgTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"configmenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button configmenu\";\n            }\n            document.getElementById(cfgTabButton).classList = \"w3-bar-item w3-button configmenu w3-light-green\";\n\n            document.getElementById(\"activecfgtab\").value = cfgTabName\n\n        }\n\n        function sensorModeChanged(srcId, tgtId, ref1Id, ref2Id, ref3Id) {\n            // React on a value change of sensorMode\n            // srcId: id of source element (sensor mode selection)\n            // tgtId: id of field where details of sensor mode shall be shown\n            // refnId: IDs for fields which need to be enabled ar disabled\n            var src = document.getElementById(srcId);\n            var val = src.value;\n            var tgt = document.getElementById(tgtId);\n            if (val == \"custom\"){\n                tgt.innerText = \"\"\n                document.getElementById(ref1Id).disabled = false;\n                document.getElementById(ref2Id).disabled = false;\n                document.getElementById(ref3Id).disabled = false;\n            }else{\n                var txt = document.getElementById(\"sensormode_ref\" + val).innerText;\n                tgt.innerText = txt\n                document.getElementById(ref1Id).value = \"\";\n                document.getElementById(ref2Id).value = \"\";\n                document.getElementById(ref1Id).disabled = true;\n                document.getElementById(ref2Id).disabled = true;\n                document.getElementById(ref3Id).disabled = true;\n            }\n        }\n        \n        function syncAspectRatio(form) {\n            document.getElementById(form).submit();\n        }\n\n        function confirmDeleteTuningFile(form) {\n            if (confirm(\"Do you want to delete the active tuning file?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function blockTuningFileDeletion() {\n            document.getElementById(\"deletetuningfilebtn\").disabled = true;\n        }\n\n        const upld = document.getElementById(\"uploadtuningfile\");\n        upld.addEventListener(\"change\", function (e) {\n            //On change of input file (file(s) have been selected),\n            //Change label to the selected file.\n            //Modified source from \n            //https://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/\n            var upldlbl = document.getElementById(\"uploadtuningfilelbl\");\n            labelVal = upldlbl.innerHTML;\n            //console.log(\"upldlbl.innerHTML:\", labelVal);\n            var fileName = '';\n            if (this.files && this.files.length > 1)\n                fileName = (this.getAttribute('data-multiple-caption') || '').replace('{count}', this.files.length) + ' Files selected';\n            else\n                fileName = e.target.value.split('\\\\').pop();\n\n            //console.log(\"fileName:\", fileName);\n            if (fileName)\n                upldlbl.innerHTML = fileName;\n            else\n                upldlbl.innerHTML = labelVal;\n        })\n\n        function enableAi(elmt, form) {\n            var chk = document.getElementById(elmt);\n            if (chk.checked) {\n                var msg = \"Do you want to enable AI processing for the active camera?\\nThe camera will work with the selected model file\";\n            } else {\n                var msg = \"Do you want to disable AI processing for the active camera?\\n\\nBe aware: To reset the camera to work without AI,\\nraspiCamSrv will stop and restart the camera system for the Active Camera.\";\n            }\n            if (confirm(msg)) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function confirmModelBase(form) {\n            if (confirm(\"Do you want to continue with the given model location and task?\\nThe system will now look for suitable models.\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function doSubmit(form) {\n            document.getElementById(form).submit();\n        }\n\n        function onlineHelp() {\n            window.open(\"{{ sc.getBaseHelpUrl() }}/Configuration/\");\n        }\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "raspiCamSrv/templates/console/console.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Console{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Console menue -->\n        {% if sc.lastConsoleTab == \"versbuttons\" %}\n        <button class=\"w3-bar-item w3-button consolemenu w3-light-green\" id=\"versbuttonsbtn\"\n            onclick=\"openConsoleTab('versbuttons', 'versbuttonsbtn')\">Versatile Buttons</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button consolemenu\" id=\"consoleversbuttonsbtn\"\n            onclick=\"openConsoleTab('versbuttons', 'versbuttonsbtn')\">Versatile Buttons</button>\n        {% endif %}\n        {% if sc.lastConsoleTab == \"actionbuttons\" %}\n        <button class=\"w3-bar-item w3-button consolemenu w3-light-green\" id=\"actionbuttonsbtn\"\n            onclick=\"openConsoleTab('actionbuttons', 'actionbuttonsbtn')\">Action Buttons</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button consolemenu\" id=\"actionbuttonsbtn\"\n            onclick=\"openConsoleTab('actionbuttons', 'actionbuttonsbtn')\">Action Buttons</button>\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\"\n                    class=\"w3-text w3-tag\">Online help from GitHub\n                </span>\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\"\n                    alt=\"Online help\" style=\"height:34px; width:34px\"\n                    onclick=\"onlineHelp()\">\n            </div>\n        </div>\n    </div>\n    {% if sc.lastConsoleTab == \"versbuttons\" %}\n    <div id=\"versbuttons\" class=\"consolegroup\">\n    {% else %}\n    <div id=\"versbuttons\" class=\"consolegroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Versatile Buttons</h4>\n        {% if sc.vButtonsRows == 0 %}\n        <p>There are no buttons configured.</p>\n        <p>You can configure buttons for execution of OS commands in Settings/Versatile Buttons</p>\n        {% else %}\n        <div>\n            {% set nrcols=sc.vButtonsCols %}\n            {% set cw = 100/nrcols %}\n            {% set cws = cw|string() %}\n    <!--       \n            {% set cst = 'style=width:' + cws + '%'%}\n    -->\n            {% set cst = 'width:' + cws + '%; min-height:38.5px; height:38.5px' %}\n            <table style=\"table-layout:fixed; width:100%\">\n                {% for r in sc.vButtons %}\n                <tr style=\"height:38.5px\">\n                    {% for btn in r %}\n                    {% set shape = btn[\"buttonShape\"] %}\n                    {% set color = btn[\"buttonColor\"] %}\n                    {% if shape == \"Circular\" %}\n                    <!-- Attantion: Syntax check will not be OK for the following line -->\n                    <td style=\"{{ cst }}\"; align=\"center\">\n                    {% elif shape == \"Square\" %}\n                    <!-- Attantion: Syntax check will not be OK for the following line -->\n                    <td {{ cst }} align=\"center\">\n                    {% else %}\n                    <td {{ cst }}>\n                    {% endif %}\n                        {% if btn[\"isVisible\"] == True %}\n\n                        {% set cls1 = 'w3-button' %}\n                        {% set cls = cls1 + ' w3-black' %}\n\n                        {% if shape == \"Rectangle\" %}\n                            {% set btnshp = \"w3-button\" %}\n                            {% set style = 'width: 100%' %}\n                        {% elif shape == \"Rounded\" %}\n                            {% set btnshp = 'w3-button w3-round-xxlarge' %}\n                            {% set style = 'width: 100%' %}\n                        {% elif shape == \"Circular\" %}\n                            {% set btnshp = 'w3-button w3-circle' %}\n                            {% set style = 'text-align:center' %}\n                        {% elif shape == \"Square\" %}\n                            {% set btnshp = 'w3-button w3-large' %}\n                            {% set style = 'text-align:center' %}\n                        {% endif %}\n                        {% if color == \"Black\" %}\n                            {% set btnall = btnshp + ' w3-black' %}\n                        {% elif color == \"Red\" %}\n                            {% set btnall = btnshp + ' w3-red' %}\n                        {% elif color == \"Green\" %}\n                            {% set btnall = btnshp + ' w3-green' %}\n                        {% elif color == \"Yellow\" %}\n                            {% set btnall = btnshp + ' w3-yellow' %}\n                        {% elif color == \"Blue\" %}\n                            {% set btnall = btnshp + ' w3-blue' %}\n                        {% elif color == \"White\" %}\n                            {% set btnall = btnshp + ' w3-white' %}\n                        {% endif %}\n                        {% set buttonid = \"vbtn_\" ~ btn['row'] ~ btn['col'] ~ \"_form\" %}\n                        {% set buttonex = btn['buttonExec'] %}\n                        {% set buttonconf = btn['needsConfirm'] %}\n                        <form id=\"{{ buttonid }}\" \n                            method=\"post\" action=\"{{ url_for('console.execute', row=btn['row'], col=btn['col']) }}\">\n                            <!-- Attantion: Syntax check will not be OK for the following line-->\n                            <input class=\"{{ btnall }}\" style=\"{{ style }}\" type=\"submit\" \n                                onclick=\"confirmExecution('{{ buttonid }}', '{{ buttonex }}', '{{ buttonconf }}')\"\n                                value=\"{{ btn['buttonText'] }}\">\n                        </form>\n                        {% endif %}\n                    </td>\n                    {% endfor %}\n                </tr>\n                {% endfor %}\n            </table>\n        </div>\n        <p>&nbsp;</p>\n        <hr>\n        <p>&nbsp;</p>\n        <h4>Execution Result</h4>\n        <table  class=\"w3-table-all\">\n            <tr>\n                <td style=\"width:15%\">\n                    <b>Command:</b>\n                </td>\n                <td style=\"width:80%\">\n                    {% if sc.vButtonHasCommandLine == False %}\n                    {% if sc.vButtonCommand is not none %}\n                    {{ sc.vButtonCommand }}\n                    {% endif %}\n                    {% else %}\n                    <form id=\"commandlineform\" method=\"post\" action=\"{{ url_for('console.execCommandline') }}\">\n                        <input style=\"width: 100%\" type=\"text\" id=\"commandline\" name=\"commandline\" \n                            onchange=\"doSubmit('commandlineform')\"\n                            value=\"{{ sc.vButtonCommand }}\">\n                    </form>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">\n                    <b>Run Arguments:</b>\n                </td>\n                <td style=\"width:80%\">\n                    {% if sc.vButtonArgs is not none %}\n                    {{ sc.vButtonArgs|string() }}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">\n                    <b>Return Code:</b>\n                </td>\n                <td style=\"width:80%\">\n                    {% if sc.vButtonReturncode is not none %}\n                    {{ sc.vButtonReturncode }}\n                    {% else %}\n                    &nbsp;\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">\n                    <b>Stdout:</b>\n                </td>\n                <td style=\"width:80%\">\n                    {% if sc.vButtonStdout is not none %}\n                    {% if sc.vButtonStdout|length() > 0 %}\n                    <textarea cols=\"100\" rows=\"6\" readonly>{{ sc.vButtonStdout }}</textarea>\n                    {% else %}\n                    &nbsp;\n                    {% endif %}\n                    {% else %}\n                    &nbsp;\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">\n                    <b>Sterr:</b>\n                </td>\n                <td style=\"width:80%\">\n                    {% if sc.vButtonStderr is not none %}\n                    {% if sc.vButtonStderr|length() > 0 %}\n                    <textarea cols=\"100\" rows=\"6\" readonly>{{ sc.vButtonStderr }}</textarea>\n                    {% else %}\n                    &nbsp;\n                    {% endif %}\n                    {% else %}\n                    &nbsp;\n                    {% endif %}\n                </td>\n            </tr>\n        </table>\n        {% endif %}\n    </div>\n    {% if sc.lastConsoleTab == \"actionbuttons\" %}\n    <div id=\"actionbuttons\" class=\"consolegroup\">\n    {% else %}\n    <div id=\"actionbuttons\" class=\"consolegroup\" style=\"display:none\">\n    {% endif %}\n        <h4>Action Buttons</h4>\n        {% if sc.aButtonsRows == 0 %}\n        <p>There are no action buttons configured.</p>\n        <p>You can configure buttons for execution of actions in Settings/Action Buttons</p>\n        {% else %}\n        <div>\n            {% set nrcols=sc.aButtonsCols %}\n            {% set cw = 100/nrcols %}\n            {% set cws = cw|string() %}\n    <!--       \n            {% set cst = 'style=width:' + cws + '%'%}\n    -->\n            {% set cst = 'width:' + cws + '%; min-height:38.5px; height:38.5px' %}\n            <table style=\"table-layout:fixed; width:100%\">\n                {% for r in sc.aButtons %}\n                <tr style=\"height:38.5px\">\n                    {% for btn in r %}\n                    {% set shape = btn[\"buttonShape\"] %}\n                    {% set color = btn[\"buttonColor\"] %}\n                    {% if shape == \"Circular\" %}\n                    <!-- Attantion: Syntax check will not be OK for the following line -->\n                    <td style=\"{{ cst }}\"; align=\"center\">\n                    {% elif shape == \"Square\" %}\n                    <!-- Attantion: Syntax check will not be OK for the following line -->\n                    <td {{ cst }} align=\"center\">\n                    {% else %}\n                    <td {{ cst }}>\n                    {% endif %}\n                        {% if btn[\"isVisible\"] == True %}\n\n                        {% set cls1 = 'w3-button' %}\n                        {% set cls = cls1 + ' w3-black' %}\n\n                        {% if shape == \"Rectangle\" %}\n                            {% set btnshp = \"w3-button\" %}\n                            {% set style = 'width: 100%' %}\n                        {% elif shape == \"Rounded\" %}\n                            {% set btnshp = 'w3-button w3-round-xxlarge' %}\n                            {% set style = 'width: 100%' %}\n                        {% elif shape == \"Circular\" %}\n                            {% set btnshp = 'w3-button w3-circle' %}\n                            {% set style = 'text-align:center' %}\n                        {% elif shape == \"Square\" %}\n                            {% set btnshp = 'w3-button w3-large' %}\n                            {% set style = 'text-align:center' %}\n                        {% endif %}\n                        {% if color == \"Black\" %}\n                            {% set btnall = btnshp + ' w3-black' %}\n                        {% elif color == \"Red\" %}\n                            {% set btnall = btnshp + ' w3-red' %}\n                        {% elif color == \"Green\" %}\n                            {% set btnall = btnshp + ' w3-green' %}\n                        {% elif color == \"Yellow\" %}\n                            {% set btnall = btnshp + ' w3-yellow' %}\n                        {% elif color == \"Blue\" %}\n                            {% set btnall = btnshp + ' w3-blue' %}\n                        {% endif %}\n                        {% set buttonid = \"abtn_\" ~ btn['row'] ~ btn['col'] ~ \"_form\" %}\n                        {% set buttonaction = btn['buttonAction'] %}\n                        {% set buttonconf = btn['needsConfirm'] %}\n                        <form id=\"{{ buttonid }}\" \n                            method=\"post\" action=\"{{ url_for('console.do_action', row=btn['row'], col=btn['col']) }}\">\n                            <!-- Attantion: Syntax check will not be OK for the following line-->\n                            <input class=\"{{ btnall }}\" style=\"{{ style }}\" type=\"submit\" \n                                onclick=\"confirmAction('{{ buttonid }}', '{{ buttonaction }}', '{{ buttonconf }}')\"\n                                value=\"{{ btn['buttonText'] }}\">\n                        </form>\n                        {% endif %}\n                    </td>\n                    {% endfor %}\n                </tr>\n                {% endfor %}\n            </table>\n        </div>\n        {% endif %}\n    </div>\n    <script>\n        function openConsoleTab(infoTabName, infoTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"consolegroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(infoTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"consolemenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button consolemenu\";\n            }\n            document.getElementById(infoTabButton).classList = \"w3-bar-item w3-button consolemenu w3-light-green\";\n        }\n        function doSubmit(form) {\n            document.getElementById(form).submit();\n        }\n        function confirmExecution(form, command, needsConfirm) {\n            //console.log(\"confirmExecution - needsConfirm=\", needsConfirm);\n            if (needsConfirm == \"True\") {\n                if (confirm(\"Do you want to execute the following command?\\n\" + command)) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n        function confirmAction(form, action, needsConfirm) {\n            //console.log(\"confirmExecution - needsConfirm=\", needsConfirm);\n            if (needsConfirm == \"True\") {\n                if (confirm(\"Do you want to execute the following action?\\n\" + action)) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n        function onlineHelp(){\n            window.open(\"{{ sc.getBaseHelpUrl() }}/Console/\");\n        }\n    </script>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/home/index.html",
    "content": "<!--\nRaspiCamSrv's Live page    \n-->\n{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Live{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-row w3-border\">\n        <!-- First row -->\n        <div class=\"w3-half\" style=\"min-height: 440px;\">\n            <!-- Live stream -->\n            {% if sc.isLiveStream %}\n            <img \n                src=\"{{ url_for('home.live_view_feed') }}\" \n                class=\"w3-image\" id=\"liveviewimage\" \n                alt=\"Camera Live View\"\n                title=\"Click to open Direct Control Panel\"\n                style=\"cursor:pointer\"\n                onclick=\"window.location.href='{{ url_for('home.live_direct_control') }}'\"\n            >\n            {% else %}\n            {% if sc.isVideoRecording %}\n            {% if sc.recordAudio == False %}\n            <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n            {% else %}\n            <img src=\"{{ url_for('static', filename='recordingvideo_sound.jpg') }}\" class=\"w3-image\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n            {% endif %}\n            {% elif sc.isPhotoSeriesRecording %}\n            <img src=\"{{ url_for('static', filename='recordingphotoseries.jpg') }}\" class=\"w3-image\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n            {% endif %}\n            {% endif %}\n        </div>\n        <div class=\"w3-half\">\n            <!-- Camara control sections \n                Settings changed here are applied to the camera while the live stream is active\n            -->\n            <div class=\"w3-bar w3-green\">\n                <!-- Controls menue -->\n                {% if sc.lastLiveTab == \"focus\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"focusbtn\" onclick=\"openCtrlTab('focus','focusbtn')\">Focus</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"focusbtn\" onclick=\"openCtrlTab('focus','focusbtn')\">Focus</button>\n                {% endif %}\n                {% if sc.lastLiveTab == \"zoom\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"zoombtn\" onclick=\"openCtrlTab('zoom','zoombtn')\">Zoom</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"zoombtn\" onclick=\"openCtrlTab('zoom','zoombtn')\">Zoom</button>\n                {% endif %}\n                {% if sc.lastLiveTab == \"autoexposure\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"aebtn\" onclick=\"openCtrlTab('autoexposure','aebtn')\">Auto-Exposure</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"aebtn\" onclick=\"openCtrlTab('autoexposure','aebtn')\">Auto-Exposure</button>\n                {% endif %}\n                {% if sc.lastLiveTab == \"exposure\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"expbtn\" onclick=\"openCtrlTab('exposure','expbtn')\">Exposure</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"expbtn\" onclick=\"openCtrlTab('exposure','expbtn')\">Exposure</button>\n                {% endif %}\n                {% if sc.lastLiveTab == \"image\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"imagebtn\" onclick=\"openCtrlTab('image','imagebtn')\">Image</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"imagebtn\" onclick=\"openCtrlTab('image','imagebtn')\">Image</button>\n                {% endif %}\n                {% if sc.lastLiveTab == \"control\" %}\n                <button class=\"w3-bar-item w3-button ctrlmenu w3-light-green\" id=\"controlbtn\" onclick=\"openCtrlTab('control','controlbtn')\">Ctrl</button>\n                {% else %}\n                <button class=\"w3-bar-item w3-button ctrlmenu\" id=\"controlbtn\" onclick=\"openCtrlTab('control','controlbtn')\">Ctrl</button>\n                {% endif %}\n                <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n                    <div class=\"w3-tooltip\">\n                        <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from\n                            GitHub\n                        </span>\n                        <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                            style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n                    </div>\n                </div>\n            </div>\n            {% if sc.lastLiveTab == \"focus\" %}\n            <div id=\"focus\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"focus\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Focus handling -->\n                <h4>Focus handling</h4>\n                {% if cp.hasFocus %}\n                <div>\n                    <form method=\"post\" id=\"formfocuscontrol\" action=\"{{ url_for('home.focus_control') }}\">\n                        <table class=\"w3-table-all\">\n                            {% if sc.activeCameraIsUsb == False or \"AfMode\" in cc.usbCamControls %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afMode == True %}\n                                    <input type=\"checkbox\" id=\"include_afmode\" name=\"include_afmode\" aria-label=\"include_afmode\"\n                                        onclick=\"includeCtrl('include_afmode', 'afmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afmode\" name=\"include_afmode\" aria-label=\"include_afmode\"\n                                        onclick=\"includeCtrl('include_afmode', 'afmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The autofocus mode.\n                                    </span>\n                                    <label for=\"afmode\">Autofocus Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_afMode == True %}\n                                    <select name=\"afmode\" id=\"afmode\">\n                                    {% else %}\n                                    <select name=\"afmode\" id=\"afmode\" disabled>\n                                    {% endif %}\n                                        {% if cc.afMode == 0 %}\n                                        <option value=\"0\" selected>Manual</option>\n                                        {% else %}\n                                        <option value=\"0\">Manual</option>\n                                        {% endif %}\n                                        {% if cc.afMode == 1 %}\n                                        <option value=\"1\" selected>Auto</option>\n                                        {% else %}\n                                        <option value=\"1\">Auto</option>\n                                        {% endif %}\n                                        {% if cc.afMode == 2 %}\n                                        <option value=\"2\" selected>Continuous</option>\n                                        {% else %}\n                                        <option value=\"2\">Continuous</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"LensPosition\" in cc.usbCamControls %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% set min = \"0.001\" %}\n                            {% set max = \"999\" %}\n                            {% set step = \"0.001\" %}\n                            {% else %}\n                            {% set min = cc.usbCamControls[\"LensPosition\"][\"min\"] %}\n                            {% set max = cc.usbCamControls[\"LensPosition\"][\"max\"] %}\n                            {% set step = cc.usbCamControls[\"LensPosition\"][\"step\"] %}\n                            {% set default = cc.usbCamControls[\"LensPosition\"][\"default\"] %}\n                            {% endif %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_lensPosition == True %}\n                                    <input type=\"checkbox\" id=\"include_lensposition\" name=\"include_lensposition\" aria-label=\"include_lensposition\"\n                                        onclick=\"includeCtrl('include_lensposition', 'fdist')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_lensposition\" name=\"include_lensposition\" aria-label=\"include_lensposition\"\n                                        onclick=\"includeCtrl('include_lensposition', 'fdist')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        {% if sc.activeCameraIsUsb == False %}\n                                        The focal distance in m. <br>(pressing Enter will automatically submit)\n                                        {% else %}\n                                        The focal distance for the USB camera. <br>\n                                        Range: {{ min }} ... {{ max }}. <br>\n                                        Default: {{ default }} <br>\n                                        (pressing Enter will automatically submit)\n                                        {% endif %}\n                                    </span>\n                                    {% if sc.activeCameraIsUsb == False %}\n                                    <label for=\"fdist\">Focal Distance [m]:</label>\n                                    {% else %}\n                                    <label for=\"fdist\">Focus:</label>\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {% if cc.include_lensPosition == True %}\n                                    <input type=\"number\" id=\"fdist\" name=\"fdist\" min=\"{{ min }}\" max=\"{{ max }}\" step=\"{{ step }}\" value=\"{{ cc.focalDistance }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"fdist\" name=\"fdist\" min=\"{{ min }}\" max=\"{{ max }}\" step=\"{{ step }}\" value=\"{{ cc.focalDistance }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afMetering == True %}\n                                    <input type=\"checkbox\" id=\"include_afmetering\" name=\"include_afmetering\" aria-label=\"include_afmetering\"\n                                        onclick=\"includeCtrl('include_afmetering', 'afmetering')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afmetering\" name=\"include_afmetering\" aria-label=\"include_afmetering\"\n                                        onclick=\"includeCtrl('include_afmetering', 'afmetering')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Specifies where focus should be measured.\n                                    </span>\n                                    <label for=\"afmetering\">Autofocus Metering:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_afMetering == True %}\n                                    <select name=\"afmetering\" id=\"afmetering\">\n                                    {% else %}\n                                    <select name=\"afmetering\" id=\"afmetering\" disabled>\n                                    {% endif %}\n                                        {% if cc.afMetering == 0 %}\n                                        <option value=\"0\" selected>Auto</option>\n                                        {% else %}\n                                        <option value=\"0\">Auto</option>\n                                        {% endif %}\n                                        {% if cc.afMetering == 1 %}\n                                        <option value=\"1\" selected>Windows</option>\n                                        {% else %}\n                                        <option value=\"1\">Windows</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afPause == True %}\n                                    <input type=\"checkbox\" id=\"include_afpause\" name=\"include_afpause\" aria-label=\"include_afpause\"\n                                        onclick=\"includeCtrl('include_afpause', 'afpause')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afpause\" name=\"include_afpause\" aria-label=\"include_afpause\"\n                                        onclick=\"includeCtrl('include_afpause', 'afpause')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Pause continuous autofocus. Only has any effect when in continuous autofocus mode.\n                                    </span>\n                                    <label for=\"afpause\">Autofocus Pause:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_afPause == True %}\n                                    <select name=\"afpause\" id=\"afpause\">\n                                    {% else %}\n                                    <select name=\"afpause\" id=\"afpause\" disabled>\n                                    {% endif %}\n                                        {% if cc.afPause == 0 %}\n                                        <option value=\"0\" selected>Immediate</option>\n                                        {% else %}\n                                        <option value=\"0\">Immediate</option>\n                                        {% endif %}\n                                        {% if cc.afPause == 1 %}\n                                        <option value=\"1\" selected>Deferred</option>\n                                        {% else %}\n                                        <option value=\"1\">Deferred</option>\n                                        {% endif %}\n                                        {% if cc.afPause == 2 %}\n                                        <option value=\"2\" selected>Resume</option>\n                                        {% else %}\n                                        <option value=\"2\">Resume</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afRange == True %}\n                                    <input type=\"checkbox\" id=\"include_afrange\" name=\"include_afrange\" aria-label=\"include_afrange\"\n                                        onclick=\"includeCtrl('include_afrange', 'afrange')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afrange\" name=\"include_afrange\" aria-label=\"include_afrange\"\n                                        onclick=\"includeCtrl('include_afrange', 'afrange')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <label for=\"afrange\">Autofocus Range:</label>\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Range of lens positions to search.\n                                    </span>\n                                </td>\n                                <td>\n                                    {% if cc.include_afRange == True %}\n                                    <select name=\"afrange\" id=\"afrange\">\n                                    {% else %}\n                                    <select name=\"afrange\" id=\"afrange\" disabled>\n                                    {% endif %}\n                                        {% if cc.afRange == 0 %}\n                                        <option value=\"0\" selected>Normal</option>\n                                        {% else %}\n                                        <option value=\"0\">Normal</option>\n                                        {% endif %}\n                                        {% if cc.afRange == 1 %}\n                                        <option value=\"1\" selected>Macro</option>\n                                        {% else %}\n                                        <option value=\"1\">Macro</option>\n                                        {% endif %}\n                                        {% if cc.afRange == 2 %}\n                                        <option value=\"2\" selected>Full</option>\n                                        {% else %}\n                                        <option value=\"2\">Full</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afSpeed == True %}\n                                    <input type=\"checkbox\" id=\"include_afspeed\" name=\"include_afspeed\" aria-label=\"include_afspeed\"\n                                        onclick=\"includeCtrl('include_afspeed', 'afspeed')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afspeed\" name=\"include_afspeed\" aria-label=\"include_afspeed\"\n                                        onclick=\"includeCtrl('include_afspeed', 'afspeed')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <label for=\"afspeed\">Autofocus Speed:</label>\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Speed of the autofocus search.\n                                    </span>\n                                </td>\n                                <td>\n                                    {% if cc.include_afSpeed == True %}\n                                    <select name=\"afspeed\" id=\"afspeed\">\n                                    {% else %}\n                                    <select name=\"afspeed\" id=\"afspeed\" disabled >\n                                    {% endif %}\n                                        {% if cc.afSpeed == 0 %}\n                                        <option value=\"0\" selected>Normal</option>\n                                        {% else %}\n                                        <option value=\"0\">Normal</option>\n                                        {% endif %}\n                                        {% if cc.afSpeed == 1 %}\n                                        <option value=\"1\" selected>Fast</option>\n                                        {% else %}\n                                        <option value=\"1\">Fast</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_afWindows == True %}\n                                    <input type=\"checkbox\" id=\"include_afwindows\" name=\"include_afwindows\" aria-label=\"include_afwindows\"\n                                        onclick=\"drawAfWindow('include_afwindows', 'afwindows', '{{ sc.scalerCropLiveView }}')\"\n                                        onclick=\"includeCtrl('include_afwindows', 'afwindows')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_afwindows\"  name=\"include_afwindows\" aria-label=\"include_afwindows\"\n                                        onclick=\"drawAfWindow('include_afwindows', 'afwindows', '{{ sc.scalerCropLiveView }}')\"\n                                        onclick=\"includeCtrl('include_afwindows', 'afwindows')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <label for=\"afwindows\">Autofocus Windows:</label>\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Location of the windows in the image to use to\n                                        measure focus.<br>\n                                        A list of rectangles (tuples of 4 numbers\n                                        denoting x_offset, y_offset, width and\n                                        height). The rectangle units refer to the\n                                        maximum scaler crop window.\n                                    </span>\n                                </td>\n                                <td>\n                                    {% if cc.include_afWindows == True %}\n                                    <input type=\"text\" id=\"afwindows\" name=\"afwindows\" value=\"{{ cc.afWindowsStr }}\" disabled>\n                                    {% else %}\n                                    <input type=\"text\" id=\"afwindows\" name=\"afwindows\" value=\"{{ cc.afWindowsStr }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                        </table>\n                    </form>\n                    <p style=\"margin-bottom: 0; margin-top: 5px\"></p>\n                    <table>\n                        <tr>\n                            <td style=\"width: 50%\">\n                                <input class=\"w3-button w3-black\" onclick=\"enableAfWindows()\" form=\"formfocuscontrol\" type=\"submit\" value=\"Submit\">\n                            </td>\n                            {% if sc.activeCameraIsUsb == False %}\n                            <td style=\"width: 50%\">\n                                <form method=\"post\" action=\"{{ url_for('home.trigger_autofocus') }}\">\n                                    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Trigger Autofocus\">\n                                </form>\n                            </td>\n                            {% else %}\n                            <td></td>\n                            {% endif %}\n                        </tr>\n                    </table>\n                </div>\n                {% else %}\n                <p>Not available for this camera</p>\n                {% endif %}\n            </div>\n            {% if sc.lastLiveTab == \"zoom\" %}\n            <div id=\"zoom\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"zoom\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Zoom & Pan -->\n                <h4>Zoom and pan</h4>\n                <div>\n                    <form id=\"zoomform\" method=\"post\" action=\"{{ url_for('home.set_zoom') }}\">\n                        <table class=\"w3-table-all\">\n                            <tr>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The current zoom factor in %\n                                    </span>\n                                    <label for=\"zoomfactor\">Current zoom factor in %:</label>\n                                </td>\n                                <td>\n                                    <input type=\"number\" id=\"zoomfactor\" name=\"zoomfactor\" value=\"{{ sc.zoomFactor }}\" disabled>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The increment or decrement (in %) by which the zoom factor will be modified in each step\n                                    </span>\n                                    <label for=\"zoomfactorstep\">Zoom & pan step in %:</label>\n                                </td>\n                                <td>\n                                    <input type=\"number\" id=\"zoomfactorstep\" name=\"zoomfactorstep\" value=\"{{ sc.zoomFactorStep }}\" min=\"2\" max=\"20\">\n                                </td>\n                            </tr>\n                            <tr>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The scaler crop rectangle determines which part of the image received from the sensor is\n                                        cropped and then scaled to produce an output image of the correct size.\n                                        This value refers to the maximum sensor resolution.<br>\n                                        (x_offset, y_offset, width, height)\n                                    </span>\n                                    <label for=\"scalercrop\">Current ScalerCrop (Zoom):</label>\n                                </td>\n                                <td>\n                                    <input type=\"text\" id=\"scalercrop\" name=\"scalercrop\" value=\"{{ cc.scalerCropStr }}\" disabled>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Scaler Crop for the live view from live view metadata. \n                                        May be different from value for Zoom due to Sensor Mode restrictions<br>\n                                        (x_offset, y_offset, width, height)\n                                    </span>\n                                    <label for=\"scalercroplive\">Current ScalerCrop (Live View):</label>\n                                </td>\n                                <td>\n                                    <input type=\"text\" id=\"scalercroplive\" name=\"scalercroplive\" value=\"{{ sc.scalerCropLiveViewStr }}\" disabled>\n                                </td>\n                            </tr>\n                        </table>\n                        <input class=\"w3-button w3-black\" style =\"display:none\" type=\"submit\" value=\"Submit\">\n                    </form>\n                    <p style=\"margin-bottom: 0; margin-top: 0\"></p>\n                    <table class=\"w3-table\">\n                        <tr>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.zoom_in') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Zoom in\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Zoom in\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.pan_up') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan up\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan up\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.zoom_out') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Zoom out\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Zoom out\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.pan_left') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan left\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan left\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.pan_center') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Center\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Center\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.pan_right') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan right\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan right\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.zoom_full') }}\">\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Full\">\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.pan_down') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan down\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Pan down\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                {% if sc.isZoomModeDraw == False %}\n                                <form method=\"post\" action=\"{{ url_for('home.zoom_draw') }}\">\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" \n                                        onclick=\"drawZoomWindow('{{ sc.isZoomModeDraw }}','scalercrop', '{{ sc.scalerCropLiveView }}')\"\n                                        type=\"submit\"\n                                        value=\"Draw\">\n                                </form>\n                                {% else %}\n                                <button class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" \n                                    onclick=\"enableScalerCrop()\"\n                                    form=\"zoomform\">\n                                    Submit\n                                </button>\n                                {% endif %}\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                                <form method=\"post\" action=\"{{ url_for('home.zoom_default') }}\">\n                                    {% if sc.isZoomModeDraw == False %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Default\">\n                                    {% else %}\n                                    <input class=\"w3-button w3-black\" style=\"width:80%; padding-bottom:6px; padding-top:6px\" type=\"submit\" value=\"Default\" disabled>\n                                    {% endif %}\n                                </form>\n                            </td>\n                            <td style=\"padding-top:4px; padding-bottom:0px\">\n                            </td>\n                        </tr>\n                    </table>\n                </div>\n            </div>\n            {% if sc.lastLiveTab == \"autoexposure\" %}\n            <div id=\"autoexposure\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"autoexposure\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Control settings for auto-exposure -->\n                <h4>Auto Exposure</h4>\n                {% if sc.activeCameraIsUsb == True %}\n                <p>Not available for this camera</p>\n                {% else %}\n                <div>\n                    <form method=\"post\" action=\"{{ url_for('home.ae_control') }}\">\n                        <table class=\"w3-table-all\">\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeEnable == True %}\n                                    <input type=\"checkbox\" id=\"include_aeenable\" name=\"include_aeenable\" aria-label=\"include_aeenable\"\n                                        onclick=\"includeCtrl('include_aeenable', 'aeenable')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aeenable\" name=\"include_aeenable\" aria-label=\"include_aeenable\" \n                                        onclick=\"includeCtrl('include_aeenable', 'aeenable')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Enable or disable the Automatic Exposure (AE).\n                                    </span>\n                                    <label for=\"aeenable\">AE enabled:</label>\n                                </td>\n                                <td>\n                                    {% if cc.aeEnable == True %}\n                                    {% if cc.include_aeEnable == True %}\n                                    <input type=\"checkbox\" id=\"aeenable\" name=\"aeenable\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"aeenable\" name=\"aeenable\" value=\"1\" checked disabled>\n                                    {% endif %}\n                                    {% else %}\n                                    {% if cc.include_aeEnable == True%}\n                                    <input type=\"checkbox\" id=\"aeenable\" name=\"aeenable\" value=\"0\">\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"aeenable\" name=\"aeenable\" value=\"0\" disabled>\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeMeteringMode == True %}\n                                    <input type=\"checkbox\" id=\"include_aemeteringmode\" name=\"include_aemeteringmode\" aria-label=\"include_aemeteringmode\" \n                                        onclick=\"includeCtrl('include_aemeteringmode', 'aemeteringmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aemeteringmode\" name=\"include_aemeteringmode\" aria-label=\"include_aemeteringmode\" \n                                        onclick=\"includeCtrl('include_aemeteringmode', 'aemeteringmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the metering mode of the algorithm for \n                                        automatic gain control (AGC) and automatic exposure control (AEC).\n                                    </span>\n                                    <label for=\"aemeteringmode\">AEC/AGC Metering Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_aeMeteringMode == True %}\n                                    <select name=\"aemeteringmode\" id=\"aemeteringmode\">\n                                    {% else %}\n                                    <select name=\"aemeteringmode\" id=\"aemeteringmode\" disabled>\n                                    {% endif %}\n                                        {% if cc.aeMeteringMode == 0 %}\n                                        <option value=\"0\" selected>Center Weighted</option>\n                                        {% else %}\n                                        <option value=\"0\">Center Weighted</option>\n                                        {% endif %}\n                                        {% if cc.aeMeteringMode == 1 %}\n                                        <option value=\"1\" selected>Spot</option>\n                                        {% else %}\n                                        <option value=\"1\">Spot</option>\n                                        {% endif %}\n                                        {% if cc.aeMeteringMode == 2 %}\n                                        <option value=\"2\" selected>Matrix</option>\n                                        {% else %}\n                                        <option value=\"2\">Matrix</option>\n                                        {% endif %}\n                                        {% if cc.aeMeteringMode == 3 %}\n                                        <option value=\"3\" selected>Custom</option>\n                                        {% else %}\n                                        <option value=\"3\">Custom</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeExposureMode == True %}\n                                    <input type=\"checkbox\" id=\"include_aeexposuremode\" name=\"include_aeexposuremode\" aria-label=\"include_aeexposuremode\"\n                                        onclick=\"includeCtrl('include_aeexposuremode', 'aeexposuremode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aeexposuremode\" name=\"include_aeexposuremode\" aria-label=\"include_aeexposuremode\"\n                                        onclick=\"includeCtrl('include_aeexposuremode', 'aeexposuremode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the constraint mode of the algorithm for \n                                        automatic gain control (AGC) and automatic exposure control (AEC).\n                                    </span>\n                                    <label for=\"aeexposuremode\">AEC/AGC Exposure Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_aeExposureMode == True %}\n                                    <select name=\"aeexposuremode\" id=\"aeexposuremode\">\n                                    {% else %}\n                                    <select name=\"aeexposuremode\" id=\"aeexposuremode\" disabled>\n                                    {% endif %}\n                                        {% if cc.aeExposureMode == 0 %}\n                                        <option value=\"0\" selected>Normal</option>\n                                        {% else %}\n                                        <option value=\"0\">Normal</option>\n                                        {% endif %}\n                                        {% if cc.aeExposureMode == 1 %}\n                                        <option value=\"1\" selected>Short</option>\n                                        {% else %}\n                                        <option value=\"1\">Short</option>\n                                        {% endif %}\n                                        {% if cc.aeExposureMode == 2 %}\n                                        <option value=\"2\" selected>Long</option>\n                                        {% else %}\n                                        <option value=\"2\">Long</option>\n                                        {% endif %}\n                                        {% if cc.aeExposureMode == 3 %}\n                                        <option value=\"3\" selected>Custom</option>\n                                        {% else %}\n                                        <option value=\"3\">Custom</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeConstraintMode == True %}\n                                    <input type=\"checkbox\" id=\"include_aeconstraintmode\" name=\"include_aeconstraintmode\" aria-label=\"include_aeconstraintmode\"\n                                        onclick=\"includeCtrl('include_aeconstraintmode', 'aeconstraintmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aeconstraintmode\" name=\"include_aeconstraintmode\" aria-label=\"include_aeconstraintmode\"\n                                        onclick=\"includeCtrl('include_aeconstraintmode', 'aeconstraintmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the constraint mode of the algorithm for \n                                        automatic gain control (AGC) and automatic exposure control (AEC).\n                                    </span>\n                                    <label for=\"aeconstraintmode\">AEC/AGC Constraint Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_aeConstraintMode == True %}\n                                    <select name=\"aeconstraintmode\" id=\"aeconstraintmode\">\n                                    {% else %}\n                                    <select name=\"aeconstraintmode\" id=\"aeconstraintmode\" disabled>\n                                    {% endif %}\n                                        {% if cc.aeConstraintMode == 0 %}\n                                        <option value=\"0\" selected>Normal</option>\n                                        {% else %}\n                                        <option value=\"0\">Normal</option>\n                                        {% endif %}\n                                        {% if cc.aeConstraintMode == 1 %}\n                                        <option value=\"1\" selected>Highlight</option>\n                                        {% else %}\n                                        <option value=\"1\">Highlight</option>\n                                        {% endif %}\n                                        {% if cc.aeConstraintMode == 2 %}\n                                        <option value=\"2\" selected>Shadows</option>\n                                        {% else %}\n                                        <option value=\"2\">Shadows</option>\n                                        {% endif %}\n                                        {% if cc.aeConstraintMode == 3 %}\n                                        <option value=\"3\" selected>Custom</option>\n                                        {% else %}\n                                        <option value=\"3\">Custom</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% if cp.hasFlicker == True %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeFlickerMode == True %}\n                                    <input type=\"checkbox\" id=\"include_aeflickermode\" name=\"include_aeflickermode\" aria-label=\"include_aeflickermode\"\n                                        onclick=\"includeCtrl('include_aeflickermode', 'aeflickermode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aeflickermode\" name=\"include_aeflickermode\" aria-label=\"include_aeflickermode\"\n                                        onclick=\"includeCtrl('include_aeflickermode', 'aeflickermode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the flicker avoidance mode of the algorithm for\n                                        automatic gain control (AGC) and automatic exposure control (AEC).\n                                    </span>\n                                    <label for=\"aeflickermode\">AEC/AGC Flicker Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_aeFlickerMode == True %}\n                                    <select name=\"aeflickermode\" id=\"aeflickermode\">\n                                    {% else %}\n                                    <select name=\"aeflickermode\" id=\"aeflickermode\" disabled>\n                                    {% endif %}\n                                        {% if cc.aeFlickerMode == 0 %}\n                                        <option value=\"0\" selected>Off</option>\n                                        {% else %}\n                                        <option value=\"0\">Off</option>\n                                        {% endif %}\n                                        {% if cc.aeFlickerMode == 1 %}\n                                        <option value=\"1\" selected>Manual</option>\n                                        {% else %}\n                                        <option value=\"1\">Manual</option>\n                                        {% endif %}\n                                        {% if cc.aeFlickerMode == 2 %}\n                                        <option value=\"2\" selected>Auto</option>\n                                        {% else %}\n                                        <option value=\"2\">Auto</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_aeFlickerPeriod == True %}\n                                    <input type=\"checkbox\" id=\"include_aeflickerperiod\" name=\"include_aeflickerperiod\" aria-label=\"include_aeflickerperiod\"\n                                        onclick=\"includeCtrl('include_aeflickerperiod', 'aeflickerperiod')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_aeflickerperiod\" name=\"include_aeflickerperiod\" aria-label=\"include_aeflickerperiod\"\n                                        onclick=\"includeCtrl('include_aeflickerperiod', 'aeflickerperiod')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the lighting flicker period in microseconds.<br>\n                                        For example, for 50Hz mains lighting the flicker occurs at 100Hz, \n                                        so the period would be 10000 microseconds\n                                    </span>\n                                    <label for=\"aeflickerperiod\">AEC/AGC Flicker Period:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_aeFlickerPeriod == True %}\n                                    <input type=\"number\" id=\"aeflickerperiod\" name=\"aeflickerperiod\" min=\"0\" value=\"{{ cc.aeFlickerPeriod }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"aeflickerperiod\" name=\"aeflickerperiod\" min=\"0\" value=\"{{ cc.aeFlickerPeriod }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                        </table>\n                        <p style=\"margin-bottom: 0\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                    </form>\n                </div>\n                {% endif %}\n            </div>\n            {% if sc.lastLiveTab == \"exposure\" %}\n            <div id=\"exposure\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"exposure\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Control settings for manual exposure control -->\n                <h4>Exposure</h4>\n                {% if sc.activeCameraIsUsb == True %}\n                <p>Not available for this camera</p>\n                {% else %}\n                <div>\n                    <form method=\"post\" action=\"{{ url_for('home.exposure_control') }}\">\n                        <table class=\"w3-table-all\">\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_exposureTime == True %}\n                                    <input type=\"checkbox\" id=\"include_exposuretime\" name=\"include_exposuretime\" aria-label=\"include_exposuretime\"\n                                        onclick=\"includeCtrl('include_exposuretime', 'exposuretimesec')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_exposuretime\" name=\"include_exposuretime\" aria-label=\"include_exposuretime\"\n                                        onclick=\"includeCtrl('include_exposuretime', 'exposuretimesec')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Exposure time (shutter speed) in sec. <br>A value of 0 will activate AE.\n                                    </span>\n                                    <label for=\"exposuretimesec\">Exposure Time in sec:</label>\n                                </td>\n                                <td colspan=\"2\">\n                                    {% if cc.include_exposureTime == True %}\n                                    <input type=\"number\" id=\"exposuretimesec\" name=\"exposuretimesec\" min=\"0.0\" max=\"999\" step=\"0.000001\" \n                                        value=\"{{ cc.exposureTimeSec }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"exposuretimesec\" name=\"exposuretimesec\" min=\"0.0\" max=\"999\" step=\"0.000001\" \n                                        value=\"{{ cc.exposureTimeSec }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_exposureValue == True %}\n                                    <input type=\"checkbox\" id=\"include_exposurevalue\" name=\"include_exposurevalue\" aria-label=\"include_exposurevalue\"\n                                        onclick=\"includeCtrl('include_exposurevalue', 'exposurevalue')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_exposurevalue\" name=\"include_exposurevalue\" aria-label=\"include_exposurevalue\"\n                                        onclick=\"includeCtrl('include_exposurevalue', 'exposurevalue')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Exposure compensation value in \"stops\", \n                                        which adjusts the target of the AEC/AGC algorithm.\n                                        Positive values increase the target brightness,\n                                        and negative values decrease it.<br>\n                                        Zero represents the base or \"normal\" exposure level.\n                                    </span>\n                                    <label for=\"exposurevalue\">Exposure Value:</label>\n                                </td>\n                                <td colspan=\"2\">\n                                    {% if cc.include_exposureValue == True %}\n                                    <input type=\"number\" id=\"exposurevalue\" name=\"exposurevalue\" min=\"-8.0\" max=\"8.0\" step=\"0.1\" \n                                        value=\"{{ cc.exposureValue }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"exposurevalue\" name=\"exposurevalue\" min=\"-8.0\" max=\"8.0\" step=\"0.1\" \n                                        value=\"{{ cc.exposureValue }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_analogueGain == True %}\n                                    <input type=\"checkbox\" id=\"include_analoguegain\" name=\"include_analoguegain\" aria-label=\"include_analoguegain\"\n                                        onclick=\"includeCtrl('include_analoguegain', 'analoguegain')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_analoguegain\" name=\"include_analoguegain\" aria-label=\"include_analoguegain\"\n                                        onclick=\"includeCtrl('include_analoguegain', 'analoguegain')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Analogue gain applied by the sensor\n                                    </span>\n                                    <label for=\"analoguegain\">Analogue Gain:</label>\n                                </td>\n                                <td colspan=\"2\">\n                                    {% if cc.include_analogueGain == True %}\n                                    <input type=\"number\" id=\"analoguegain\" name=\"analoguegain\" min=\"1.0\" max=\"99.0\" step=\"0.1\" \n                                        value=\"{{ cc.analogueGain }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"analoguegain\" name=\"analoguegain\" min=\"1.0\" max=\"99.0\" step=\"0.1\" \n                                        value=\"{{ cc.analogueGain }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_colourGains == True %}\n                                    <input type=\"checkbox\" id=\"include_colourgains\" name=\"include_colourgains\" aria-label=\"include_colourgains\"\n                                        onclick=\"includeCtrl('include_colourgains', 'colourgainred', 'colourgainblue')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_colourgains\" name=\"include_colourgains\" aria-label=\"include_colourgains\"\n                                        onclick=\"includeCtrl('include_colourgains', 'colourgainred', 'colourgainblue')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Colour gain for red (left) and blue (right). The gain applied to red / blue pixels \n                                        by the Automatic White Balance algorithm (AWB)\n                                    </span>\n                                    <label for=\"colourgainred\">Colour Gain:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_colourGains == True %}\n                                    <input type=\"number\" id=\"colourgainred\" name=\"colourgainred\" min=\"0.0\" max=\"32.0\" step=\"0.1\" \n                                        value=\"{{ cc.colourGainRed }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"colourgainred\" name=\"colourgainred\" min=\"0.0\" max=\"32.0\" step=\"0.1\" \n                                        value=\"{{ cc.colourGainRed }}\" disabled>\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {% if cc.include_colourGains == True %}\n                                    <input type=\"number\" id=\"colourgainblue\" name=\"colourgainblue\" aria-label=\"colourgainblue\" min=\"0.0\" max=\"32.0\" step=\"0.1\" \n                                        value=\"{{ cc.colourGainBlue }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"colourgainblue\" name=\"colourgainblue\" aria-label=\"colourgainblue\" min=\"0.0\" max=\"32.0\" step=\"0.1\" \n                                        value=\"{{ cc.colourGainBlue }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_frameDurationLimits == True %}\n                                    <input type=\"checkbox\" id=\"include_framedurationlimits\" name=\"include_framedurationlimits\" aria-label=\"include_framedurationlimits\"\n                                        onclick=\"includeCtrl('include_framedurationlimits', 'framedurationlimitmax', 'framedurationlimitmin')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_framedurationlimits\" name=\"include_framedurationlimits\" aria-label=\"include_framedurationlimits\"\n                                        onclick=\"includeCtrl('include_framedurationlimits', 'framedurationlimitmax', 'framedurationlimitmin')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The maximum time (left) and minimum time (right) that the\n                                        sensor can take to deliver a frame, measured\n                                        in microseconds. So the reciprocal of these\n                                        values (first divided by 1000000) will give the\n                                        minimum / maximum framerates that the sensor can deliver.<br>\n                                        A value of 0 reverts to using the camera default.\n                                    </span>\n                                    <label for=\"framedurationlimitmax\">Frame Duration Lim Max/Min (μs):</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_frameDurationLimits == True %}\n                                    <input type=\"number\" id=\"framedurationlimitmax\" name=\"framedurationlimitmax\" min=\"0\" max=\"1000000\" step=\"1\" \n                                        value=\"{{ cc.frameDurationLimitMax }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"framedurationlimitmax\" name=\"framedurationlimitmax\" min=\"0\" max=\"1000000\" step=\"1\" \n                                        value=\"{{ cc.frameDurationLimitMax }}\" disabled>\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {% if cc.include_frameDurationLimits == True %}\n                                    <input type=\"number\" id=\"framedurationlimitmin\" name=\"framedurationlimitmin\" aria-label=\"framedurationlimitmin\" min=\"0\" max=\"1000000\" step=\"1\" \n                                        value=\"{{ cc.frameDurationLimitMin }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"framedurationlimitmin\" name=\"framedurationlimitmin\" aria-label=\"framedurationlimitmin\" min=\"0\" max=\"1000000\" step=\"1\" \n                                        value=\"{{ cc.frameDurationLimitMin }}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% if cp.hasHdr == True %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_hdrMode == True %}\n                                    <input type=\"checkbox\" id=\"include_hdrmode\" name=\"include_hdrmode\" aria-label=\"include_hdrmode\"\n                                        onclick=\"includeCtrl('include_hdrmode', 'hdrmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_hdrmode\" name=\"include_hdrmode\" aria-label=\"include_hdrmode\"\n                                        onclick=\"includeCtrl('include_hdrmode', 'hdrmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Whether to run the camera in an HDR mode\n                                        (distinct from the in-camera HDR supported by\n                                        the Camera Module 3). Most of these HDR\n                                        features work only on Pi 5 or later devices.\n                                    </span>\n                                    <label for=\"hdrmode\">HDR Mode:</label>\n                                </td>\n                                <td colspan=\"2\">\n                                    {% if cc.include_hdrMode == True %}\n                                    <select name=\"hdrmode\" id=\"hdrmode\">\n                                    {% else %}\n                                    <select name=\"hdrmode\" id=\"hdrmode\" disabled>\n                                    {% endif %}\n                                        {% if cc.hdrMode == 0 %}\n                                        <option value=\"0\" selected>Off</option>\n                                        {% else %}\n                                        <option value=\"0\">Off</option>\n                                        {% endif %}\n                                        {% if cc.hdrMode == 1 %}\n                                        <option value=\"1\" selected>MultiExposureUnmerged</option>\n                                        {% else %}\n                                        <option value=\"1\">MultiExposureUnmerged</option>\n                                        {% endif %}\n                                        {% if cc.hdrMode == 2 %}\n                                        <option value=\"2\" selected>MultiExposure</option>\n                                        {% else %}\n                                        <option value=\"2\">MultiExposure</option>\n                                        {% endif %}\n                                        {% if cc.hdrMode == 3 %}\n                                        <option value=\"3\" selected>SingleExposure</option>\n                                        {% else %}\n                                        <option value=\"3\">SingleExposure</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                        </table>\n                        <p style=\"margin-bottom: 0\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                    </form>\n                </div>\n                {% endif %}\n            </div>\n            {% if sc.lastLiveTab == \"image\" %}\n            <div id=\"image\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"image\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Control settings for other image parameters -->\n                <h4>Image</h4>\n                <div>\n                    <form method=\"post\" action=\"{{ url_for('home.image_control') }}\">\n                        <table class=\"w3-table-all\">\n                            {% if sc.activeCameraIsUsb == False or \"AwbEnable\" in cc.usbCamControls %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_awbEnable == True %}\n                                    <input type=\"checkbox\" id=\"include_awbenable\" name=\"include_awbenable\" aria-label=\"include_awbenable\"\n                                        onclick=\"includeCtrl('include_awbenable', 'awbenable')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_awbenable\" name=\"include_awbenable\" aria-label=\"include_awbenable\"\n                                        onclick=\"includeCtrl('include_awbenable', 'awbenable')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Turn the auto white balance (AWB) algorithm\n                                        on or off. When it is off, there will be no\n                                        automatic updates to the colour gains.\n                                    </span>\n                                    <label for=\"awbenable\">AWB enabled:</label>\n                                </td>\n                                <td>\n                                    {% if cc.awbEnable == True %}\n                                    {% if cc.include_awbEnable == True %}\n                                    <input type=\"checkbox\" id=\"awbenable\" name=\"awbenable\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"awbenable\" name=\"awbenable\" value=\"1\" checked disabled>\n                                    {% endif %}\n                                    {% else %}\n                                    {% if cc.include_awbEnable == True%}\n                                    <input type=\"checkbox\" id=\"awbenable\" name=\"awbenable\" value=\"0\">\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"awbenable\" name=\"awbenable\" value=\"0\" disabled>\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"AwbMode\" in cc.usbCamControls %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_awbMode == True %}\n                                    <input type=\"checkbox\" id=\"include_awbmode\" name=\"include_awbmode\" aria-label=\"include_awbmode\"\n                                        onclick=\"includeCtrl('include_awbmode', 'awbmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_awbmode\" name=\"include_awbmode\" aria-label=\"include_awbmode\"\n                                        onclick=\"includeCtrl('include_awbmode', 'awbmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Sets the mode of the Automatic White Balance algorithm.\n                                    </span>\n                                    <label for=\"awbmode\">AWB Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_awbMode == True %}\n                                    <select name=\"awbmode\" id=\"awbmode\">\n                                        {% else %}\n                                        <select name=\"awbmode\" id=\"awbmode\" disabled>\n                                            {% endif %}\n                                            {% if cc.awbMode == 0 %}\n                                            <option value=\"0\" selected>Auto</option>\n                                            {% else %}\n                                            <option value=\"0\">Auto</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 2 %}\n                                            <option value=\"2\" selected>Tungsten</option>\n                                            {% else %}\n                                            <option value=\"2\">Tungsten</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 3 %}\n                                            <option value=\"3\" selected>Fluorescent</option>\n                                            {% else %}\n                                            <option value=\"3\">Fluorescent</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 4 %}\n                                            <option value=\"4\" selected>Indoor</option>\n                                            {% else %}\n                                            <option value=\"4\">Indoor</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 5 %}\n                                            <option value=\"5\" selected>Daylight</option>\n                                            {% else %}\n                                            <option value=\"5\">Daylight</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 6 %}\n                                            <option value=\"6\" selected>Cloudy</option>\n                                            {% else %}\n                                            <option value=\"6\">Cloudy</option>\n                                            {% endif %}\n                                            {% if cc.awbMode == 7 %}\n                                            <option value=\"7\" selected>Custom</option>\n                                            {% else %}\n                                            <option value=\"7\">Custom</option>\n                                            {% endif %}\n                                        </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_noiseReductionMode == True %}\n                                    <input type=\"checkbox\" id=\"include_noisereductionmode\" name=\"include_noisereductionmode\" aria-label=\"include_noisereductionmode\"\n                                        onclick=\"includeCtrl('include_noisereductionmode', 'noisereductionmode')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_noisereductionmode\" name=\"include_noisereductionmode\" aria-label=\"include_noisereductionmode\"\n                                        onclick=\"includeCtrl('include_noisereductionmode', 'noisereductionmode')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Selects a suitable noise reduction mode.\n                                        Normally Picamera2’s configuration will select\n                                        an appropriate mode automatically, so it\n                                        should not normally be necessary to change it.\n                                        The HighQuality noise reduction mode can be\n                                        expected to affect the maximum achievable\n                                        framerate.\n                                    </span>\n                                    <label for=\"noisereductionmode\">Noise Reduction Mode:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_noiseReductionMode == True %}\n                                    <select name=\"noisereductionmode\" id=\"noisereductionmode\">\n                                        {% else %}\n                                        <select name=\"noisereductionmode\" id=\"noisereductionmode\" disabled>\n                                            {% endif %}\n                                            {% if cc.noiseReductionMode == 0 %}\n                                            <option value=\"0\" selected>Off</option>\n                                            {% else %}\n                                            <option value=\"0\">Off</option>\n                                            {% endif %}\n                                            {% if cc.noiseReductionMode == 1 %}\n                                            <option value=\"1\" selected>Fast</option>\n                                            {% else %}\n                                            <option value=\"1\">Fast</option>\n                                            {% endif %}\n                                            {% if cc.noiseReductionMode == 2 %}\n                                            <option value=\"2\" selected>highQuality</option>\n                                            {% else %}\n                                            <option value=\"2\">highQuality</option>\n                                            {% endif %}\n                                        </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"Sharpness\" in cc.usbCamControls %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% set min = \"0.0\" %}\n                            {% set max = \"16.0\" %}\n                            {% set step = \"0.1\" %}\n                            {% else %}\n                            {% set min = cc.usbCamControls[\"Sharpness\"][\"min\"] %}\n                            {% set max = cc.usbCamControls[\"Sharpness\"][\"max\"] %}\n                            {% set step = cc.usbCamControls[\"Sharpness\"][\"step\"] %}\n                            {% set default = cc.usbCamControls[\"Sharpness\"][\"default\"] %}\n                            {% endif %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_sharpness == True %}\n                                    <input type=\"checkbox\" id=\"include_sharpness\" name=\"include_sharpness\" aria-label=\"include_sharpness\"\n                                        onclick=\"includeCtrl('include_sharpness', 'sharpness')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_sharpness\" name=\"include_sharpness\" aria-label=\"include_sharpness\"\n                                        onclick=\"includeCtrl('include_sharpness', 'sharpness')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        {% if sc.activeCameraIsUsb == False %}\n                                        Sets the image sharpness, where zero implies\n                                        no additional sharpening is performed, 1.0 is\n                                        the default \"normal\" level of sharpening, and\n                                        larger values apply proportionately stronger\n                                        sharpening.\n                                        {% else %}\n                                        Sets the image sharpness for the USB camera.<br> \n                                        Range: ({{ min }}, {{ max }}).<br>\n                                        Default: {{ default }}.\n                                        {% endif %}\n                                    </span>\n                                    <label for=\"sharpness\">Sharpness:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_sharpness == True %}\n                                    <input type=\"number\" id=\"sharpness\" name=\"sharpness\" min={{ min }} max={{ max }} step={{ step }}\n                                        value=\"{{ cc.sharpness }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"sharpness\" name=\"sharpness\" min={{ min }} max={{ max }} step={{ step }}\n                                        value=\"{{ cc.sharpness}}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"Contrast\" in cc.usbCamControls %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% set min = \"0.0\" %}\n                            {% set max = \"32.0\" %}\n                            {% set step = \"0.1\" %}\n                            {% else %}\n                            {% set min = cc.usbCamControls[\"Contrast\"][\"min\"] %}\n                            {% set max = cc.usbCamControls[\"Contrast\"][\"max\"] %}\n                            {% set step = cc.usbCamControls[\"Contrast\"][\"step\"] %}\n                            {% set default = cc.usbCamControls[\"Contrast\"][\"default\"] %}\n                            {% endif %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_contrast == True %}\n                                    <input type=\"checkbox\" id=\"include_contrast\" name=\"include_contrast\" aria-label=\"include_contrast\"\n                                        onclick=\"includeCtrl('include_contrast', 'contrast')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_contrast\" name=\"include_contrast\" aria-label=\"include_contrast\"\n                                        onclick=\"includeCtrl('include_contrast', 'contrast')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        {% if sc.activeCameraIsUsb == False %}\n                                        Sets the contrast of the image, where zero\n                                        means \"no contrast\", 1.0 is the default \"normal\"\n                                        contrast, and larger values increase the\n                                        contrast proportionately.\n                                        {% else %}\n                                        Sets the image contrast for the USB camera.<br> \n                                        Range: ({{ min }}, {{ max }}).<br>\n                                        Default: {{ default }}.\n                                        {% endif %}\n                                    </span>\n                                    <label for=\"contrast\">Contrast:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_contrast == True %}\n                                    <input type=\"number\" id=\"contrast\" name=\"contrast\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.contrast }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"contrast\" name=\"contrast\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.contrast}}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"Saturation\" in cc.usbCamControls %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% set min = \"0.0\" %}\n                            {% set max = \"32.0\" %}\n                            {% set step = \"0.1\" %}\n                            {% else %}\n                            {% set min = cc.usbCamControls[\"Saturation\"][\"min\"] %}\n                            {% set max = cc.usbCamControls[\"Saturation\"][\"max\"] %}\n                            {% set step = cc.usbCamControls[\"Saturation\"][\"step\"] %}\n                            {% set default = cc.usbCamControls[\"Saturation\"][\"default\"] %}\n                            {% endif %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_saturation == True %}\n                                    <input type=\"checkbox\" id=\"include_saturation\" name=\"include_saturation\" aria-label=\"include_saturation\"\n                                        onclick=\"includeCtrl('include_saturation', 'saturation')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_saturation\" name=\"include_saturation\" aria-label=\"include_saturation\"\n                                        onclick=\"includeCtrl('include_saturation', 'saturation')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        {% if sc.activeCameraIsUsb == False %}\n                                        Amount of colour saturation, where zero\n                                        produces greyscale images, 1.0 represents\n                                        default \"normal\" saturation, and higher values\n                                        produce more saturated colours.\n                                        {% else %}\n                                        Sets the image saturation for the USB camera.<br> \n                                        Range: ({{ min }}, {{ max }}).<br>\n                                        Default: {{ default }}.\n                                        {% endif %}\n                                    </span>\n                                    <label for=\"saturation\">Saturation:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_saturation == True %}\n                                    <input type=\"number\" id=\"saturation\" name=\"saturation\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.saturation }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"saturation\" name=\"saturation\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.saturation}}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                            {% if sc.activeCameraIsUsb == False or \"Brightness\" in cc.usbCamControls %}\n                            {% if sc.activeCameraIsUsb == False %}\n                            {% set min = \"-1.0\" %}\n                            {% set max = \"1.0\" %}\n                            {% set step = \"0.1\" %}\n                            {% else %}\n                            {% set min = cc.usbCamControls[\"Brightness\"][\"min\"] %}\n                            {% set max = cc.usbCamControls[\"Brightness\"][\"max\"] %}\n                            {% set step = cc.usbCamControls[\"Brightness\"][\"step\"] %}\n                            {% set default = cc.usbCamControls[\"Brightness\"][\"default\"] %}\n                            {% endif %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {% if cc.include_brightness == True %}\n                                    <input type=\"checkbox\" id=\"include_brightness\" name=\"include_brightness\" aria-label=\"include_brightness\"\n                                        onclick=\"includeCtrl('include_brightness', 'brightness')\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"include_brightness\" name=\"include_brightness\" aria-label=\"include_brightness\"\n                                        onclick=\"includeCtrl('include_brightness', 'brightness')\" value=\"0\">\n                                    {% endif %}\n                                </td>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        {% if sc.activeCameraIsUsb == False %}\n                                        Adjusts the image brightness where -1.0 is\n                                        very dark, 1.0 is very bright, and 0.0 is the\n                                        default \"normal\" brightness.\n                                        {% else %}\n                                        Sets the image brightness for the USB camera.<br> \n                                        Range: ({{ min }}, {{ max }}).<br>\n                                        Default: {{ default }}.\n                                        {% endif %}\n                                    </span>\n                                    <label for=\"brightness\">Brightness:</label>\n                                </td>\n                                <td>\n                                    {% if cc.include_brightness == True %}\n                                    <input type=\"number\" id=\"brightness\" name=\"brightness\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.brightness }}\">\n                                    {% else %}\n                                    <input type=\"number\" id=\"brightness\" name=\"brightness\" min={{ min }} max={{ max }} step={{ step }} value=\"{{ cc.brightness}}\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endif %}\n                        </table>\n                        <p style=\"margin-bottom: 0; margin-top: 5px\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                    </form>\n                </div>\n            </div>\n            {% if sc.lastLiveTab == \"control\" %}\n            <div id=\"control\" class=\"w3-container controlgroup\">\n            {% else %}\n            <div id=\"control\" class=\"w3-container controlgroup\" style=\"display:none\">\n            {% endif %}\n                <!-- Control panel -->\n                <h4>Control</h4>\n                <div>\n                    {% if sc.lButtonsRows == 0 %}\n                    <p>There are no live buttons configured.</p>\n                    <p>You can configure buttons for execution of OS commands</p>\n                    <p>and/or actions in Settings/Live Buttons</p>\n                    {% else %}\n                    <div>\n                        {% set nrcols=sc.lButtonsCols %}\n                        {% set cw = 100/nrcols %}\n                        {% set cws = cw|string() %}\n                <!--       \n                        {% set cst = 'style=width:' + cws + '%'%}\n                -->\n                        {% set cst = 'width:' + cws + '%; min-height:38.5px; height:38.5px' %}\n                        <table style=\"table-layout:fixed; width:100%\">\n                            {% for r in sc.lButtons %}\n                            <tr style=\"height:38.5px\">\n                                {% for btn in r %}\n                                {% set shape = btn[\"buttonShape\"] %}\n                                {% set color = btn[\"buttonColor\"] %}\n                                {% if shape == \"Circular\" %}\n                                <!-- Attantion: Syntax check will not be OK for the following line -->\n                                <td style=\"{{ cst }}\"; align=\"center\">\n                                {% elif shape == \"Square\" %}\n                                <!-- Attantion: Syntax check will not be OK for the following line -->\n                                <td {{ cst }} align=\"center\">\n                                {% else %}\n                                <td {{ cst }}>\n                                {% endif %}\n                                    {% if btn[\"isVisible\"] == True %}\n\n                                    {% set cls1 = 'w3-button' %}\n                                    {% set cls = cls1 + ' w3-black' %}\n\n                                    {% if shape == \"Rectangle\" %}\n                                        {% set btnshp = \"w3-button\" %}\n                                        {% set style = 'width: 100%' %}\n                                    {% elif shape == \"Rounded\" %}\n                                        {% set btnshp = 'w3-button w3-round-xxlarge' %}\n                                        {% set style = 'width: 100%' %}\n                                    {% elif shape == \"Circular\" %}\n                                        {% set btnshp = 'w3-button w3-circle' %}\n                                        {% set style = 'text-align:center' %}\n                                    {% elif shape == \"Square\" %}\n                                        {% set btnshp = 'w3-button w3-large' %}\n                                        {% set style = 'text-align:center' %}\n                                    {% endif %}\n                                    {% if color == \"Black\" %}\n                                        {% set btnall = btnshp + ' w3-black' %}\n                                    {% elif color == \"Red\" %}\n                                        {% set btnall = btnshp + ' w3-red' %}\n                                    {% elif color == \"Green\" %}\n                                        {% set btnall = btnshp + ' w3-green' %}\n                                    {% elif color == \"Yellow\" %}\n                                        {% set btnall = btnshp + ' w3-yellow' %}\n                                    {% elif color == \"Blue\" %}\n                                        {% set btnall = btnshp + ' w3-blue' %}\n                                    {% endif %}\n                                    {% set buttonid = \"lbtn_\" ~ btn['row'] ~ btn['col'] ~ \"_form\" %}\n                                    {% set isaction = btn['isAction'] %}\n                                    {% set buttonaction = btn['buttonAction'] %}\n                                    {% set buttonexec = btn['buttonExec'] %}\n                                    {% set buttonconf = btn['needsConfirm'] %}\n                                    {% if isaction == True %}\n                                    <form id=\"{{ buttonid }}\" \n                                        method=\"post\" action=\"{{ url_for('home.live_do_action', row=btn['row'], col=btn['col']) }}\">\n                                        <!-- Attantion: Syntax check will not be OK for the following line-->\n                                        <input class=\"{{ btnall }}\" style=\"{{ style }}\" type=\"submit\" \n                                            onclick=\"confirmAction('{{ buttonid }}', '{{ buttonaction }}', '{{ buttonconf }}')\"\n                                            value=\"{{ btn['buttonText'] }}\">\n                                    </form>\n                                    {% else %}\n                                    <form id=\"{{ buttonid }}\" \n                                        method=\"post\" action=\"{{ url_for('home.live_execute', row=btn['row'], col=btn['col']) }}\">\n                                        <!-- Attantion: Syntax check will not be OK for the following line-->\n                                        <input class=\"{{ btnall }}\" style=\"{{ style }}\" type=\"submit\" \n                                            onclick=\"confirmExecution('{{ buttonid }}', '{{ buttonexec }}', '{{ buttonconf }}')\"\n                                            value=\"{{ btn['buttonText'] }}\">\n                                    </form>\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                                {% endfor %}\n                            </tr>\n                            {% endfor %}\n                        </table>\n                    </div>\n                    {% endif %}\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"w3-row w3-border\">\n        <div class=\"w3-half\">\n            <!-- Lower left area -->\n            <table class=\"w3-table-all\">\n                <tr>\n                    <td style=\"width:56%\">\n                        <!-- Photo button -->\n                        {% if sc.isVideoRecording or sc.isPhotoSeriesRecording %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.take_photo') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo\" disabled>\n                        </form>\n                        {% else %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.take_photo') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo\">\n                        </form>\n                        {% endif %}\n\n                        <!-- raw button -->\n                        {% if sc.isVideoRecording or sc.isPhotoSeriesRecording %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.take_raw_photo') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw\" disabled>\n                        </form>\n                        {% else %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.take_raw_photo') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw\">\n                        </form>\n                        {% endif %}\n                        {% if sc.isPhotoSeriesRecording %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.record_video') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Video\" disabled>\n                        </form>\n                        {% else %}\n                        {% if sc.isVideoRecording == false %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.record_video') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" onclick=\"removeAfWindow()\" type=\"submit\" value=\"Video\">\n                        </form>\n                        {% endif %}\n                        {% if sc.isVideoRecording == true %}\n                        <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('home.stop_recording') }}\">\n                            <input class=\"w3-button w3-black w3-round-xxlarge\" onclick=\"showAfWindows('{{ sc.lastLiveTab }}', '{{ sc.isVideoRecording }}', 'include_afwindows', '{{ sc.isZoomModeDraw }}', '{{ sc.scalerCropLiveView }}')\" type=\"submit\" value=\"Stop\">\n                        </form>\n                        {% endif %}\n                        {% endif %}\n                    </td>\n                    <td colspan=\"2\" style=\"width:20%\">\n                        <p style=\"margin-bottom: 0;\">{{ sc.displayBufferIndex }}</p>\n                    </td>\n                    <td style=\"width:12%\">\n                        <!-- Show button: shows photo and meta -->\n                        {% if sc.isDisplayHidden == true and sc.displayPhoto != None %}\n                        <form method=\"post\" action=\"{{ url_for('home.show_photo') }}\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Show\">\n                        </form>\n                        {% endif %}\n                        {% if sc.isDisplayHidden == false and sc.displayPhoto != None %}\n                        <form method=\"post\" action=\"{{ url_for('home.hide_photo') }}\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Hide\">\n                        </form>\n                        {% endif %}\n                    </td>\n                    <td style=\"width:12%\">\n                        <!-- Clear button: clears Photo buffer -->\n                        {% if sc.displayBufferCount > 0 or sc.displayPhoto != None %}\n                        <form method=\"post\" action=\"{{ url_for('home.clear_buffer') }}\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"{{ sc.buttonClear }}\">\n                        </form>\n                        {% endif %}\n                    </td>\n                </tr>\n                {% if sc.isDisplayHidden == false and sc.displayPhoto != None %}\n                <tr>\n                    <td style=\"width:40%\">\n                        <!-- Photo filename -->\n                        <p style=\"margin-bottom: 0\">{{ sc.displayFile }}</p>\n                    </td>\n                    <td style=\"width:15%\">\n                        <!-- Button +: Add to buffer -->\n                        {% if sc.isDisplayBufferIn() == false %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_add') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\"+\">\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_add') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\"+\" disabled>\n                        </form>\n                        {% endif %}\n                    </td>\n                    <td style=\"width:15%\">\n                        <!-- Button -: Remove from buffer -->\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_remove') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\"-\">\n                        </form>\n                    </td>\n                    <td style=\"width:15%\">\n                        <!-- Button <: Prev -->\n                        {% if (sc.isDisplayBufferIn() == true and sc.isDisplayBufferFirst() == false) or sc.isDisplayBufferIn() == flase  %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_prev') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\"<\">\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_prev') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\"<\" disabled>\n                        </form>\n                        {% endif %}\n                    </td>\n                    <td style=\"width:15%\">\n                        <!-- Button >: Next -->\n                        {% if (sc.isDisplayBufferIn() == true and sc.isDisplayBufferLast() == false) or sc.isDisplayBufferIn() == flase %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_next') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\">\">\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.photoBuffer_next') }}\">\n                            <input class=\"w3-button w3-circle w3-black\" type=\"submit\" value=\">\" disabled>\n                        </form>\n                        {% endif %}\n                    </td>\n                </tr>\n                <tr>\n                    <!-- The active Photo-->\n                    <td colspan=\"5\">\n                        <img\n                            style=\"width: 100%; height: 480px; object-fit: scale-down; cursor: pointer;\"\n                            src=\"{{ url_for('static', filename=sc.displayPhoto) }}\" \n                            class=\"w3-image\"\n                            onclick=\"openMedia(this.src, '{{ sc.displayFile }}')\">\n                    </td>\n                </tr>\n                {% endif %}\n            </table>\n        </div>\n        <div class=\"w3-half\">\n            <!-- Lower right area: Metadata-->\n            {% if sc.isDisplayHidden == false and sc.displayMeta != None %}\n            {% if sc.displayContent == \"meta\" %}\n            <h4 style=\"min-height: 36px; padding-left: 17px\">Metadata</h4>\n            <div style=\"max-height:45vh;overflow-y:auto\">\n                <table class=\"w3-table-all\">\n                    <thead>\n                        <tr>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">Property</th>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">Value</th>\n                        </tr>\n                    </thead>\n                    <tbody>\n                        {% for prop in sc.displayMeta %}\n                        {% if loop.index >= sc.displayMetaFirst and loop.index < sc.displayMetaLast and prop !=\"PispStatsOutput\" %}\n                        <tr>\n                            <td class=\"w3-tooltip\">\n                                {% if prop|string() == \"Camera\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Camera by which the photo was taken.<br>\n                                    This information is not contained in the metadata provided by Picamera2.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"SensorTimestamp\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The time this frame was produced by the\n                                    sensor, measured in nanoseconds since the\n                                    system booted. The time is sampled on the\n                                    camera start of frame interrupt, which occurs\n                                    as the first pixel of the new frame is written\n                                    out by the sensor. This control appears only in\n                                    captured image metadata and is read-only.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"ColourCorrectionMatrix\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The 3×3 matrix used within the image signal\n                                    processor (ISP) to convert the raw camera\n                                    colours to sRGB. This control appears only in\n                                    captured image metadata and is read-only.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"FocusFoM\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Reports a Figure of Merit (FoM) to indicate how in-focus the frame is. A larger FocusFoM value indicates a more in-focus\n                                    frame. This singular value may be based on a combination of statistics gathered from multiple focus regions within an\n                                    image. The number of focus regions and method of combination is platform dependent. In this respect, it is not\n                                    necessarily aimed at providing a way to implement a focus algorithm by the application, rather an indication of how\n                                    in-focus a frame is.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"ColourTemperature\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    An estimate of the colour temperature (in\n                                    Kelvin) of the current image. It is only available\n                                    in captured image metadata, and is read-only\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"ColourGains\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Pair of numbers where the first is the red gain\n                                    (the gain applied to red pixels by the AWB\n                                    algorithm) and the second is the blue gain.\n                                    Setting these numbers disables AWB.<br>\n                                    Range: [0.0,32.0]\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"AeLocked\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Report the lock status of a running AE algorithm.<br>\n                                    If the AE algorithm is locked the value shall be set to true, \n                                    if it's converging it shall be set to false. \n                                    If the AE algorithm is not running the control shall \n                                    not be present in the metadata control list.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"Lux\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    An estimate of the brightness (in lux) of the\n                                    scene. It is available only in captured image\n                                    metadata and is read-only.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"FrameDuration\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The amount of time (in microseconds) since\n                                    the previous camera frame. This value is only\n                                    available in captured image metadata and is\n                                    read-only. To change the camera’s framerate,\n                                    the \"FrameDurationLimits\" control should be\n                                    used.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"SensorBlackLevels\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The black levels of the raw sensor image. This\n                                    control appears only in captured image\n                                    metadata and is read-only. One value is\n                                    reported for each of the four Bayer channels,\n                                    scaled up as if the full pixel range were 16 bits\n                                    (so 4096 represents a black level of 16 in 10-\n                                    bit raw data).\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"DigitalGain\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The amount of digital gain applied to an\n                                    image. Digital gain is used automatically when\n                                    the sensor’s analogue gain control cannot go\n                                    high enough, and so this value is only reported\n                                    in captured image metadata. It cannot be set\n                                    directly - users should set the AnalogueGain\n                                    instead and digital gain will be used when\n                                    needed.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"AnalogueGain\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Analogue gain applied by the sensor.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"ScalerCrop\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    The scaler crop rectangle determines which\n                                    part of the image received from the sensor is\n                                    cropped and then scaled to produce an output\n                                    image of the correct size. It can be used to\n                                    implement digital pan and zoom. The\n                                    coordinates are always given from within the\n                                    full sensor resolution.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"ExposureTime\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Exposure time used for the sensor, measured\n                                    in microseconds.<br>\n                                    In brackets for exposure times < 1s: rounded reciprocal value\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"AfState\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Reports the current state of the Autofocus (AF) algorithm in conjunction \n                                    with the reported AfMode value and (in continuous AF mode) the AfPauseState value.<br> \n                                    If the AfMode is set to AfModeManual, then the AfState will always report \n                                    AfStateIdle (0) (even if the lens is subsequently moved). \n                                    Changing to the AfModeManual state does not initiate any lens movement.<br>\n                                    If the AfMode is set to AfModeAuto then the AfState will report AfStateIdle (0). \n                                    However, if AfModeAuto and AfTriggerStart are sent together \n                                    then AfState will omit AfStateIdle (0) and move straight to AfStateScanning (1) (and start a scan).<br>\n                                    If the AfMode is set to AfModeContinuous then the AfState will initially report AfStateScanning (1).\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"AfPauseState\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Only applicable in continuous (AfModeContinuous) mode, \n                                    this reports whether the algorithm is currently running (0), paused (2)\n                                    or pausing (1) (that is, will pause as soon as any in-progress scan completes).<br>\n                                    Any change to AfMode will cause AfPauseStateRunning (0) to be reported.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"SensorTemperature\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Temperature measure from the camera sensor in Celsius. \n                                    This is typically obtained by a thermal sensor present on-die or\n                                    in the camera module. The range of reported temperatures is device dependent.<br>\n                                    The SensorTemperature control will only be returned in metadata if a themal sensor is present.\n                                </span>\n                                {% endif %}\n                                {% if prop|string() == \"LensPosition\" %}\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Position of the lens. The units are dioptres\n                                    (reciprocal of the distance in metres).<br>\n                                    In brackets: focal distance in m.\n                                </span>\n                                {% endif %}\n                                {{ prop|string() }}\n                            </td>\n                            {% if prop|string() == \"AfState\" %}\n                                {% if sc.displayMeta[prop]|string() == \"0\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Idle)</td>\n                                {% elif sc.displayMeta[prop]|string() == \"1\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Scanning)</td>\n                                {% elif sc.displayMeta[prop]|string() == \"2\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Focused)</td>\n                                {% elif sc.displayMeta[prop]|string() == \"3\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Failed)</td>\n                                {% else %}\n                                    <td>{{ sc.displayMeta[prop]|string() }}</td>\n                                {% endif %}\n                            {% elif prop|string() == \"AfPauseState\" %}\n                                {% if sc.displayMeta[prop]|string() == \"0\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Running)</td>\n                                {% elif sc.displayMeta[prop]|string() == \"1\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Pausing)</td>\n                                {% elif sc.displayMeta[prop]|string() == \"2\" %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (Paused)</td>\n                                {% else %}\n                                    <td>{{ sc.displayMeta[prop]|string() }}</td>\n                                {% endif %}\n                            {% elif prop|string() == \"LensPosition\" %}\n                                {% set fdr = (1000.0/sc.displayMeta[prop])|int() %}\n                                {% set fd = fdr/1000.0 %}\n                                <td>{{ sc.displayMeta[prop]|string() }} ({{ fd|string() }} m)</td>\n                            {% elif prop|string() == \"ExposureTime\" %}\n                                {% if sc.displayMeta[prop] < 1000000 %}\n                                    {% set rt = (1000000/sc.displayMeta[prop])|int() %}\n                                    <td>{{ sc.displayMeta[prop]|string() }} (1/{{ rt|string() }} s)</td>\n                                {% else %}\n                                    <td>{{ sc.displayMeta[prop]|string() }}</td>\n                                {% endif %}\n                            {% else %}\n                            <td>{{ sc.displayMeta[prop]|string() }}</td>\n                            {% endif %}\n                        </tr>\n                        {% endif%}\n                        {% endfor %}\n                    </tbody>\n                </table>\n            </div>\n            {% else %}\n            <table>\n                <tr>\n                    <td style=\"width:100%\">\n                        <img style=\"max-height: 600px;\" src=\"{{ url_for('static', filename=sc.displayHistogram) }}\" class=\"w3-image\">\n                    </td>\n                </tr>\n            </table>\n            {% endif %}\n            <p> </p>\n            <table class=\"w3-table\">\n                <tr>\n                    <td style=\"width:25%\">\n                        {% if sc.useHistograms == True %}\n                        {% if sc.displayContent == \"meta\" %}\n                        <form method=\"post\" action=\"{{ url_for('home.show_histogram') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:100%\" type=\"submit\" value=\"Show Histogram\">\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.show_metadata') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:100%\" type=\"submit\" value=\"Show Metadata\">\n                        </form>\n                        {% endif %}\n                        {% endif %}\n                    </td>\n                    <td style=\"width:25%\">\n                    </td>\n                    <td style=\"width:25%\">\n                        {% if sc.displayContent == \"meta\" %}\n                        {% if sc.displayMetaFirst == 0 %}\n                        <form method=\"post\" action=\"{{ url_for('home.meta_prev') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:80%\" type=\"submit\" value=\"Previous\" disabled>\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.meta_prev') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:80%\" type=\"submit\" value=\"Previous\">\n                        </form>\n                        {% endif %}\n                        {% endif %}\n                    </td>\n                    <td style=\"width:25%\">\n                        {% if sc.displayContent == \"meta\" %}\n                        {% if sc.displayMetaLast < 999 %}\n                        <form method=\"post\" action=\"{{ url_for('home.meta_next') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:80%\" type=\"submit\" value=\"Next\">\n                        </form>\n                        {% else %}\n                        <form method=\"post\" action=\"{{ url_for('home.meta_next') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:80%\" type=\"submit\" value=\"Next\" disabled>\n                        </form>\n                        {% endif %}\n                        {% endif %}\n                    </td>\n                </tr>\n            </table>\n\n            {% endif %}\n        </div>\n    </div>\n    <script>\n        var activeTab = \"\"\n        function openCtrlTab(ctrlTabName, ctrlTabBtn) {\n            /*  Opens the selected Tab for camera controls\n                ctrlTabName:    Name of the tab to be opened\n                ctrlTabBtn:     Button in the button-bar to be highlighted\n            */\n            var i;\n            var x = document.getElementsByClassName(\"controlgroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(ctrlTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"ctrlmenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button ctrlmenu\";\n            }\n            document.getElementById(ctrlTabBtn).classList = \"w3-bar-item w3-button ctrlmenu w3-light-green\";\n\n            activeTab = ctrlTabName\n\n            if ('{{ sc.isVideoRecording }}' == \"False\") {\n                if (ctrlTabName == \"focus\") {\n                    removeAfWindowCanvas()\n                    if ('{{ cp.hasFocus }}' == \"True\") {\n                        drawAfWindow('include_afwindows', 'afwindows', '{{ sc.scalerCropLiveView }}')\n                        drawAllAfWindows()\n                    }\n                } else if (ctrlTabName == \"zoom\") {\n                    removeAfWindowCanvas()\n                    drawZoomWindow('{{ sc.isZoomModeDraw }}', 'scalercrop', '{{ sc.scalerCropLiveView }}')\n                    drawAllAfWindows()\n                } else {\n                    removeAfWindowCanvas()\n                }\n            } else {\n                removeAfWindowCanvas()\n            }\n        }\n    </script>\n    <script>\n        function includeCtrl(cbId, ctrlId, ctrlId2) {\n            /*  The function enables one or two controls (ctrlId) associated with a checkbox (cbId)\n            */\n            var cb = document.getElementById(cbId);\n            if (cb.checked == true) {\n                document.getElementById(ctrlId).disabled = false;\n                if (ctrlId2 !== undefined) {\n                    document.getElementById(ctrlId2).disabled = false;\n                }\n            } else {\n                document.getElementById(ctrlId).disabled = true;\n                if (ctrlId2 !== undefined) {\n                    document.getElementById(ctrlId2).disabled = true;\n                }\n            }\n        }\n    </script>\n    <script>\n        function enableAfWindows() {\n            /*  enable tha afwindows text field to allow Flask to request its content\n            */\n            document.getElementById(\"afwindows\").disabled = false\n        }\n\n        function enableScalerCrop() {\n            /*  enable tha scalercrop text field to allow Flask to request its content\n            */\n            document.getElementById(\"scalercrop\").disabled = false\n            //console.log(\"scalercrop enabled\");\n        }\n    </script>\n    <script>\n        function showAfWindows(tabSelected, vrActive, trgAf, trgZoom, camRef) {\n            /*  If camera has focus and if focus tab is selected,\n                or if zoom tab is selected and zoomModeDraw is active\n                show th AF Windows\n                This function is called after the page finished loading\n                in order to assure initial visibility of AF windows\n                tabSelected:    name of the initially selected tab\n                vrActive:       \"True\"/\"False\" if video recording is active\n                trgAf:          Trigger for Autofocus (checkbox)\n                                If checked, canvas and AF Windows are drawn\n                                If unchecked, canvas is removed\n                trgZoom:        Trigger for Zoom (ServerConfig.isZoomModeDraw)\n                                If true, canvas is drawn\n                                If false, canvas is removed\n                canRef:         Current scaler crop\n            */\n            //console.log(\"showAfWindows - tabselected:\", tabSelected, \" activeTab:\", activeTab, \" vrActive:\", vrActive)\n\n            // We need to handle the case where a specific tab has been selected without posting.\n            // In this case, tabSelected is not the actually selected tab\n            // but the tab on which the last post has been made\n\n            currentTab = tabSelected\n            if (activeTab != \"\") {\n                currentTab = activeTab\n            }\n\n            if (currentTab == \"focus\") {\n                var trgEl = document.getElementById(trgAf);\n                // First remove the existing canvas, which is required for resize\n                removeAfWindow();\n                if (trgEl) {\n                    if (trgEl.checked == true) {\n                        if (vrActive == \"False\") {\n                            // Then create a new canvas\n                            drawAfWindow(trgAf, 'afwindows', camRef);\n                            // ... and draw the windows\n                            drawAllAfWindows();\n                        }\n                    }\n                }\n            }\n            if (currentTab == \"zoom\") {\n                if (trgZoom == \"True\") {\n                    // First remove the existing canvas, which is required for resize\n                    removeAfWindow();\n                    if (vrActive == \"False\") {\n                        // Then create a new canvas\n                        drawZoomWindow('True', 'scalercrop', camRef);\n                        // ... and draw the window\n                        drawAllAfWindows();\n                    }\n                }\n            }\n        }\n\n    </script>\n    <script>\n        var useCase;\n        var doDraw;\n        var afWinTarget;\n        var liveCanvas;\n        var liveCanvasCtx;\n        var liveCanvasOffsetX;\n        var liveCanvasOffsetY;\n        var mouseIsDown;\n        var mouseStartX;\n        var mouseStartY;\n        var camXOffset;\n        var camYOffset;\n        var camWidth;\n        var camHeight;\n        var winXOffset;\n        var winYOffset;\n        var winWidth;\n        var winHeight;\n\n        function removeAfWindowCanvas(){\n            /*  Removes the canvas for AF Windows\n            */\n            var cnvEl = document.getElementById(\"livecanvas\");\n            if (cnvEl) {\n                // Unregister event listeners\n                document.getElementById('livecanvas').removeEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });   \n\n\n                document.getElementById('livecanvas').removeEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('livecanvas').removeEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });                \n                // Remove canvas             \n                document.body.removeChild(cnvEl);\n                liveCanvas = null;\n                liveCanvasCtx = null;\n            }\n        }\n\n        function parseWindows(wins) {\n            /*  Parses the tuple of one or multiple rectangles\n                ((x,x,x,x),(x,x,x,x))\n                and returns an array of rectangles as strings\n            */\n            var resa = [];\n            var cnt = 0;\n            if (wins[0] == \"(\") {\n                var wns = wins.slice(1);\n                if (wns[wns.length - 1] == \")\") {\n                    wns = wns.slice(0, wns.length - 1);\n                    while (wns.length > 0) {\n                        var i = wns.indexOf(\")\");\n                        if ( i > 0) {\n                            wn = wns.slice(0, i + 1);\n                            resa[cnt] = wn;\n                            cnt = cnt + 1;\n                            if (i < wns.length) {\n                                wns = wns.slice(i + 2);\n                            } else {\n                                wns = \"\";\n                            }\n                        } else {\n                            wns = \"\";\n                        }\n                    }\n                }\n            }\n            return resa;\n        }\n\n        function parseWindow(wins) {\n            /*  Parses the rectangle\n                (x,x,x,x)\n                and returns an array of one rectangle as string\n            */\n            var resa = [] \n            resa[0]= wins;\n            return resa;\n        }\n\n        function drawCnvWindow(win, index, winlist) {\n            /*  Draw the given window on the canvas\n            */\n            //console.log(\"drawCnvWindow win:\", win);\n\n            // Parse the window spec for the AF Window\n            var afWina = parseRectTuple(win);\n            var afWinXOffset = afWina[0];\n            var afWinYOffset = afWina[1];\n            var afWinWidth = afWina[2];\n            var afWinHeight = afWina[3];\n            //console.log(\"cnv: \", liveCanvasOffsetX, liveCanvasOffsetY, liveCanvas.width, liveCanvas.height)\n            //console.log(\"cam: \", camXOffset, camYOffset, camWidth, camHeight)\n            //console.log(\"afw: \", afWinXOffset, afWinYOffset, afWinWidth, afWinHeight)\n\n            // Scale to canvas\n            var winWidth = Math.round(afWinWidth * liveCanvas.width / camWidth);\n            var winHeight = Math.round(afWinHeight * liveCanvas.height / camHeight);\n            var winXOffset = Math.round((afWinXOffset - camXOffset) * liveCanvas.width / camWidth);\n            var winYOffset = Math.round((afWinYOffset - camYOffset) * liveCanvas.height / camHeight);\n            //console.log(\"win: \", winXOffset, winYOffset, winWidth, winHeight)\n\n            // Draw\n            liveCanvasCtx.strokeRect(winXOffset, winYOffset, winWidth, winHeight);\n        }\n\n        function drawAllAfWindows() {\n            /*  Parses the windows entry in the target element\n                and draws all resulting windows\n            */\n            //console.log(\"drawAllAfWindows - useCase=\" + useCase)\n            var afWindows = afWinTarget.value;\n            //console.log(\"afWindows:\", afWindows)\n            if (useCase == \"afwindows\") {\n                var winlist = parseWindows(afWindows)\n            } else if (useCase == \"zoom\") {\n                var winlist = parseWindow(afWindows)\n            } else {\n                winlist = []\n            }\n            //console.log(\"winlist:\", winlist)\n\n            if (liveCanvasCtx) {\n                // Clear canvas\n                liveCanvasCtx.clearRect(0, 0, liveCanvas.width, liveCanvas.height);\n\n                // Draw all windows\n                winlist.forEach(drawCnvWindow)\n            }\n        }\n\n        function addNewAfWindow() {\n            /*  add a new AF Window to the tuple of windows available in the target element\n            */\n            // Scale\n            //console.log('addNewAfWindow');\n            //console.log(\"cnv: \", liveCanvasOffsetX, liveCanvasOffsetY, liveCanvas.width, liveCanvas.height)\n            //console.log(\"cam: \", camXOffset, camYOffset, camWidth, camHeight)\n            //console.log(\"win: \", winXOffset, winYOffset, winWidth, winHeight)\n            var afWinWidth = Math.round(winWidth * camWidth / liveCanvas.width);\n            var afWinHeight = Math.round(winHeight * camHeight / liveCanvas.height);\n            var afWinXOffset = Math.round(camXOffset + winXOffset * camWidth / liveCanvas.width);\n            var afWinYOffset = Math.round(camYOffset + winYOffset * camHeight / liveCanvas.height);\n            var afWindows = afWinTarget.value;\n            //console.log(\"afWindows old:\", afWindows);\n\n            // Concatenate to afWindows\n            afWindows = afWindows.slice(0, afWindows.length - 1);\n            if (afWindows.length > 1) {\n                afWindows = afWindows + \",\"\n            }\n            afWindows = afWindows \n                + \"(\" \n                + afWinXOffset.toString() + \",\" \n                + afWinYOffset.toString() + \",\"\n                + afWinWidth.toString() + \",\"\n                + afWinHeight.toString()\n                + \")\";\n            afWindows = afWindows + \")\";\n            //console.log(\"afWindows new:\", afWindows);\n\n            // Store in target\n            afWinTarget.value = afWindows;\n        }\n\n        function setZoomWindow() {\n            /*  Set the window for zoom\n            */\n            // Scale\n            var zoomWinWidth = Math.round(winWidth * camWidth / liveCanvas.width);\n            var zoomWinHeight = Math.round(winHeight * camHeight / liveCanvas.height);\n            var zoomWinXOffset = Math.round(camXOffset + winXOffset * camWidth / liveCanvas.width);\n            var zoomWinYOffset = Math.round(camYOffset + winYOffset * camHeight / liveCanvas.height);\n\n            zoomWindow = \"(\" \n                + zoomWinXOffset.toString() + \",\" \n                + zoomWinYOffset.toString() + \",\"\n                + zoomWinWidth.toString() + \",\"\n                + zoomWinHeight.toString()\n                + \")\";\n\n            // Store in target\n            afWinTarget.value = zoomWindow;\n        }\n\n        function handleMouseDown(e) {\n            /*  In case of mouse down, memorize point as start of rectangle\n            */\n            //console.log('handleMouseDown')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // save the starting x/y of the rectangle\n            mouseStartX = parseInt(e.clientX - liveCanvasOffsetX);\n            mouseStartY = parseInt(e.clientY - liveCanvasOffsetY);\n\n            // set a flag indicating the drag has begun\n            mouseIsDown = true;\n        }\n\n        function handleTouchStart(e) {\n            /*  In case of mouse down, memorize point as start of rectangle\n            */\n            //console.log('handleMouseDown')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // save the starting x/y of the rectangle\n            mouseStartX = parseInt(e.targetTouches[0].clientX - liveCanvasOffsetX);\n            mouseStartY = parseInt(e.targetTouches[0].clientY - liveCanvasOffsetY);\n\n            // set a flag indicating the drag has begun\n            mouseIsDown = true;\n        }\n\n        function handleMouseUp(e) {\n            /*  In case of mouse up, register end of drawing\n            */\n            //console.log('handleMouseUp');\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // the drag is over, clear the dragging flag\n            mouseIsDown = false;\n\n            if (useCase == \"afwindows\") {\n                // Add new window\n                addNewAfWindow();\n            }\n\n            if (useCase == \"zoom\") {\n                // Add new window\n                setZoomWindow();\n            }\n        }\n\n        function handleMouseOut(e) {\n            /*  In case of mouse leaving the canvas,\n                Draw all windows\n            */\n            //console.log('handleMouseOut')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // the drag is over, clear the dragging flag\n            mouseIsDown = false;\n            if (useCase == \"afwindows\") {\n                // Draw all windows\n                drawAllAfWindows();\n            }\n        }\n\n        function handleMouseMove(e) {\n            /*  While mouse is moving with mouse down,\n                Clear canvas from previous rectangle\n                and draw rectangle with new coordinates.\n                Memorize rectangla parameters.\n            */\n            //console.log('handleMouseMove')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // if we're not dragging, just return\n            if (!mouseIsDown) {\n                return;\n            }\n\n            // get the current mouse position\n            mouseX = parseInt(e.clientX - liveCanvasOffsetX);\n            mouseY = parseInt(e.clientY - liveCanvasOffsetY);\n\n            // clear the canvas\n            liveCanvasCtx.clearRect(0, 0, liveCanvas.width, liveCanvas.height);\n\n            // calculate the rectangle width/height based\n            // on starting vs current mouse position\n            var width = mouseX - mouseStartX;\n            var height = mouseY - mouseStartY;\n\n            // For zoom preserve aspect ratio\n            if (useCase == \"zoom\") {\n                if (Math.abs(width) > Math.abs(height)) {\n                    height = Math.sign(width) * Math.sign(height) * Math.round(width * camHeight / camWidth)\n                } else {\n                    width = Math.sign(width) * Math.sign(height) * Math.round(height * camWidth / camHeight)\n                }\n            }\n\n            // draw a new rect from the start position \n            // to the current mouse position\n            liveCanvasCtx.strokeRect(mouseStartX, mouseStartY, width, height);\n            if (width >= 0) {\n                winXOffset = mouseStartX;\n                winWidth = width;\n            } else {\n                winXOffset = mouseX;\n                winWidth = -1 * width;\n            }\n            if (height >= 0) {\n                winYOffset = mouseStartY;\n                winHeight = height;\n            } else {\n                winYOffset = mouseY;\n                winHeight = -1 * height;\n            }\n        }\n\n        function handleTouchMove(e) {\n            /*  While mouse is moving with mouse down,\n                Clear canvas from previous rectangle\n                and draw rectangle with new coordinates.\n                Memorize rectangla parameters.\n            */\n            //console.log('handleMouseMove')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // if we're not dragging, just return\n            if (!mouseIsDown) {\n                return;\n            }\n\n            // get the current mouse position\n            mouseX = parseInt(e.targetTouches[0].clientX - liveCanvasOffsetX);\n            mouseY = parseInt(e.targetTouches[0].clientY - liveCanvasOffsetY);\n\n            // clear the canvas\n            liveCanvasCtx.clearRect(0, 0, liveCanvas.width, liveCanvas.height);\n\n            // calculate the rectangle width/height based\n            // on starting vs current mouse position\n            var width = mouseX - mouseStartX;\n            var height = mouseY - mouseStartY;\n\n            // For zoom preserve aspect ratio\n            if (useCase == \"zoom\") {\n                if (Math.abs(width) > Math.abs(height)) {\n                    height = Math.sign(width) * Math.sign(height) * Math.round(width * camHeight / camWidth)\n                } else {\n                    width = Math.sign(width) * Math.sign(height) * Math.round(height * camWidth / camHeight)\n                }\n            }\n\n            // draw a new rect from the start position \n            // to the current mouse position\n            liveCanvasCtx.strokeRect(mouseStartX, mouseStartY, width, height);\n            if (width >= 0) {\n                winXOffset = mouseStartX;\n                winWidth = width;\n            } else {\n                winXOffset = mouseX;\n                winWidth = -1 * width;\n            }\n            if (height >= 0) {\n                winYOffset = mouseStartY;\n                winHeight = height;\n            } else {\n                winYOffset = mouseY;\n                winHeight = -1 * height;\n            }\n        }\n\n        function parseRectTuple(tuple) {\n            /*  Parse a Python tuple for libcamera.Rectangle\n                (xOffset, yOffset, width, height)\n            */\n            resn = [0, 0, 0, 0]\n            if (tuple[0] == \"(\") {\n                var tpl = tuple.slice(1)\n                if (tpl[tpl.length - 1] == \")\") {\n                    tpl = tpl.slice(0, tpl.length - 1)\n                    var res = tpl.split(\",\")\n                    if (res.length == 4) {\n                        resn = [parseInt(res[0]), parseInt(res[1]), parseInt(res[2]), parseInt(res[3])]\n                    }\n                }\n            }\n            return resn\n        }\n\n        function drawZoomWindow(trg, tgt, camRef) {\n            /*  Initialize the drawing infrastructure for AF Windows\n                trg: Trigger (ServerConfig.isZoomModeDraw)\n                tgt: Target element taking zoom window\n                camRef: Scaler crop for Live View as base for scaling\n            */\n            useCase = \"zoom\";\n            if (trg == \"True\") {\n                doDraw = true;\n            } else {\n                doDraw = false;\n            }\n            drawWindow(tgt, camRef);\n        }\n\n        function drawAfWindow(trg, tgt, camRef) {\n            /*  Initialize the drawing infrastructure for AF Windows\n                trg: Trigger (checkbox)\n                     If checked, canvas and AF Windows are drawn\n                     If unchecked, canvas is removed\n                tgt: Target element taking AF windows\n                camRef: Scaler crop for Live View as base for scaling\n            */\n            useCase = \"afwindows\";\n            var trgEl = document.getElementById(trg);\n            if (trgEl) {\n                if (trgEl.checked == true) {\n                    doDraw = true;\n                } else {\n                    doDraw = false;\n                }\n                drawWindow(tgt, camRef);\n            }\n        }\n\n        function drawWindow(tgt, camRef) {\n            /*  Creates canvas over live view image and allows drawing of AF Windows\n                tgt: Target element taking windows\n                camRef: Scaler crop for Live View as base for scaling\n            */\n            //console.log(\"drawWindow tgt:\", tgt, \" camRef\", camRef);\n            afWinTarget = document.getElementById(tgt);\n            // calculate camera crop window params\n            var camCrop = parseRectTuple(camRef);\n            camXOffset = camCrop[0];\n            camYOffset = camCrop[1];\n            camWidth = camCrop[2];\n            camHeight = camCrop[3];\n            //console.log(camXOffset, camYOffset, camWidth, camHeight);\n\n            if (doDraw == true) {\n                // Trigger active -> prepare canvas\n                var img = document.getElementById(\"liveviewimage\");\n                // Craete the canvas\n                liveCanvas = document.createElement('canvas');\n                liveCanvas.id = \"livecanvas\";\n                document.body.appendChild(liveCanvas);\n                // Position and size the canvas\n                bRect = img.getBoundingClientRect();\n                //console.log(\"img top:\", bRect.top, \" left:\", bRect.left, \" width:\", bRect.width, \" height:\", bRect.height)\n                liveCanvas.style.position = \"absolute\";\n                liveCanvas.style.top = bRect.top + \"px\";\n                liveCanvas.style.left = bRect.left + \"px\";\n                liveCanvas.width = bRect.width;\n                liveCanvas.height = bRect.height;\n                // Set context\n                liveCanvasCtx = liveCanvas.getContext(\"2d\");\n                liveCanvasCtx.strokeStyle = \"red\";\n                liveCanvasCtx.lineWidth = 2;\n                // Determine canvas position\n                liveCanvasOffset = liveCanvas.getBoundingClientRect();\n                liveCanvasOffsetX = liveCanvasOffset.left\n                liveCanvasOffsetY = liveCanvasOffset.top\n                // Initialize mouse down\n                mouseIsDown = false;\n                // Register event listeners\n                document.getElementById('livecanvas').addEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('livecanvas').addEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('livecanvas').addEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').addEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });                \n\n                document.getElementById('livecanvas').addEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('livecanvas').addEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('livecanvas').addEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').addEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });                \n            } else {\n                // Trigger inactive -> destroy canvas and clear AFWindows\n                removeAfWindow()\n                // Clear afWindows field\n                if (useCase == \"afwindows\") {\n                    afWinTarget.value = \"()\";\n                }\n            }\n        }\n\n        function removeAfWindow() {\n            /*  Remove AfWindow canvas infrastructure\n            */\n            var cnvEl = document.getElementById(\"livecanvas\");\n            if (cnvEl) {\n                // Unregister event listeners\n                document.getElementById('livecanvas').removeEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });\n\n                document.getElementById('livecanvas').removeEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('livecanvas').removeEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('livecanvas').removeEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });                \n                // Remove canvas             \n                document.body.removeChild(cnvEl);\n                liveCanvas = null;\n                liveCanvasCtx = null;\n            }\n            \n        }\n        function onlineHelp() {\n            window.open(\"{{ sc.getBaseHelpUrl() }}/LiveScreen/\");\n        }\n    </script>\n    <script>\n        function openMedia(src, fileName) {\n            const parts = src.split(\"/\");\n            const fnParts = fileName.split(\".\");\n            const ext = fnParts[fnParts.length - 1].toLowerCase();\n            if (ext === \"mp4\") {\n                parts[parts.length - 1] = fileName;\n                src = parts.join(\"/\");\n                window.open(\n                    '/media-viewer?type=video&src=' + encodeURIComponent(src),\n                    '_blank',\n                    'noopener'\n                );\n            } else {\n                window.open(\n                    '/media-viewer?type=image&src=' + encodeURIComponent(src),\n                    '_blank',\n                    'noopener'\n                );\n            }\n        }\n\n        function confirmExecution(form, command, needsConfirm) {\n            // console.log(\"index.confirmExecution - needsConfirm=\", needsConfirm);\n            if (needsConfirm == \"True\") {\n                if (confirm(\"Do you want to execute the following command?\\n\" + command)) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n\n        function confirmAction(form, action, needsConfirm) {\n            // console.log(\"index.confirmAction - needsConfirm=\", needsConfirm);\n            if (needsConfirm == \"True\") {\n                if (confirm(\"Do you want to execute the following action?\\n\" + action)) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n\n    </script>\n\n    <style>\n        canvas {\n            border: 1px solid red;\n        }\n    </style>\n\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/home/liveDirectPanel.html",
    "content": "<!--\nRaspiCamSrv's Live page    \n-->\n{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Live - Direct Control{% endblock %}\n{% endblock %}\n\n{% block content %}\n<div class=\"w3-bar w3-green\">\n    <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n        <div class=\"w3-tooltip\">\n            <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from\n                GitHub\n            </span>\n            <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\"\n                alt=\"Online help\" style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n        </div>\n    </div>\n</div>\n<div>\n    <p>&nbsp;</p>\n    <div class=\"w3-grid\" style=\"grid-template-columns: 1fr 4fr 1fr\">\n        <div></div>\n        <div>\n            <table style=\"margin: auto; width: 310px;\">\n                {% if sc.activeCameraIsUsb == False or \"LensPosition\" in cc.usbCamControls %}\n                {% if cc.include_lensPosition == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.001 %}\n                {% set max = 999.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"LensPosition\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"LensPosition\"][\"max\"] | float %}\n                {% endif %}\n                {% set minVal = ((min/max)**(1.0/3.0))|round(3, \"common\") %}\n                {% set val = ((cc.focalDistance/max)**(1.0/3.0))|round(3, \"common\") %}\n                <tr>\n                    <td style=\"width: 120px;\">\n                        <label for=\"focaldistance\">Focal Distance:</label>\n                    </td>\n                    <td style=\"width: 145px;\">\n                        <input \n                            style=\"width: 100%;\"\n                            type=\"range\" \n                            id=\"focaldistance\" \n                            name=\"focaldistance\" \n                            min=0.0 \n                            max=1.0\n                            step=0.001 \n                            value=\"{{ val }}\"\n                            onchange=\"setFocalDistance(this.value, {{ minVal }})\"\n                            oninput=\"focaldistanceoutput.value = this.value\"\n                        >\n                    </td>\n                    <td style=\"width: 45px;\">\n                        <output name=\"focaldistanceoutput\" id=\"focaldistanceoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n            </table>\n        </div>\n        <div></div>\n        <div>\n            <table style =\"width: 180px; margin-left: auto; margin-right: 10px;\">\n                {% if sc.activeCameraIsUsb == False or \"ExposureTime\" in cc.usbCamControls %}\n                {% if cc.include_exposureTime == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 10.0 %}\n                {% set default = 0.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"ExposureTime\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"ExposureTime\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"ExposureTime\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.exposureTimeSec) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"exposuretimesec\">Exposure Time:</label>\n                        <output style=\"display:inline-block\" name=\"exposuretimesecoutput\" id=\"exposuretimesecoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"exposuretimesec\" \n                            name=\"exposuretimesec\" \n                            min=-1.0 \n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setExposureTimeSec(this.value, 0.0)\"\n                            oninput=\"exposuretimesecoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"ExposureValue\" in cc.usbCamControls %}\n                {% if cc.include_exposureValue == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = -8.0 %}\n                {% set max = 8.0 %}\n                {% set default = 0.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"ExposureValue\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"ExposureValue\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"ExposureValue\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.exposureValue) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"exposurevalue\">Exposure Value:</label>\n                        <output style=\"display:inline-block\" name=\"exposurevalueoutput\" id=\"exposurevalueoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"exposurevalue\" \n                            name=\"exposurevalue\" \n                            min=-1.0 \n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setExposureValue(this.value)\"\n                            oninput=\"exposurevalueoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"AnalogueGain\" in cc.usbCamControls %}\n                {% if cc.include_analogueGain == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 1.0 %}\n                {% set max = 99.0 %}\n                {% set default = 1.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"AnalogueGain\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"AnalogueGain\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"AnalogueGain\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.analogueGain) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"analoguegain\">Analogue Gain:</label>\n                        <output style=\"display:inline-block\" name=\"analoguegainoutput\" id=\"analoguegainoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"analoguegain\" \n                            name=\"analoguegain\" \n                            min=-1.0\n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setAnalogueGain(this.value, 0.0)\"\n                            oninput=\"analoguegainoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"ColourGainRed\" in cc.usbCamControls %}\n                {% if cc.include_colourGains == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 32.0 %}\n                {% set default = 0.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"ColourGainRed\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"ColourGainRed\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"ColourGainRed\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.colourGains[0]) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"colourgainred\">Colour Gain Red:</label>\n                        <output style=\"display:inline-block\" name=\"colourgainredoutput\" id=\"colourgainredoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"colourgainred\" \n                            name=\"colourgainred\" \n                            min=-1.0\n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setColourGainRed(this.value, 0.0)\"\n                            oninput=\"colourgainredoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"ColourGainBlue\" in cc.usbCamControls %}\n                {% if cc.include_colourGains == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 32.0 %}\n                {% set default = 0.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"ColourGainBlue\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"ColourGainBlue\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"ColourGainBlue\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.colourGains[1]) %}\n                <tr>\n                    <td>\n                        <label for=\"colourgainblue\">Colour Gain Blue:</label>\n                        <output style=\"display:inline\" name=\"colourgainblueoutput\" id=\"colourgainblueoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"colourgainblue\" \n                            name=\"colourgainblue\" \n                            min=-1.0\n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setColourGainBlue(this.value, 0.0)\"\n                            oninput=\"colourgainblueoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n            </table>\n        </div>\n        <div class=\"w3-border\" style=\"justify-content: center; align-items: center;\">\n            <img \n                src=\"{{ url_for('home.live_view_feed') }}\" \n                class=\"w3-image\" \n                id=\"liveviewimage\" \n                alt=\"Camera Live View\"\n                style=\"width: 100%; height: 100%; object-fit: contain;\"\n            >\n        </div>\n        <div>\n            <table>\n                {% if sc.activeCameraIsUsb == False or \"Sharpness\" in cc.usbCamControls %}\n                {% if cc.include_sharpness == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 32.0 %}\n                {% set default = 1.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"Sharpness\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"Sharpness\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"Sharpness\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.sharpness) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"sharpness\">Sharpness:</label>\n                        <output style=\"display:inline-block\" name=\"sharpnessoutput\" id=\"sharpnessoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"sharpness\" \n                            name=\"sharpness\" \n                            min=-1.0 \n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setSharpness(this.value)\"\n                            oninput=\"sharpnessoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"Contrast\" in cc.usbCamControls %}\n                {% if cc.include_contrast == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 32.0 %}\n                {% set default = 1.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"Contrast\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"Contrast\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"Contrast\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.contrast) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"contrast\">Contrast:</label>\n                        <output style=\"display:inline-block\" name=\"contrastoutput\" id=\"contrastoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"contrast\" \n                            name=\"contrast\" \n                            min=-1.0 \n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setContrast(this.value)\"\n                            oninput=\"contrastoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"Saturation\" in cc.usbCamControls %}\n                {% if cc.include_saturation == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = 0.0 %}\n                {% set max = 32.0 %}\n                {% set default = 1.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"Saturation\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"Saturation\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"Saturation\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.saturation) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"saturation\">Saturation:</label>\n                        <output style=\"display:inline-block\" name=\"saturationoutput\" id=\"saturationoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"saturation\" \n                            name=\"saturation\" \n                            min=-1.0\n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setSaturation(this.value)\"\n                            oninput=\"saturationoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n                {% if sc.activeCameraIsUsb == False or \"Brightness\" in cc.usbCamControls %}\n                {% if cc.include_brightness == True %}\n                {% if sc.activeCameraIsUsb == False %}\n                {% set min = -1.0 %}\n                {% set max = 1.0 %}\n                {% set default = 0.0 %}\n                {% else %}\n                {% set min = cc.usbCamControls[\"Brightness\"][\"min\"] | float %}\n                {% set max = cc.usbCamControls[\"Brightness\"][\"max\"] | float %}\n                {% set default = cc.usbCamControls[\"Brightness\"][\"default\"] | float %}\n                {% endif %}\n                {% set val = sc.ctrlValToSliderPos(min, max, default, cc.brightness) %}\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <label style=\"display:inline-block\" for=\"brightness\">Brightness:</label>\n                        <output style=\"display:inline-block\" name=\"brightnessoutput\" id=\"brightnessoutput\">{{ val }}</output>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <input \n                            type=\"range\" \n                            id=\"brightness\" \n                            name=\"brightness\" \n                            min=-1.0\n                            max=1.0 \n                            step=0.01 \n                            value=\"{{ val }}\"\n                            onchange=\"setBrightness(this.value)\"\n                            oninput=\"brightnessoutput.value = this.value\"\n                        >\n                    </td>\n                </tr>\n                {% endif %}\n                {% endif %}\n\n            </table>\n        </div>\n        <div></div>\n        <div>\n            <table style=\"margin: auto; width: 310px;\">\n                <tr>\n                    <td style=\"width: 120px;\">\n                        <label for=\"zoomfactor\">Zoom Factor:</label>\n                    </td>\n                    {% set max = sc.zoomFactor %}\n                    <td style=\"width: 145px;\">\n                        <input \n                            style=\"width: 100%;\"\n                            type=\"range\" \n                            id=\"zoomfactor\" \n                            name=\"zoomfactor\" \n                            min=10.0\n                            max=100.0 \n                            step=0.01 \n                            value=\"{{ sc.zoomFactor }}\"\n                            onchange=\"setZoomFactor(this.value, {{ max }})\"\n                            oninput=\"zoomfactoroutput.value = this.value\"\n                        >\n                    </td>\n                    <td  style=\"width: 45px;\">\n                        <output name=\"zoomfactoroutput\" id=\"zoomfactoroutput\">{{ sc.zoomFactor }}</output>\n                    </td>\n                </tr>\n            </table>\n        </div>\n        <div>\n        </div>\n    </div>\n</div>\n<script>\n    function showAfWindows(tabSelected, vrActive, trgAf, trgZoom, camRef) {\n        const x = 0;\n    }\n\n    function doSubmit(form) {\n        document.getElementById(form).submit();\n    }\n\n    function setSharpness(val) {\n        fetch(\"{{ url_for('home.dc_set_Sharpness') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setContrast(val) {\n        fetch(\"{{ url_for('home.dc_set_Contrast') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setSaturation(val) {\n        fetch(\"{{ url_for('home.dc_set_Saturation') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setBrightness(val) {\n        fetch(\"{{ url_for('home.dc_set_Brightness') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setExposureTimeSec(val, min) {\n        if (val < min) {\n            val = min;\n            document.getElementById(\"exposuretimesec\").value = min;\n            document.getElementById(\"exposuretimesecoutput\").value = min;\n        }\n        fetch(\"{{ url_for('home.dc_set_exposureTimeSec') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setExposureValue(val) {\n        fetch(\"{{ url_for('home.dc_set_exposureValue') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setAnalogueGain(val, min) {\n        if (val < min) {\n            val = min;\n            document.getElementById(\"analoguegain\").value = min;\n            document.getElementById(\"analoguegainoutput\").value = min;\n        }\n        fetch(\"{{ url_for('home.dc_set_AnalogueGain') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setColourGainRed(val, min) {\n        if (val < min) {\n            val = min;\n            document.getElementById(\"colourgainred\").value = min;\n            document.getElementById(\"colourgainredoutput\").value = min;\n        }\n        fetch(\"{{ url_for('home.dc_set_ColourGainRed') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setColourGainBlue(val, min) {\n        if (val < min) {\n            val = min;\n            document.getElementById(\"colourgainblue\").value = min;\n            document.getElementById(\"colourgainblueoutput\").value = min;\n        }\n        fetch(\"{{ url_for('home.dc_set_ColourGainBlue') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setFocalDistance(val, min) {\n        if (val < min) {\n            val = min;\n            document.getElementById(\"focaldistance\").value = min;\n            document.getElementById(\"focaldistanceoutput\").value = min;\n        }\n        fetch(\"{{ url_for('home.dc_set_FocalDistance') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n\n    function setZoomFactor(val, max) {\n        if (val > max) {\n            val = max;\n            document.getElementById(\"zoomfactor\").value = val;\n            document.getElementById(\"zoomfactoroutput\").value = val;\n        }\n        fetch(\"{{ url_for('home.dc_set_ZoomFactor') }}\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ value: val })\n        });\n    }\n    function onlineHelp() {\n        window.open(\"{{ sc.getBaseHelpUrl() }}/LiveDirectControl/\");\n    }\n</script>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/images/main.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Photos{% endblock %}\n    <style>\n        /* Custom styles for the photo viewer page */\n        .video-wrapper {\n        position: relative;\n        width: 100%;\n        }\n\n        /* Native video */\n        .video-wrapper video {\n        width: 100%;\n        display: block;\n        }\n\n        /* Overlay captures clicks on video surface only */\n        .open-overlay {\n        position: absolute;\n        left: 0;\n        right: 0;\n        top: 0;\n        bottom: 20%; /* height of native controls */\n        cursor: pointer;\n        background: transparent;\n        }\n    </style>\n{% endblock %}\n\n{% block content %}\n<div class=\"w3-bar w3-green\">\n    <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n        <div class=\"w3-tooltip\">\n            <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from\n                GitHub\n            </span>\n            <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\"\n                alt=\"Online help\" style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n        </div>\n    </div>\n</div>\n<div class=\"w3-row\">\n    <!-- Controls -->\n    <p style=\"height: 15px; margin:0\"></p>\n    <table>\n        <tr>\n            <form method=\"post\" id=\"pvcontrol\" action=\"{{ url_for('images.control') }}\">\n                <td>\n                    Camera:\n                </td>\n                <td>\n                    <select name=\"camera\" id=\"camera\" onchange=\"dosubmit('pvcontrol')\">\n                        {% for cam in cs %}\n                        {% if cam.isUsb == False or sc.useUsbCameras == True %}\n                        {% set model = cam.model[0:15] %}\n                        {% if sc.pvCamera == cam.num %}\n                        <option value=\"{{ cam.num }}\" selected>{{ cam.num }}: {{ model }}</option>\n                        {% else %}\n                        <option value=\"{{ cam.num }}\">{{ cam.num }}: {{ model }}</option>\n                        {% endif %}\n                        {% endif %}\n                        {% endfor %}\n                        {% if sc.useStereo == True %}\n                        {% if sc.pvCamera == \"S\" %}\n                        <option value=\"S\" selected>Stereo</option>\n                        {% else %}\n                        <option value=\"S\">Stereo</option>\n                        {% endif %}\n                        {% endif %}\n                    </select>\n                </td>\n                <td>\n                    &nbsp;From:\n                </td>\n                <td>\n                    <input style=\"width:100%\" type=\"date\" onchange=\"dosubmit('pvcontrol')\" id=\"pvfrom\" name=\"pvfrom\"\n                        value=\"{{ sc.pvFromStr }}\">\n                </td>\n                <td>\n                    &nbsp;To:\n                </td>\n                <td>\n                    <input style=\"width:100%\" type=\"date\" onchange=\"dosubmit('pvcontrol')\" id=\"pvto\" name=\"pvto\"\n                        value=\"{{ sc.pvToStr }}\">\n                </td>\n            </form>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" id=\"setevstartnow\" method=\"post\" action=\"{{ url_for('images.today') }}\">\n                    <input class=\"w3-button w3-sand\" type=\"submit\" value=\"Today\">\n                </form>\n            </td>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('images.all') }}\">\n                    <input class=\"w3-button w3-sand\" type=\"submit\" value=\"All\">\n                </form>\n            </td>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('images.select_all') }}\">\n                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\" \n                        value=\"Select all\">\n                </form>\n            </td>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('images.deselect_all') }}\">\n                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\"\n                        value=\"Deselect all\">\n                </form>\n            </td>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" id=\"deleteselected\" method=\"post\" action=\"{{ url_for('images.delete_selected') }}\">\n                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\"\n                        onclick=\"confirmDelete('deleteselected')\" value=\"Delete\">\n                </form>\n            </td>\n            <td>\n                &nbsp;\n            </td>\n            <td>\n                <form style=\"display:inline-block\" id=\"downloadselected\" method=\"post\" action=\"{{ url_for('images.download_selected') }}\">\n                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\"\n                        onclick=\"confirmDownload('downloadselected')\" value=\"Download\">\n                </form>\n            </td>\n        </tr>\n    </table>\n</div>\n<hr>\n<div class=\"w3-row\">\n    <!-- Images and Videos -->\n    <div class=\"w3-quarter\" style=\"height:1000px; overflow: auto\">\n        <!-- Overview tab-->\n        <form method=\"post\" id=\"pvselect\" action=\"{{ url_for('images.select') }}\">\n            <table>\n                {% for entry in sc.pvList %}\n                {% set urlMini=url_for('static', filename=entry['path']) %}\n                {% set urlDetail=url_for('static', filename=entry['detailPath']) %}\n                {% set file=entry['file'] %}\n                {% set name=entry['name'] %}\n                {% set type=entry['type'] %}\n                {% set sel=entry['sel'] %}\n                <tr>\n                    <td style=\"width: 10%\">\n                        {% if sel == True %}\n                        <input form=\"pvselect\" type=\"checkbox\" onchange=\"dosubmit('pvselect')\" id=photo_{{ name }} name=photo_{{ name }} value=\"1\" checked>\n                        {% else %}\n                        <input form=\"pvselect\" type=\"checkbox\" onchange=\"dosubmit('pvselect')\" id=photo_{{ name }} name=photo_{{ name }} value=\"0\">\n                        {% endif %}\n                    </td>\n                    <td style=\"width: 90%;min-width: 180px\">\n                        <div class=\"w3-display-container w3-text-white\">\n                            <img \n                                style=\"width: 100%; height: 110px; object-fit: scale-down\"\n                                src=\"{{ urlMini }}\" \n                                alt=\"{{ name }}\" \n                                onclick=\"showDetail('{{ type }}', '{{ urlDetail }}', 'detailphoto', '{{ file }}', '{{ name }}')\"\n                            >\n                            <div class=\"w3-display-bottommiddle w3-container\"><p>{{ file }}</p></div>\n                        </div>\n                    </td>\n                </tr>\n                {% endfor %}\n            </table>\n        </form>\n    </div>\n    <div class=\"w3-threequarter\">\n        <!-- Detail-->\n        <div id=\"detailphoto\" class=\"w3-container w3-center\">\n        </div>\n\n    </div>\n</div>\n<script>\n    function showDetail(type, url, tgtPhoto, file, name) {\n        var tgtP = document.getElementById(tgtPhoto);\n        if (type != \"video\") {\n            tag = \"<img style='width: 100%; height: 900px; object-fit: scale-down; cursor:pointer'\"\n            tag = tag + \" src='\" + url + \"'\"\n            tag = tag + \" class='w3-border w3-padding'\"\n            tag = tag + \" alt='\" + name + \"'\"\n            tag = tag + \" onclick='openMedia(this.src)'\"\n            tag = tag + \">\"\n            tag = tag + \"<p>\" + file + \"</p>\"\n            //console.log(\"tag:\", tag)\n            tgtP.innerHTML = tag\n        } else {\n            tag =       \"<div class='video-wrapper'>\"\n            tag = tag + \"   <video style='width: 100%; height: 900px; object-fit: scale-down'\"\n            tag = tag + \"          class='w3-border w3-padding'\"\n            tag = tag + \"          controls>\"\n            tag = tag + \"       <source src='\" + url + \"'\" + \" type='video/mp4'>\"\n            tag = tag + \"       Your browser does not support mp4 video\"\n            tag = tag + \"   </video>\"\n            tag = tag + \"   <div class='open-overlay'\"\n            tag = tag + \"        onclick='openVideo(\\\"\" + url + \"\\\")'\"\n            tag = tag + \"        title='Open in new window'>\"\n            tag = tag + \"    </div>\"\n            tag = tag + \" </div>\"\n            tag = tag + \"<p>\" + file + \"</p>\"\n            //console.log(\"tag:\", tag)\n            tgtP.innerHTML = tag\n        }\n    }\n\n    function dosubmit(form) {\n        document.getElementById(form).submit();\n    }\n\n    function confirmDelete(form) {\n        if (confirm(\"Do you want to permanently delete the selected photos?\")) {\n            document.getElementById(form).method = \"post\";\n            document.getElementById(form).submit();\n        } else {\n            document.getElementById(form).method = \"get\";\n        }\n    }\n\n    function confirmDownload(form) {\n        if (confirm(\"Do you want to download the selected photos?\")) {\n            document.getElementById(form).method = \"post\";\n            document.getElementById(form).submit();\n        } else {\n            document.getElementById(form).method = \"get\";\n        }\n    }\n    function onlineHelp() {\n        window.open(\"{{ sc.getBaseHelpUrl() }}/PhotoViewer/\");\n    }\n\n    function openMedia(src) {\n        window.open(\n            '/media-viewer?src=' + encodeURIComponent(src),\n            '_blank',\n            'noopener'\n        );\n    }\n\n    function openVideo(src) {\n        window.open(\n            '/media-viewer?type=video&src=' + encodeURIComponent(src),\n            '_blank',\n            'noopener'\n        );\n    }\n\n</script>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/info/info.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Information{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Info menue -->\n        {% if sc.noCamera == False %}\n        <button class=\"w3-bar-item w3-button infomenu w3-light-green\" id=\"systembtn\" onclick=\"openInfoTab('system', 'systembtn')\">System</button>\n        <button class=\"w3-bar-item w3-button infomenu\" id=\"camerasbtn\" onclick=\"openInfoTab('cameras', 'camerasbtn')\">Cameras</button>\n        <button class=\"w3-bar-item w3-button infomenu\" id=\"campropbtn\" onclick=\"openInfoTab('camprops', 'campropbtn')\">Camera Properties</button>\n        {% if sm|length <= 5 %}\n        {% set btnWidth = 100 // (sm|length) %}\n        {% for mode in sm %}\n        <button class=\"w3-bar-item w3-button infomenu\" id=\"{{ mode.tabButtonId }}\" onclick=\"openInfoTab('{{ mode.tabId }}', '{{ mode.tabButtonId }}')\">{{ mode.tabTitle}}</button>\n        {% endfor %}\n        {% else %}\n        {% for mode in sm %}\n        {% if loop.index <= 4 %}\n        <button class=\"w3-bar-item w3-button infomenu\" id=\"{{ mode.tabButtonId }}\" onclick=\"openInfoTab('{{ mode.tabId }}', '{{ mode.tabButtonId }}')\">{{ mode.tabTitle}}</button>\n        {% endif %}\n        {% endfor %}\n        <select class=\"w3-bar-item w3-button infomenu\" name=\"sensormodemenu\" id=\"sensormodemenu\" \n            onchange=\"openInfoTabSensorMode()\" onclick=\"openInfoTabSensorMode()\">\n            {% for mode in sm %}\n            {% if loop.index > 4 %}\n            {% if cfg.sensor_mode == mode.id %}\n            <option value=\"{{ mode.id }}\" selected>Sensor Mode {{ mode.id }}</option>\n            {% else %}\n            <option value=\"{{ mode.id }}\">Sensor Mode {{ mode.id }}</option>\n            {% endif %}\n            {% endif %}\n            {% endfor%}\n            {% if cfg.sensor_mode == \"custom\" %}\n            <option value=\"custom\" selected>Custom</option>\n            {% else %}\n            <option value=\"custom\">Custom</option>\n            {% endif %}\n        </select>\n        {% endif %}\n        {% else %}\n        <button class=\"w3-bar-item w3-button infomenu w3-light-green\" id=\"systembtn\" onclick=\"openInfoTab('system', 'systembtn')\">System (No Camera available)</button>\n        {% if cs|length() > 0 %}\n        <button class=\"w3-bar-item w3-button infomenu\" id=\"camerasbtn\" onclick=\"openInfoTab('cameras', 'camerasbtn')\">Cameras</button>\n        {% endif %}\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from\n                    GitHub\n                </span>\n                {% if sc.NoCamera == True %}\n                {% set noCam = 1 %}\n                {% else %}\n                {% set noCam = 0 %}\n                {% endif %}\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                    style=\"height:34px; width:34px\" onclick=\"onlineHelp({{ noCam }})\">\n            </div>\n        </div>\n    </div>\n    <div id=\"system\" class=\"infogroup\" style=\"height:calc(100vh - 120px); overflow: auto\">\n        <h4>Hardware and OS</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <td style=\"width:15%\">Model</td>\n                <td style=\"width:55%\">{{sc.raspiModelFull}}</td>\n                <td style=\"width:30%\"></td>\n            </tr>\n            <tr>\n                <td>Board Revision</td>\n                <td>{{sc.boardRevision}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>Kernel Version</td>\n                <td>{{sc.kernelVersion}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>Debian Version</td>\n                <td>{{sc.debianVersion}}</td>\n                <td></td>\n            </tr>\n        </table>\n        <h4>Processes</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <td style=\"width:15%\">Environment</td>\n                <td style=\"width:55%\">{{sc.environmentInfo}}</td>\n                <td style=\"width:30%\"></td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">Server Process</td>\n                <td style=\"width:55%\">{{sc.startupInfo}}</td>\n                <td style=\"width:30%\"></td>\n            </tr>\n            <tr>\n                <td style=\"width:15%\">WSGI Server</td>\n                <td style=\"width:55%\">{{sc.wsgiInfo}}</td>\n                <td style=\"width:30%\"></td>\n            </tr>\n            <tr>\n                <td>Process Info</td>\n                <td>{{sc.processInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>FFmpeg Info</td>\n                <td>{{sc.ffmpegProcessInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>raspiCamSrv Start</td>\n                <td>{{sc.serverStartTimeStr}}</td>\n                <td></td>\n            </tr>\n        </table>\n        <h4>Software Stack</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <td style=\"width:15%\">Python</td>\n                <td style=\"width:55%\">{{sc.pythonInfo}}</td>\n                <td style=\"width:30%\"></td>\n            </tr>\n            <tr>\n                <td>Flask</td>\n                <td>{{sc.flaskInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>libcamera</td>\n                <td>{{sc.libcameraInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>Picamera2</td>\n                <td>{{sc.picamera2Info}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>OpenCV</td>\n                <td>{{sc.openCvInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>numpy</td>\n                <td>{{sc.numpyInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>matplotlib</td>\n                <td>{{sc.matplotlibInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>flask_jwt_extended</td>\n                <td>{{sc.flask_jwt_extended}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>imx500-all</td>\n                <td>{{sc.imx500Info}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>munkres</td>\n                <td>{{sc.munkresInfo}}</td>\n                <td></td>\n            </tr>\n            <tr>\n                <td>gunicorn</td>\n                <td>{{sc.gunicornInfo}}</td>\n                <td></td>\n            </tr>\n        </table>\n        {% if sc.noCamera == False %}\n        <h4>Streaming Clients</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <th style=\"width:15%\">IP Address</th>\n                <th style=\"width:55%\">Streams</th>\n                <th style=\"width:30%\"></th>\n            </tr>\n            {% for cli in sc.streamingClients %}\n            {% set ip = cli[\"ipaddr\"] %}\n            <tr>\n                <td>{{ ip }}</td>\n                <td>{{ sc.streamingClientStreams(ip) }}</td>\n                <td></td>\n            </tr>\n            {% endfor %}\n            {% if sc.streamingClients|length == 0 %}\n            <tr>\n                <td>None</td>\n                <td></td>\n                <td></td>\n            </tr>\n            {% endif %}\n        </table>\n        {% endif %}\n    </div>\n    <div id=\"cameras\" class=\"infogroup\" style=\"height:calc(100vh - 120px); overflow: auto; display:none\">\n        <h4>Installed Cameras</h4>\n        {% for cam in cs %}\n        {% if cam.num == sc.activeCamera %}\n        {% if cam.isUsb == true %}\n        {% if sc.noCamera == true %}\n        <h4>Camera {{ cam.num }} (Not in use) (USB Camera at {{ cam.usbDev }})</h4>\n        {% else %}\n        <h4>Camera {{ cam.num }} (currently active) (USB Camera at {{ cam.usbDev }})</h4>\n        {% endif %}\n        {% else %}\n        <h4>Camera {{ cam.num }} (currently active)</h4>\n        {% endif %}\n        {% else %}\n        {% if cam.isUsb == true %}\n        {% if sc.noCamera == true %}\n        <h4>Camera {{ cam.num }} (Not in use) (USB Camera at {{ cam.usbDev }})</h4>\n        {% else %}\n        <h4>Camera {{ cam.num }} (USB Camera at {{ cam.usbDev }})</h4>\n        {% endif %}\n        {% else %}\n        <h4>Camera {{ cam.num }}</h4>\n        {% endif %}\n        {% endif %}\n        {% set camnum = cam.num|string %}\n        {% set tc = tcs[camnum] %}\n        <table class=\"w3-table-all\">\n            <tr>\n                <th style=\"width:15%\">Property</th>\n                <th style=\"width:55%\">Value</th>\n                <th style=\"width:30%\">Description</th>\n            </tr>\n            <tr>\n                <td>\n                    Model\n                </td>\n                <td>\n                    {{ cam.model }}\n                </td>\n                <td>\n                    Model name of the camera\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Location\n                </td>\n                <td>\n                    {{ cam.location }}\n                </td>\n                <td>\n                    Location of the camera\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Rotation\n                </td>\n                <td>\n                    {{ cam.rotation }}\n                </td>\n                <td>\n                    Rotation of the camera\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    ID\n                </td>\n                <td>\n                    {{ cam.id }}\n                </td>\n                <td>\n                    ID of the camera\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Current Status\n                </td>\n                <td>\n                    {{ cam.status }}\n                </td>\n                <td>\n                    Current status of the camera\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    AI Features\n                </td>\n                <td>\n                    {% if cam.hasAi %}\n                    {% if sc.useCameraAi == True %}\n                    Available\n                    {% else %}\n                    Disabled in settings\n                    {% endif %}\n                    {% else %}\n                    Not Available\n                    {% endif %}\n                </td>\n                <td>\n                    AI features support\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Tuning File\n                </td>\n                <td>\n                    {% if tc.loadTuningFile == True %}\n                        {{ tc.tuningFilePath }}\n                    {% else %}\n                    Default\n                    {% endif %}\n                </td>\n                <td>\n                    Tuning file to be loaded\n                </td>\n            </tr>\n        </table>\n        {% endfor %}\n    </div>\n    <div id=\"camprops\" class=\"infogroup\" style=\"display:none\">\n        <h4>Camera Properties</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <th>Property</th>\n                <th>Value</th>\n                <th>Description</th>\n            </tr>\n            <tr>\n                <td>\n                    Model\n                </td>\n                <td>\n                    {{ cp.model }}\n                </td>\n                <td>The name that the attached sensor advertises.</td>\n            </tr>\n            <tr>\n                <td>\n                    UnitCellSize\n                </td>\n                <td>\n                    {{ cp.unitCellSize }}\n                </td>\n                <td>\n                    The physical size of this sensor’s pixels, if known. \n                    Given as an (x, y) tuple in units of nanometres.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Location\n                </td>\n                <td>\n                    {{ cp.location }}\n                </td>\n                <td>\n                    An integer which specifies where on the device the camera is \n                    situated (for example, front or back). For the Raspberry Pi, \n                    the value has no meaning.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Rotation\n                </td>\n                <td>\n                    {{ cp.rotation }}\n                </td>\n                <td>\n                    The rotation of the sensor relative to the camera board. \n                    On many Raspberry Pi devices, the sensor is actually upside down \n                    when the camera board is held with the connector at the bottom, \n                    and these will return a value of 180° here.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    ColorFilterArrangement\n                </td>\n                <td>\n                    {{ cp.colorFilterArrangement }}\n                </td>\n                <td>\n                    A number representing the native Bayer order of sensor \n                    (before any rotation is taken into account).\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    ColorSpace\n                </td>\n                <td>\n                    {{ cp.colorSpace }}\n                </td>\n                <td>\n                    Color space of the sensor. (Only available for USB cameras)\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    PixelArraySize\n                </td>\n                <td>\n                    {{ cp.pixelArraySize }}\n                </td>\n                <td>\n                    The size of the active pixel area as an (x, y) tuple. \n                    This is the full available resolution of the sensor.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    PixelArrayActiveAreas\n                </td>\n                <td>\n                    {{ cp.pixelArrayActiveAreas }}\n                </td>\n                <td>\n                    The active area of the sensor’s pixel array within the entire sensor pixel array. \n                    Given as a tuple of (x_offset, y_offset, width, height) values.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    ScalerCropMaximum\n                </td>\n                <td>\n                    {{ cp.scalerCropMaximum }}\n                </td>\n                <td>\n                    This value is updated when a camera mode is configured. \n                    It returns the rectangle as a (x_offset, y_offset, width, height) tuple \n                    within the pixel area active area, that is read out by this camera mode.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    SystemDevices\n                </td>\n                <td>\n                    {{ cp.systemDevices }}\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    SensorSensitivity\n                </td>\n                <td>\n                    {{ cp.sensorSensitivity }}\n                </td>\n                <td>\n                    This value is updated when a camera mode is configured. \n                    It represents a relative sensitivity of this camera mode compared to \n                    other camera modes. Usually, camera modes all have the same sensitivity \n                    so that the same exposure time and gain yield an image of the same brightness. \n                    Sometimes cameras have modes where this is not true, and to get the same \n                    brightness you would have to adjust the total requested exposure by the ratio \n                    of these sensitivities. For most sensors this will always return 1.0.\n                </td>\n            </tr>\n        </table>\n    </div>\n    {% for mode in sm %}\n    <div id={{ mode.tabId }} class=\"w3-container infogroup\" style=\"display:none\">\n        <h4>{{ mode.tabTitle }}</h4>\n        <table class=\"w3-table-all\">\n            <tr>\n                <th>Property</th>\n                <th>Value</th>\n                <th>Description</th>\n            </tr>\n            <tr>\n                <td>\n                    Size\n                </td>\n                <td>\n                    {{ mode.size }}\n                </td>\n                <td>\n                    the resolution of the sensor output.\n                    This value can be passed as the \"size\" when requesting the raw stream.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Unpacked\n                </td>\n                <td>\n                    {{ mode.unpacked }}\n                </td>\n                <td>\n                    Use this in place of the earlier format in the raw stream request \n                    if unpacked raw images are required. We recommend anyone wanting \n                    to access the raw pixel data to ask for the unpacked version of the format.\n                </td>\n            <tr>\n                <td>\n                    Format\n                </td>\n                <td>\n                    {{ mode.format }}\n                </td>\n                <td>\n                    The packed sensor format. This can be passed as the \"format\" \n                    when requesting the raw stream.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Bit Depth\n                </td>\n                <td>\n                    {{ mode.bit_depth }}\n                </td>\n                <td>\n                    The number of bits in each pixel sample.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    FPS\n                </td>\n                <td>\n                    {{ mode.fps }}\n                </td>\n                <td>\n                    The maximum framerate supported by this mode.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Crop Limits\n                </td>\n                <td>\n                    {{ mode.crop_limits }}\n                </td>\n                <td>\n                    This tells us the exact field of view of this mode \n                    within the full resolution sensor output. <br>\n                    This needs to be compared with \"PixelArraySize\" in Camera Properies.\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    Exposure Limits\n                </td>\n                <td>\n                    {{ mode.exposure_limits }}\n                </td>\n                <td>\n                    The minimum and maximum exposure times (in microseconds) permitted in this mode.\n                </td>\n            </tr>\n        </table>\n    </div>\n    {% endfor %}\n    <script>\n        function openInfoTab(infoTabName, infoTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"infogroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(infoTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"infomenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button infomenu\";\n            }\n            document.getElementById(infoTabButton).classList = \"w3-bar-item w3-button infomenu w3-light-green\";\n        }\n\n        function openInfoTabSensorMode() {\n            var i;\n\n            var b = document.getElementById(\"sensormodemenu\");\n            var sel = b.value;\n\n            var x = document.getElementsByClassName(\"infogroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            var infoTabName = \"sensormode\" + sel;\n            document.getElementById(infoTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"infomenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button infomenu\";\n            }\n            document.getElementById(\"sensormodemenu\").classList = \"w3-bar-item w3-button infomenu w3-light-green\";\n\n        }\n\n        function onlineHelp(noCam) {\n            if (noCam == 0) {\n                window.open(\"{{ sc.getBaseHelpUrl() }}/Information/\");\n            } else {\n                window.open(\"{{ sc.getBaseHelpUrl() }}/Information_NoCam/\");\n            }\n        }\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "raspiCamSrv/templates/media_viewer.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>{{ filename }}</title>\n    <meta name=\"viewport\" content=\"width=device-width, height=device-height, initial-scale=1.0\">\n    <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\">\n\n    <style>\n      html, body {\n        margin: 0;\n        width: 100%;\n        height: 100%;\n        background: #000;\n        display: flex;\n        flex-direction: column;\n        overflow: hidden;\n      }\n\n      .viewer {\n        flex: 1;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n      }\n\n      img, video {\n        max-width: 100vw;\n        max-height: calc(100vh);\n        object-fit: contain;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"viewer\">\n      {% if media_type == \"video\" %}\n        <video src=\"{{ src }}\" controls autoplay></video>\n      {% else %}\n        <img src=\"{{ src }}\" alt=\"{{ filename }}\">\n      {% endif %}\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "raspiCamSrv/templates/photoseries/main.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Photo Series{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Info menue -->\n        {% if sc.lastPhotoSeriesTab == \"series\" %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu w3-light-green\" id=\"tlseriesbtn\"\n            onclick=\"openPhotoseriesTab('tlseries', 'tlseriesbtn')\">Series</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu\" id=\"tlseriesbtn\"\n            onclick=\"openPhotoseriesTab('tlseries', 'tlseriesbtn')\">Series</button>\n        {% endif %}\n        {% if sc.lastPhotoSeriesTab == \"tldetails\" %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu w3-light-green\" id=\"tldetailsbtn\"\n            onclick=\"openPhotoseriesTab('tldetails', 'tldetailsbtn')\">Timelapse Series</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu\" id=\"tldetailsbtn\"\n            onclick=\"openPhotoseriesTab('tldetails', 'tldetailsbtn')\">Timelapse Series</button>\n        {% endif %}\n        {% if sc.activeCameraIsUsb == False %}\n        {% if sc.lastPhotoSeriesTab == \"exposure\" %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu w3-light-green\" id=\"tlexposurebtn\"\n            onclick=\"openPhotoseriesTab('tlexposure', 'tlexposurebtn')\">Exposure Series</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu\" id=\"tlexposurebtn\"\n            onclick=\"openPhotoseriesTab('tlexposure', 'tlexposurebtn')\">Exposure Series</button>\n        {% endif %}\n        {% if sc.lastPhotoSeriesTab == \"focusstack\" %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu w3-light-green\" id=\"tlfocusstackbtn\"\n            onclick=\"openPhotoseriesTab('tlfocusstack', 'tlfocusstackbtn')\">Focus Stack</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button photoseriesmenu\" id=\"tlfocusstackbtn\"\n            onclick=\"openPhotoseriesTab('tlfocusstack', 'tlfocusstackbtn')\">Focus Stack</button>\n        {% endif %}\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from GitHub\n                </span>\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                    style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n            </div>\n        </div>\n    </div>\n    {% if sc.lastPhotoSeriesTab == \"series\" %}\n    <div id=\"tlseries\" class=\"photoseriesgroup\">\n    {% else %}\n    <div id=\"tlseries\" class=\"photoseriesgroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"w3-twothird\">\n            <!-- Series configuration -->\n            <table>\n                <tr>\n                    <form method=\"post\" id=\"newtlseries\" action=\"{{ url_for('photoseries.new_series') }}\">\n                        <td class=\"w3-tooltip\" style=\"width:25%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                To create a new Photoseries, enter a unique name for the series.<br>\n                                The name will be used as prefix for images.\n                            </span>\n                            <label for=\"tlnewseries\">New Photo Series:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input type=\"text\" id=\"tlnewseries\" name=\"tlnewseries\" value=\"\" aria-label=\"Name for new series\">\n                        </td>\n                        <td style=\"width:13%\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Create\">\n                        </td>\n                    </form>\n                    <td style=\"width:12%\">\n                    </td>\n                    <td style=\"width:13%\">\n                    </td>\n                    <td style=\"width:12%\">\n                    </td>\n                </tr>\n                <tr>\n                    <td colspan=\"6\" style=\"padding-top: 0px; padding-bottom: 0px\">\n                        &nbsp;\n                    </td>\n                </tr>\n                <tr>\n                    <td colspan=\"6\" class=\"w3-border-top w3-border-black\">\n                        &nbsp; \n                    </td>\n                </tr>\n                <tr>\n                    {% if tl.hasCurSeries == True %}\n                    <form method=\"post\" id=\"series\" action=\"{{ url_for('photoseries.select_series') }}\">\n                        <td style=\"width:13%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                {% if sr.status == \"NEW\" %}\n                                The photo series is new and needs to be configured (see below).\n                                {% elif sr.status == \"READY\" %}\n                                The photo series has been configured and can be started.\n                                {% elif sr.status == \"ACTIVE\" %}\n                                The photo series is currently active.\n                                {% elif sr.status == \"PAUSED\" %}\n                                The photo series is paused.\n                                {% elif sr.status == \"FINISHED\" %}\n                                The photo series is finised.\n                                {% endif %}\n                            </span>\n                            <label for=\"series\">Status: {{ sr.status }}</label>\n                        </td>\n                        <td style=\"width:12%\">\n                            <select style=\"width:100%; height:28px\" name=\"selectseries\" id=\"selectseries\">\n                                {% for sername in tl.seriesNames %}\n                                {% if sername == sr.name %}\n                                    <option value=\"{{ sername }}\" selected>{{ sername }}</option>\n                                {% else %}\n                                    <option value=\"{{ sername }}\">{{ sername }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </form>\n                    <td style=\"width:13%\">\n                        {% for action in sr.nextActions %}\n                        {% if action == \"start\" %}\n                        <form method=\"post\" id=\"startseries\" action=\"{{ url_for('photoseries.start_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Start\">\n                        </form>\n                        {% elif action == \"pause\" %}\n                        <form method=\"post\" id=\"pauseseries\" action=\"{{ url_for('photoseries.pause_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Pause\">\n                        </form>\n                        {% elif action == \"continue\" %}\n                        <form method=\"post\" id=\"continueseries\" action=\"{{ url_for('photoseries.continue_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Continue\">\n                        </form>\n                        {% elif action == \"remove\" and (sr.nextActions|length == 1 or sr.status == \"NEW\") %}\n                        <form method=\"post\" id=\"removeseries\" action=\"{{ url_for('photoseries.remove_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Remove\" onclick=\"confirmRemove('removeseries')\">\n                        </form>\n                        {% endif %}\n                        {% endfor %}\n                    </td>\n                    <td style=\"width:12%\">\n                        {% for action in sr.nextActions %}\n                        {% if (sr.status != \"NEW\" and action == \"remove\" and sr.nextActions|length > 1) %}\n                        <form method=\"post\" id=\"removeseries2\" action=\"{{ url_for('photoseries.remove_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Remove\" onclick=\"confirmRemove('removeseries2')\">\n                        </form>\n                        {% elif action == \"finish\" %}\n                        <form method=\"post\" id=\"finishseries\" action=\"{{ url_for('photoseries.finish_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Finish\">\n                        </form>\n                        {% endif %}\n                        {% endfor %}\n                    </td>\n                    <td colspan=\"2\" style=\"width: 25;\">\n                        <form method=\"post\" id=\"downloadseries\" action=\"{{ url_for('photoseries.download_series') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Download\" onclick=\"confirmDownload('downloadseries')\">\n                        </form>\n                    </td>\n                    {% endif %}\n                </tr>\n                {% if tl.hasCurSeries == True %}\n                <form method=\"post\" id=\"seriesprops\" action=\"{{ url_for('photoseries.series_properties') }}\">\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                A series can be specialized as 'Timelapse Series' 'Exposure Series' or \"Focus Stack\"\n                            </span>\n                            <label for=\"sertype\">Series Type:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.isExposureSeries %}\n                            <input style=\"width:100%\" type=\"text\" id=\"sertype\" name=\"sertype\" value=\"Exposure Series\" disabled>\n                            {% elif sr.isFocusStackingSeries %}\n                            <input style=\"width:100%\" type=\"text\" id=\"sertype\" name=\"sertype\" value=\"Focus Stack\" disabled>\n                            {% elif sr.isSunControlledSeries %}\n                            <input style=\"width:100%\" type=\"text\" id=\"sertype\" name=\"sertype\" value=\"Timelapse\" disabled>\n                            {% else %}\n                            <input style=\"width:100%\" type=\"text\" id=\"sertype\" name=\"sertype\" value=\"Normal\" disabled>\n                            {% endif %}\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Name of the series.<br>\n                                Every series is identified by its unique name.\n                            </span>\n                            <label for=\"sername\">Name:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"sername\" name=\"sername\" value=\"{{ sr.name }}\" disabled>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Path where series resources are stored.\n                            </span>\n                            <label for=\"serpath\">Path:</label>\n                        </td>\n                        <td>\n                            <input style=\"width:100%\" type=\"text\" id=\"serpath\" name=\"serpath\" value=\"{{ sr.path }}\" disabled>\n                        </td style=\"width:75%\" colspan=\"3\">\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Name of the log file for the series.\n                            </span>\n                            <label for=\"serlog\">Log File Name:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"serlog\" name=\"serlog\" value=\"{{ sr.logFileName }}\" disabled>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Name of the JSON file with series configuration and attached camera configuration & control data.\n                            </span>\n                            <label for=\"sercfg\">Configuration File Name:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"sercfg\" name=\"sercfg\" value=\"{{ sr.cfgFileName }}\" disabled>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Name of the JSON file with camera configuration and & control data used for each photo of the series.\n                            </span>\n                            <label for=\"sercfg\">Camera File Name:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"sercfg\" name=\"sercfg\" value=\"{{ sr.camFileName }}\" disabled>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Type of photos. Either 'jpg' or 'raw+jpg'\n                            </span>\n                            <label for=\"imgtype\">Photo Type:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <select style=\"width:100%; height:28px\" name=\"imgtype\" id=\"imgtype\">\n                                {% if sr.type == \"jpg\" %}\n                                <option value=\"jpg\" selected>jpg</option>\n                                {% else %}\n                                <option value=\"jpg\">jpg</option>\n                                {% endif %}\n                                {% if sr.type == \"raw+jpg\" %}\n                                <option value=\"raw+jpg\" selected>raw+jpg</option>\n                                {% else %}\n                                <option value=\"raw+jpg\">raw+jpg</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Date and time when the series shall start.<br>\n                                This will not be changed with other parameters.\n                            </span>\n                            <label for=\"serstart\">Start:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"serstart\" name=\"serstart\" value=\"{{ sr.startIso }}\">\n                        </td>\n                        {% if sr.startedIso is not none %}\n                        <td colspan=\"2\" style=\"width:25%\" class=\"w3-tooltip w3-right-align\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Date and time when the series has been started.\n                            </span>\n                            <label for=\"serstarted\">Started:</label>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"serstarted\" name=\"serstarted\" value=\"{{ sr.startedIso }}\" disabled>\n                        </td>\n                        {% else %}\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Date and time when the series shall end.<br>\n                                This will be adjusted if interval and/or number of shots are changed.\n                            </span>\n                            <label for=\"serend\">End:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"serend\" name=\"serend\" value=\"{{ sr.endIso }}\">\n                        </td>\n                        {% if sr.endedIso is not none %}\n                        <td colspan=\"2\" style=\"width:25%\" class=\"w3-tooltip w3-right-align\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Date and time when the series has been ended.\n                            </span>\n                            <label for=\"serended\">Ended:</label>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"serended\" name=\"serended\" value=\"{{ sr.endedIso }}\" disabled>\n                        </td>\n                        {% else %}\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\" class=\"w3-tooltip w3-right-align\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Date and time when the series has been downloaded.\n                            </span>\n                            <label for=\"serdownloaded\">Downloaded:</label>\n                        </td>\n                        {% if sr.downloadedIso is not none %}\n                        <td colspan=\"2\" style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"serdownloaded\" name=\"serdownloaded\" value=\"{{ sr.downloadedIso }}\"\n                                disabled>\n                        </td>\n                        {% else %}\n                        <td colspan=\"2\" style=\"width:25%\">\n                            Never\n                        </td>\n                        {% endif %}\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Interval between shots in seconds.<br>\n                                This will be adjusted if series end or number of shots are changed.\n                            </span>\n                            <label for=\"serinterval\">Interval [sec]:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"serinterval\" name=\"serinterval\" min=\"0.1\" max=\"999999\" step=\"0.1\" value=\"{{ sr.interval }}\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                               Photos are taken on dial marks, for example, if interval is 900: at :00, :15, :30, :45\n                            </span>\n                            <label for=\"serondialmarks\">On Dial Marks:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.onDialMarks == True %}\n                            <input type=\"checkbox\" id=\"serondialmarks\" name=\"serondialmarks\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"serondialmarks\" name=\"serondialmarks\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Number of shots.<br>\n                                This will be adjusted if series end or interval are changed. \n                            </span>\n                            <label for=\"sernrshots\">Number of shots:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"sernrshots\" name=\"sernrshots\" min=\"1\" step=\"1\" value=\"{{ sr.nrShots }}\">\n                        </td>\n                        {% if sr.curShots is not none %}\n                        <td colspan=\"2\" style=\"width:25%\" class=\"w3-tooltip w3-right-align\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Current number of shots.\n                            </span>\n                            <label for=\"sercurshots\">Current shots:</label>\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"sercurshots\" name=\"sercurshots\" min=\"1\" max=\"99999\" step=\"1\" value=\"{{ sr.curShots }}\" disabled>\n                        </td>\n                        {% else %}\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate this checkbox if you want an active series \n                                to be automatically continued on server start.\n                            </span>\n                            <label for=\"isautocontinue\">Cont. on Server Start:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.continueOnServerStart == True %}\n                            <input type=\"checkbox\" id=\"isautocontinue\" name=\"isautocontinue\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"isautocontinue\" name=\"isautocontinue\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td colspan=\"6\">\n                            &nbsp;\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                        <td colspan=\"2\" style=\"width:25%\">\n                        </td>\n                    </tr>\n                </form>\n                <tr>\n                    <td colspan=\"6\" style=\"width:25%\">\n                        <form method=\"post\" id=\"attachcamcfg\" action=\"{{ url_for('photoseries.attach_camera_cfg') }}\">\n                            {% if sr.cameraConfig is none %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Attach Camera Config\">\n                            {% else %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Attach Camera Config\" onclick=\"confirmAttachConfig('attachcamcfg')\">\n                            {% endif %}\n                        </form>\n                    </td>\n                </tr>\n                <tr>\n                    <td colspan=\"6\" style=\"width:25%\">\n                        {% if sr.cameraConfig %}\n                        <form method=\"post\" id=\"activatecamcfg\" action=\"{{ url_for('photoseries.activate_camera_cfg') }}\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Activate attached Camera Config\" onclick=\"confirmActivateConfig('activatecamcfg')\">\n                        </form>\n                        {% endif %}\n                    </td>\n                </tr>\n                {% endif %}\n            </table>\n            {% if sr.error %}\n            <p>&nbsp;</p>\n            <hr>\n            <div class=\"w3-panel w3-pale-red w3-border\">\n            <h3>Photoseries paused with error:</h3>\n            <p>Error in {{ sr.errorSource }}: {{ sr.error }}</p>\n            {% if sr.error2 != None %}\n            <p>{{ sr.error2 }}</p>\n            {% endif %}\n            </div>\n            {% endif %}\n        </div>\n        <div class=\"w3-third\">\n            <!-- Series Preview -->\n            {% if not sc.error %}\n            <div style=\"margin-top: 5px; margin-left:10px\">\n                {% if sr.status == \"ACTIVE\" or sr.status == \"PAUSED\" or sr.status == \"FINISHED\" %}\n                {% if sr.showPreview == False %}\n                <form method=\"post\" id=\"showpreview\" action=\"{{ url_for('photoseries.show_preview') }}\">\n                    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Show Preview\">\n                </form>\n                {% else %}\n                <form method=\"post\" id=\"hidepreview\" action=\"{{ url_for('photoseries.hide_preview') }}\">\n                    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Hide Preview\">\n                </form>\n                {% endif %}\n                {% if sr.showPreview == True %}\n                <table style=\"width:100%\">\n                    <tr>\n                        {% if sr.status == \"ACTIVE\" and sc.isPhotoSeriesRecording == True %}\n                        <td style=\"width:80%\">\n                            <div class=\"w3-light-grey\">\n                                <div id=\"progressbar\" class=\"w3-container w3-red w3-center\" style=\"height:24px; width:0%\"></div>\n                            </div>\n                        </td>\n                        <td style=\"width:20%\">\n                            <p>{{ sr.nextTimeOnlyAsStr() }}</p>\n                        </td>\n                        {% else %}\n                        <td style=\"width:80%\">\n                            <p>&nbsp;</p>\n                        </td>\n                        <td style=\"width:20%\">\n                            <p>&nbsp;</p>\n                        </td>\n                        {% endif %}\n                    </tr>\n                    <tr>\n                        {% if sr.status == \"ACTIVE\" %}\n                        <td colspan=\"2\">\n                            <!--Here is the invisible datetime reference for the progress bar -->\n                            <p id=\"timertarget\" style=\"display:none;\">{{ sr.nextTimeIso() }}</p>\n                        </td>\n                        {% else %}\n                        <td colspan=\"2\">\n                            <p style=\"display:none;\">&nbsp;</p>\n                        </td>\n                        {% endif %}\n                    </tr>\n                </table>\n                <div style=\"height:1000px; overflow: auto\">\n                    <table>\n                        {% for entry in sr.getPreviewList() %}\n                        {% set url=url_for('static', filename=entry['relPath']) %}\n                        {% set name=entry['name'] %}\n                        <tr>\n                            <td>\n                                <img style=\"width: 100%; height: 150px; object-fit: scale-down; cursor: pointer\"\n                                    src=\"{{ url }}\" \n                                    alt=\"{{ name }}\"\n                                    onclick=\"openMedia(this.src)\"\n                                >\n                            </td>\n                        </tr>\n                        <tr>\n                            <td>\n                                <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ name }}</p>\n                            </td>\n                        </tr>\n                        {% endfor %}\n                    </table>\n                </div>\n                {% endif %}\n                {% endif %}\n            </div>\n            {% endif %}\n        </div>\n    </div>\n    {% if sc.lastPhotoSeriesTab == \"tldetails\" %}\n    <div id=\"tldetails\" class=\"photoseriesgroup\">\n    {% else %}\n    <div id=\"tldetails\" class=\"photoseriesgroup\" style=\"display:none\">\n    {% endif %}\n        <!-- Timelapse -->\n        <div>\n            <!-- Timelapse definitions -->\n            <p>Here, you can specify special settings for timelapse series.</p>\n            {% if tl.hasCurSeries %}\n            {% if sr.status == \"NEW\" or sr.status == \"READY\" %}\n            {% set locked = \"\" %}\n            {% else %}\n            {% set locked = \"disabled\" %}\n            {% endif %}\n            <table style=\"width:100%\">\n                <form method=\"post\" id=\"tlseriesform\" action=\"{{ url_for('photoseries.tlseries_properties') }}\">\n                    <tr>\n                        <td style=\"width:30%\">\n                            <p style=\"margin-bottom:0\">Series:</p>\n                        </td>\n                        <td style=\"width:20%\">\n                            <p style=\"margin-bottom:0\">{{ sr.name }}</p>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Interval between shots in seconds.<br>\n                                You may adjust this here or on page 'Series'.\n                            </span>\n                            <label for=\"serinterval2\">Interval [sec]:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"serinterval2\" name=\"serinterval2\" min=\"0.1\" max=\"999999\" step=\"0.1\"\n                                value=\"{{ sr.interval }}\" {{ locked }}>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The number of shots will be recalculated based on settings for number\n                                of days as well as Start and End .\n                            </span>\n                            <label for=\"tlnrshots\">Number of Shots:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <p id=\"tlnrshots\", name=\"tlnrshots\" style=\"margin-top:0\">{{ sr.nrShots }}</p>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate this checkbox if you want the series to be controlled by sunrise and/or sunset.\n                            </span>\n                            <label for=\"issuncontrolled\">Sun-controlled Series:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if sr.isSunControlledSeries == True %}\n                            <input type=\"checkbox\" id=\"issuncontrolled\" name=\"issuncontrolled\" value=\"1\" checked {{ locked }}>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"issuncontrolled\" name=\"issuncontrolled\" value=\"0\" {{ locked }}>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate this checkbox if you want the series to be controlled by sunrise and/or sunset.\n                            </span>\n                            <label for=\"sunctrlmode\">Sun-control Mode:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <select name=\"sunctrlmode\" id=\"sunctrlmode\" onchange=\"changeSunCtrlMode()\" {{ locked }}>\n                                {% for mode in sr.SUNCONTROLMODES %}\n                                {% if sr.sunCtrlMode == loop.index %}\n                                <option value=\"{{ loop.index }}\" selected>{{ mode }}</option>\n                                {% else %}\n                                <option value=\"{{ loop.index }}\">{{ mode }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the number of days for which the series shall be active.\n                            </span>\n                            <label for=\"sunctrlperiods\">Number of Days:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"sunctrlperiods\" name=\"sunctrlperiods\" min=\"1\" max=\"100000000\" step=\"1\"\n                                value=\"{{ sr.sunCtrlPeriods }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\">\n                            <p>&nbsp;</p>\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    {% if sr.sunCtrlMode == 1 %}\n                    <tbody id=\"sunctrlmode_1\">\n                    {% else %}\n                    <tbody id=\"sunctrlmode_1\" style=\"display:none\">\n                    {% endif %}\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Todays sunrise.<br>\n                                    Show/Refresh with Submit.\n                                </span>\n                                <label for=\"sunrise\">Sunrise:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunrise\" name=\"sunrise\" value=\"{{ sr.sunriseIso }}\" disabled>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Todays sunset.<br>\n                                    Show/Refresh with Submit.\n                                </span>\n                                <label for=\"sunset\">Sunset:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunset\" name=\"sunset\" value=\"{{ sr.sunsetIso }}\" disabled>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Reference</b></p>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Shift [Minutes]</b></p>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Todays Value</b></p>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Start of first period per day.<br>\n                                    Specify reference (Sunrise/Sunset) and shift.<br>\n                                    Result for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlstart1trg\">Period 1 - Start:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlstart1trg\" id=\"sunctrlstart1trg\" {{ locked }}>\n                                    {% if sr.sunCtrlStart1Trg == 0 %}\n                                    <option value=\"0\" selected>Unused</option>\n                                    {% else %}\n                                    <option value=\"0\">Unused</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlStart1Trg == 1 %}\n                                    <option value=\"1\" selected>Sunrise</option>\n                                    {% else %}\n                                    <option value=\"1\">Sunrise</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlStart1Trg == 2 %}\n                                    <option value=\"2\" selected>Sunset</option>\n                                    {% else %}\n                                    <option value=\"2\">Sunset</option>\n                                    {% endif %}\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">+</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunctrlstart1shft\" name=\"sunctrlstart1shft\" min=\"-1440\" max=\"1440\" step=\"1\"\n                                    value=\"{{ sr.sunCtrlStart1Shft }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">=</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunctrlstart1\" name=\"sunctrlstart1\" \n                                    value=\"{{ sr.sunCtrlStart1Iso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    End of first period per day.<br>\n                                    Specify reference (Sunrise/Sunset) and shift.<br>\n                                    Result for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlend1trg\">Period 1 - End:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlend1trg\" id=\"sunctrlend1trg\" {{ locked }}>\n                                    {% if sr.sunCtrlEnd1Trg == 0 %}\n                                    <option value=\"0\" selected>Unused</option>\n                                    {% else %}\n                                    <option value=\"0\">Unused</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlEnd1Trg == 1 %}\n                                    <option value=\"1\" selected>Sunrise</option>\n                                    {% else %}\n                                    <option value=\"1\">Sunrise</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlEnd1Trg == 2 %}\n                                    <option value=\"2\" selected>Sunset</option>\n                                    {% else %}\n                                    <option value=\"2\">Sunset</option>\n                                    {% endif %}\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">+</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunctrlend1shft\" name=\"sunctrlend1shft\" min=\"-1440\" max=\"1440\"\n                                    step=\"1\" value=\"{{ sr.sunCtrlEnd1Shft }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">=</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunctrlend1\" name=\"sunctrlend1\"\n                                    value=\"{{ sr.sunCtrlEnd1Iso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Start of second period per day.<br>\n                                    Specify reference (Sunrise/Sunset) and shift.<br>\n                                    Result for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlstart2trg\">Period 2 - Start:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlstart2trg\" id=\"sunctrlstart2trg\" {{ locked }}>\n                                    {% if sr.sunCtrlStart2Trg == 0 %}\n                                    <option value=\"0\" selected>Unused</option>\n                                    {% else %}\n                                    <option value=\"0\">Unused</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlStart2Trg == 1 %}\n                                    <option value=\"1\" selected>Sunrise</option>\n                                    {% else %}\n                                    <option value=\"1\">Sunrise</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlStart2Trg == 2 %}\n                                    <option value=\"2\" selected>Sunset</option>\n                                    {% else %}\n                                    <option value=\"2\">Sunset</option>\n                                    {% endif %}\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">+</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunctrlstart2shft\" name=\"sunctrlstart2shft\" min=\"-1440\" max=\"1440\"\n                                    step=\"1\" value=\"{{ sr.sunCtrlStart2Shft }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">=</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunctrlstart2\" name=\"sunctrlstart2\"\n                                    value=\"{{ sr.sunCtrlStart2Iso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    End of second period per day.<br>\n                                    Specify reference (Sunrise/Sunset) and shift.<br>\n                                    Result for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlend2trg\">Period 2 - End:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlend2trg\" id=\"sunctrlend2trg\" {{ locked }}>\n                                    {% if sr.sunCtrlEnd2Trg == 0 %}\n                                    <option value=\"0\" selected>Unused</option>\n                                    {% else %}\n                                    <option value=\"0\">Unused</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlEnd2Trg == 1 %}\n                                    <option value=\"1\" selected>Sunrise</option>\n                                    {% else %}\n                                    <option value=\"1\">Sunrise</option>\n                                    {% endif %}\n                                    {% if sr.sunCtrlEnd2Trg == 2 %}\n                                    <option value=\"2\" selected>Sunset</option>\n                                    {% else %}\n                                    <option value=\"2\">Sunset</option>\n                                    {% endif %}\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">+</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunctrlend2shft\" name=\"sunctrlend2shft\" min=\"-1440\" max=\"1440\"\n                                    step=\"1\" value=\"{{ sr.sunCtrlEnd2Shft }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\">=</p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunctrlend2\" name=\"sunctrlend2\"\n                                    value=\"{{ sr.sunCtrlEnd2Iso }}\" disabled>\n                            </td>\n                        </tr>\n                    </tbody>\n                    {% if sr.sunCtrlMode == 2 %}\n                    <tbody id=\"sunctrlmode_2\">\n                    {% else %}\n                    <tbody id=\"sunctrlmode_2\" style=\"display:none\">\n                    {% endif %}\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Time for Azimuth<br>\n                                    Update to current time with Submit.\n                                </span>\n                                <label for=\"sunazimuthtime\">Time for Azimuth:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunazimuthtime\" name=\"sunazimuthtime\" value=\"{{ sr.sunAzimuthTimeIso }}\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Azimuth at given time.<br>\n                                    Update with Submit.\n                                </span>\n                                <label for=\"sunazimuth\">Azimuth [°]:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunazimuth\" name=\"sunazimuth\" min=\"0.00\" max=\"365.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunAzimuth }}\" disabled>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Elevation at given time.<br>\n                                    Update with Submit.\n                                </span>\n                                <label for=\"sunelevation\">Elevation [°]:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunelevation\" name=\"sunelevation\" min=\"0.00\" max=\"90.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunElevation }}\" disabled>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Reference</b></p>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Azimuth [°]</b></p>\n                            </td>\n                            <td style=\"width:5%\">\n                            </td>\n                            <td style=\"width:20%\">\n                                <p style=\"margin-bottom:0\"><b>Todays Time</b></p>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    First Azimuth per day.<br>\n                                    Time for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlazimuth1\">Azimuth 1:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlazimuth1\" id=\"sunctrlazimuth1\" disabled>\n                                    <option value=\"0\" selected>Azimuth</option>\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunazimuth1\" name=\"sunazimuth1\" min=\"0.00\" max=\"365.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunAzimuth1 }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunazimuth1Time\" name=\"sunazimuth1Time\" \n                                    value=\"{{ sr.sunAzimuth1TimeIso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Second Azimuth per day.<br>\n                                    Time for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlazimuth2\">Azimuth 2:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlazimuth2\" id=\"sunctrlazimuth2\" disabled>\n                                    <option value=\"0\" selected>Azimuth</option>\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunazimuth2\" name=\"sunazimuth2\" min=\"0.00\" max=\"365.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunAzimuth2 }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunazimuth2Time\" name=\"sunazimuth2Time\" \n                                    value=\"{{ sr.sunAzimuth2TimeIso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Third Azimuth per day.<br>\n                                    Time for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlazimuth3\">Azimuth 3:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlazimuth3\" id=\"sunctrlazimuth3\" disabled>\n                                    <option value=\"0\" selected>Azimuth</option>\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunazimuth3\" name=\"sunazimuth3\" min=\"0.00\" max=\"365.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunAzimuth3 }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunazimuth3Time\" name=\"sunazimuth3Time\" \n                                    value=\"{{ sr.sunAzimuth3TimeIso }}\" disabled>\n                            </td>\n                        </tr>\n                        <tr>\n                            <td style=\"width:30%\" class=\"w3-tooltip\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Fourth Azimuth per day.<br>\n                                    Time for today will be shown after Submit.\n                                </span>\n                                <label for=\"sunctrlazimuth4\">Azimuth 4:</label>\n                            </td>\n                            <td style=\"width:20%\">\n                                <select name=\"sunctrlazimuth4\" id=\"sunctrlazimuth4\" disabled>\n                                    <option value=\"0\" selected>Azimuth</option>\n                                </select>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"number\" id=\"sunazimuth4\" name=\"sunazimuth4\" min=\"0.00\" max=\"365.00\" step=\"0.01\"\n                                    value=\"{{ sr.sunAzimuth4 }}\" {{ locked }}>\n                            </td>\n                            <td style=\"width:5%\">\n                                <p style=\"margin:0\"></p>\n                            </td>\n                            <td style=\"width:20%\">\n                                <input style=\"width:100%\" type=\"datetime-local\" id=\"sunazimuth4Time\" name=\"sunazimuth4Time\" \n                                    value=\"{{ sr.sunAzimuth4TimeIso }}\" disabled>\n                            </td>\n                        </tr>\n                    </tbody>\n                    <tr>\n                        <td style=\"width:30%\">\n                            <p>&nbsp;</p>\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Next shot.<br>\n                                Push Submit to refresh!\n                            </span>\n                            <label for=\"nexttime\">Next Shot:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width:100%\" type=\"datetime-local\" id=\"nexttime\" name=\"nexttime\" value=\"{{ sr.nextTimeIso() }}\" disabled>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:30%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:20%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n            {% endif %}\n        </div>\n    </div>\n    {% if sc.lastPhotoSeriesTab == \"exposure\" %}\n    <div id=\"tlexposure\" class=\"photoseriesgroup\">\n    {% else %}\n    <div id=\"tlexposure\" class=\"photoseriesgroup\" style=\"display:none\">\n    {% endif %}\n        <!-- Exposure -->\n        <div class=\"w3-half\">\n            <!-- Exposure definitions -->\n            <p>Here, you can specify a series as an exposure series.</p>\n            <p>Since Raspberry Pi cameras have fixed or manual aperture, \n                only exposure time (shutter speed) and analogue gain (instead of ISO) can be varied.</p>\n            <p class=\"w3-text-red\"><b>Be aware that the live stream will be affected by changing control parameters<br>\n                While the series is active, Automatic Exposure (AE) \n                and Automatic White Balance (AWB) will be disabled.<br>\n                It is recommended to activate these parameters before the series is started.\n                </b></p>\n            {% if tl.hasCurSeries %}\n            <table>\n                <form method=\"post\" id=\"expseriesform\" action=\"{{ url_for('photoseries.expseries_properties') }}\">\n                    <tr>\n                        <td style=\"width:40%\">\n                            <p style=\"margin-bottom:0\">Series:</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <p style=\"margin-bottom:0\">{{ sr.name }}</p>\n                        </td>\n                        <td style=\"width:10%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The number of shots will be recalculated based on Start, Stop and Interval \n                                for the variable parameter.\n                            </span>\n                            <label for=\"isexposure\">Number of Shots:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <p style=\"margin-top:0\">{{ sr.nrShots }}</p>\n                        </td>\n                        <td style=\"width:10%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate this checkbox if you want to use the series as exposure series.\n                            </span>\n                            <label for=\"isexposure\">Exposure Series:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.isExposureSeries == True %}\n                            <input type=\"checkbox\" id=\"isexposure\" name=\"isexposure\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"isexposure\" name=\"isexposure\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:10%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\">\n                        </td>\n                        <td style=\"width:25%\">\n                            <p><b>Exposure Time</b></p>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\">\n                            <p><b>Analogue Gain</b></p>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select the parameter to be kept constant for the series.\n                            </span>\n                            <label for=\"isexptimefix\">Fix:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.isExpExpTimeFix == True %}\n                            <input type=\"checkbox\" id=\"isexptimefix\" name=\"isexptimefix\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"isexptimefix\" name=\"isexptimefix\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr._isExpGainFix == True %}\n                            <input type=\"checkbox\" id=\"isexpgainfix\" name=\"isexpgainfix\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"isexpgainfix\" name=\"isexpgainfix\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the start value for the variable parameter and the constant value for the fix parameter.\n                            </span>\n                            <label for=\"exptimestart\">Start:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"exptimestart\" name=\"exptimestart\" min=\"125\" max=\"100000000\" step=\"1\"\n                                value=\"{{ sr.expTimeStart }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">μs</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"expgainstart\" name=\"expgainstart\" min=\"1.00\" max=\"16.00\" step=\"0.01\"\n                                value=\"{{ sr.expGainStart }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the stop value for the variable parameter.\n                            </span>\n                            <label for=\"exptimestop\">Stop:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"exptimestop\" name=\"exptimestop\" min=\"125\" max=\"100000000\" step=\"1\"\n                                value=\"{{ sr.expTimeStop }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">μs</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"expgainstop\" name=\"expgainstop\" min=\"1.00\" max=\"16.00\" step=\"0.01\"\n                                value=\"{{ sr.expGainStop }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the step to be used in the series.<br>\n                                May be 1 EV or 1/3 EV (Exposure Value)\n                            </span>\n                            <label for=\"exptimestep\">Interval:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <select style=\"width:100%; height:28px\" name=\"exptimestep\" id=\"exptimestep\">\n                                {% if sr.expTimeStep == 1 %}\n                                <option value=\"1\" selected>1/3</option>\n                                {% else %}\n                                <option value=\"1\">1/3</option>\n                                {% endif %}\n                                {% if sr.expTimeStep == 0 %}\n                                <option value=\"0\" selected>1</option>\n                                {% else %}\n                                <option value=\"0\">1</option>\n                                {% endif %}\n                                {% if sr.expTimeStep == 2 %}\n                                <option value=\"2\" selected>2</option>\n                                {% else %}\n                                <option value=\"2\">2</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">EV</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <select style=\"width:100%; height:28px\" name=\"expgainstep\" id=\"expgainstep\">\n                                {% if sr.expGainStep == 1 %}\n                                <option value=\"1\" selected>1/3</option>\n                                {% else %}\n                                <option value=\"1\">1/3</option>\n                                {% endif %}\n                                {% if sr.expGainStep == 0 %}\n                                <option value=\"0\" selected>1</option>\n                                {% else %}\n                                <option value=\"0\">1</option>\n                                {% endif %}\n                                {% if sr.expTimeStep == 2 %}\n                                <option value=\"2\" selected>2</option>\n                                {% else %}\n                                <option value=\"2\">2</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">EV</p>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n            {% endif %}\n        </div>\n        <div style=\"height:1000px; overflow: auto\" class=\"w3-half\">\n            <!-- Exposure results -->\n            {% if sr.isExposureSeries %}\n            <table>\n                {% for entry in sr.getPreviewListHistDetail() %}\n                {% set relPathPhoto=entry['relPhotoPath'] %}\n                {% set relPathHisto=entry['relHistoPath'] %}\n                {% set params=entry['params'] %}\n                {% set name=entry['name'] %}\n                <tr>\n                    {% if relPathPhoto %}\n                    {% set urlphoto=url_for('static', filename=relPathPhoto) %}\n                    <td style=\"width:30%;\">\n                        <img style=\"width: 100%; height: 150px; object-fit: scale-down; cursor: pointer\" onclick=\"openMedia(this.src)\" src=\"{{ urlphoto }}\" alt=\"{{ name }}\">\n                    </td>\n                    {% else %}\n                    <td style=\"width:30%;\">\n                    </td>\n                    {% endif %}\n                    {% if relPathHisto %}\n                    {% set urlhisto=url_for('static', filename=relPathHisto) %}\n                    <td style=\"width:30%;\">\n                        <img style=\"width: 100%; height: 150px; object-fit: scale-down\" src=\"{{ urlhisto }}\" alt=\"{{ name }}\">\n                    </td>\n                    {% else %}\n                    <td style=\"width:30%;\">\n                    </td>\n                    {% endif %}\n                    <td style=\"width:40%;\">\n                        <p style=\"margin-top:0; margin-bottom:0\">Exp: {{ params[\"ExposureTime\"] }}</p>\n                        <p style=\"margin-top:0; margin-bottom:0\">Gain: {{ params[\"AnalogueGain\"] }}</p>\n                        <p style=\"margin-top:0; margin-bottom:0\">Lux: {{ params[\"Lux\"] }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td style=\"width:40%;\">\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ name }}</p>\n                    </td>\n                    <td style=\"width:40%;\">\n                    </td>\n                    <td style=\"width:20%;\">\n                    </td>\n                </tr>\n                {% endfor %}\n            </table>\n            {% endif %}\n        </div>\n    </div>\n    {% if sc.lastPhotoSeriesTab == \"focusstack\" %}\n    <div id=\"tlfocusstack\" class=\"photoseriesgroup\">\n    {% else %}\n    <div id=\"tlfocusstack\" class=\"photoseriesgroup\" style=\"display:none\">\n    {% endif %}\n        {% if cp.hasFocus %}\n        <!-- Focus stack -->\n        <div class=\"w3-half\">\n            <!-- Focus stack definitions -->\n            <p>Here, you can specify that you want to use a series for focus stacking.</p>\n            <p>Only the Lens Position (1 / Focal Distance) will be varied</p>\n            <p class=\"w3-text-red\"><b>While the series is active, \n                    Autofocus Mode will be set to \"Manual\".<br>\n                    Be aware that the live stream may be affected by changing control parameters</b></p>\n            {% if tl.hasCurSeries %}\n            <table>\n                <form method=\"post\" id=\"expseriesform\" action=\"{{ url_for('photoseries.focusstack_properties') }}\">\n                    <tr>\n                        <td style=\"width:40%\">\n                            <p style=\"margin-bottom:0\">Series:</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <p style=\"margin-bottom:0\">{{ sr.name }}</p>\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The number of shots will be recalculated based on Start, Stop and Interval\n                                for Focal Distance.\n                            </span>\n                            <p style=\"margin-top:0\">Number of Shots</p>\n                        </td>\n                        <td style=\"width:25%\">\n                            <p style=\"margin-top:0\">{{ sr.nrShots }}</p>\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate this checkbox if you want to use the series for focus stacking.\n                            </span>\n                            <label for=\"isfocusstack\">Focus Stacking Series:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            {% if sr.isFocusStackingSeries == True %}\n                            <input type=\"checkbox\" id=\"isfocusstack\" name=\"isfocusstack\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"isfocusstack\" name=\"isfocusstack\" value=\"0\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\">\n                        </td>\n                        <td style=\"width:25%\">\n                            <p><b>Focal Distance</b></p>\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the start value for the focal distance.\n                            </span>\n                            <label for=\"focaldiststart\">Start:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"focaldiststart\" name=\"focaldiststart\" min=\"0.001\"\n                                max=\"999\" step=\"0.001\" value=\"{{ sr.focalDistStart }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">m</p>\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the stop value for the focal distance.\n                            </span>\n                            <label for=\"focaldiststop\">Stop:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"focaldiststop\" name=\"focaldiststop\" min=\"0.001\"\n                                max=\"999\" step=\"0.001\" value=\"{{ sr.focalDistStop }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">m</p>\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Specify the step to be used in the series.\n                            </span>\n                            <label for=\"focaldiststep\">Interval:</label>\n                        </td>\n                        <td style=\"width:25%\">\n                            <input style=\"width:100%\" type=\"number\" id=\"focaldiststep\" name=\"focaldiststep\" min=\"-999\"\n                                max=\"999\" step=\"0.001\" value=\"{{ sr.focalDistStep }}\">\n                        </td>\n                        <td style=\"width:5%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">m</p>\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:40%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n            {% endif %}\n        </div>\n        <div style=\"height:1000px; overflow: auto\" class=\"w3-half\">\n            <!-- Focus stack results -->\n            {% if sr.isFocusStackingSeries %}\n            <table>\n                {% for entry in sr.getPreviewListHistDetail() %}\n                {% set relPathPhoto=entry['relPhotoPath'] %}\n                {% set params=entry['params'] %}\n                {% set name=entry['name'] %}\n                <tr>\n                    {% if relPathPhoto %}\n                    {% set urlphoto=url_for('static', filename=relPathPhoto) %}\n                    <td style=\"width:50%;\">\n                        <img style=\"width: 100%; height: 150px; object-fit: scale-down; cursor: pointer\" onclick=\"openMedia(this.src)\" src=\"{{ urlphoto }}\" alt=\"{{ name }}\">\n                    </td>\n                    {% else %}\n                    <td style=\"width:50%;\">\n                    </td>\n                    {% endif %}\n                    <td style=\"width:50%;\">\n                        <p style=\"margin-top:0; margin-bottom:0\">Lens Position: {{ params[\"LensPosition\"] }}</p>\n                        <p style=\"margin-top:0; margin-bottom:0\">Focal Distance: {{ params[\"FocalDistance\"] }}</p>\n                        <p style=\"margin-top:0; margin-bottom:0\">Focus FoM: {{ params[\"FocusFoM\"] }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td style=\"width:50%;\">\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ name }}</p>\n                    </td>\n                    <td style=\"width:50%;\">\n                    </td>\n                </tr>\n                {% endfor %}\n            </table>\n            {% endif %}\n        </div>\n        {% else %}\n        <p>This camera has no focus control. Focus stacks are not supported.</p>\n        {% endif %}\n    </div>\n        \n    {% if sr.status == \"ACTIVE\" and sr.showPreview == True and sc.isPhotoSeriesRecording %}\n    <script>\n        showProgress()\n\n        function sleep(milliseconds) {\n            return new Promise(resolve => setTimeout(resolve, milliseconds));\n        }        \n\n        function showProgress() {\n            // Show the remaining time until next shot as progress bar\n            console.log(\"showProgress\");\n            var timeStr = document.getElementById(\"timertarget\").innerText\n            console.log(\"showProgress - timeStr=\" + timeStr);\n            var timeTgt = new Date(timeStr);\n            var timeTgtMs = timeTgt.getTime();\n            var nowMs = Date.now();\n            var delta0 = (timeTgtMs - nowMs) / 1000;\n            var delta = delta0;\n            console.log(\"delta0: \" + delta0 + \" delta: \" + delta)\n            var elem = document.getElementById(\"progressbar\");\n            var width = 0;\n            var id = setInterval(frame, 100);\n            async function frame() {\n                var disp;\n                var dispm;\n                var disph;\n                delta = (timeTgtMs - Date.now()) / 1000;\n                if (delta < 300) {\n                    disp = delta.toFixed(0) + ' s';\n                } else if (delta < 7200) {\n                    dispm = delta / 60;\n                    disp = dispm.toFixed(0) + ' min';\n                } else {\n                    disph = delta / 3600;\n                    disp = disph.toFixed(1) + ' h';\n                }\n                width = 100 * (delta0 - delta) / delta0\n                console.log(\"delta: \" + delta + \" width: \" + width)\n                if (width < 0) {\n                    clearInterval(id);\n                } else if (width >= 100) {\n                    console.log(\"Sleeping 2 sec\");\n                    await sleep(2000)\n                    console.log(\"Reloading\")\n                    await sleep(2000)\n                    window.location.reload();\n                } else {\n                    elem.style.width = width + '%';\n                    elem.innerHTML = disp;\n                }\n            }\n        }\n    </script>\n    {% endif %}\n    <script>\n        const selseries = document.getElementById(\"selectseries\");\n        selseries.addEventListener(\"change\", function() {\n            document.getElementById(\"series\").submit();\n        });\n\n        function openPhotoseriesTab(tlTabName, tlabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"photoseriesgroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(tlTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"photoseriesmenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button photoseriesmenu\";\n            }\n            document.getElementById(tlabButton).classList = \"w3-bar-item w3-button photoseriesmenu w3-light-green\";\n        }\n\n        function confirmRemove(form) {\n            if (confirm(\"Do you want to remove this series\\nThis will delete all related rosources!\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function confirmAttachConfig(form) {\n            if (confirm(\"The Photo Series has already camera configuration and controls attached.\\n\\n\"\n                      + \"Do you want to overwrite the attached config & ctrl with the current 'Photo'/'Raw Photo' configuration and controls?\"  \n                        )\n            ) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function confirmActivateConfig(form) {\n            if (confirm(\"Do you want to replace the active 'Photo'/Raw Photo' configuration and controls\\n\"\n                      + \"with the settings attached to the Photo Series?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function confirmDownload(form) {\n            if (confirm(\"Do you want to download the active series?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function changeSunCtrlMode() {\n            mode = document.getElementById(\"sunctrlmode\").value;\n            if (mode == \"1\") {\n                document.getElementById(\"sunctrlmode_1\").style.display = \"table-row-group\";\n                document.getElementById(\"sunctrlmode_2\").style.display = \"none\";\n            }\n            if (mode == \"2\") {\n                document.getElementById(\"sunctrlmode_1\").style.display = \"none\";\n                document.getElementById(\"sunctrlmode_2\").style.display = \"table-row-group\";\n            }\n        }\n\n        function onlineHelp() {\n            window.open(\"{{ sc.getBaseHelpUrl() }}/PhotoSeries/\");\n        }\n\n        function openMedia(src) {\n            window.open(\n                '/media-viewer?src=' + encodeURIComponent(src),\n                '_blank',\n                'noopener'\n            );\n        }\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "raspiCamSrv/templates/settings/main.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n    {% block title %}Server Settings{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div id=\"settingsmenubar\" class=\"w3-bar w3-green\">\n        <!-- Settings menue -->\n        {% if sc.lastSettingsTab == \"settingsparams\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsgparamsbtn\"\n            onclick=\"openSettingsTab('settingsparams', 'settingsgparamsbtn')\">Parameters</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsgparamsbtn\"\n            onclick=\"openSettingsTab('settingsparams', 'settingsgparamsbtn')\">Parameters</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsconfig\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsconfigbtn\"\n            onclick=\"openSettingsTab('settingsconfig', 'settingsconfigbtn')\">Configuration</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsconfigbtn\"\n            onclick=\"openSettingsTab('settingsconfig', 'settingsconfigbtn')\">Configuration</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsusers\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsusersbtn\"\n            onclick=\"openSettingsTab('settingsusers', 'settingsusersbtn')\">Users</button>\n        {% else %}\n        {% set activeuser = g.user %}\n        {% if activeuser[\"issuperuser\"] == 1 %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsusersbtn\"\n            onclick=\"openSettingsTab('settingsusers', 'settingsusersbtn')\">Users</button>\n        {% endif %}\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsapi\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsapibtn\"\n            onclick=\"openSettingsTab('settingsapi', 'settingsapibtn')\">API</button>\n        {% else %}\n        {% if sc.useAPI == True %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsapibtn\"\n            onclick=\"openSettingsTab('settingsapi', 'settingsapibtn')\">API</button>\n        {% endif %}\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsvbuttons\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsvbuttonsbtn\"\n            onclick=\"openSettingsTab('settingsvbuttons', 'settingsvbuttonsbtn')\">Versatile Buttons</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsvbuttonsbtn\"\n            onclick=\"openSettingsTab('settingsvbuttons', 'settingsvbuttonsbtn')\">Versatile Buttons</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsabuttons\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsabuttonsbtn\"\n            onclick=\"openSettingsTab('settingsabuttons', 'settingsabuttonsbtn')\">Action Buttons</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsabuttonsbtn\"\n            onclick=\"openSettingsTab('settingsabuttons', 'settingsabuttonsbtn')\">Action Buttons</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingslbuttons\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingslbuttonsbtn\"\n            onclick=\"openSettingsTab('settingslbuttons', 'settingslbuttonsbtn')\">Live Buttons</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingslbuttonsbtn\"\n            onclick=\"openSettingsTab('settingslbuttons', 'settingslbuttonsbtn')\">Live Buttons</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsdevices\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsdevicesbtn\"\n            onclick=\"openSettingsTab('settingsdevices', 'settingsdevicesbtn')\">Devices</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsdevicesbtn\"\n            onclick=\"openSettingsTab('settingsdevices', 'settingsdevicesbtn')\">Devices</button>\n        {% endif %}\n        {% if sc.lastSettingsTab == \"settingsupdate\" %}\n        <button class=\"w3-bar-item w3-button settingsmenu w3-light-green\" id=\"settingsupdatebtn\"\n            onclick=\"openSettingsTab('settingsupdate', 'settingsupdatebtn')\">Updates</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button settingsmenu\" id=\"settingsupdatebtn\"\n            onclick=\"openSettingsTab('settingsupdate', 'settingsupdatebtn')\">Updates</button>\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from GitHub\n                </span>\n                {% if sc.NoCamera == True %}\n                {% set noCam = 1 %}\n                {% else %}\n                {% set noCam = 0 %}\n                {% endif %}\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                    style=\"height:34px; width:34px\" onclick=\"onlineHelp({{ noCam }})\">\n            </div>\n        </div>\n    </div>\n    {% if sc.lastSettingsTab == \"settingsparams\" %}\n    <div id=\"settingsparams\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsparams\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>General Parameters</h3>\n        <div>\n            <form method=\"post\" action=\"{{ url_for('settings.serverconfig') }}\">\n                <table class=\"w3-table-all\">\n                    {% if sc.noCamera == False %}\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The camera used by the server.<br>\n                                In case that multiple cameras are connected to the server device,\n                                the active camera can be selected here\n                            </span>\n                            <label for=\"activecamera\">Active Camera:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <select name=\"activecamera\" id=\"activecamera\">\n                            {% for cam in cs %}\n                                {% if cam.usbDev != \"UNKNOWN\" %}\n                                {% if cam.isUsb == False or sc.useUsbCameras == True %}\n                                {% if sc.activeCamera == cam.num %}\n                                <option value=\"{{ cam.num }}\" selected>{{ cam.num }}: {{ cam.model }}</option>\n                                {% else %}\n                                <option value=\"{{ cam.num }}\">{{ cam.num }}: {{ cam.model }}</option>\n                                {% endif %}\n                                {% endif %}\n                                {% endif %}\n                            {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                    {% endif %}\n                    {% if sc.usbCamAvailable == True %}\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select if you want to use connected USB cameras.\n                            </span>\n                            <label for=\"useusbcameras\">Use USB Cameras:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsUsbCamera == True %}\n                            {% if sc.useUsbCameras == True %}\n                            <input type=\"checkbox\" id=\"useusbcameras\" name=\"useusbcameras\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useusbcameras\" name=\"useusbcameras\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useusbcameras\" name=\"useusbcameras\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                        {% if sc.supportsUsbCamera == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsUsbCamera|safe }}</p>\n                        {% endif %}\n                        </td>\n                    </tr>\n                    {% else %}\n                    {% if sc.noCamera == False %}\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select if you want to use connected USB cameras.\n                            </span>\n                            <label for=\"useusbcameras\">Use USB Cameras:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsUsbCamera == True %}\n                            <input type=\"checkbox\" id=\"useusbcameras\" name=\"useusbcameras\" value=\"0\" disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useusbcameras\" name=\"useusbcameras\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                        {% if sc.supportsUsbCamera == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsUsbCamera|safe }}</p>\n                        {% else %}\n                            <p style=\"margin-top:0; margin-bottom:0\">No USB cameras connected</p>\n                        {% endif %}\n                        </td>\n                    </tr>\n                    {% endif %}\n                    {% endif %}\n                    {% if sc.noCamera == False %}\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                If one or multiple microphones are accessible through PulseAudio, \n                                the description for the default microphone is shown.<br>\n                                This will be the microphone used for audio recording.\n                            </span>\n                            <label for=\"defaultmic\">Default Microphone:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            <input type=\"text\" id=\"defaultmic\" name=\"defaultmic\" value=\"{{ sc.defaultMic }}\" disabled>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.isMicMuted == True %}\n                            <input type=\"text\" id=\"micmuted\" name=\"micmuted\" value=\"M U T E D\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select whether audio shall be recorded along with videos.<br>\n                                This is only available for selection if a microphone is connected \n                                and accessible through PulseAudio.\n                            </span>\n                            <label for=\"recordaudio\">Record Audio along with Video:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            {% if sc.hasMicrophone == True %}\n                            {% if sc.recordAudio == True %}\n                            <input type=\"checkbox\" id=\"recordaudio\" name=\"recordaudio\" aria-label=\"recordaudio\" value=\"1\"\n                                checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"recordaudio\" name=\"recordaudio\" aria-label=\"recordaudio\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"recordaudio\" name=\"recordaudio\" aria-label=\"recordaudio\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The time shift in seconds to apply between the audio and video streams.<br>\n                                This may need tweaking to improve the audio/video synchronisation.\n                            </span>\n                            <label for=\"audiosync\">Audio Timeshift [sec]:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <input type=\"number\" id=\"audiosync\" name=\"audiosync\" min=\"-2.00\" max=\"2.00\" step=\"0.01\" value=\"{{ sc.audioSync }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The system path where photos and videos will be stored.\n                            </span>\n                            <label for=\"photopath\">Path for Photos/Videos:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <input style=\"width:70%\" type=\"text\" id=\"photopath\" name=\"photopath\" value=\"{{ sc.photoRoot }}/{{ sc.cameraPhotoSubPath }}\" disabled>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The file format to be used for photos\n                            </span>\n                            <label for=\"phototype\">Photo Type:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <select name=\"phototype\" id=\"phototype\">\n                                {% if sc.photoType == \"jpeg\" %}\n                                <option value=\"jpeg\" selected>jpeg</option>\n                                {% else %}\n                                <option value=\"jpeg\">jpeg</option>\n                                {% endif %}\n                                {% if sc.photoType == \"jpg\" %}\n                                <option value=\"jpg\" selected>jpg</option>\n                                {% else %}\n                                <option value=\"jpg\">jpg</option>\n                                {% endif %}\n                                {% if sc.photoType == \"bmp\" %}\n                                <option value=\"bmp\" selected>bmp</option>\n                                {% else %}\n                                <option value=\"bmp\">bmp</option>\n                                {% endif %}\n                                {% if sc.photoType == \"png\" %}\n                                <option value=\"png\" selected>png</option>\n                                {% else %}\n                                <option value=\"png\">png</option>\n                                {% endif %}\n                                {% if sc.photoType == \"gif\" %}\n                                <option value=\"gif\" selected>gif</option>\n                                {% else %}\n                                <option value=\"gif\">gif</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The file format to be used for raw photos\n                            </span>\n                            <label for=\"rawphototype\">Raw Type:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <select name=\"rawphototype\" id=\"rawphototype\">\n                                {% if sc.rawPhotoType == \"dng\" %}\n                                <option value=\"dng\" selected>dng</option>\n                                {% else %}\n                                <option value=\"dng\">dng</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The file format to be used for videos\n                            </span>\n                            <label for=\"videotype\">Video Type:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <select name=\"videotype\" id=\"videotype\">\n                                {% if sc.videoType == \"h264\" %}\n                                <option value=\"h264\" selected>h264</option>\n                                {% else %}\n                                <option value=\"h264\">h264</option>\n                                {% endif %}\n                                {% if sc.videoType == \"mp4\" %}\n                                <option value=\"mp4\" selected>mp4</option>\n                                {% else %}\n                                <option value=\"mp4\">mp4</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select if you want to use two cameras as stereo cameras.\n                            </span>\n                            <label for=\"usestereo\">Use Stereo Vision:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsStereo == True %}\n                            {% if sc.useStereo == True %}\n                            <input type=\"checkbox\" id=\"usestereo\" name=\"usestereo\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"usestereo\" name=\"usestereo\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"usestereo\" name=\"usestereo\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                        {% if sc.supportsStereo == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsStereo|safe }}</p>\n                        {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select if you want to use AI features of a connected AI Camera (IMX500).\n                            </span>\n                            <label for=\"usecameraai\">Use Camera AI:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if (sc.aiCamAvailable == True) and (sc.imx500Available == True) %}\n                            {% if sc.useCameraAi == True %}\n                            <input type=\"checkbox\" id=\"usecameraai\" name=\"usecameraai\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"usecameraai\" name=\"usecameraai\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"usecameraai\" name=\"usecameraai\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsAiCamera|safe }}</p>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select whether you want to see histograms along with specific photo series.\n                            </span>\n                            <label for=\"showhistograms\">Show Histograms:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsHistograms == True %}\n                            {% if sc.useHistograms == True %}\n                            <input type=\"checkbox\" id=\"showhistograms\" name=\"showhistograms\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"showhistograms\" name=\"showhistograms\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"showhistograms\" name=\"showhistograms\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                        {% if sc.supportsHistograms == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsHistograms|safe }}</p>\n                        {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                This indicates whether advanced algorithms can be used for motion detection:<br>\n                                Frame Differencing, Optical Flow, Background Subtraction\n                            </span>\n                            <label for=\"supportsextmotiondetection\">Ext. Motion Detection supported:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsExtMotionDetection == True %}\n                            <input type=\"checkbox\" id=\"supportsextmotiondetection\" name=\"supportsextmotiondetection\" value=\"1\" checked disabled>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"supportsextmotiondetection\" name=\"supportsextmotiondetection\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                        {% if sc.supportsExtMotionDetection == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotsupportsExtMotionDetection|safe }}</p>\n                        {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select whether streaming requires authentication.\n                            </span>\n                            <label for=\"requireAuthForStreaming\">Req. Auth for Streaming:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            {% if sc.requireAuthForStreaming == True %}\n                            <input type=\"checkbox\" id=\"requireAuthForStreaming\" name=\"requireAuthForStreaming\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"requireAuthForStreaming\" name=\"requireAuthForStreaming\" value=\"0\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    {% endif %}\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select whether you want to allow API access to the raspiCamSrv server.\n                            </span>\n                            <label for=\"useapi\">Allow access through API:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsAPI == True %}\n                            {% if sc.useAPI == True %}\n                            <input type=\"checkbox\" id=\"useapi\" name=\"useapi\" onchange=\"confirmApiChange()\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useapi\" name=\"useapi\" onchange=\"confirmApiChange()\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useapi\" name=\"useapi\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if sc.supportsAPI == False %}\n                            <p style=\"margin-top:0; margin-bottom:0\">{{ sc.whyNotSupportsAPI|safe }}</p>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Geographic Latitude of camera position.<br>\n                                (Required for sunrise/sunset calculation)\n                            </span>\n                            <label for=\"loclatitude\">Latitude:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <input type=\"number\" id=\"loclatitude\" name=\"loclatitude\" min=\"-90.000000\" max=\"90.000000\" step=\"0.000001\"\n                                value=\"{{ sc.locLatitude }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Geographic Longitude of camera position.<br>\n                                (Required for sunrise/sunset calculation)\n                            </span>\n                            <label for=\"loclongitude\">Longitude:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <input type=\"number\" id=\"loclongitude\" name=\"loclongitude\" min=\"-180.000000\" max=\"180.000000\" step=\"0.000001\"\n                                value=\"{{ sc.locLongitude }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Geographic Elevation of camera position (in m).<br>\n                                (Required for sunrise/sunset calculation)\n                            </span>\n                            <label for=\"locelevation\">Elevation:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <input type=\"number\" id=\"locelevation\" name=\"locelevation\" min=\"-1000.0\" max=\"9000.0\" step=\"0.1\"\n                                value=\"{{ sc.locElevation }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width:25%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Time zone of camera position.<br>\n                                (Required for sunrise/sunset calculation)\n                            </span>\n                            <label for=\"loctzkey\">Time Zone:</label>\n                        </td>\n                        <td style=\"width:70%\" colspan=\"2\">\n                            <select name=\"loctzkey\" id=\"loctzkey\">\n                                {% for tz in sc.timeZoneKeys() %}\n                                {% if sc.locTzKey == tz %}\n                                <option value=\"{{ tz }}\" selected>{{ tz }}</option>\n                                {% else %}\n                                <option value=\"{{ tz }}\">{{ tz }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastSettingsTab == \"settingsconfig\" %}\n    <div id=\"settingsconfig\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsconfig\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Configuration Management</h3>\n        <table>\n            <tr>\n                <td>\n                    <form id=\"storeconfig\" method=\"post\" action=\"{{ url_for('settings.store_config') }}\">\n                        <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" onclick=\"confirmStore('storeconfig')\" value=\"Store Configuration\">\n                    </form>\n                </td>\n                <td>\n                    <form id=\"loadconfig\" method=\"post\" action=\"{{ url_for('settings.load_config') }}\">\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\"  onclick=\"confirmLoad('loadconfig')\" value=\"Load Stored Configuration\">\n                    </form>\n                </td>\n                <td>\n                    <form id=\"reloadCameras\" method=\"post\" action=\"{{ url_for('settings.reloadCameras') }}\">\n                        <input class=\"w3-button w3-black\" style=\"width: 16ch\" type=\"submit\"  onclick=\"confirmReloadCams('reloadCameras')\" value=\"Reload Cameras\">\n                    </form>\n                </td>\n                <td>\n                    <form id=\"resetserver\" method=\"post\" action=\"{{ url_for('settings.resetServer') }}\">\n                        <input class=\"w3-button w3-black\" style=\"width: 16ch\" type=\"submit\"  onclick=\"confirmReset('resetserver')\" value=\"Reset Server\">\n                    </form>\n                </td>\n                <td style=\"width:25%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        If selected, the system will load the stored configuration on server start.<br>\n                        Otherwise, the default configuration will be applied.\n                    </span>\n                    <label for=\"loadconfigonstartcb\">Start server with stored Configuration:</label>\n                </td>\n                <td>\n                    <form id=\"loadconfigonstart\" method=\"post\" action=\"{{ url_for('settings.loadConfigOnStart') }}\">\n                        {% if los == True %}\n                        <input type=\"checkbox\" id=\"loadconfigonstartcb\" name=\"loadconfigonstartcb\" onchange=\"loadCfgOnStart('loadconfigonstart')\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"loadconfigonstartcb\" name=\"loadconfigonstartcb\" onchange=\"loadCfgOnStart('loadconfigonstart')\" value=\"0\">\n                        {% endif %}\n                    </form>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    &nbsp;\n                </td>\n                <td>\n                </td>\n                <td>\n                </td>\n                <td>\n                </td>\n                <td>\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <form method=\"post\" id=\"configbackup\" action=\"{{ url_for('settings.configBackup') }}\">\n                    <td>\n                        <input type=\"text\" id=\"configbackupname\" name=\"configbackupname\" style=\"width: 22ch\" value=\"\" aria-label=\"Name for the backup\">\n                    </td>\n                    <td>\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" onclick=\"confirmBackupConfig('configbackup', 'configbackupname')\" value=\"Backup Stored Data\">\n                    </td>\n                    <td colspan=\"4\">\n                        Backup all persisted data (Configuration, Photos, Photo Series, Events, Calibration, Database)\n                    </td>\n                </form>\n            </tr>                \n            <tr>\n                <form method=\"post\" id=\"configrestore\" action=\"{{ url_for('settings.configRestore') }}\">\n                    <td>\n                        <select name=\"configrestorename\" id=\"configrestorename\" style=\"width: 22ch\">\n                            {% for bu in backups %}\n                            <option value=\"{{ bu }}\">{{ bu }}</option>\n                            {% endfor %}\n                        </select>\n                    </td>\n                    <td>\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" onclick=\"confirmBackupRestore('configrestore', 'configrestorename')\" value=\"Restore Backup\">\n                    </td>\n                </form>\n                <td>\n                    Requires server restart!\n                </td>\n                <form method=\"post\" id=\"serverrestart\" action=\"{{ url_for('settings.serverRestart') }}\">\n                    <td>\n                        <input class=\"w3-button w3-black\" style=\"width: 16ch\" type=\"submit\" onclick=\"confirmServerRestart('serverrestart')\" value=\"Restart Server\">\n                    </td>\n                </form>\n                <td>\n                </td>\n            </tr>                \n            <tr>\n                <form method=\"post\" id=\"configremove\" action=\"{{ url_for('settings.configRemove') }}\">\n                    <td>\n                         <select name=\"configremovename\" id=\"configremovename\" style=\"width: 22ch\">\n                            {% for bu in backups %}\n                            <option value=\"{{ bu }}\">{{ bu }}</option>\n                            {% endfor %}\n                        </select>\n                    </td>\n                    <td>\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" onclick=\"confirmBackupRemove('configremove', 'configremovename')\" value=\"Remove Backup\">\n                    </td>\n                    <td>\n                    </td>\n                    <td>\n                    </td>\n                    <td>\n                    </td>\n                    <td>\n                    </td>\n                </form>\n            </tr>                \n        </table>\n        <h3>Unsaved Configuration Changes:</h3>\n        <div style=\"max-height:65vh;overflow-y:auto\">\n            <table class=\"w3-table-all\">\n                <thead>\n                    <tr>\n                        <th style=\"width:20%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                            Time\n                        </th>\n                        <th style=\"width:80%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                            Action\n                        </th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {% for entry in sc.changeLog %}\n                    <tr>\n                        <td style=\"width:20%\">\n                            {{ entry[\"time\"].strftime(\"%Y-%m-%d %H:%M:%S\") }}\n                        </td>\n                        <td style=\"width:80%\">\n                            {{ entry[\"entry\"] }}\n                        </td>\n                    </tr>\n                    {% endfor %}\n                </tbody>\n            </table>\n        </div>\n\n    </div>\n    {% if sc.lastSettingsTab == \"settingsusers\" %}\n    <div id=\"settingsusers\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsusers\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        {% set activeuser = g.user %}\n        {% if activeuser[\"issuperuser\"] == 1 %}\n        <h3>Users</h3>\n        <div>\n            <form id=\"removeusersform\" method=\"post\" action=\"{{ url_for('settings.remove_users') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <th>\n                            Sel\n                        </th>\n                        <th>\n                            ID\n                        </th>\n                        <th>\n                            Name\n                        </th>\n                        <th>\n                            Initial\n                        </th>\n                        <th>\n                            SuperUser\n                        </th>\n                    </tr>\n                    {% for user in g.users %}\n                    <tr>\n                        <td style=\"width:5%\">\n                            <input type=\"checkbox\" id=\"sel_{{ user['id'] }}\" name=\"sel_{{ user['id'] }}\"\n                                aria-label=\"sel_{{ user['id'] }}\" value=\"0\">\n                        </td>\n                        <td>\n                            {{ user[\"id\"] }}\n                        </td>\n                        <td>\n                            {{ user[\"username\"] }}\n                        </td>\n                        <td>\n                            {% if 'isinitial' is in user %}\n                            {% if user[\"isinitial\"] == 0 %}\n                            No\n                            {% else %}\n                            Yes\n                            {% endif %}\n                            {% else %}\n                            Unknown\n                            {% endif %}\n                        </td>\n                        <td>\n                            {% if 'issuperuser' is in user %}\n                            {% if user[\"issuperuser\"] == 0 %}\n                            No\n                            {% else %}\n                            Yes\n                            {% endif %}\n                            {% else %}\n                            Unknown\n                            {% endif %}\n                        </td>\n                    </tr>\n                    {% endfor %}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" onclick=\"confirmRemove('removeusersform')\" value=\"Remove Selected Users\">\n                <p style=\"margin-bottom: 0\"></p>\n            </form>\n            <form method=\"post\" action=\"{{ url_for('settings.register_user') }}\">\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" style=\"width: 22ch\" type=\"submit\" value=\"Register New User\">\n            </form>\n            {% set user = g.users[0] %}\n            {% if not 'issuperuser' is in user %}\n            <p>\n                You are using an outdated database schema for users.<br>\n                To update to the new schema, run the following command in raspi-cam-srv, \n                with activated virtual environment:<br>\n                flask --app raspiCamSrv init-db\n            </p>\n            {% endif %}\n        </div>\n        <br>\n        {% endif %}\n    </div>\n    {% if sc.useAPI == True %}\n    {% if sc.lastSettingsTab == \"settingsapi\" %}\n    <div id=\"settingsapi\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsapi\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <div>\n            <h3>API Settings</h3>\n            <p>API access to the rapiCamSrv server is secured through JSON Web Tokens (JWT).</p>\n            <p>JWT requires a secret private key for signing tokens<br>\n               In order to enable API access, you need to specify the path of the secrets file \n               where the secret key will be stored.<br>\n               It is recommended using the following path:<br>\n               /home/%user%/.secrets/raspiCamSrv.secrets\n            </p>\n            <p>Furthermore, you can specify expiration for the access token.</p>\n            <p>In case that the access token will expire, \n                also the expiration period for the refresh token needs to be specified</p>\n            {% if sc.API_active == True %}\n            {% if sc.jwtAuthenticationActive == True %}\n            <h3 class=\"w3-green\">raspiCamSrv API is active</h3>\n            {% else %}\n            {% if sc.jwtKeyStore|length == 0 %}\n            <h3 class=\"w3-yellow\">To deactivate the API, store configuration and restart server with stored configuration</h3>\n            {% else %}\n            <h3 class=\"w3-yellow\">To update token expiration, store configuration and restart server with stored configuration</h3>\n            {% endif %}\n            {% endif %}\n            {% else %}\n            {% if sc.jwtAuthenticationActive == False %}\n            <h3 class=\"w3-red\">API not active. Please specify JWT authentication settings</h3>\n            {% else %}\n            <h3 class=\"w3-yellow\">To activate the API, store configuration and restart server with stored configuration</h3>\n            {% endif %}\n            {% endif %}\n            <form method=\"post\" action=\"{{ url_for('settings.api_config') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Full path to secrets file \n                                for storage of secret key for JWT authentication.<br>\n                                A blank value will deactivate the API.\n                            </span>\n                            <label for=\"jwtkeystore\">JWT Secret Key File Path:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"jwtkeystore\" name=\"jwtkeystore\"\n                                value=\"{{ sc.jwtKeyStore }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Expiration time (in minutes) for the short-lived access token.<br>\n                                0 = no expiration\n                            </span>\n                            <label for=\"jwtaccesstokenexpirationmin\">Access Token Expiration in Minutes:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"jwtaccesstokenexpirationmin\" name=\"jwtaccesstokenexpirationmin\" min=\"0\" max=\"999999\" step=\"1\"\n                                value=\"{{ sc.jwtAccessTokenExpirationMin }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Expiration time (in days) for the long-lived refresh token,\n                                which can be used to refresh the access token.<br>\n                                0 = no expiration\n                            </span>\n                            <label for=\"jwtrefreshtokenexpirationdays\">Refresh Token Expiration in Days:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"jwtrefreshtokenexpirationdays\" name=\"jwtrefreshtokenexpirationdays\" min=\"0\" max=\"999999\"\n                                step=\"1\" value=\"{{ sc.jwtRefreshTokenExpirationDays }}\">\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n            <p>&nbsp;</p>\n            <table style=\"width: 100%\">\n                <tr>\n                    <td>\n                        <form id=\"generatetoken\" method=\"post\" action=\"{{ url_for('settings.generate_token') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width: 30ch\" type=\"submit\"\n                                onclick=\"doSubmit('generatetoken')\" value=\"Generate Access Token\">\n                        </form>\n                    </td>\n                </tr>\n                {% if access_token|length > 0 %}\n                <tr style=\"width: 100%\">\n                    <td>\n                        <form>\n                            <textarea id=\"jwttoken\" name=\"jwttoken\" rows=\"5\" style=\"width: 100%\" class=\"w3-input w3-border\" disabled>\n                                {{ access_token }}\n                            </textarea>\n                        </form>\n                    </td>\n                </tr>\n                {% endif %}\n            </table>\n        </div>\n    </div>\n    {% endif %}\n    {% if sc.lastSettingsTab == \"settingsvbuttons\" %}\n    <div id=\"settingsvbuttons\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsvbuttons\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Versatile Buttons</h3>\n        <p>\n            On this page you can configure the versatile buttons shown under Console/Versatile Buttons.<br>\n            Buttons are arranged in a matrix-like structure with rows and columns.<br>\n        </p>\n        <div>\n            <table style=\"width:100%\" id=\"vbuttondimtab\">\n                <form id=\"vbuttondimensions\" method=\"post\" action=\"{{ url_for('settings.vbutton_dimensions') }}\">\n                    <tr>\n                        <td style=\"width: 15%\">\n                            Number of Rows:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"vbuttonsrows\" name=\"vbuttonsrows\" min=\"0\" max=\"99\" step=\"1\" \n                                value=\"{{ sc.vButtonsRows }}\">\n                        </td>\n                        <td style=\"width: 15%\">\n                            Number of Columns:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"vbuttonscols\" name=\"vbuttonscols\" min=\"0\" max=\"99\" step=\"1\"\n                            value=\"{{ sc.vButtonsCols }}\">\n                        </td>\n                        <td style=\"width: 17%\">\n                            Interactive Commandline:\n                        </td>\n                        <td style=\"width:5%\">\n                            {% if sc.vButtonHasCommandLine == True %}\n                            <input type=\"checkbox\" id=\"vbuttonhascommandline\"\n                                name=\"vbuttonhascommandline\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"vbuttonhascommandline\"\n                                name=\"vbuttonhascommandline\" value=\"1\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width: 5%\">\n                        </td>\n                        <td style=\"width: 20%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width: 13%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n        </div>\n        {% if sc.vButtonsRows == 0 or sc.vButtonsCols == 0 %}\n        <div style=\"display:none\">\n        {% else %}\n        <div>\n        {% endif %}\n            <h3>Button Settings</h1>\n            <div>\n                <form method=\"post\" action=\"{{ url_for('settings.vbutton_settings') }}\">\n                    <div style=\"max-height:63.6vh;overflow-y:auto\">\n                        <table class=\"w3-table-all\">\n                            <thead>\n                                <tr>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Row\n                                    </th>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Col\n                                    </th>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Visible\n                                    </th>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Shape\n                                    </th>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Color\n                                    </th>\n                                    <th style=\"width:20%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Button Text\n                                    </th>\n                                    <th style=\"width:50%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Command\n                                    </th>\n                                    <th style=\"width:5%; width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                        Conf\n                                    </th>\n                                </tr>\n                            </thead>\n                            <tbody>\n                                {% for r in sc.vButtons %}\n                                {% for btn in r %}\n                                <tr>\n                                    <td style=\"width:5%\">\n                                        {{ btn[\"row\"] + 1 }}\n                                    </td>\n                                    <td style=\"width:5%\">\n                                        {{ btn[\"col\"] + 1 }}\n                                    </td>\n                                    <td style=\"width:5%\">\n                                        {% if btn[\"isVisible\"] == True %}\n                                        <input type=\"checkbox\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" \n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\" checked>\n                                        {% else %}\n                                        <input type=\"checkbox\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\"\n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\">\n                                        {% endif %}\n                                    </td>\n                                    <td style=\"width:5%\">\n                                        <select name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_shape\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_shape\">\n                                            {% if btn[\"buttonShape\"] == \"Rectangle\" %}\n                                            <option value=\"Rectangle\" selected>Rectangle</option>\n                                            {% else %}\n                                            <option value=\"Rectangle\">Rectangle</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonShape\"] == \"Rounded\" %}\n                                            <option value=\"Rounded\" selected>Rounded</option>\n                                            {% else %}\n                                            <option value=\"Rounded\">Rounded</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonShape\"] == \"Circular\" %}\n                                            <option value=\"Circular\" selected>Circular</option>\n                                            {% else %}\n                                            <option value=\"Circular\">Circular</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonShape\"] == \"Square\" %}\n                                            <option value=\"Square\" selected>Square</option>\n                                            {% else %}\n                                            <option value=\"Square\">Square</option>\n                                            {% endif %}\n                                        </select>\n                                    </td>\n                                    <td style=\"width:5%\">\n                                        <select name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_color\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_color\">\n                                            {% if btn[\"buttonColor\"] == \"Black\" %}\n                                            <option value=\"Black\" selected>Black</option>\n                                            {% else %}\n                                            <option value=\"Black\">Black</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonColor\"] == \"Red\" %}\n                                            <option value=\"Red\" selected>Red</option>\n                                            {% else %}\n                                            <option value=\"Red\">Red</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonColor\"] == \"Green\" %}\n                                            <option value=\"Green\" selected>Green</option>\n                                            {% else %}\n                                            <option value=\"Green\">Green</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonColor\"] == \"Yellow\" %}\n                                            <option value=\"Yellow\" selected>Yellow</option>\n                                            {% else %}\n                                            <option value=\"Yellow\">Yellow</option>\n                                            {% endif %}\n                                            {% if btn[\"buttonColor\"] == \"Blue\" %}\n                                            <option value=\"Blue\" selected>Blue</option>\n                                            {% else %}\n                                            <option value=\"Blue\">Blue</option>\n                                            {% endif %}\n                                        </select>\n                                    </td>\n                                    <td style=\"width:20%\">\n                                        <input style=\"width: 100%\" type=\"text\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\"\n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\" value=\"{{ btn['buttonText'] }}\">\n                                    </td>\n                                    <td style=\"width:50%\">\n                                        <input style=\"width: 100%\" type=\"text\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_buttonexec\"\n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_buttonexec\" value=\"{{ btn['buttonExec'] }}\">\n                                    </td>\n                                    <td style=\"width:5%\">\n                                        {% if btn[\"needsConfirm\"] == True %}\n                                        <input type=\"checkbox\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\" checked>\n                                        {% else %}\n                                        <input type=\"checkbox\" id=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                            name=\"vbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\">\n                                        {% endif %}\n                                    </td>\n                                </tr>\n                                {% endfor %}\n                                {% endfor %}\n                            </tbody>\n                        </table>\n                    </div>\n                    <p style=\"margin-bottom: 0\"></p>\n                    <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                </form>\n            </div>\n        </div>\n    </div>\n    {% if sc.lastSettingsTab == \"settingsabuttons\" %}\n    <div id=\"settingsabuttons\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsabuttons\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Action Buttons</h3>\n        <p>\n            On this page you can configure the action buttons shown under Console/Action Buttons.<br>\n            With these buttons you can execute actions which are configured on page Trigger/Actions<br>\n            Buttons are arranged in a matrix-like structure with rows and columns.<br>\n        </p>\n        <div>\n            <table style=\"width:100%\" id=\"abuttondimtab\">\n                <form id=\"abuttondimensions\" method=\"post\" action=\"{{ url_for('settings.abutton_dimensions') }}\">\n                    <tr>\n                        <td style=\"width: 15%\">\n                            Number of Rows:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"abuttonsrows\" name=\"abuttonsrows\" min=\"0\" max=\"99\" step=\"1\" \n                                value=\"{{ sc.aButtonsRows }}\">\n                        </td>\n                        <td style=\"width: 15%\">\n                            Number of Columns:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"abuttonscols\" name=\"abuttonscols\" min=\"0\" max=\"99\" step=\"1\"\n                            value=\"{{ sc.aButtonsCols }}\">\n                        </td>\n                        <td style=\"width: 17%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width: 5%\">\n                        </td>\n                        <td style=\"width: 20%\">\n                        </td>\n                        <td style=\"width: 13%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n        </div>\n        {% if sc.abuttonsRows == 0 or sc.abuttonsCols == 0 %}\n        <div style=\"display:none\">\n        {% else %}\n        <div>\n        {% endif %}\n            <h3>Action Button Settings</h1>\n            <form method=\"post\" action=\"{{ url_for('settings.abutton_settings') }}\">\n                <div style=\"max-height:63.6vh;overflow-y:auto\">\n                    <table class=\"w3-table-all\">\n                        <thead>\n                            <tr>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Row\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Col\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Visible\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Shape\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Color\n                                </th>\n                                <th style=\"width:20%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Button Text\n                                </th>\n                                <th style=\"width:50%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Action\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Conf\n                                </th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            {% for r in sc.aButtons %}\n                            {% for btn in r %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {{ btn[\"row\"] + 1 }}\n                                </td>\n                                <td style=\"width:5%\">\n                                    {{ btn[\"col\"] + 1 }}\n                                </td>\n                                <td style=\"width:5%\">\n                                    {% if btn[\"isVisible\"] == True %}\n                                    <input type=\"checkbox\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" \n                                        name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_visible\"\n                                        name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\">\n                                    {% endif %}\n                                </td>\n                                <td style=\"width:5%\">\n                                    <select name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_shape\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_shape\">\n                                        {% if btn[\"buttonShape\"] == \"Rectangle\" %}\n                                        <option value=\"Rectangle\" selected>Rectangle</option>\n                                        {% else %}\n                                        <option value=\"Rectangle\">Rectangle</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Rounded\" %}\n                                        <option value=\"Rounded\" selected>Rounded</option>\n                                        {% else %}\n                                        <option value=\"Rounded\">Rounded</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Circular\" %}\n                                        <option value=\"Circular\" selected>Circular</option>\n                                        {% else %}\n                                        <option value=\"Circular\">Circular</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Square\" %}\n                                        <option value=\"Square\" selected>Square</option>\n                                        {% else %}\n                                        <option value=\"Square\">Square</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                                <td style=\"width:5%\">\n                                    <select name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_color\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_color\">\n                                        {% if btn[\"buttonColor\"] == \"Black\" %}\n                                        <option value=\"Black\" selected>Black</option>\n                                        {% else %}\n                                        <option value=\"Black\">Black</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"White\" %}\n                                        <option value=\"White\" selected>White</option>\n                                        {% else %}\n                                        <option value=\"White\">White</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Red\" %}\n                                        <option value=\"Red\" selected>Red</option>\n                                        {% else %}\n                                        <option value=\"Red\">Red</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Green\" %}\n                                        <option value=\"Green\" selected>Green</option>\n                                        {% else %}\n                                        <option value=\"Green\">Green</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Yellow\" %}\n                                        <option value=\"Yellow\" selected>Yellow</option>\n                                        {% else %}\n                                        <option value=\"Yellow\">Yellow</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Blue\" %}\n                                        <option value=\"Blue\" selected>Blue</option>\n                                        {% else %}\n                                        <option value=\"Blue\">Blue</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                                <td style=\"width:20%\">\n                                    <input style=\"width: 100%\" type=\"text\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\"\n                                        name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\" value=\"{{ btn['buttonText'] }}\">\n                                </td>\n                                <td style=\"width:50%\">\n                                    <select name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_action\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_action\">\n                                        {% if btn[\"buttonAction\"] == \"\" %}\n                                        <option value=\"\" selected></option>\n                                        {% else %}\n                                        <option value=\"\"></option>\n                                        {% endif %}\n                                        {% for action in tc.actions %}\n                                        {% if btn[\"buttonAction\"] == action.id %}\n                                        <option value=\"{{ action.id }}\" selected>{{ action.id }}</option>\n                                        {% else %}\n                                        <option value=\"{{ action.id }}\">{{ action.id }}</option>\n                                        {% endif %}\n                                        {% endfor %}\n                                    </select>\n                                </td>\n                                <td style=\"width:5%\">\n                                    {% if btn[\"needsConfirm\"] == True %}\n                                    <input type=\"checkbox\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                        name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                        name=\"abtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\">\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endfor %}\n                            {% endfor %}\n                        </tbody>\n                    </table>\n                </div>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastSettingsTab == \"settingslbuttons\" %}\n    <div id=\"settingslbuttons\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingslbuttons\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Live Buttons</h3>\n        <p>\n            On this page you can configure function buttons shown on the Ctrl tab of the Live screen.<br>\n            With these buttons you can execute OS commands or actions which are configured on page Trigger/Actions<br>\n            Buttons are arranged in a matrix-like structure with rows and columns.<br>\n        </p>\n        <div>\n            <table style=\"width:100%\" id=\"lbuttondimtab\">\n                <form id=\"lbuttondimensions\" method=\"post\" action=\"{{ url_for('settings.lbutton_dimensions') }}\">\n                    <tr>\n                        <td style=\"width: 15%\">\n                            Number of Rows:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"lbuttonsrows\" name=\"lbuttonsrows\" min=\"0\" max=\"99\" step=\"1\" \n                                value=\"{{ sc.lButtonsRows }}\">\n                        </td>\n                        <td style=\"width: 15%\">\n                            Number of Columns:\n                        </td>\n                        <td style=\"width: 5%\">\n                            <input type=\"number\" id=\"lbuttonscols\" name=\"lbuttonscols\" min=\"0\" max=\"99\" step=\"1\"\n                            value=\"{{ sc.lButtonsCols }}\">\n                        </td>\n                        <td style=\"width: 17%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:5%\">\n                        </td>\n                        <td style=\"width: 5%\">\n                        </td>\n                        <td style=\"width: 20%\">\n                        </td>\n                        <td style=\"width: 13%\">\n                        </td>\n                    </tr>\n                </form>\n            </table>\n        </div>\n        {% if sc.lbuttonsRows == 0 or sc.lbuttonsCols == 0 %}\n        <div style=\"display:none\">\n        {% else %}\n        <div>\n        {% endif %}\n            <h3>Live Button Settings</h1>\n            <form method=\"post\" action=\"{{ url_for('settings.lbutton_settings') }}\">\n                <div style=\"max-height:63.6vh;overflow-y:auto\">\n                    <table class=\"w3-table-all\">\n                        <thead>\n                            <tr>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Row\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Col\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Visible\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Shape\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Color\n                                </th>\n                                <th style=\"width:20%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Button Text\n                                </th>\n                                <th style=\"width:25%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Command\n                                </th>\n                                <th style=\"width:25%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Action\n                                </th>\n                                <th style=\"width:5%; position:sticky;top:0;z-index:1000; background-color: #f1f1f1;\">\n                                    Conf\n                                </th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            {% for r in sc.lButtons %}\n                            {% for btn in r %}\n                            <tr>\n                                <td style=\"width:5%\">\n                                    {{ btn[\"row\"] + 1 }}\n                                </td>\n                                <td style=\"width:5%\">\n                                    {{ btn[\"col\"] + 1 }}\n                                </td>\n                                <td style=\"width:5%\">\n                                    {% if btn[\"isVisible\"] == True %}\n                                    <input type=\"checkbox\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" \n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\"\n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_visible\" value=\"1\">\n                                    {% endif %}\n                                </td>\n                                <td style=\"width:5%\">\n                                    <select name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_shape\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_shape\">\n                                        {% if btn[\"buttonShape\"] == \"Rectangle\" %}\n                                        <option value=\"Rectangle\" selected>Rectangle</option>\n                                        {% else %}\n                                        <option value=\"Rectangle\">Rectangle</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Rounded\" %}\n                                        <option value=\"Rounded\" selected>Rounded</option>\n                                        {% else %}\n                                        <option value=\"Rounded\">Rounded</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Circular\" %}\n                                        <option value=\"Circular\" selected>Circular</option>\n                                        {% else %}\n                                        <option value=\"Circular\">Circular</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonShape\"] == \"Square\" %}\n                                        <option value=\"Square\" selected>Square</option>\n                                        {% else %}\n                                        <option value=\"Square\">Square</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                                <td style=\"width:5%\">\n                                    <select name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_color\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_color\">\n                                        {% if btn[\"buttonColor\"] == \"Black\" %}\n                                        <option value=\"Black\" selected>Black</option>\n                                        {% else %}\n                                        <option value=\"Black\">Black</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"White\" %}\n                                        <option value=\"White\" selected>White</option>\n                                        {% else %}\n                                        <option value=\"White\">White</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Red\" %}\n                                        <option value=\"Red\" selected>Red</option>\n                                        {% else %}\n                                        <option value=\"Red\">Red</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Green\" %}\n                                        <option value=\"Green\" selected>Green</option>\n                                        {% else %}\n                                        <option value=\"Green\">Green</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Yellow\" %}\n                                        <option value=\"Yellow\" selected>Yellow</option>\n                                        {% else %}\n                                        <option value=\"Yellow\">Yellow</option>\n                                        {% endif %}\n                                        {% if btn[\"buttonColor\"] == \"Blue\" %}\n                                        <option value=\"Blue\" selected>Blue</option>\n                                        {% else %}\n                                        <option value=\"Blue\">Blue</option>\n                                        {% endif %}\n                                    </select>\n                                </td>\n                                <td style=\"width:20%\">\n                                    <input style=\"width: 100%\" type=\"text\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\"\n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_buttontext\" value=\"{{ btn['buttonText'] }}\">\n                                </td>\n                                <td style=\"width:25%\">\n                                    <input style=\"width: 100%\" type=\"text\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_buttonexec\"\n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_buttonexec\" value=\"{{ btn['buttonExec'] }}\">\n                                </td>\n                                <td style=\"width:25%\">\n                                    <select name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_action\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_action\">\n                                        {% if btn[\"buttonAction\"] == \"\" %}\n                                        <option value=\"\" selected></option>\n                                        {% else %}\n                                        <option value=\"\"></option>\n                                        {% endif %}\n                                        {% for action in tc.actions %}\n                                        {% if btn[\"buttonAction\"] == action.id %}\n                                        <option value=\"{{ action.id }}\" selected>{{ action.id }}</option>\n                                        {% else %}\n                                        <option value=\"{{ action.id }}\">{{ action.id }}</option>\n                                        {% endif %}\n                                        {% endfor %}\n                                    </select>\n                                </td>\n                                <td style=\"width:5%\">\n                                    {% if btn[\"needsConfirm\"] == True %}\n                                    <input type=\"checkbox\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\"\n                                        name=\"lbtn_{{ btn['row'] }}{{ btn['col'] }}_confirm\" value=\"1\">\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endfor %}\n                            {% endfor %}\n                        </tbody>\n                    </table>\n                </div>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastSettingsTab == \"settingsdevices\" %}\n    <div id=\"settingsdevices\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsdevices\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"w3-row\">\n            <div class=\"w3-half\">\n                <h3>GPIO-Connected Devices</h3>\n                <h4>New Device</h4>\n                <table>\n                    <form method=\"post\" id=\"newdevice\" action=\"{{ url_for('settings.new_device') }}\">\n                        <tr>\n                            <td class=\"w3-tooltip\" style=\"width:35%\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Select one of the available device types.<br>\n                                    raspiCamSrv supports various gpizero device types.\n                                </span>\n                                <label for=\"newdevicetype\">Device Type:</label>\n                            </td>\n                            <td style=\"width:35%\">\n                                {% set urlStatic = url_for('static', filename='' ) %}\n                                <select style=\"width:100%; height:28px\" name=\"newdevicetype\" id=\"newdevicetype\"\n                                        onchange=\"changeDeviceTypeImage('newdevicetype', 'deviceimage', '{{ urlStatic }}')\">\n                                    {% if sc.curDeviceType %}\n                                    {% set curDeviceType = sc.curDeviceType.type %}\n                                    {% for devicetype in sc.deviceTypes %}\n                                    {% if devicetype.type == curDeviceType %}\n                                    <option value=\"{{ devicetype.type}}\" selected>{{ devicetype.type }}</option>\n                                    {% else %}\n                                    <option value=\"{{ devicetype.type}}\">{{ devicetype.type }}</option>\n                                    {% endif %}\n                                    {% endfor %}\n                                    {% else %}\n                                    {% for devicetype in sc.deviceTypes %}\n                                    <option value=\"{{ devicetype.type}}\">{{ devicetype.type }}</option>\n                                    {% endfor %}\n                                    {% endif %}\n                                </select>\n                            </td>\n                            <td style=\"width:30%\">\n                                <!-- This cell is used to store an invisible list of device type images -->\n                                {% for deviceType in sc.deviceTypes %}\n                                {% if \"image\" in deviceType %}\n                                <p id=\"devicetypeimage_{{ deviceType.type }}\" style=\"display:none\">{{ deviceType.image }}</p>\n                                {% endif%}\n                                {% endfor%}\n                            </td>\n                        </tr>\n                        <tr>\n                            <td class=\"w3-tooltip\" style=\"width:35%\">\n                                <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                    Enter a unique ID under which the new device will be identified\n                                    within the system.\n                                </span>\n                                <label for=\"newdeviceid\">ID:</label>\n                            </td>\n                            <td style=\"width:35%\">\n                                <input type=\"text\" id=\"newdeviceid\" name=\"newdeviceid\" value=\"\" aria-label=\"ID for device\">\n                            </td>\n                            <td style=\"width:30%\">\n                                <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Create\">\n                            </td>\n                        </tr>\n                    </form>\n                </table>\n            </div>\n            <div class=\"w3-half\">\n                {% if sc.curDeviceType %}\n                {% set devImage = sc.curDeviceType.image %}\n                {% else %}\n                {% set devType = sc.deviceTypes[0] %}\n                {% set devImage = devType.image %}\n                {% endif %}\n                {% if devImage %}\n                {% if devImage|length() > 0 %}\n                {% set urlImage=url_for('static', filename=devImage) %}\n                <img src=\"{{ urlImage }}\" class=\"w3-image\" id=\"deviceimage\" alt=\"Device image\"\n                    style=\"height:200px; object-fit:scale-down\">\n                {% endif %}\n                {% endif %}\n            </div>\n        </div>\n        <div class=\"w3-row\">\n            <hr>\n        </div>\n        {% if sc.gpioDevices|length() > 0 %}\n        <div class=\"w3-half\">\n            <h3>Device Configuration</h3>\n            <table>\n                <tr>\n                    {% if sc.curDeviceId != \"\" %}\n                    <form method=\"post\" id=\"device\" action=\"{{ url_for('settings.select_device') }}\">\n                        <td style=\"width:35%\">\n                        </td>\n                        <td style=\"width:35%\">\n                            <select style=\"width:100%; height:28px\" name=\"selectdevice\" id=\"selectdevice\" onchange=\"doSubmit('device')\">\n                                {% for device in sc.gpioDevices %}\n                                {% if device.id == sc.curDeviceId %}\n                                <option value=\"{{ device.id }}\" selected>{{ device.id }}</option>\n                                {% else %}\n                                <option value=\"{{ device.id }}\">{{ device.id }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </form>\n                    <td style=\"width:30%\">\n                        <form method=\"post\" id=\"deletedevice\" action=\"{{ url_for('settings.delete_device') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Delete\"\n                                onclick=\"confirmDeleteDevice('deletedevice')\">\n                        </form>\n                    </td>\n                    {% endif %}\n                </tr>\n                {% if sc.curDeviceId != \"\" %}\n                {% set device = sc.curDevice %}\n                <form method=\"post\" id=\"deviceprops\" action=\"{{ url_for('settings.device_properties') }}\">\n                    <tr>\n                        <td style=\"width:35%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The status shows whether or not a device has been completely configured \n                                and tested so that it can be used.\n                            </span>\n                            <label for=\"devicestatus\">Config Status:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            {% if device.isOk == True %}\n                            <input style=\"width:100%\" type=\"text\" id=\"devicestatus\" name=\"devicestatus\" value=\"OK\" disabled>\n                            {% else %}\n                            <input style=\"width:100%\" type=\"text\" id=\"devicestatus\" name=\"devicestatus\" value=\"Not OK\" disabled>\n                            {% endif %}\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:35%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The device type is one of the supported gpiozero device types.\n                            </span>\n                            <label for=\"devicetype\">Device Type:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"devicetype\" name=\"devicetype\" value=\"{{ device.type }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:35%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Usage shows whether the device is an input or output device.\n                            </span>\n                            <label for=\"deviceusage\">Usage:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            <input style=\"width:100%\" type=\"text\" id=\"deviceusage\" name=\"deviceusage\" value=\"{{ device.usage }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width:35%\" class=\"w3-tooltip\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                gpiozero documentation for this device type.\n                            </span>\n                            <label for=\"devicedocurl\">gpiozero Doc:</label>\n                        </td>\n                        <td style=\"width:35%\">\n                            <a id=\"devicedocurl\" href=\"{{ device.docUrl }}\" target=\"_blank\">Link to gpiozero</a>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    {% set deviceType = sc.curDeviceType %}\n                    {% for key, value in device.params.items() %}\n                    {% for dtKey, dtValue in deviceType.params.items() %}\n                    {% if key == dtKey %}\n                    <tr>\n                        <td style=\"width:35%\">\n                            {{ key }}\n                        </td>\n                        {% if dtValue.type == \"str\" %}\n                        <td style=\"width:35%\">\n                            <input type=\"text\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" value=\"{{ value }}\">\n                        </td>\n                        {% elif dtValue.type == \"int\"%}\n                        <td style=\"width:35%\">\n                            <input type=\"number\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" min=\"{{ dtValue.min }}\" max=\"{{ dtValue.max }}\"\n                                step=\"1\" value=\"{{ value }}\">\n                        </td>\n                        {% elif dtValue.type == \"float\"%}\n                        <td style=\"width:35%\">\n                            <input type=\"number\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" min=\"{{ dtValue.min }}\" max=\"{{ dtValue.max }}\"\n                                step=\"0.0001\" value=\"{{ value }}\">\n                        </td>\n                        {% elif dtValue.type == \"floatOrNone\"%}\n                        <td style=\"width:35%\">\n                            <input type=\"text\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" value=\"{{ value }}\">\n                        </td>\n                        {% elif dtValue.type == \"bool\"%}\n                        <td style=\"width:35%\">\n                            {% if value == True %}\n                            <input type=\"checkbox\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" value=\"1\">\n                            {% endif %}\n                        </td>\n                        {% elif dtValue.type == \"boolOrNone\"%}\n                        <td style=\"width:35%\">\n                            <select style=\"width:100%; height:28px\" name=\"param_{{ key }}\" id=\"param_{{ key }}\">\n                                {% if value is none %}\n                                <option value=\"None\" selected>None</option>\n                                {% else %}\n                                <option value=\"None\">None</option>\n                                {% endif %}\n                                {% if value == True %}\n                                <option value=\"True\" selected>True</option>\n                                {% else %}\n                                <option value=\"True\">True</option>\n                                {% endif %}\n                                {% if value == False %}\n                                <option value=\"False\" selected>False</option>\n                                {% else %}\n                                <option value=\"False\">False</option>\n                                {% endif %}\n                            </select>\n                        </td>\n                        {% else %}\n                        <td style=\"width:35%\">\n                            <input type=\"text\" id=\"param_{{ key }}\" name=\"param_{{ key }}\" value=\"{{ value }}\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    {% endif %}\n                    {% endfor %}\n                    {% endfor %}\n                    <tr>\n                        <td style=\"width:35%\">\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                        </td>\n                        <td style=\"width:35%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                </form>\n                {% endif %}\n            </table>\n            {% set device = sc.curDevice %}\n            {% if device.isOk == True %}\n            <hr>\n            {% if device.needsCalibration == True %}\n            <h3>Device Test & Calibration</h3>\n            {% else %}\n            <h3>Device Test</h3>\n            {% endif %}\n            <table style=\"table-layout: fixed; width:100%\">\n                <tr>\n                    <td style=\"width:25%\">\n                        <form method=\"post\" id=\"testdevice\" action=\"{{ url_for('settings.test_device') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Test\" onclick=\"hideTestResults()\">\n                        </form>\n                    </td>\n                    <td style=\"width:25%\">\n                        {% if device.needsCalibration == True %}\n                        <form method=\"post\" id=\"calibratedevice\" action=\"{{ url_for('settings.calibrate_device') }}\">\n                            <input class=\"w3-button w3-black\" style=\"width:95px\" type=\"submit\" value=\"Calibrate\" onclick=\"hideTestResults()\">\n                        </form>\n                        {% endif %}\n                    </td>\n                    {% if device.isCalibrating == True %}\n                    <td style=\"width:10%\">\n                        <a href=\"{{ url_for('settings.calibrate_fbwd') }}\" class=\"w3-button w3-sand\">&laquo;</a>\n                    </td>\n                    <td style=\"width:10%\">\n                        <a href=\"{{ url_for('settings.calibrate_bwd') }}\" class=\"w3-button w3-sand\">&lt</a>\n                    </td>\n                    <td style=\"width:10%\">\n                        <a href=\"{{ url_for('settings.docalibrate') }}\" class=\"w3-button w3-sand\">OK</a>\n                    </td>\n                    <td style=\"width:10%\">\n                        <a href=\"{{ url_for('settings.calibrate_fwd') }}\" class=\"w3-button w3-sand\">&gt</a>\n                    </td>\n                    <td style=\"width:10%\">\n                        <a href=\"{{ url_for('settings.calibrate_ffwd') }}\" class=\"w3-button w3-sand\">&raquo;</a>\n                    </td>\n                    {% else %}\n                    <td style=\"width:50%\">\n                    </td>\n                    {% endif %}\n                </tr>\n            </table>\n            {% if result|length() > 0 %}\n            <div style=\"height:200px; overflow: auto\">\n                <table id=\"testresults\" class=\"w3-table-all\">\n                    {% for key, value in result.items() %}\n                    <tr>\n                        <td style=\"width:35%\">\n                            {{ key }}\n                        </td>\n                        <td colspan=\"2\" style=\"width:65%\">\n                            {{ value }}\n                        </td>\n                    </tr>\n                    {% endfor %}\n                </table>\n            </div>\n            {% endif %}\n            {% endif %}\n        </div>\n        {% endif %}\n        {% if sc.gpioDevices|length() > 0 %}\n        <div class=\"w3-half\">\n            <h3>Devices</h3>\n            <p>\n                Unused GPIO Pins:\n                {% for pin in sc.freeGpioPins %}{{ pin }}{% if not loop.last %},{% endif %}{% endfor %}\n            </p>\n            <div style=\"max-height:58.5vh;overflow-y:auto\">\n                <table class=\"w3-table-all\">\n                    <thead>\n                        <tr>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                ID\n                            </th>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                Type\n                            </th>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                Usage\n                            </th>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                GPIO Pins\n                            </th>\n                            <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                OK\n                            </th>\n                        </tr>\n                    </thead>\n                    <tbody>\n                    {% for device in sc.gpioDevices %}\n                        <tr>\n                            <td>\n                                {{ device.id }}\n                            </td>\n                            <td>\n                                {{ device.type }}\n                            </td>\n                            <td>\n                                {{ device.usage }}\n                            </td>\n                            <td>\n                                {{ device.usedPins }}\n                            </td>\n                            <td>\n                                {{ device.isOk }}\n                            </td>\n                        </tr>\n                    {% endfor %}\n                    </tbody>\n                </table>\n            </div>\n        </div>\n        {% endif %}\n    </div>\n    {% if sc.lastSettingsTab == \"settingsupdate\" %}\n    <div id=\"settingsupdate\" class=\"settingsgroup\">\n    {% else %}\n    <div id=\"settingsupdate\" class=\"settingsgroup\" style=\"display:none\">\n    {% endif %}\n        <h3>raspiCamSrv Updates</h3>\n        <table>\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        If selected, the system will check for updates and indicate when an update is available.\n                    </span>\n                    <label for=\"versioncheckenabledcb\">Check for Updates :</label>\n                </td>\n                <td style=\"width:20%\">\n                    <form id=\"versioncheckenabled\" method=\"post\" action=\"{{ url_for('settings.versionCheckEnabled') }}\">\n                        {% if sc.versionCheckEnabled == True %}\n                        <input type=\"checkbox\" id=\"versioncheckenabledcb\" name=\"versioncheckenabledcb\" onchange=\"doSubmit('versioncheckenabled')\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"versioncheckenabledcb\" name=\"versioncheckenabledcb\" onchange=\"doSubmit('versioncheckenabled')\" value=\"0\">\n                        {% endif %}\n                    </form>\n                </td>\n                <td style=\"width:25%\">\n                </td>\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            {% if sc.versionCheckEnabled == True %}\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        The latest version of raspiCamSrv, as checked at the indicated time.\n                    </span>\n                    <label for=\"latestversion\">Latest Version:</label>\n                </td>\n                <td style=\"width:20%\">\n                    <input style=\"width: 18ch\" type=\"text\" id=\"latestversion\" name=\"latestversion\" value=\"{{ sc.versionLatest }}\" readonly>\n                </td>\n                <td style=\"width:25%\">\n                    <a href=\"https://signag.github.io/raspi-cam-srv/latest/ReleaseNotes/\" target=\"_blank\">See Release Notes</a>\n                </td>\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        The currently installed version of raspiCamSrv.\n                    </span>\n                    <label for=\"currentversion\">Installed Version:</label>\n                </td>\n                <td style=\"width:20%\">\n                    <input style=\"width: 18ch\" type=\"text\" id=\"currentversion\" name=\"currentversion\" value=\"{{ sc.versionCurrent }}\" readonly>\n                </td>\n                {% if (sc.canUpdate == True and sc.updateDone == False) or (sc.canUpdate == False and sc.isLaterVersion(sc.versionLatest, sc.versionCurrent)) %}\n                <form method=\"post\" id=\"serverupdate\" action=\"{{ url_for('settings.serverUpdate') }}\">\n                    <td style=\"width:25%\">\n                        <input class=\"w3-button w3-black\" style=\"width:26ch\" type=\"submit\" onclick=\"confirmServerUpdate('serverupdate')\" value=\"Update to {{ sc.versionLatest }}\">\n                    </td>\n                </form>\n                {% endif %}\n                {% if sc.updateDone == True %}\n                <form method=\"post\" id=\"serverrestartupdate\" action=\"{{ url_for('settings.serverRestart') }}\">\n                    <td style=\"width:25%\">\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" onclick=\"confirmServerRestart('serverrestartupdate')\" value=\"Restart Server\">\n                    </td>\n                </form>\n                {% endif %}\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        You will be notified if the latest version is later than the version specified here.\n                    </span>\n                    <label for=\"versioncheckfrom\">Notify on Version later than:</label>\n                </td>\n                <td style=\"width:20%\">\n                    <input style=\"width: 18ch\" type=\"text\" id=\"versioncheckfrom\" name=\"versioncheckfrom\" value=\"{{ sc.versionCheckFrom }}\" readonly>\n                </td>\n                {% if sc.canUpdate == True %}\n                <form method=\"post\" id=\"updateignorelatest\" action=\"{{ url_for('settings.updateIgnoreLatest') }}\">\n                    <td style=\"width:25%\">\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" onclick=\"confirmIgnoreLatest('updateignorelatest')\" value=\"Ignore version {{ sc.versionLatest }}\">\n                    </td>\n                </form>\n                {% endif %}\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        The time when the latest version check was performed.\n                    </span>\n                    <label for=\"versionchecktime\">Latest Version checked at:</label>\n                </td>\n                <td style=\"width:20%\">\n                    <input style=\"width: 18ch\" type=\"datetime-local\" id=\"versionchecktime\" name=\"versionchecktime\" value=\"{{ sc.versionCheckTimeIso }}\" readonly>\n                </td>\n                <td style=\"width:25%\">\n                </td>\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width:20%\" class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        The latest version of raspiCamSrv, as checked at the indicated time.\n                    </span>\n                    <label for=\"versioncheckenabledcb\">Check Interval (Hours):</label>\n                </td>\n                <td style=\"width:20%\">\n                    <form method=\"post\" id=\"versioncheckintervalhoursfrm\" action=\"{{ url_for('settings.versionCheckIntervalHours') }}\">\n                        <input style=\"width: 18ch\" type=\"number\" onchange=\"doSubmit('versioncheckintervalhoursfrm')\" id=\"versioncheckintervalhours\" name=\"versioncheckintervalhours\" min=\"\" max=\"200\" step=\"1\" value=\"{{ sc.versionCheckIntervalHours }}\">\n                    </form>\n                </td>\n                <form method=\"post\" id=\"versionchecknow\" action=\"{{ url_for('settings.versionCheckNow') }}\">\n                    <td style=\"width:25%\">\n                        <input class=\"w3-button w3-black\" style=\"width: 26ch\" type=\"submit\" value=\"Check Now\">\n                    </td>\n                </form>\n                <td style=\"width:35%\">\n                </td>\n            </tr>\n            {% endif %}\n        </table>\n    </div>\n    \n    <script>\n        var deviceTypeImages;\n\n        function initDeviceTypeImages(){\n            deviceTypeImages = new Map();\n        }\n\n        function appendDeviceTypeImages(type, image){\n            deviceTypeImages.set(type, image);\n        }\n\n        function openSettingsTab(settingsTabName, settingsTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"settingsgroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(settingsTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"settingsmenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button settingsmenu\";\n            }\n            document.getElementById(settingsTabButton).classList = \"w3-bar-item w3-button settingsmenu w3-light-green\";\n        }\n        function confirmRemove(form) {\n            if (confirm(\"Do you want to remove the selected users?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        \n        function confirmReloadCams(form) {\n            if (confirm(\"Do you want to reload the camera system?\\nThe entire camera configuration will be adjusted to the currently connected cameras!\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function confirmReset(form) {\n            if (confirm(\"Do you want to reset the server?\\nThe entire configuration will be reset to default!\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmStore(form) {\n            if (confirm(\"Do you want to replace the stored configuration with the current configuration?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmLoad(form) {\n            if (confirm(\"Do you want to replace the current configuration with the stored configuration?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmBackupConfig(form, nameElementId) {\n            backupName = document.getElementById(nameElementId).value;\n            if (confirm(\"Do you want to backup stored data as '\" + backupName + \"'?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmBackupRestore(form, nameElementId) {\n            backupName = document.getElementById(nameElementId).value;\n            if (confirm(\"Do you want to restore the backup '\" + backupName + \"'?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmBackupRemove(form, nameElementId) {\n            backupName = document.getElementById(nameElementId).value;\n            if (confirm(\"Do you want to remove the backup '\" + backupName + \"'?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmServerRestart(form) {\n            if (confirm(\"Do you want to restart the raspiCamSrv Flask server?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmServerUpdate(form) {\n            if (confirm(\"Do you want to update raspiCamSrv to the latest version?\\nYou will need to restart the server afterwards.\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmIgnoreLatest(form) {\n            if (confirm(\"Do you want to be no longer informed about update to version {{ sc.versionLatest }}?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmApiChange(form) {\n            confirm(\"A change of API usage will only be effective for clients if you save the configuration and restart the server with stored configuration.\")\n        }\n        function loadCfgOnStart(form) {\n                document.getElementById(form).submit();\n        }\n        function doSubmit(form) {\n            document.getElementById(form).submit();\n        }\n\n        function onlineHelp(noCam) {\n            if (noCam == 0) {\n                window.open(\"{{ sc.getBaseHelpUrl() }}/Settings/\");\n            } else {\n                window.open(\"{{ sc.getBaseHelpUrl() }}/Settings_NoCam/\");\n            }\n        }\n        function confirmDeleteDevice(form) {\n            if (confirm(\"Do you want to delete this device?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function changeDeviceTypeImage(src, tgt, urlRoot){\n            deviceType = document.getElementById(src).value;\n            imgId = \"devicetypeimage_\" + deviceType;\n            image = document.getElementById(imgId).innerText;\n            file = urlRoot + image;\n            tgtEl = document.getElementById(tgt);\n            tgtEl.src = file\n        }\n        function hideTestResults(){\n            tr = document.getElementById(\"testresults\")\n            tr.display = \"none\"\n        }\n    </script>\n\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/trigger/trigger.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Trigger{% endblock %}\n    <style>\n        /* Custom styles for the photo viewer page */\n        .video-wrapper {\n        position: relative;\n        width: 100%;\n        }\n\n        /* Native video */\n        .video-wrapper video {\n        width: 100%;\n        display: block;\n        }\n\n        /* Overlay captures clicks on video surface only */\n        .open-overlay {\n        position: absolute;\n        left: 0;\n        right: 0;\n        top: 0;\n        bottom: 20%; /* height of native controls */\n        cursor: pointer;\n        background: transparent;\n        }\n    </style>\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Trigger menu -->\n        {% if sc.lastTriggerTab == \"trgcontrol\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgCtrlBtn\"\n            onclick=\"openTriggerTab('trgcontrol', 'trgCtrlBtn')\">Control</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgCtrlBtn\"\n            onclick=\"openTriggerTab('trgcontrol', 'trgCtrlBtn')\">Control</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgtriggers\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgTriggersBtn\"\n            onclick=\"openTriggerTab('trgtriggers', 'trgTriggersBtn')\">Triggers</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgTriggersBtn\"\n            onclick=\"openTriggerTab('trgtriggers', 'trgTriggersBtn')\">Triggers</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgactions\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgActionsBtn\"\n            onclick=\"openTriggerTab('trgactions', 'trgActionsBtn')\">Actions</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgActionsBtn\"\n            onclick=\"openTriggerTab('trgactions', 'trgActionsBtn')\">Actions</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgtriggeractions\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgTriggerActionsBtn\"\n            onclick=\"openTriggerTab('trgtriggeractions', 'trgTriggerActionsBtn')\">Trigger-Actions</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgTriggerActionsBtn\"\n            onclick=\"openTriggerTab('trgtriggeractions', 'trgTriggerActionsBtn')\">Trigger-Actions</button>\n        {% endif %}\n        {% if sc.noCamera == False %}\n        {% if sc.lastTriggerTab == \"trgmotion\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgmotionbtn\"\n            onclick=\"openTriggerTab('trgmotion', 'trgmotionbtn')\">Motion</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgmotionbtn\"\n            onclick=\"openTriggerTab('trgmotion', 'trgmotionbtn')\">Motion</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgaction\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgactionbtn\"\n            onclick=\"openTriggerTab('trgaction', 'trgactionbtn')\">Camera</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgactionbtn\"\n            onclick=\"openTriggerTab('trgaction', 'trgactionbtn')\">Camera</button>\n        {% endif %}\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgnotify\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgnotifybtn\"\n            onclick=\"openTriggerTab('trgnotify', 'trgnotifybtn')\">Notification</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgnotifybtn\"\n            onclick=\"openTriggerTab('trgnotify', 'trgnotifybtn')\">Notification</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgevents\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgeventsbtn\"\n            onclick=\"openTriggerTab('trgevents', 'trgeventsbtn')\">Events</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgeventsbtn\"\n            onclick=\"openTriggerTab('trgevents', 'trgeventsbtn')\">Events</button>\n        {% endif %}\n        {% if sc.lastTriggerTab == \"trgcalendar\" %}\n        <button class=\"w3-bar-item w3-button triggermenu w3-light-green\" id=\"trgcalendarbtn\"\n            onclick=\"openTriggerTab('trgcalendar', 'trgcalendarbtn')\">Calendar</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button triggermenu\" id=\"trgcalendarbtn\"\n            onclick=\"openTriggerTab('trgcalendar', 'trgcalendarbtn')\">Calendar</button>\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\" class=\"w3-text w3-tag\">Online help from GitHub\n                </span>\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\" alt=\"Online help\"\n                    style=\"height:34px; width:34px\" onclick=\"onlineHelp()\">\n            </div>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgcontrol\" %}\n    <div id=\"trgcontrol\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgcontrol\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <form method=\"post\" action=\"{{ url_for('trigger.trgcontrol') }}\">\n            <h4>Triggers and Actions</h4>\n            <table class=\"w3-table-all\">\n                <tr>\n                    <th colspan=\"2\">\n                        Triggers\n                    </th>\n                    {% if sc.noCamera == False %}\n                    <th colspan=\"2\">\n                        Actions\n                    </th>\n                    {% else %}\n                    <th>\n                    </th>\n                    {% endif %}\n                    <th>\n                    </th>\n                </tr>\n                {% if sc.noCamera == False %}\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:15%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Activate if you wish to trigger actions by motion detection.<br>\n                            You need to specify details on the Motion tab\n                        </span>\n                        <label for=\"triggerbymotion\">Motion Detection:</label>\n                    </td>\n                    <td style=\"width:10%\">\n                        {% if tc.triggeredByMotion == True %}\n                        <input type=\"checkbox\" id=\"triggerbymotion\" name=\"triggerbymotion\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"triggerbymotion\" name=\"triggerbymotion\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    <td class=\"w3-tooltip\" style=\"width:15%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Activate if you wish to capture video on a trigger event.<br>\n                            You need to specify details on the Camera tab.\n                        </span>\n                        <label for=\"triggervideo\">Record Video:</label>\n                    </td>\n                    <td style=\"width:10%\">\n                        {% if tc.actionVideo == True %}\n                        <input type=\"checkbox\" id=\"triggervideo\" name=\"triggervideo\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"triggervideo\" name=\"triggervideo\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    <td style=\"width: 50%;\">\n                    </td>\n                </tr>\n                {% endif %}\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:15%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Activate if you wish to use general event handling.<br>\n                            Details need to be configured on the Triggers, Actions and Trigger-Actions tabs.\n                        </span>\n                        <label for=\"triggerbyevents\">Configured Triggers:</label>\n                    </td>\n                    <td style=\"width:10%\">\n                        {% if tc.triggeredByEvents == True %}\n                        <input type=\"checkbox\" id=\"triggerbyevents\" name=\"triggerbyevents\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"triggerbyevents\" name=\"triggerbyevents\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    {% if sc.noCamera == False %}\n                    <td class=\"w3-tooltip\" style=\"width:15%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Activate if you wish to capture images on a trigger event.<br>\n                            You need to specify details on the Camera tab\n                        </span>\n                        <label for=\"triggerphoto\">Take Photo:</label>\n                    </td>\n                    <td style=\"width:10%;\">\n                        {% if tc.actionPhoto == True %}\n                        <input type=\"checkbox\" id=\"triggerphoto\" name=\"triggerphoto\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"triggerphoto\" name=\"triggerphoto\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    {% else %}\n                    <td>\n                    </td>\n                    {% endif %}\n                    <td style=\"width:50%;\">\n                    </td>\n                </tr>\n                {% if sc.noCamera == False %}\n                <tr>\n                    <td style=\"width:15%\">\n                    </td>\n                    <td style=\"width:10%\">\n                    </td>\n                    <td class=\"w3-tooltip\" style=\"width:15%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Activate if you wish to send a notification on a trigger event.<br>\n                            You need to specify details on the Notification tab.\n                        </span>\n                        <label for=\"triggernotify\">Notification:</label>\n                    </td>\n                    <td style=\"width:10%;\">\n                        {% if tc.actionNotify == True %}\n                        <input type=\"checkbox\" id=\"triggernotify\" name=\"triggernotify\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"triggernotify\" name=\"triggernotify\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    <td style=\"width:50%;\">\n                    </td>\n                </tr>\n                {% endif %}\n            </table>\n            <p style=\"margin-bottom: 0\"></p>\n            <h4>Operation</h4>\n            <table class=\"w3-table-all\">\n                <tr>\n                    <td style=\"width:25%\">\n                    </td>\n                    <td style=\"width: 5%;\">Mon</td>\n                    <td style=\"width: 5%;\">Tue</td>\n                    <td style=\"width: 5%;\">Wed</td>\n                    <td style=\"width: 5%;\">Thu</td>\n                    <td style=\"width: 5%;\">Fri</td>\n                    <td style=\"width: 5%;\">Sat</td>\n                    <td style=\"width: 5%;\">Sun</td>\n                    <td style=\"width: 40%;\"></td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Select the weekdays when triggers shall be active\n                        </span>\n                        <label for=\"opweekday1\">Operation Weekdays:</label>\n                    </td>\n                    {% for key, value in tc.operationWeekdays.items()  %}\n                    <td style=\"width: 5%;\">\n                        {% if value == True %}\n                        <input type=\"checkbox\" id=\"opweekday{{key}}\" name=\"opweekday{{key}}\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"opweekday{{key}}\" name=\"opweekday{{key}}\" value=\"1\">\n                        {% endif %}\n                    </td>\n                    {% endfor %}\n                    <td style=\"width: 40%;\"></td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Time when triggering starts at every selected day\n                        </span>\n                        <label for=\"opstart\">Operation Start:</label>\n                    </td>\n                    <td colspan=\"2\" style=\"width: 10%;\">\n                        <input style=\"width:100%\" type=\"time\" id=\"opstart\" name=\"opstart\" value=\"{{ tc.operationStartStr }}\">\n                    </td>\n                    <td colspan=\"6\" style=\"width: 65%;\"></td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Time when triggering ends at every selected day\n                        </span>\n                        <label for=\"opend\">Operation End:</label>\n                    </td>\n                    <td colspan=\"2\" style=\"width: 10%;\">\n                        <input style=\"width:100%\" type=\"time\" id=\"opend\" name=\"opend\" value=\"{{ tc.operationEndStr }}\">\n                    </td>\n                    <td colspan=\"6\" style=\"width: 65%;\"></td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Automatically start with server start.<br>\n                            Requires to store configuration and start server with stored configuration\n                        </span>\n                        <label for=\"opautostart\">Automatic Start with Server:</label>\n                    </td>\n                    <td style=\"width:5%;\">\n                        {% if tc.operationAutoStart == True %}\n                        <input type=\"checkbox\" id=\"opautostart\" name=\"opautostart\" value=\"1\" checked>\n                        {% else %}\n                        <input type=\"checkbox\" id=\"opautostart\" name=\"opautostart\" value=\"0\">\n                        {% endif %}\n                    </td>\n                    <td colspan=\"6\" style=\"width: 70%;\"></td>\n                </tr>\n                <tr>\n                    <td colspan=\"9\" style=\"width: 100%;\">&nbsp;</td>\n                </tr>\n                {% if sc.noCamera == False %}\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Delay in seconds for starting actions after trigger event\n                        </span>\n                        <label for=\"opdelay\">Detection Delay (Sec):</label>\n                    </td>\n                    <td colspan=\"2\" style=\"width: 10%;\">\n                        <input type=\"number\" id=\"opdelay\" name=\"opdelay\" min=\"0\" max=\"9999\" step=\"1\" value=\"{{ tc.detectionDelaySec }}\">\n                    </td>\n                    <td colspan=\"6\" style=\"width: 65%;\"></td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Pause in seconds after a trigger event in which actions are not started\n                        </span>\n                        <label for=\"oppause\">Detection Pause (Sec):</label>\n                    </td>\n                    <td colspan=\"2\" style=\"width: 10%;\">\n                        <input type=\"number\" id=\"oppause\" name=\"oppause\" min=\"0\" max=\"9999\" step=\"1\" value=\"{{ tc.detectionPauseSec }}\">\n                    </td>\n                    <td colspan=\"6\" style=\"width: 65%;\"></td>\n                </tr>\n                {% endif %}\n                <tr>\n                    <td class=\"w3-tooltip\" style=\"width:25%\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Number of days for which data are kept on the server.<br>\n                            Older data will be automatically deleted\n                        </span>\n                        <label for=\"retentionperiod\">Retention Period (days):</label>\n                    </td>\n                    <td colspan=\"2\" style=\"width: 10%;\">\n                        <input type=\"number\" id=\"retentionperiod\" name=\"retentionperiod\" min=\"0\" max=\"9999\" step=\"1\" value=\"{{ tc.retentionPeriod }}\">\n                    </td>\n                    <td colspan=\"6\" style=\"width: 65%;\"></td>\n                </tr>\n            </table>\n            <p style=\"margin-bottom: 0\"></p>\n            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n        </form>\n        <p style=\"margin-bottom: 0\"></p>\n        {% if sc.isTriggerRecording == False and sc.isEventhandling == False %}\n        <form method=\"post\" action=\"{{ url_for('trigger.start_triggered_capture') }}\">\n            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Start\">\n        </form>\n        {% else %}\n        {% if sc.isTriggerTesting == False %}\n        <form method=\"post\" action=\"{{ url_for('trigger.stop_triggered_capture') }}\">\n            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop\">\n        </form>\n        {% else %}\n        <form method=\"post\" action=\"{{ url_for('trigger.stop_triggered_capture') }}\">\n            <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop\" disabled>\n        </form>\n        {% endif %}\n        {% endif %}\n        {% if tc.error %}\n        <p>&nbsp;</p>\n        <hr>\n        <div class=\"w3-panel w3-pale-red w3-border\">\n            <h3>Triggered capture stopped with error:</h3>\n            <p>Error in {{ tc.errorSource }}: {{ tc.error }}</p>\n            {% if tc.error2 != None %}\n            <p>{{ tc.error2 }}</p>\n            {% endif %}\n        </div>\n        {% endif %}\n    </div>\n    {% if sc.lastTriggerTab == \"trgtriggers\" %}\n    <div id=\"trgtriggers\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgtriggers\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"wr-row\">\n            <h3>Triggers</h3>\n            <h4>New Trigger</h4>\n            {% set triggerSource = \"\" %}\n            {% set triggerDevice = \"\" %}\n            {% set triggerEvent = \"\" %}\n            {% set triggerEventSettings = \"\" %}\n            {% set triggerId = \"\" %}\n            {% if tmp|length() > 0 %}\n            {% if \"triggerSource\" in tmp %}\n            {% set triggerSource = tmp.triggerSource %}\n            {% endif %}\n            {% if \"triggerDevice\" in tmp %}\n            {% set triggerDevice = tmp.triggerDevice %}\n            {% endif %}\n            {% if \"triggerEvent\" in tmp %}\n            {% set triggerEvent = tmp.triggerEvent %}\n            {% endif %}\n            {% if \"triggerEventSettings\" in tmp %}\n            {% set triggerEventSettings = tmp.triggerEventSettings %}\n            {% endif %}\n            {% if \"triggerControl\" in tmp %}\n            {% set triggerControl = tmp.triggerControl %}\n            {% endif %}\n            {% if \"triggerId\" in tmp %}\n            {% set triggerId = tmp.triggerId %}\n            {% endif %}\n            {% endif %}\n            <form id=\"newtriggerform\" method=\"post\" action=\"{{ url_for('trigger.new_trigger') }}\">\n                <table>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the source system for the trigger: Camera, GPIO or Motion Detector.\n                            </span>\n                            <label for=\"triggersource\">Trigger Source:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <select style=\"width:100%; height:28px\" name=\"triggersource\" id=\"triggersource\"\n                                onchange=\"dosubmitTriggerSource()\">\n                                {% if triggerSource == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for source in tc.triggerSources() %}\n                                {% if source == triggerSource %}\n                                <option value=\"{{ source }}\" selected>{{ source }}</option>\n                                {% else %}\n                                <option value=\"{{ source }}\">{{ source }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the device for which to register a trigger with.\n                            </span>\n                            <label for=\"triggerdevice\">Trigger Device:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if triggerSource == \"\" %}\n                            <select style=\"width:100%; height:28px\" name=\"triggerdevice\" id=\"triggerdevice\" disabled></select>\n                            {% else %}\n                            <select style=\"width:100%; height:28px\" name=\"triggerdevice\" id=\"triggerdevice\"\n                                onchange=\"dosubmitTriggerDevice()\">\n                                {% if triggerDevice == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for device in tc.triggerDevices(triggerSource) %}\n                                {% if device == triggerDevice %}\n                                <option value=\"{{ device }}\" selected>{{ device }}</option>\n                                {% else %}\n                                <option value=\"{{ device }}\">{{ device }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            {% endif %}\n                            </select>\n                        </td>\n                        {% if triggerSource == \"GPIO\" and triggerDevice != \"\" %}\n                        {% set device = sc.getDevice(triggerDevice) %}\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"triggerdevicetype\" name=\"triggerdevicetype\" value=\"{{ device.type }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            {% set device = sc.getDeviceType(device.type) %}\n                            <a id=\"triggerdevicedocurl\" href=\"{{ device.docUrl }}\" target=\"_blank\">Link to gpiozero</a>\n                        </td>\n                        {% else %}\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    {% set events, eventSettings, control = tc.triggerEvents(triggerSource, triggerDevice) %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the triggering event for the selected device\n                            </span>\n                            <label for=\"triggerevent\">Trigger Event:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if triggerDevice == \"\" %}\n                            <select style=\"width:100%; height:28px\" name=\"triggerevent\" id=\"triggerevent\" disabled></select>\n                            {% else %}\n                            <select style=\"width:100%; height:28px\" name=\"triggerevent\" id=\"triggerevent\"\n                                onchange=\"dosubmitTriggerEvent()\">\n                                {% if triggerEvent == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for event in events %}\n                                {% if event == triggerEvent %}\n                                <option value=\"{{ event }}\" selected>{{ event }}</option>\n                                {% else %}\n                                <option value=\"{{ event }}\">{{ event }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <div id=\"triggereventsettingsarea\">\n                    {% if triggerEvent != \"\" and triggerEventSettings|length() > 0 %}\n                    {% for param, val in triggerEventSettings.items() %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The selected event requires specific parameters to be set.\n                            </span>\n                            <label for=\"trigger{{ param }}\">Event Parameter:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"trigger_{{ param }}\" name=\"trigger_{{ param }}\" value=\"{{ param }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"trigger_{{ param }}_value\" name=\"trigger_{{ param }}_value\"\n                                value=\"{{ val }}\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    {% endfor %}\n                    {% endif %}\n                    </div>\n                    <div id=\"triggercontrolarea\">\n                    {% if triggerEvent != \"\" and triggerControl|length() > 0 %}\n                    {% for ctrlparam, ctrlval in triggerControl.items() %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The selected event requires specific control parameters to be set.\n                            </span>\n                            <label for=\"triggerctrl{{ ctrlparam }}\">Control Parameter:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"triggerctrl{{ ctrlparam }}\" name=\"triggerctrl{{ ctrlparam }}\" value=\"{{ ctrlparam }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"triggerctrl{{ ctrlparam }}_value\" name=\"triggerctrl{{ ctrlparam }}_value\"\n                                value=\"{{ ctrlval }}\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    {% endfor %}\n                    {% endif %}\n                    </div>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Every trigger must be identified by a unique ID.\n                            </span>\n                            <label for=\"triggerid\">Unique Trigger ID:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if triggerEvent == \"\" %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"triggerid\" name=\"triggerid\" value=\"{{ triggerId }}\" disabled>\n                            {% else %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"triggerid\" name=\"triggerid\" value=\"{{ triggerId }}\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:30%\">\n                            {% if triggerEvent == \"\" %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" disabled>\n                            {% else %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                </table>\n            </form>\n        </div>\n        <div class=\"w3-row\">\n            <hr>\n        </div>\n        <div class=\"w3-row\">\n            <h4>Trigger Overview</h4>\n            <form method=\"post\" id=\"formtriggers\" action=\"{{ url_for('trigger.trigger_activation') }}\">\n                <div style=\"max-height:58.5vh;overflow-y:auto\">\n                    <table class=\"w3-table-all\">\n                        <thead>\n                            <tr>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    ID\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Active\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Source\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Device\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Event\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Parameters\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Control\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Delete\n                                </th>\n                            </tr>\n                        </thead>\n                        {% if tc.triggers|length() > 0 %}\n                        <tbody>\n                            {% for trigger in tc.triggers %}\n                            <tr>\n                                <td>\n                                    {{ trigger.id }}\n                                </td>\n                                <td>\n                                    {% if sc.isEventhandling %}\n                                    {% if trigger.isActive == True %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_isactive\" name=\"trigger{{ trigger.id }}_isactive\" value=\"1\" checked disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_isactive\" name=\"trigger{{ trigger.id }}_isactive\" value=\"0\" disabled>\n                                    {% endif %}\n                                    {% else %}\n                                    {% if trigger.isActive == True %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_isactive\" name=\"trigger{{ trigger.id }}_isactive\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_isactive\" name=\"trigger{{ trigger.id }}_isactive\" value=\"0\">\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {{ trigger.source }}\n                                </td>\n                                <td>\n                                    {{ trigger.device }}\n                                </td>\n                                <td>\n                                    {{ trigger.event }}\n                                </td>\n                                <td>\n                                    {{ trigger.params }}\n                                </td>\n                                <td>\n                                    {{ trigger.control }}\n                                </td>\n                                <td>\n                                    {% if sc.isEventhandling %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_delete\" name=\"trigger{{ trigger.id }}_delete\" value=\"0\"\n                                        onchange=\"countTriggerDelete('trigger{{ trigger.id }}_delete')\" disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"trigger{{ trigger.id }}_delete\" name=\"trigger{{ trigger.id }}_delete\" value=\"0\"\n                                        onchange=\"countTriggerDelete('trigger{{ trigger.id }}_delete')\">\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endfor %}\n                        </tbody>\n                        {% endif %}\n                    </table>\n                </div>\n                <p style=\"margin-bottom: 0\"></p>\n                {% if tc.triggers|length() > 0 %}\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" onclick=\"confirmDeleteTriggers('formtriggers')\">\n                {% else %}\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" disabled>\n                {% endif %}\n            </form>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgactions\" %}\n    <div id=\"trgactions\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgactions\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"wr-row\">\n            <h3>Actions</h3>\n            <h4>New Action</h4>\n            {% set actionSource = \"\" %}\n            {% set actionDevice = \"\" %}\n            {% set actionTarget = \"\" %}\n            {% set actionId = \"\" %}\n            {% if tmp|length() > 0 %}\n            {% if \"actionSource\" in tmp %}\n            {% set actionSource = tmp.actionSource %}\n            {% endif %}\n            {% if \"actionDevice\" in tmp %}\n            {% set actionDevice = tmp.actionDevice %}\n            {% endif %}\n            {% if \"actionTarget\" in tmp %}\n            {% set actionTarget = tmp.actionTarget %}\n            {% endif %}\n            {% if \"actionId\" in tmp %}\n            {% set actionId = tmp.actionId %}\n            {% endif %}\n            {% endif %}\n            <form id=\"newactionform\" method=\"post\" action=\"{{ url_for('trigger.new_action') }}\">\n                <table>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the source system for the action: Camera, GPIO or SMTP\n                            </span>\n                            <label for=\"actionsource\">Action Source:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <select style=\"width:100%; height:28px\" name=\"actionsource\" id=\"actionsource\"\n                                onchange=\"dosubmitActionSource()\">\n                                {% if actionSource == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for source in tc.actionSources() %}\n                                {% if source == actionSource %}\n                                <option value=\"{{ source }}\" selected>{{ source }}</option>\n                                {% else %}\n                                <option value=\"{{ source }}\">{{ source }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the device for which to register an action with.\n                            </span>\n                            <label for=\"actiondevice\">Action Device:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if actionSource == \"\" %}\n                            <select style=\"width:100%; height:28px\" name=\"actiondevice\" id=\"actiondevice\" disabled></select>\n                            {% else %}\n                            <select style=\"width:100%; height:28px\" name=\"actiondevice\" id=\"actiondevice\"\n                                onchange=\"dosubmitActionDevice()\">\n                                {% if actionDevice == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% endif %}\n                                {% for device in tc.actionDevices(actionSource) %}\n                                {% if device == actionDevice %}\n                                <option value=\"{{ device }}\" selected>{{ device }}</option>\n                                {% else %}\n                                <option value=\"{{ device }}\">{{ device }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            {% endif %}\n                            </select>\n                        </td>\n                        {% if actionSource == \"GPIO\" and actionDevice != \"\" %}\n                        {% set device = sc.getDevice(actionDevice) %}\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"actiondevicetype\" name=\"actiondevicetype\" value=\"{{ device.type }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            {% set device = sc.getDeviceType(device.type) %}\n                            <a id=\"devicedocurl\" href=\"{{ device.docUrl }}\" target=\"_blank\">Link to gpiozero</a>\n                        </td>\n                        {% else %}\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        {% endif %}\n                    </tr>\n                    {% set actionTargets = tc.actionTargets(actionSource, actionDevice) %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the action method for the selected device\n                            </span>\n                            <label for=\"actionmethod\">Action Method:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if actionDevice == \"\" %}\n                            <select style=\"width:100%; height:28px\" name=\"actionmethod\" id=\"actionmethod\" disabled></select>\n                            {% else %}\n                            <select style=\"width:100%; height:28px\" name=\"actionmethod\" id=\"actionmethod\"\n                                onchange=\"dosubmitActionMethod()\">\n                                {% if actionTarget == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% else %}\n                                {% if actionTarget.method == \"\" %}\n                                <option value=\"\" selected></option>\n                                {% else %}\n                                {% set method = actionTarget.method %}\n                                {% endif %}\n                                {% endif %}\n                                {% for target in actionTargets %}\n                                {% if target.method == method %}\n                                <option value=\"{{ target.method }}\" selected>{{ target.method }}</option>\n                                {% else %}\n                                <option value=\"{{ target.method }}\">{{ target.method }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            {% endif %}\n                            </select>\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    <div id=\"actiontargetsettingsarea\">\n                    {% if actionTarget != \"\" %}\n                    {% for target in actionTargets %}\n                    {% if target.method == actionTarget.method %}\n                    {% set method = actionTarget.method %}\n                    {% if \"params\" in actionTarget %}\n                    {% set params = actionTarget.params %}\n                    {% else %}\n                    {% set params = target.params %}\n                    {% endif %}\n                    {% if params|length() > 0 %}\n                    {% for param, value in params.items() %}\n                    {% if value is mapping %}\n                    {% if \"value\" in value %}\n                    {% set vval = value.value %}\n                    {% if vval is none %}\n                    {% set val = \"None\" %}\n                    {% else %}\n                    {% set val = vval %}\n                    {% endif %}\n                    {% else %}\n                    {% set val = \"\" %}\n                    {% endif %}\n                    {% else %}\n                    {% set val = value %}\n                    {% endif %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The selected action requires specific parameters to be set.\n                            </span>\n                            <label for=\"action_{{ method }}_param_{{ param }}_value\">Action Parameter:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"action_{{ method }}_param_{{ param }}\" name=\"action_{{ method }}_param_{{ param }}\" value=\"{{ param }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"action_{{ method }}_param_{{ param }}_value\" name=\"action_{{ method }}_param_{{ param }}_value\"\n                                value=\"{{ val }}\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    {% endfor %}\n                    {% endif %}\n                    {% if \"control\" in actionTarget %}\n                    {% set control = actionTarget.control %}\n                    {% else %}\n                    {% set control = target.control %}\n                    {% endif %}\n                    {% if control|length() > 0 %}\n                    {% for ctrl, value in control.items() %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The selected action requires specific control parameters \n                                which may, for example specify the duration of the target device status\n                                or the status shall be gradually reached.\n                            </span>\n                            <label for=\"action_{{ method }}_control_{{ ctrl }}_value\">Control Parameter:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"action_{{ method }}_control_{{ ctrl }}\"\n                                name=\"action_{{ method }}_control_{{ ctrl }}\" value=\"{{ ctrl }}\" disabled>\n                        </td>\n                        <td style=\"width:30%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"action_{{ method }}_control_{{ ctrl }}_value\"\n                                name=\"action_{{ method }}_control_{{ ctrl }}_value\" value=\"{{ value }}\">\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                    {% endfor %}\n                    {% endif %}\n                    {% endif %}\n                    {% endfor %}\n                    {% endif %}\n                    </div>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:20%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Every action should and must be identified by a unique ID.\n                            </span>\n                            <label for=\"actionid\">Unique Action ID:</label>\n                        </td>\n                        <td style=\"width:20%\">\n                            {% if actionTarget == \"\" %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"actionid\" name=\"actionid\" value=\"{{ actionId }}\" disabled>\n                            {% else %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"actionid\" name=\"actionid\" value=\"{{ actionId }}\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:30%\">\n                            {% if actionTarget == \"\" %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" disabled>\n                            {% else %}\n                            <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n                            {% endif %}\n                        </td>\n                        <td style=\"width:30%\">\n                        </td>\n                    </tr>\n                </table>\n            </form>\n        </div>\n        <div class=\"w3-row\">\n            <hr>\n        </div>\n        <div class=\"w3-row\">\n            <h4>Action Overview</h4>\n            <form id=\"formactions\" method=\"post\" action=\"{{ url_for('trigger.action_activation') }}\">\n                <div style=\"max-height:58.5vh;overflow-y:auto\">\n                    <table class=\"w3-table-all\">\n                        <thead>\n                            <tr>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    ID\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Active\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Source\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Device\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Action Method\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Parameters\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Control\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Del\n                                </th>\n                                <th style=\"position:sticky;top:0;z-index:10; background-color: #f1f1f1;\">\n                                    Test\n                                </th>\n                            </tr>\n                        </thead>\n                        {% if tc.actions|length() > 0 %}\n                        <tbody>\n                            {% for action in tc.actions %}\n                            <tr>\n                                <td>\n                                    {{ action.id }}\n                                </td>\n                                <td>\n                                    {% if sc.isEventhandling %}\n                                    {% if action.isActive == True %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_isactive\" name=\"action{{ action.id }}_isactive\" value=\"1\" checked disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_isactive\" name=\"action{{ action.id }}_isactive\" value=\"0\" disabled>\n                                    {% endif %}\n                                    {% else %}\n                                    {% if action.isActive == True %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_isactive\" name=\"action{{ action.id }}_isactive\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_isactive\" name=\"action{{ action.id }}_isactive\" value=\"0\">\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {{ action.source }}\n                                </td>\n                                <td>\n                                    {{ action.device }}\n                                </td>\n                                <td>\n                                    {{ action.method }}\n                                </td>\n                                <td>\n                                    {{ action.params }}\n                                </td>\n                                <td>\n                                    {{ action.control }}\n                                </td>\n                                <td>\n                                    {% if sc.isEventhandling %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_delete\" name=\"action{{ action.id }}_delete\" value=\"0\"\n                                        onchange=\"countActionDelete('action{{ action.id }}_delete')\" disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_delete\" name=\"action{{ action.id }}_delete\" value=\"0\"\n                                        onchange=\"countActionDelete('action{{ action.id }}_delete')\">\n                                    {% endif %}\n                                </td>\n                                <td>\n                                    {% if sc.isEventhandling %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_test\" name=\"action{{ action.id }}_test\" value=\"0\"\n                                        onchange=\"countActionTest('action{{ action.id }}_test')\" disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"action{{ action.id }}_test\" name=\"action{{ action.id }}_test\" value=\"0\"\n                                        onchange=\"countActionTest('action{{ action.id }}_test')\">\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            {% endfor %}\n                        </tbody>\n                        {% endif %}\n                    </table>\n                </div>\n                <p style=\"margin-bottom: 0\"></p>\n                {% if tc.actions|length() > 0 %}\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" onclick=\"confirmDeleteActions('formactions')\">\n                {% else %}\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\" disabled>\n                {% endif %}\n            </form>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgtriggeractions\" %}\n    <div id=\"trgtriggeractions\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgtriggeractions\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Trigger Actions</h3>\n        <div>\n            <form method=\"post\" action=\"{{ url_for('trigger.trigger_action') }}\">\n                <div style=\"max-height:78.5vh;max-width:95vw;overflow-x:auto;overflow-y:auto\">\n                    <table class=\"w3-table-all\" style=\" width: max-content;\">\n                        <thead>\n                            <tr>\n                                <th class=\"width:10%\" style=\"background-color: #f1f1f1;position:sticky;top:0;left:0;z-index:12\">\n                                    Trigger\n                                </th>\n                                {% for action in tc.actions %}\n                                <th class=\"width:5%; w3-center\" style=\"background-color: #f1f1f1;position:sticky;top:0;z-index:10\">\n                                    {{ action.id }}\n                                </th>\n                                {% endfor %}\n                                <th class=\"width:90%\"></th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            {% for trigger in tc.triggers %}\n                            {% set triggerId = trigger.id %}\n                            {% set triggerActions = trigger.actions %}\n                            <tr>\n                                <th class=\"width:10%\" style=\"position:sticky; left:0; background-color: #f1f1f1;z-index:11;\">\n                                    {{ trigger.id }}\n                                </th>\n                                {% for action in tc.actions %}\n                                {% set actionId = action.id %}\n                                <td class=\"width:5%; w3-center\">\n                                    {% if sc.isEventhandling %}\n                                    {% if triggerActions[actionId] == True %}\n                                    <input type=\"checkbox\" id=\"triggeraction_{{ triggerId }}_{{ actionId }}\" name=\"triggeraction_{{ triggerId }}_{{ actionId }}\" value=\"1\" checked disabled>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"triggeraction_{{ triggerId }}_{{ actionId }}\" name=\"triggeraction_{{ triggerId }}_{{ actionId }}\" value=\"0\" disabled>\n                                    {% endif %}\n                                    {% else %}\n                                    {% if triggerActions[actionId] == True %}\n                                    <input type=\"checkbox\" id=\"triggeraction_{{ triggerId }}_{{ actionId }}\" name=\"triggeraction_{{ triggerId }}_{{ actionId }}\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"triggeraction_{{ triggerId }}_{{ actionId }}\" name=\"triggeraction_{{ triggerId }}_{{ actionId }}\" value=\"0\">\n                                    {% endif %}\n                                    {% endif %}\n                                </td>\n                                {% endfor %}\n                                <td class=\"width:90%\"></td>\n                            </tr>\n                            {% endfor %}\n                        </tbody>\n                    </table>\n                </div>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgmotion\" %}\n    <div id=\"trgmotion\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgmotion\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"w3-half\">\n            <h3>Motion Detection Configuration</h3>\n            <form method=\"post\" action=\"{{ url_for('trigger.motion') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Choose the algorithm for motion detection\n                            </span>\n                            <label for=\"motiondetectionalgo\">Motion Detection Algorithm:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <select name=\"motiondetectionalgo\" id=\"motiondetectionalgo\" onchange=\"motionDetectionAlgoChanged()\">\n                                {% for algo in tc.motionDetectAlgos %}\n                                {% if tc.motionDetectAlgo == loop.index %}\n                                <option value=\"{{ loop.index }}\" selected>{{ algo }}</option>\n                                {% else %}\n                                <option value=\"{{ loop.index }}\">{{ algo }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Threshold for mean square deviation between successive frames\n                                above which motion is detected.\n                            </span>\n                            <label for=\"msdthreshold\">Mean Square Threshold:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.motionDetectAlgo == 1 %}\n                            <input type=\"number\" id=\"msdthreshold\" name=\"msdthreshold\" min=\"0\" max=\"100\" step=\"1\" value=\"{{ tc.msdThreshold }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"msdthreshold\" name=\"msdthreshold\" min=\"0\" max=\"100\" step=\"1\" value=\"{{ tc.msdThreshold }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Minimum area for declaring a bounding box.\n                            </span>\n                            <label for=\"bboxthreshold\">Bounding Box Threshold:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.motionDetectAlgo == 2 or tc.motionDetectAlgo == 3 or tc.motionDetectAlgo == 4 %}\n                            <input type=\"number\" id=\"bboxthreshold\" name=\"bboxthreshold\" min=\"0\" max=\"10000\" step=\"1\" value=\"{{ tc.bboxThreshold }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"bboxthreshold\" name=\"bboxthreshold\" min=\"0\" max=\"10000\" step=\"1\" value=\"{{ tc.bboxThreshold }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                IOU (Intersection over Union) threshold for computing Non-Maximal Supression\n                            </span>\n                            <label for=\"nmsthreshold\">IOU Threshold:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.motionDetectAlgo == 2 or tc.motionDetectAlgo == 3 or tc.motionDetectAlgo == 4 %}\n                            <input type=\"number\" id=\"nmsthreshold\" name=\"nmsthreshold\" min=\"0.000001\" max=\"1\" step=\"0.000001\" value=\"{{ tc.nmsThreshold }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"nmsthreshold\" name=\"nmsthreshold\" min=\"0.000001\" max=\"1\" step=\"0.000001\" value=\"{{ tc.nmsThreshold }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Minimum threshold for Optical Flow algorithm.\n                            </span>\n                            <label for=\"motionthreshold\">Motion Threshold:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.motionDetectAlgo == 3 %}\n                            <input type=\"number\" id=\"motionthreshold\" name=\"motionthreshold\" min=\"0\" max=\"100\" step=\"1\" value=\"{{ tc.motionThreshold }}\">\n                            {% else %}\n                            <input type=\"number\" id=\"motionthreshold\" name=\"motionthreshold\" min=\"0\" max=\"100\" step=\"1\" value=\"{{ tc.motionThreshold }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Background Subtraction Model:<br>\n                                MOG2: Mixture of Gaussians 2<br>\n                                KNN: K Nearest Neighbors\n                            </span>\n                            <label for=\"backsubmodel\">Background Subtraction Model:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.motionDetectAlgo == 4 %}\n                            <select name=\"backsubmodel\" id=\"backsubmodel\">\n                            {% else %}\n                            <select name=\"backsubmodel\" id=\"backsubmodel\" disabled>\n                            {% endif %}\n                                {% for bsm in tc.backgroundSubtractionModels %}\n                                {% if tc.backSubModel == loop.index %}\n                                <option value=\"{{ loop.index }}\" selected>{{ bsm }}</option>\n                                {% else %}\n                                <option value=\"{{ loop.index }}\">{{ bsm }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                {% if sc.cv2Available == True %}\n                                Restrict motion detection to specific regions of interest.\n                                {% else %}\n                                OpenCV is not available. Regions of Interest cannot be used.\n                                {% endif %}\n                            </span>\n                            <label for=\"useroi\">Use Regions of Interest:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if sc.cv2Available == True %}\n                            {% if tc.useRoI == True %}\n                            <input type=\"checkbox\" id=\"useroi\" name=\"useroi\" \n                                onclick=\"drawRoiWindow('useroi', '{{ sc.scalerCropLiveView }}')\"\n                                value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useroi\" name=\"useroi\" \n                                onclick=\"drawRoiWindow('useroi', '{{ sc.scalerCropLiveView }}')\"\n                                value=\"1\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"useroi\" name=\"useroi\" \n                                onclick=\"drawRoiWindow('useroi', '{{ sc.scalerCropLiveView }}')\"\n                                value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <label for=\"regionofinterest\">Regions of Interest:</label>\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Regions of interest for motion detection<br>\n                                A list of rectangles (tuples of 4 numbers\n                                denoting x_offset, y_offset, width and\n                                height). The rectangle units refer to the\n                                maximum scaler crop window.\n                            </span>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.useRoI == True %}\n                            <input type=\"text\" id=\"regionofinterest\" name=\"regionofinterest\" value=\"{{ tc.regionOfInterestStr }}\" disabled>\n                            {% else %}\n                            <input type=\"text\" id=\"regionofinterest\" name=\"regionofinterest\" value=\"{{ tc.regionOfInterestStr }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <label for=\"regionofnointerest\">Regions of No Interest:</label>\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Regions of no interest for motion detection<br>\n                                Motion within these regions will not trigger an event.                         </span>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.useRoI == True %}\n                            <input type=\"text\" id=\"regionofnointerest\" name=\"regionofnointerest\" value=\"{{ tc.regionOfNoInterestStr }}\" disabled>\n                            {% else %}\n                            <input type=\"text\" id=\"regionofnointerest\" name=\"regionofnointerest\" value=\"{{ tc.regionOfNoInterestStr }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Show bounding boxes of motion detection on recorded video.\n                            </span>\n                            <label for=\"videobboxes\">Video with Bounding Boxes:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.videoBboxes == True %}\n                            <input type=\"checkbox\" id=\"videobboxes\" name=\"videobboxes\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"videobboxes\" name=\"videobboxes\" value=\"1\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                {% if sc.cv2Available == True %}\n                                Show Regions of Interest on recorded photos and videos.<br>\n                                (For videos not available for Mean Square Difference algorithm)\n                                {% else %}\n                                Regions of Interest can not be shown on photos because OpenCV is not available.\n                                {% endif %}\n                            </span>\n                            <label for=\"photorois\">Photos/Videos with RoI/RoNI:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if sc.cv2Available == True %}\n                            {% if tc.photoRois == True %}\n                            <input type=\"checkbox\" id=\"photorois\" name=\"photorois\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"photorois\" name=\"photorois\" value=\"0\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"photorois\" name=\"photorois\" value=\"0\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" onclick=\"enableRegionOfInterest()\" type=\"submit\" value=\"Submit\">\n            </form>\n            <hr>\n            <!-- Live stream -->\n            {% if tc.useRoI == True %}\n            {% if sc.isLiveStream %}\n            <div id=\"drawroictrl\">\n                <table>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:40%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select to draw regions of interest for motion detection.\n                            </span>\n                            <label for=\"useroi\">Draw Regions of Interest:</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            <input type=\"checkbox\" id=\"drawroi\" name=\"drawroi\" \n                                onclick=\"switchRoiType('drawroi', 'drawroni')\"\n                                value=\"1\" checked>\n                        </td>\n                        <td class=\"w3-tooltip\" style=\"width:40%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Select to draw regions of NO interest for motion detection.\n                            </span>\n                            <label for=\"useroi\">Draw Regions of NO Interest:</label>\n                        </td>\n                        <td style=\"width:10%\">\n                            <input type=\"checkbox\" id=\"drawroni\" name=\"drawroni\"\n                                onclick=\"switchRoiType('drawroni', 'drawroi')\"\n                                value=\"0\">\n                        </td>\n                    </tr>\n                </table>\n                <div id=\"roi-container\" style=\"position: relative;\">\n                    <img src=\"{{ url_for('trigger.trg_live_view_feed') }}\" class=\"w3-image\" id=\"liveviewforroi\" alt=\"Camera Live View\">\n                    <canvas id=\"canvasforroi\"></canvas>\n                </div>\n            </div>\n            {% endif %}\n            {% endif %}\n        </div>\n        <div class=\"w3-half\" style=\"height:88vh; overflow: auto\">\n            <div class=\"w3-center\">\n                <div class=\"w3-bar\" style=\"margin-top:10px; margin-bottom:10px\">\n                    {% if tc.motionDetectAlgo > 1 %}\n                    {% if sc.isTriggerTesting == False %}\n                    {% if sc.isTriggerRecording == False %}\n                    <form method=\"post\" action=\"{{ url_for('trigger.test_motion_detection') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Test Motion Detection\">\n                    </form>\n                    {% else %}\n                    <form method=\"post\" action=\"{{ url_for('trigger.test_motion_detection') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Test Motion Detection\" disabled>\n                    </form>\n                    {% endif %}\n                    {% else %}\n                    <form method=\"post\" action=\"{{ url_for('trigger.stop_test_motion_detection') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop Test Motion Detection\">\n                    </form>\n                    {% endif %}\n                    {% else %}\n                    <form method=\"post\" action=\"{{ url_for('trigger.test_motion_detection') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Test Motion Detection\" disabled>\n                    </form>\n                    {% endif %}\n                </div>\n            </div>\n            {% if sc.isTriggerTesting == True %}\n            <p style=\"margin-top:0px; margin-bottom:5px\"> Reference: \n            <a href=\"{{ tc.motionRefURL }}\" target=\"_blank\">{{ tc.motionRefTit }}</a>\n            </p>\n            <table>\n                <tr>\n                    <td>\n                        <img  style=\"width: 100%; height: 205px; object-fit: scale-down\"\n                            src=\"{{ url_for('trigger.test_frame1_feed') }}\" \n                            alt=\"Test stream 1\">\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ tc.motionTestFrame1Title }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <img  style=\"width: 100%; height: 205px; object-fit: scale-down\"\n                            src=\"{{ url_for('trigger.test_frame2_feed') }}\" \n                            alt=\"Test stream 2\">\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ tc.motionTestFrame2Title }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <img  style=\"width: 100%; height: 205px; object-fit: scale-down\"\n                            src=\"{{ url_for('trigger.test_frame3_feed') }}\" \n                            alt=\"Test stream 3\">\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ tc.motionTestFrame3Title }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <img  style=\"width: 100%; height: 205px; object-fit: scale-down\"\n                            src=\"{{ url_for('trigger.test_frame4_feed') }}\" \n                            alt=\"Test stream 4\">\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <p style=\"margin-top:0px; margin-bottom:5px; text-align:center\">{{ tc.motionTestFrame4Title }}</p>\n                    </td>\n                </tr>\n                <tr>\n                    <td>\n                        <p style=\"text-align:center; display:inline-block\">Current Framerate: {{ tc.motionTestFramerate|round(1) }} fps</p>\n                        <button class=\"w3-button w3-circle w3-green\" \n                                style=\"display:inline-block\" \n                                onclick=\"location.reload()\">\n                                <span style=\"font-family: 'Wingdings 3'\">&#x0050;</span>\n                        </button>\n                    </td>\n                </tr>\n            </table>\n            {% endif %}\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgaction\" %}\n    <div id=\"trgaction\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgaction\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Camera Actions</h3>\n        <div class=\"w3-half\">\n            <form method=\"post\" action=\"{{ url_for('trigger.action') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Normal recording starts when triggered.<br>\n                                Circular recording continuously buffers in a circular buffer\n                                and starts recording some seconds before being triggered.\n                            </span>\n                            <label for=\"actionvr\">Video Recording Type:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <select name=\"actionvr\" id=\"actionvr\">\n                                {% for vr in tc.videoRecorders %}\n                                {% if tc.actionVR == loop.index %}\n                                <option value=\"{{ loop.index }}\" selected>{{ vr }}</option>\n                                {% else %}\n                                <option value=\"{{ loop.index }}\">{{ vr }}</option>\n                                {% endif %}\n                                {% endfor %}\n                            </select>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Size of circular buffer (pre-recording, buffered video to have before a event occurs) in seconds.<br>\n                                The recorded video starts the given number of seconds before the event. Recommended max of 10 seconds (Longer = More used memory).\n                            </span>\n                            <label for=\"actioncircsize\">Pre-Record Length (sec):</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"actioncircsize\" name=\"actioncircsize\" min=\"0\" max=\"10\" step=\"1\"\n                                value=\"{{ tc.actionCircSize }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The intended duration of videos recorded on an event.\n                            </span>\n                            <label for=\"actionvideoduration\">Video Duration (sec):</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"actionvideoduration\" name=\"actionvideoduration\" min=\"0\" max=\"9999\" step=\"1\"\n                                value=\"{{ tc.actionVideoDuration }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The number of photos to be taken in sequence for each event.\n                            </span>\n                            <label for=\"actionphotoburst\">Photo Burst - Number of Photos:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"actionphotoburst\" name=\"actionphotoburst\" min=\"1\" max=\"10\" step=\"1\"\n                                value=\"{{ tc.actionPhotoBurst }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The time in seconds between successive photos of a photo burst.\n                            </span>\n                            <label for=\"actionphotoburstdelaysec\">Photo Burst Interval (sec):</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"actionphotoburstdelaysec\" name=\"actionphotoburstdelaysec\" min=\"0\" max=\"10\" step=\"1\"\n                                value=\"{{ tc.actionPhotoBurstDelaySec }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Folder for storage of event data, photos and videos.\n                            </span>\n                            <label for=\"actionpath\">Action Data Path:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input style=\"width: 100%; text-align:right\" type=\"text\" id=\"actionpath\" name=\"actionpath\"\n                                value=\"{{ tc.actionPath }}\" disabled>\n                        </td>\n                    </tr>\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgnotify\" %}\n    <div id=\"trgnotify\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgnotify\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <h3>Notification Settings</h3>\n        <div class=\"w3-half\">\n            {% if tc.notifyConOK == False %}\n            <h3 class=\"w3-red\">Please enter credentials to check notification recipient</h3>\n            {% else %}\n            <h3 class=\"w3-green\">Mail account for notification is already verified</h3>\n            {% endif %}\n            <form method=\"post\" action=\"{{ url_for('trigger.notify') }}\">\n                <table class=\"w3-table-all\">\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                SMTP Server\n                            </span>\n                            <label for=\"notifyhost\">SMTP Server:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"text\" id=\"notifyhost\" name=\"notifyhost\" value=\"{{ tc.notifyHost }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Port\n                            </span>\n                            <label for=\"notifyport\">Port:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"notifyport\" name=\"notifyport\" min=\"1\" max=\"65535\" step=\"1\"\n                                value=\"{{ tc.notifyPort }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Use SSL.\n                            </span>\n                            <label for=\"notifyusessl\">Use SSL:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyUseSSL == True %}\n                            <input type=\"checkbox\" id=\"notifyusessl\" name=\"notifyusessl\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifyusessl\" name=\"notifyusessl\" value=\"1\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 50%;\">&nbsp;</td>\n                        <td style=\"width: 50%;\">&nbsp;</td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Check if server requires authentication.\n                            </span>\n                            <label for=\"notifyauthenticate\">Server requires Authentication:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyAuthenticate == True %}\n                            <input type=\"checkbox\" id=\"notifyauthenticate\" name=\"notifyauthenticate\" \n                                onclick=\"mailServerAuthenicationChanged()\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifyauthenticate\" name=\"notifyauthenticate\" \n                                onclick=\"mailServerAuthenicationChanged()\" value=\"1\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                User<br>\n                                Leave this empty if a E-Mail account is already verified.\n                            </span>\n                            <label for=\"notifyuser\">User:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyAuthenticate == True %}\n                            <input type=\"text\" id=\"notifyuser\" name=\"notifyuser\" value=\"\">\n                            {% else %}\n                            <input type=\"text\" id=\"notifyuser\" name=\"notifyuser\" value=\"\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Password<br>\n                                Leave this empty if a E-Mail account is already verified.\n                            </span>\n                            <label for=\"notifypassword\">Password:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyAuthenticate == True %}\n                            <input type=\"password\" id=\"notifypassword\" name=\"notifypassword\" value=\"\">\n                            {% else %}\n                            <input type=\"password\" id=\"notifypassword\" name=\"notifypassword\" value=\"\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate checkbox if you want the password to be stored in a file.\n                            </span>\n                            <label for=\"notifysavepwd\">Store Credentials in File:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyAuthenticate == True %}\n                            {% if tc.notifySavePwd == True %}\n                            <input type=\"checkbox\" id=\"notifysavepwd\" name=\"notifysavepwd\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifysavepwd\" name=\"notifysavepwd\" value=\"1\">\n                            {% endif %}\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifysavepwd\" name=\"notifysavepwd\" value=\"1\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Full path to credentials file.<br>\n                                Make sure that the file is securely protected.\n                            </span>\n                            <label for=\"notifypwdpath\">Credentials File Path:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyAuthenticate == True %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"notifypwdpath\" name=\"notifypwdpath\" value=\"{{ tc.notifyPwdPath }}\">\n                            {% else %}\n                            <input style=\"width: 100%\" type=\"text\" id=\"notifypwdpath\" name=\"notifypwdpath\" value=\"{{ tc.notifyPwdPath }}\" disabled>\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 50%;\">&nbsp;</td>\n                        <td style=\"width: 50%;\">&nbsp;</td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Sender Address\n                            </span>\n                            <label for=\"notifyfrom\">From:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input style=\"width: 100%\" type=\"email\" id=\"notifyfrom\" name=\"notifyfrom\" value=\"{{ tc.notifyFrom }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Receipient Address\n                            </span>\n                            <label for=\"notifyto\">To:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input style=\"width: 100%\" type=\"email\" id=\"notifyto\" name=\"notifyto\" value=\"{{ tc.notifyTo }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Message Subject\n                            </span>\n                            <label for=\"notifysubject\">Subject:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input style=\"width: 100%\" type=\"text\" id=\"notifysubject\" name=\"notifysubject\" value=\"{{ tc.notifySubject }}\">\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                The amount of time to wait before another notification can send.<br>\n                                Should be a multiple of the detection pause to be effective.\n                            </span>\n                            <label for=\"notifypause\">Notification Cooldown:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            <input type=\"number\" id=\"notifypause\" name=\"notifypause\" min=\"0\" max=\"999999\" step=\"1\" value=\"{{ tc.notifyPause }}\">\n                        </td>\n                    </tr>\n                    {% if sc.noCamera == False %}\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Activate to include the recorded video in the message.\n                            </span>\n                            <label for=\"notifyincludevideo\">Include Video:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyIncludeVideo == True %}\n                            <input type=\"checkbox\" id=\"notifyincludevideo\" name=\"notifyincludevideo\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifyincludevideo\" name=\"notifyincludevideo\" value=\"1\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    <tr>\n                        <td class=\"w3-tooltip\" style=\"width:50%\">\n                            <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                Enable this to send one, or multiple images in the message.\n                            </span>\n                            <label for=\"notifyincludephoto\">Include Photos:</label>\n                        </td>\n                        <td style=\"width:50%\">\n                            {% if tc.notifyIncludePhoto == True %}\n                            <input type=\"checkbox\" id=\"notifyincludephoto\" name=\"notifyincludephoto\" value=\"1\" checked>\n                            {% else %}\n                            <input type=\"checkbox\" id=\"notifyincludephoto\" name=\"notifyincludephoto\" value=\"1\">\n                            {% endif %}\n                        </td>\n                    </tr>\n                    {% endif %}\n                </table>\n                <p style=\"margin-bottom: 0\"></p>\n                <input class=\"w3-button w3-black\" type=\"submit\" value=\"Submit\">\n            </form>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgevents\" %}\n    <div id=\"trgevents\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgevents\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"w3-container\" style=\"padding-left: 0;\">\n            <div class=\"w3-twothird\">\n                <p style=\"margin-top: 0px; margin-bottom:0px\">&nbsp</p>\n                <div class=\"w3-bar\">\n                    <div style=\"display:inline-block\">\n                        <a href=\"{{ url_for('trigger.prev_month') }}\" class=\"w3-button w3-sand\">&laquo;</a>\n                        <a href=\"{{ url_for('trigger.prev_day') }}\" class=\"w3-button w3-sand\">&lt</a>\n                        <form style=\"display:inline-block\" id=\"setevstartdate\" method=\"post\" action=\"{{ url_for('trigger.set_date') }}\">\n                            <input style=\"width:100%\" type=\"date\" onchange=\"dosubmit('setevstartdate')\" id=\"evstartdate\" name=\"evstartdate\" value=\"{{ tc.evStartDateStr }}\">\n                        </form>\n                        <a href=\"{{ url_for('trigger.next_day') }}\" class=\"w3-button w3-sand\">&gt</a>\n                        <a href=\"{{ url_for('trigger.next_month') }}\" class=\"w3-button w3-sand\">&raquo;</a>\n                    </div>\n                    <div style=\"display:inline-block\">\n                        <p style=\"display:inline-block\">&nbsp&nbsp&nbsp</p>\n                        <a href=\"{{ url_for('trigger.prev_hor') }}\" class=\"w3-button w3-sand\">&laquo;</a>\n                        <a href=\"{{ url_for('trigger.prev_quarter') }}\" class=\"w3-button w3-sand\">&lt</a>\n                        <form style=\"display:inline-block\" id=\"setevstarttime\" method=\"post\" action=\"{{ url_for('trigger.set_time') }}\">\n                            <input style=\"width:100%\" type=\"time\" onchange=\"dosubmit('setevstarttime')\" id=\"evstarttime\" name=\"evstarttime\" value=\"{{ tc.evStartTimeStr }}\">\n                        </form>\n                        <a href=\"{{ url_for('trigger.next_quarter') }}\" class=\"w3-button w3-sand\">&gt</a>\n                        <a href=\"{{ url_for('trigger.next_hour') }}\" class=\"w3-button w3-sand\">&raquo;</a>\n                    </div>\n                    <div style=\"display:inline-block; border-left: 20px\">\n                        <form style=\"display:inline-block\" id=\"setevstartnow\" method=\"post\" action=\"{{ url_for('trigger.events_now') }}\">\n                            <input class=\"w3-button w3-sand\" type=\"submit\" value=\"Now\">\n                        </form>\n                    </div>\n                </div>\n            </div>\n            <div class=\"w3-third\">\n                <table style=\"width:100%;\">\n                    <tr>\n                        <td style=\"height: 15px;\">\n\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 5%;\">\n                            <form style=\"display:inline-block\" id=\"evincludevideofrm\" method=\"post\" action=\"{{ url_for('trigger.event_include_video') }}\">\n                                {% if tc.evIncludeVideo == True %}\n                                <input type=\"checkbox\" id=\"evincludevideo\" name=\"evincludevideo\" onchange=\"dosubmit('evincludevideofrm')\" value=\"1\" checked>\n                                {% else %}\n                                <input type=\"checkbox\" id=\"evincludevideo\" name=\"evincludevideo\" onchange=\"dosubmit('evincludevideofrm')\" value=\"0\">\n                                {% endif %}\n                            </form>\n                        </td>\n                        <td style=\"width: 35%;\">\n                            <label for=\"evincludevideo\">Include Videos</label>\n                        </td>\n                        <td rowspan=\"2\" style=\"width: 60%;\">\n                            <div class=\"w3-right\">\n                                <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('trigger.do_refresh') }}\">\n                                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\" value=\"Refresh\">\n                                </form>\n                            </div>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 5%;\">\n                            <form style=\"display:inline-block\" id=\"evincludephotofrm\" method=\"post\" action=\"{{ url_for('trigger.event_include_photo') }}\">\n                                {% if tc.evIncludePhoto == True %}\n                                <input type=\"checkbox\" id=\"evincludephoto\" name=\"evincludephoto\" onchange=\"dosubmit('evincludephotofrm')\" value=\"1\" checked>\n                                {% else %}\n                                <input type=\"checkbox\" id=\"evincludephoto\" name=\"evincludephoto\" onchange=\"dosubmit('evincludephotofrm')\" value=\"0\">\n                                {% endif %}\n                            </form>\n                        </td>\n                        <td style=\"width: 35%;\">\n                            <label for=\"evincludephoto\">Include Photos</label>\n                        </td>\n                    </tr>\n                </table>\n            </div>\n        </div>\n        <hr>\n        <div class=\"w3-container\" style=\"padding-left: 0;\">\n            <div class=\"w3-third\">\n                <div style=\"height:1000px; overflow: auto\">\n                    <table class=\"w3-table w3-bordered\">\n                        {% for ev in tc.eventList %}\n                        {% set event = ev.event %}\n                        <tr>\n                            <td style=\"width:50%;\">\n                                <div class=\"w3-card-4\" style=\"width:100%;\">\n                                    <header class=\"w3-container w3-red\">\n                                        <h5 style=\"margin-top: 0;margin-bottom:0\">{{ event.type }}</h5>\n                                    </header>\n                                    <div class=\"w3-container\">\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ event.date }}</p>\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ event.time }}</p>\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ event.trigger }}</p>\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ event.triggertype }}</p>\n                                        {% set triggerparams = event.triggerparam %}\n                                        {% for key, val in triggerparams.items() %}\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ key }}:{{ val }}</p>\n                                        {% endfor %}\n                                    </div>\n                                </div>\n                            </td>\n                            {% if tc.evIncludeVideo %}\n                            {% set video = ev.video %}\n                            {% if (video|length > 0) and (video.photo != None) %}\n                            <td style=\"width:50%;\">\n                                <div class=\"w3-card-4\" style=\"width:100%\">\n                                    {% set urlMini=url_for('static', filename='events/' + video.photo) %}\n                                    {% set urlDetail=url_for('static', filename='events/' + video.filename) %}\n                                    <img src=\"{{ urlMini }}\" \n                                         alt=\"{{ video.filename }}\" \n                                         style=\"width:100%\"\n                                         onclick=\"showDetail('video', '{{ urlDetail }}', 'detailphoto', '{{ video.filename }}', '{{ video.filename }}')\"\n                                    >\n                                    <div class=\"w3-container w3-center\">\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ video.time }}&nbsp;&nbsp;{{ video.duration }} sec</p>\n                                    </div>\n                                </div>\n                            </td>\n                            {% else %}\n                            {% if tc.evIncludePhoto %}\n                            {% set photos = ev.photos %}\n                            {% if photos|length > 0 %}\n                            {% set photo = photos[0] %}\n                            <td style=\"width:50%;\">\n                                <div class=\"w3-card-4\" style=\"width:100%\">\n                                    {% set urlMini=url_for('static', filename='events/' + photo.filename) %}\n                                    {% set urlDetail=url_for('static', filename='events/' + photo.filename) %}\n                                    <img src=\"{{ urlMini }}\" \n                                         alt=\"{{ photo.filename }}\" \n                                         style=\"width:100%\"\n                                         onclick=\"showDetail('photo', '{{ urlDetail }}', 'detailphoto', '{{ photo.filename }}', '{{ photo.filename }}')\"\n                                    >\n                                    <div class=\"w3-container w3-center\">\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ photo.time }}</p>\n                                    </div>\n                                </div>\n                            </td>\n                            {% endif %}\n                            {% endif %}\n                            {% endif %}\n                            {% else %}\n                            {% if tc.evIncludePhoto %}\n                            {% set photos = ev.photos %}\n                            {% if photos|length > 0 %}\n                            {% set photo = photos[0] %}\n                            <td style=\"width:50%;\">\n                                <div class=\"w3-card-4\" style=\"width:100%\">\n                                    {% set urlMini=url_for('static', filename='events/' + photo.filename) %}\n                                    {% set urlDetail=url_for('static', filename='events/' + photo.filename) %}\n                                    <img src=\"{{ urlMini }}\" \n                                         alt=\"{{ photo.filename }}\" \n                                         style=\"width:100%\"\n                                         onclick=\"showDetail('photo', '{{ urlDetail }}', 'detailphoto', '{{ photo.filename }}', '{{ photo.filename }}')\"\n                                    >\n                                    <div class=\"w3-container w3-center\">\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ photo.time }}</p>\n                                    </div>\n                                </div>\n                            </td>\n                            {% endif %}\n                            {% endif %}\n                            {% endif %}\n                        </tr>\n                        {% if tc.evIncludePhoto %}\n                        {% set photos = ev.photos %}\n                        {% if photos|length > 0 %}\n                        {% for photo in photos%}\n                        {% if loop.index > 1 %}\n                        <tr>\n                            <td>\n                            </td>\n                            <td style=\"width:50%;\">\n                                <div class=\"w3-card-4\" style=\"width:100%\">\n                                    {% set urlMini=url_for('static', filename='events/' + photo.filename) %}\n                                    {% set urlDetail=url_for('static', filename='events/' + photo.filename) %}\n                                    <img src=\"{{ urlMini }}\" alt=\"{{ photo.filename }}\" style=\"width:100%\"\n                                        onclick=\"showDetail('photo', '{{ urlDetail }}', 'detailphoto', '{{ photo.filename }}', '{{ photo.filename }}')\">\n                                    <div class=\"w3-container w3-center\">\n                                        <p style=\"margin-top: 0;margin-bottom:0\">{{ photo.time }}</p>\n                                    </div>\n                                </div>\n                            </td>\n                        </tr>\n                        {% endif %}\n                        {% endfor %}\n                        {% endif %}\n                        {% endif %}\n                        {% endfor %}\n                    </table>\n                </div>\n            </div>\n            <div class=\"w3-twothird\">\n                <!-- Detail-->\n                <div id=\"detailphoto\" class=\"w3-container w3-center\">\n                </div>\n            </div>\n        </div>\n    </div>\n    {% if sc.lastTriggerTab == \"trgcalendar\" %}\n    <div id=\"trgcalendar\" class=\"triggergroup\">\n    {% else %}\n    <div id=\"trgcalendar\" class=\"triggergroup\" style=\"display:none\">\n    {% endif %}\n        <div class=\"w3-container\" style=\"padding-left: 0;\">\n            <div class=\"w3-twothird\">\n                <p style=\"margin-top: 0px; margin-bottom:0px\">&nbsp</p>\n                <div class=\"w3-bar\">\n                    <div style=\"display:inline-block\">\n                        <a href=\"{{ url_for('trigger.prev_cal_month') }}\" class=\"w3-button w3-sand\">&lt</a>\n                        <form style=\"display:inline-block\" id=\"setcalmonthfrm\" method=\"post\" action=\"{{ url_for('trigger.set_cal_month') }}\">\n                            <input style=\"width:100%\" type=\"date\" onchange=\"dosubmit('setcalmonthfrm')\" id=\"setcalmonth\" name=\"setcalmonth\" value=\"{{ tc.calStartDateStr }}\">\n                        </form>\n                        <a href=\"{{ url_for('trigger.next_cal_month') }}\" class=\"w3-button w3-sand\">&gt</a>\n                    </div>\n                    <div style=\"display:inline-block; border-left: 20px\">\n                        <form style=\"display:inline-block\" id=\"setcalstartnow\" method=\"post\" action=\"{{ url_for('trigger.calendar_now') }}\">\n                            <input class=\"w3-button w3-sand\" type=\"submit\" value=\"Now\">\n                        </form>\n                    </div>\n                </div>\n            </div>\n            <div class=\"w3-third\">\n                <table style=\"width:100%;\">\n                    <tr>\n                        <td style=\"height: 15px;\"></td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 100%;\">\n                            <div class=\"w3-right\">\n                                <form style=\"display:inline-block\" id=\"dodownloadlog\" method=\"post\" action=\"{{ url_for('trigger.do_download_log') }}\">\n                                    <input class=\"w3-button w3-green w3-round-xxlarge\" style=\"display:inline-block; width: 150px\" type=\"submit\"\n                                        value=\"Download Log\">\n                                </form>\n                                <form style=\"display:inline-block\" id=\"docleanupevents\" method=\"post\" action=\"{{ url_for('trigger.do_cleanup') }}\">\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" style=\"display:inline-block; width: 100px\" type=\"submit\" \n                                        onclick=\"confirmCleanup('docleanupevents', '{{ tc.retentionPeriodStr}}')\" value=\"Cleanup\">\n                                </form>\n                                <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('trigger.do_refresh_calendar') }}\">\n                                    <input style=\"display:inline-block\" class=\"w3-button w3-green w3-round-xxlarge\" type=\"submit\" value=\"Refresh\">\n                                </form>\n                            </div>\n                        </td>\n                    </tr>\n                    <tr>\n                        <td style=\"width: 5%;\"></td>\n                        <td style=\"width: 35%;\"></td>\n                    </tr>\n                </table>\n            </div>\n        </div>\n        <hr>\n        <div class=\"w3-container\" style=\"padding-left: 0;\">\n            <h2 style=\"margin-left: 30px\"> Events for {{ tc.calendarMonthStr}}</h2>\n            <!-- Invisible input field for selected day -->\n            <form style=\"display:none;\" id=\"selecteddayfrm\" method=\"post\" action=\"{{ url_for('trigger.calendar_goto') }}\">\n                <input style=\"display:none;\" type=\"text\" id=\"selectedday\" name=\"selectedday\" value=\"\">\n            </form>\n            <table>\n                <tr>\n                    <td style=\"width:30px\"></td>\n                    <td style=\"width:50px\">Mon</td>\n                    <td style=\"width:50px\">Tue</td>\n                    <td style=\"width:50px\">Wed</td>\n                    <td style=\"width:50px\">Thu</td>\n                    <td style=\"width:50px\">Fri</td>\n                    <td style=\"width:50px\">Sat</td>\n                    <td style=\"width:50px\">Sun</td>\n                </tr>\n                {% for week in tc.calendar%}\n                <tr>\n                    <td>{{ week.week }}</td>\n                    {% set weekdays = week.weekdays %}\n                    {% for day in weekdays %}\n                    {% set data = day.data %}\n                    <td class=\"w3-border\">\n                        <div class=\"w3-panel w3-light-grey\" style=\"margin-top: 0px; margin-bottom: 0px\">\n                            <p style=\"margin-top: 8px; margin-bottom: 8px\">{{ day.day }}</p>\n                        </div>\n                        {% if data.nrevents > 0 %}\n                        <div class=\"w3-panel w3-border-red w3-pale-red\" style=\"margin-top: 0px; margin-bottom: 0px\" onclick=\"goToDay('{{ day.date }}')\">\n                            <p>{{ data.nrevents }}</p>\n                        </div>\n                        {% else %}\n                        <div class=\"w3-panel\" style=\"margin-top: 0px; margin-bottom: 0px\">\n                        <p>&nbsp;</p>\n                        </div>\n                        {% endif %}\n                    </td>\n                    {% endfor %}\n                </tr>\n                {% endfor %}\n            </table>\n        </div>\n    </div>\n    <script>\n        var triggerDelete = 0;\n        var actionDelete = 0;\n        var actionTest = 0;\n        var activeTab = \"\";\n\n        function openTriggerTab(trgTabName, trgTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"triggergroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(trgTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"triggermenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button triggermenu\";\n            }\n            document.getElementById(trgTabButton).classList = \"w3-bar-item w3-button triggermenu w3-light-green\";\n\n            activeTab = trgTabName\n\n            if ('{{ sc.isLiveStream }}' == \"True\") {\n                if (trgTabName == \"trgmotion\") {\n                    removeRoiWindowCanvas()\n                    drawRoiWindow('useroi', '{{ sc.scalerCropLiveView }}')\n                    drawAllRoiWindows();\n                } else {\n                    removeRoiWindowCanvas()\n                }\n            } else {\n                removeRoiWindowCanvas()\n            }\n        }\n\n        function dosubmit(form) {\n            document.getElementById(form).submit();\n        }\n\n        function showDetail(type, url, tgtPhoto, file, name) {\n            var tgtP = document.getElementById(tgtPhoto);\n            var dType = type\n            if (file.substring(19) == \".GIF\"){\n                type = \"gif\"\n            }\n\n            if (type != \"video\") {\n                tag = \"<img style='width: 100%; height: 900px; object-fit: scale-down; cursor:pointer'\"\n                tag = tag + \" src='\" + url + \"'\"\n                tag = tag + \" class='w3-border w3-padding'\"\n                tag = tag + \" alt='\" + name + \"'\"\n                tag = tag + \" onclick='openMedia(this.src)'\"\n                tag = tag + \">\"\n                tag = tag + \"<p>\" + file + \"</p>\"\n                //console.log(\"tag:\", tag)\n                tgtP.innerHTML = tag\n            } else {\n                tag =       \"<div class='video-wrapper'>\"\n                tag = tag + \"   <video style='width: 100%; height: 900px; object-fit: scale-down'\"\n                tag = tag + \"          class='w3-border w3-padding'\"\n                tag = tag + \"          controls>\"\n                tag = tag + \"       <source src='\" + url + \"'\" + \" type='video/mp4'>\"\n                tag = tag + \"       Your browser does not support mp4 video\"\n                tag = tag + \"   </video>\"\n                tag = tag + \"   <div class='open-overlay'\"\n                tag = tag + \"        onclick='openVideo(\\\"\" + url + \"\\\")'\"\n                tag = tag + \"        title='Open in new window'>\"\n                tag = tag + \"    </div>\"\n                tag = tag + \" </div>\"\n                tag = tag + \"<p>\" + file + \"</p>\"\n                //console.log(\"tag:\", tag)\n                tgtP.innerHTML = tag\n            }\n        }\n\n        function confirmCleanup(form, retention) {\n            if (confirm(\"Do you want to delete all events older than \" + retention + \" days?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n\n        function goToDay(date) {\n            var datefield = document.getElementById(\"selectedday\");\n            datefield.value = date\n            document.getElementById(\"selecteddayfrm\").submit();\n        }\n\n        function motionDetectionAlgoChanged() {\n            // Activate / deactivate fields depending on algorithm\n            var src = document.getElementById(\"motiondetectionalgo\");\n            var val = src.value;\n            if (val == \"1\"){\n                // Mean Square Threshold\n                document.getElementById(\"msdthreshold\").disabled = false;\n                document.getElementById(\"bboxthreshold\").disabled = true;\n                document.getElementById(\"nmsthreshold\").disabled = true;\n                document.getElementById(\"motionthreshold\").disabled = true;\n                document.getElementById(\"backsubmodel\").disabled = true;\n                document.getElementById(\"videobboxes\").disabled = true;\n            }\n            if (val == \"2\"){\n                // Frame Differencing\n                document.getElementById(\"msdthreshold\").disabled = true;\n                document.getElementById(\"bboxthreshold\").disabled = false;\n                document.getElementById(\"nmsthreshold\").disabled = false;\n                document.getElementById(\"motionthreshold\").disabled = true;\n                document.getElementById(\"backsubmodel\").disabled = true;\n                document.getElementById(\"videobboxes\").disabled = false;\n            }\n            if (val == \"3\"){\n                // Optical Flow\n                document.getElementById(\"msdthreshold\").disabled = true;\n                document.getElementById(\"bboxthreshold\").disabled = false;\n                document.getElementById(\"nmsthreshold\").disabled = false;\n                document.getElementById(\"motionthreshold\").disabled = false;\n                document.getElementById(\"backsubmodel\").disabled = true;\n                document.getElementById(\"videobboxes\").disabled = false;\n            }\n            if (val == \"4\"){\n                // Background Subtraction\n                document.getElementById(\"msdthreshold\").disabled = true;\n                document.getElementById(\"bboxthreshold\").disabled = false;\n                document.getElementById(\"nmsthreshold\").disabled = false;\n                document.getElementById(\"motionthreshold\").disabled = true;\n                document.getElementById(\"backsubmodel\").disabled = false;\n                document.getElementById(\"videobboxes\").disabled = false;\n            }            \n        }\n\n        function mailServerAuthenicationChanged() {\n            /*  The function enables/disables user/password on Notification ab\n            */\n            var cb = document.getElementById(\"notifyauthenticate\");\n            if (cb.checked == true) {\n                document.getElementById(\"notifyuser\").disabled = false;\n                document.getElementById(\"notifypassword\").disabled = false;\n                document.getElementById(\"notifysavepwd\").disabled = false;\n                document.getElementById(\"notifypwdpath\").disabled = false;\n            } else {\n                document.getElementById(\"notifyuser\").disabled = true;\n                document.getElementById(\"notifypassword\").disabled = true;\n                document.getElementById(\"notifysavepwd\").disabled = true;\n                document.getElementById(\"notifypwdpath\").disabled = true;\n            }\n        }\n        function onlineHelp() {\n            window.open(\"{{ sc.getBaseHelpUrl() }}/TriggerOverview/\");\n        }\n        function dosubmitTriggerSource(){\n            td = document.getElementById(\"triggerdevice\");\n            td.value = \"\";\n            td.disabled = true;\n            tt = document.getElementById(\"triggerdevicetype\");\n            if (tt) {\n                tt.value = \"\";\n                tt.display = \"none\";\n            }\n            tu = document.getElementById(\"triggerdevicedocurl\");\n            if (tu) {\n                tu.value = \"\";\n                tu.display = \"none\";\n            }\n            te = document.getElementById(\"triggerevent\");\n            te.value = \"\";\n            te.disabled = true;\n            tt = document.getElementById(\"triggereventsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newtriggerform\").submit();\n        }\n        function dosubmitTriggerDevice(){\n            te = document.getElementById(\"triggerevent\");\n            te.value = \"\";\n            te.disabled = true;\n            tt = document.getElementById(\"triggereventsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newtriggerform\").submit();\n        }\n        function dosubmitTriggerEvent(){\n            tt = document.getElementById(\"triggereventsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newtriggerform\").submit();\n        }\n        function dosubmitActionSource(){\n            td = document.getElementById(\"actiondevice\");\n            td.value = \"\";\n            td.disabled = true;\n            tt = document.getElementById(\"actiondevicetype\");\n            if (tt) {\n                tt.value = \"\";\n                tt.display = \"none\";\n            }\n            tu = document.getElementById(\"devicedocurl\");\n            if (tu) {\n                tu.innerHTML = \"\";\n                tu.display = \"none\";\n            }\n            te = document.getElementById(\"actionmethod\");\n            te.value = \"\";\n            te.disabled = true;\n            tt = document.getElementById(\"actiontargetsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newactionform\").submit();\n        }\n        function dosubmitActionDevice(){\n            te = document.getElementById(\"actionmethod\");\n            te.value = \"\";\n            te.disabled = true;\n            tt = document.getElementById(\"actiontargetsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newactionform\").submit();\n        }\n        function dosubmitActionMethod(){\n            tt = document.getElementById(\"actiontargetsettingsarea\");\n            tt.display = \"none\";\n            document.getElementById(\"newactionform\").submit();\n        }\n        function countTriggerDelete(id){\n            cb = document.getElementById(id);\n            if (cb.checked == true){\n                triggerDelete += 1;\n            } else {\n                triggerDelete -= 1;\n            }\n            //console.log(\"triggerDelete=\", triggerDelete, \" checked=\", cb.checked);\n        }\n        function confirmDeleteTriggers(form) {\n            if (triggerDelete > 0){\n                if (confirm(\"Do you want to delete the selected triggers?\")) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n        function countActionDelete(id){\n            cb = document.getElementById(id);\n            if (cb.checked == true){\n                actionDelete += 1;\n            } else {\n                actionDelete -= 1;\n            }\n            //console.log(\"triggerDelete=\", triggerDelete, \" checked=\", cb.checked);\n        }\n        function countActionTest(id){\n            cb = document.getElementById(id);\n            if (cb.checked == true){\n                actionTest += 1;\n            } else {\n                actionTest -= 1;\n            }\n            //console.log(\"triggerDelete=\", triggerDelete, \" checked=\", cb.checked);\n        }\n        function confirmDeleteActions(form) {\n            if (actionDelete > 0){\n                if (confirm(\"Do you want to delete the selected actions?\")) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            } else {\n                if (actionTest > 0){\n                    if (actionTest > 1){\n                        confirm(\"Only 1 action can be selected for testing\")\n                        document.getElementById(form).method = \"get\";\n                    } else {\n                        document.getElementById(form).method = \"post\";\n                        document.getElementById(form).submit();\n                    }\n                }\n            }\n        }\n    </script>\n    <script>\n        function enableRegionOfInterest() {\n            /*  enable tha ROI/RONI text fields to allow Flask to request its content\n            */\n            document.getElementById(\"regionofinterest\").disabled = false\n            document.getElementById(\"regionofnointerest\").disabled = false\n        }\n    </script>\n    <script>\n        function showRoiWindows(tabSelected, liveStreamActive, camRef) {\n            /*  If Trigger/Motion tab is selected, show th ROI/RONI Windows\n                if ROI/RONI drawing is enabled\n                This function is called after the page finished loading\n                in order to assure initial visibility of ROI windows\n                tabSelected:       name of the initially selected tab\n                liveStreamActive: \"True\"/\"False\" if live stream is active\n                camRef:            Current scaler crop\n            */\n            //console.log(\"showRoiWindows - tabselected:\", tabSelected, \" liveStreamActive:\", liveStreamActive, \" activeTab:\", activeTab)\n\n            // We need to handle the case where a specific tab has been selected without posting.\n            // In this case, tabSelected is not the actually selected tab\n            // but the tab on which the last post has been made\n\n            currentTab = tabSelected\n            if (activeTab != \"\") {\n                currentTab = activeTab;\n            }\n\n            if (currentTab == \"trgmotion\") {\n                // First remove the existing canvas, which is required for resize\n                removeRoiWindow();\n                if (liveStreamActive == \"True\") {\n                    drawRoiWindow('useroi', camRef);\n                    drawAllRoiWindows();\n                }\n            }\n        }\n        function switchRoiType(id1, id2) {\n            /*  Switch between ROI and RONI drawing mode\n            */\n            var elmt1 = document.getElementById(id1);\n            var elmt2 = document.getElementById(id2);\n            if (elmt1.checked == true) {\n                elmt2.checked = false;\n            } else {\n                elmt2.checked = true;\n            }\n            setRoiUseCase();\n        }\n    </script>\n    <script>\n        var useCase;\n        var roiColor = \"green\";\n        var doDraw;\n        var roiWinTarget;\n        var roiCanvas;\n        var roiCanvasCtx;\n        var roiCanvasOffsetX;\n        var roiCanvasOffsetY;\n        var mouseIsDown;\n        var mouseStartX;\n        var mouseStartY;\n        var camXOffset;\n        var camYOffset;\n        var camWidth;\n        var camHeight;\n        var winXOffset;\n        var winYOffset;\n        var winWidth;\n        var winHeight;\n\n        function removeRoiWindowCanvas(){\n            /*  Removes the canvas for ROI Windows\n            */\n            var cnvEl = document.getElementById(\"canvasforroi\");\n            if (cnvEl) {\n                // Unregister event listeners\n                document.getElementById('canvasforroi').removeEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });   \n                document.getElementById('canvasforroi').removeEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('canvasforroi').removeEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });\n                roiCanvas = null;\n                roiCanvasCtx = null;\n            }\n        }\n\n        function parseWindows(wins) {\n            /*  Parses the tuple of one or multiple rectangles\n                ((x,x,x,x),(x,x,x,x))\n                and returns an array of rectangles as strings\n            */\n            var resa = [];\n            var cnt = 0;\n            if (wins[0] == \"(\") {\n                var wns = wins.slice(1);\n                if (wns[wns.length - 1] == \")\") {\n                    wns = wns.slice(0, wns.length - 1);\n                    while (wns.length > 0) {\n                        var i = wns.indexOf(\")\");\n                        if ( i > 0) {\n                            wn = wns.slice(0, i + 1);\n                            resa[cnt] = wn;\n                            cnt = cnt + 1;\n                            if (i < wns.length) {\n                                wns = wns.slice(i + 2);\n                            } else {\n                                wns = \"\";\n                            }\n                        } else {\n                            wns = \"\";\n                        }\n                    }\n                }\n            }\n            return resa;\n        }\n\n        function parseWindow(wins) {\n            /*  Parses the rectangle\n                (x,x,x,x)\n                and returns an array of one rectangle as string\n            */\n            var resa = [] \n            resa[0]= wins;\n            return resa;\n        }\n\n        function drawCnvWindow(win, index, winlist) {\n            /*  Draw the given window on the canvas\n            */\n            //console.log(\"drawCnvWindow win:\", win);\n\n            // Parse the window spec for the ROI Window\n            var roiWina = parseRectTuple(win);\n            var roiWinXOffset = roiWina[0];\n            var roiWinYOffset = roiWina[1];\n            var roiWinWidth = roiWina[2];\n            var roiWinHeight = roiWina[3];\n            //console.log(\"cnv: \", roiCanvasOffsetX, roiCanvasOffsetY, roiCanvas.width, roiCanvas.height)\n            //console.log(\"cam: \", camXOffset, camYOffset, camWidth, camHeight)\n            //console.log(\"roiw: \", roiWinXOffset, roiWinYOffset, roiWinWidth, roiWinHeight)\n\n            // Scale to canvas\n            var winWidth = Math.round(roiWinWidth * roiCanvas.width / camWidth);\n            var winHeight = Math.round(roiWinHeight * roiCanvas.height / camHeight);\n            var winXOffset = Math.round((roiWinXOffset - camXOffset) * roiCanvas.width / camWidth);\n            var winYOffset = Math.round((roiWinYOffset - camYOffset) * roiCanvas.height / camHeight);\n            //console.log(\"win: \", winXOffset, winYOffset, winWidth, winHeight)\n\n            // Draw\n            roiCanvasCtx.strokeRect(winXOffset, winYOffset, winWidth, winHeight);\n        }\n\n        function drawFilledCnvWindow(win, index, winlist) {\n            /*  Draw the given window on the canvas\n            */\n            //console.log(\"drawFilledCnvWindow win:\", win);\n\n            // Parse the window spec for the ROI Window\n            var roiWina = parseRectTuple(win);\n            var roiWinXOffset = roiWina[0];\n            var roiWinYOffset = roiWina[1];\n            var roiWinWidth = roiWina[2];\n            var roiWinHeight = roiWina[3];\n            //console.log(\"cnv: \", roiCanvasOffsetX, roiCanvasOffsetY, roiCanvas.width, roiCanvas.height)\n            //console.log(\"cam: \", camXOffset, camYOffset, camWidth, camHeight)\n            //console.log(\"roiw: \", roiWinXOffset, roiWinYOffset, roiWinWidth, roiWinHeight)\n\n            // Scale to canvas\n            var winWidth = Math.round(roiWinWidth * roiCanvas.width / camWidth);\n            var winHeight = Math.round(roiWinHeight * roiCanvas.height / camHeight);\n            var winXOffset = Math.round((roiWinXOffset - camXOffset) * roiCanvas.width / camWidth);\n            var winYOffset = Math.round((roiWinYOffset - camYOffset) * roiCanvas.height / camHeight);\n            //console.log(\"win: \", winXOffset, winYOffset, winWidth, winHeight)\n\n            // Draw\n            roiCanvasCtx.fillRect(winXOffset, winYOffset, winWidth, winHeight);\n        }\n\n        function drawAllRoiWindows() {\n            /*  Parses the windows entry in the target element\n                and draws all resulting windows\n            */\n            //console.log(\"drawAllRoiWindows)\n\n            // Clear canvas\n            if (roiCanvasCtx) {\n                roiCanvasCtx.clearRect(0, 0, roiCanvas.width, roiCanvas.height);\n            }\n\n            // Regions of interest\n            var roiWinlist = [];\n            var roiWindows = document.getElementById(\"regionofinterest\").value;\n            //console.log(\"roiWindows:\", roiWindows)\n            var roiWinlist = parseWindows(roiWindows)\n            //console.log(\"roiWinlist:\", roiWinlist)\n            if (roiCanvasCtx) {\n                // Draw all windows\n                roiCanvasCtx.strokeStyle = \"green\";\n                roiCanvasCtx.lineWidth = 2;\n                roiWinlist.forEach(drawCnvWindow)\n            }\n\n            // Regions of NO interest\n            var roniWinlist = [];\n            var roniWindows = document.getElementById(\"regionofnointerest\").value;\n            //console.log(\"roniWindows:\", roniWindows)\n            var roniWinlist = parseWindows(roniWindows)\n            //console.log(\"roniWinlist:\", roniWinlist)\n            if (roiCanvasCtx) {\n                // Draw all windows\n                roiCanvasCtx.fillStyle = \"blue\";\n                roiCanvasCtx.lineWidth = 2;\n                roniWinlist.forEach(drawFilledCnvWindow)\n            }\n        }\n\n        function addNewRoiWindow() {\n            /*  add a new ROI Window to the tuple of windows available in the target element\n            */\n            // Scale\n            //console.log('addNewRoiWindow');\n            //console.log(\"cnv: \", roiCanvasOffsetX, roiCanvasOffsetY, roiCanvas.width, roiCanvas.height)\n            //console.log(\"cam: \", camXOffset, camYOffset, camWidth, camHeight)\n            //console.log(\"win: \", winXOffset, winYOffset, winWidth, winHeight)\n            var roiWinWidth = Math.round(winWidth * camWidth / roiCanvas.width);\n            var roiWinHeight = Math.round(winHeight * camHeight / roiCanvas.height);\n            var roiWinXOffset = Math.round(camXOffset + winXOffset * camWidth / roiCanvas.width);\n            var roiWinYOffset = Math.round(camYOffset + winYOffset * camHeight / roiCanvas.height);\n            var roiWindows = roiWinTarget.value;\n            //console.log(\"roiWindows old:\", roiWindows);\n\n            // Concatenate to roiWindows\n            roiWindows = roiWindows.slice(0, roiWindows.length - 1);\n            if (roiWindows.length > 1) {\n                roiWindows = roiWindows + \",\"\n            }\n            roiWindows = roiWindows \n                + \"(\" \n                + roiWinXOffset.toString() + \",\" \n                + roiWinYOffset.toString() + \",\"\n                + roiWinWidth.toString() + \",\"\n                + roiWinHeight.toString()\n                + \")\";\n            roiWindows = roiWindows + \")\";\n            //console.log(\"roiWindows new:\", roiWindows);\n\n            // Store in target\n            roiWinTarget.value = roiWindows;\n        }\n\n        function handleMouseDown(e) {\n            /*  In case of mouse down, memorize point as start of rectangle\n            */\n            //console.log('handleMouseDown')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // save the starting x/y of the rectangle\n            mouseStartX = parseInt(e.clientX - roiCanvasOffsetX);\n            mouseStartY = parseInt(e.clientY - roiCanvasOffsetY);\n\n            // set a flag indicating the drag has begun\n            mouseIsDown = true;\n        }\n\n        function handleTouchStart(e) {\n            /*  In case of mouse down, memorize point as start of rectangle\n            */\n            //console.log('handleMouseDown')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // Always get the correct offset within the canvas\n            const rect = roiCanvas.getBoundingClientRect();\n            const touch = e.touches[0];\n\n            mouseStartX = touch.clientX - rect.left;\n            mouseStartY = touch.clientY - rect.top;\n\n            // set a flag indicating the drag has begun\n            mouseIsDown = true;\n        }\n\n        function handleMouseUp(e) {\n            /*  In case of mouse up, register end of drawing\n            */\n            //console.log('handleMouseUp');\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // the drag is over, clear the dragging flag\n            mouseIsDown = false;\n\n            // Add new window\n            addNewRoiWindow();\n        }\n\n        function handleMouseOut(e) {\n            /*  In case of mouse leaving the canvas,\n                Draw all windows\n            */\n            //console.log('handleMouseOut')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // the drag is over, clear the dragging flag\n            mouseIsDown = false;\n            // Draw all windows\n            drawAllRoiWindows();\n        }\n\n        function handleMouseMove(e) {\n            /*  While mouse is moving with mouse down,\n                Clear canvas from previous rectangle\n                and draw rectangle with new coordinates.\n                Memorize rectangla parameters.\n            */\n            //console.log('handleMouseMove')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // if we're not dragging, just return\n            if (!mouseIsDown) {\n                return;\n            }\n\n            // get the current mouse position\n            mouseX = parseInt(e.clientX - roiCanvasOffsetX);\n            mouseY = parseInt(e.clientY - roiCanvasOffsetY);\n\n            // clear the canvas\n            roiCanvasCtx.clearRect(0, 0, roiCanvas.width, roiCanvas.height);\n\n            // calculate the rectangle width/height based\n            // on starting vs current mouse position\n            var width = mouseX - mouseStartX;\n            var height = mouseY - mouseStartY;\n\n            // draw a new rect from the start position \n            // to the current mouse position\n            if (useCase == \"roi\") {\n                roiCanvasCtx.strokeRect(mouseStartX, mouseStartY, width, height);\n            } else {\n                roiCanvasCtx.fillRect(mouseStartX, mouseStartY, width, height);\n            }\n            if (width >= 0) {\n                winXOffset = mouseStartX;\n                winWidth = width;\n            } else {\n                winXOffset = mouseX;\n                winWidth = -1 * width;\n            }\n            if (height >= 0) {\n                winYOffset = mouseStartY;\n                winHeight = height;\n            } else {\n                winYOffset = mouseY;\n                winHeight = -1 * height;\n            }\n        }\n\n        function handleTouchMove(e) {\n            /*  While mouse is moving with mouse down,\n                Clear canvas from previous rectangle\n                and draw rectangle with new coordinates.\n                Memorize rectangla parameters.\n            */\n            //console.log('handleMouseMove')\n            //console.log(e)\n            e.preventDefault();\n            e.stopPropagation();\n\n            // if we're not dragging, just return\n            if (!mouseIsDown) {\n                return;\n            }\n\n            // get the current mouse position\n            // Always get the correct offset within the canvas\n            const rect = roiCanvas.getBoundingClientRect();\n            const touch = e.touches[0];\n\n            mouseX = touch.clientX - rect.left;\n            mouseY = touch.clientY - rect.top;\n\n            // clear the canvas\n            roiCanvasCtx.clearRect(0, 0, roiCanvas.width, roiCanvas.height);\n\n            // calculate the rectangle width/height based\n            // on starting vs current mouse position\n            var width = mouseX - mouseStartX;\n            var height = mouseY - mouseStartY;\n\n            // draw a new rect from the start position \n            // to the current mouse position\n            if (useCase == \"roi\") {\n                roiCanvasCtx.strokeRect(mouseStartX, mouseStartY, width, height);\n            } else {\n                roiCanvasCtx.fillRect(mouseStartX, mouseStartY, width, height);\n            }\n            if (width >= 0) {\n                winXOffset = mouseStartX;\n                winWidth = width;\n            } else {\n                winXOffset = mouseX;\n                winWidth = -1 * width;\n            }\n            if (height >= 0) {\n                winYOffset = mouseStartY;\n                winHeight = height;\n            } else {\n                winYOffset = mouseY;\n                winHeight = -1 * height;\n            }\n        }\n\n        function parseRectTuple(tuple) {\n            /*  Parse a Python tuple for libcamera.Rectangle\n                (xOffset, yOffset, width, height)\n            */\n            resn = [0, 0, 0, 0]\n            if (tuple[0] == \"(\") {\n                var tpl = tuple.slice(1)\n                if (tpl[tpl.length - 1] == \")\") {\n                    tpl = tpl.slice(0, tpl.length - 1)\n                    var res = tpl.split(\",\")\n                    if (res.length == 4) {\n                        resn = [parseInt(res[0]), parseInt(res[1]), parseInt(res[2]), parseInt(res[3])]\n                    }\n                }\n            }\n            return resn\n        }\n\n        function setRoiUseCase(){\n            /*  Set the use case for ROI Windows\n                depending on selection of \"drawroi\" and \"drawroni\" checkboxes.\n            */\n            useCase = \"roi\";\n            roiWinTarget = document.getElementById(\"regionofinterest\");\n            if (roiCanvasCtx) {\n                roiCanvasCtx.strokeStyle = \"green\";\n            }\n            var roniEl = document.getElementById(\"drawroni\");\n            if (roniEl) {\n                if (roniEl.checked == true) {\n                    useCase = \"roni\";\n                    roiWinTarget = document.getElementById(\"regionofnointerest\");\n                    if (roiCanvasCtx) {\n                        roiCanvasCtx.strokeStyle = \"blue\";\n                        roiCanvasCtx.fillStyle = \"blue\";\n                    }\n                }\n            }\n        }\n\n        function clearRoi(){\n            /*  Clear the ROI/RONI windows\n            */\n            roiWinTarget = document.getElementById(\"regionofinterest\");\n            roiWinTarget.value = \"()\";\n            roniWinTarget = document.getElementById(\"regionofnointerest\");\n            roniWinTarget.value = \"()\";\n        }\n\n        function drawRoiWindow(trg, camRef) {\n            /*  Initialize the drawing infrastructure for ROI Windows\n                trg: Trigger (checkbox)\n                     If checked, canvas and ROI Windows are drawn\n                     If unchecked, canvas is removed\n                camRef: Scaler crop for Live View as base for scaling\n            */\n            //console.log(\"drawRoiWindow trg:\", trg, \" camRef\", camRef);\n\n            setRoiUseCase();\n\n            var ctrlEl = document.getElementById(\"drawroictrl\");\n            var photoRoi = document.getElementById(\"photorois\");\n\n            var trgEl = document.getElementById(trg);\n            if (trgEl) {\n                if (trgEl.checked == true) {\n                    doDraw = true;\n                    ctrlEl.style.display = \"block\";\n                    photoRoi.disabled = false;\n                } else {\n                    doDraw = false;\n                    ctrlEl.style.display = \"none\";\n                    photoRoi.checked = false;\n                    photoRoi.disabled = true;\n                }\n                //console.log(\"drawRoiWindow doDraw:\", doDraw);\n                drawWindow(camRef);\n            }\n        }\n\n        function syncCanvas() {\n            /*  Sync the canvas with the current ROI/RONI windows\n            */\n            var img = document.getElementById(\"liveviewforroi\");\n            var bRect = img.getBoundingClientRect();\n            roiCanvas.width = bRect.width;\n            roiCanvas.height = bRect.height;\n        }\n\n        function drawWindow(camRef) {\n            /*  Creates canvas over live view image and allows drawing of ROI Windows\n                tgt: Target element taking windows\n                camRef: Scaler crop for Live View as base for scaling\n            */\n            //console.log(\"drawWindow camRef\", camRef);\n            // calculate camera crop window params\n            var camCrop = parseRectTuple(camRef);\n            camXOffset = camCrop[0];\n            camYOffset = camCrop[1];\n            camWidth = camCrop[2];\n            camHeight = camCrop[3];\n            //console.log(camXOffset, camYOffset, camWidth, camHeight);\n\n            if (doDraw == true) {\n                // Trigger active -> prepare canvas\n                var img = document.getElementById(\"liveviewforroi\");\n                // Get the canvas\n                roiCanvas = document.getElementById('canvasforroi');\n                // Position and size the canvas\n                var bRect = img.getBoundingClientRect();\n                roiCanvas.style.position = \"absolute\";\n                roiCanvas.style.top = 0;\n                roiCanvas.style.left = 0;\n                roiCanvas.width = bRect.width;\n                roiCanvas.height = bRect.height;\n                // Set context\n                roiCanvasCtx = roiCanvas.getContext(\"2d\");\n                roiCanvasCtx.strokeStyle = \"green\";\n                roiCanvasCtx.fillStyle = \"blue\";\n                roiCanvasCtx.lineWidth = 2;\n                // Determine canvas position\n                liveCanvasOffset = roiCanvas.getBoundingClientRect();\n                roiCanvasOffsetX = liveCanvasOffset.left\n                roiCanvasOffsetY = liveCanvasOffset.top\n                // Initialize mouse down\n                mouseIsDown = false;\n                // Register event listeners\n                document.getElementById('canvasforroi').addEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('canvasforroi').addEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').addEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });\n            } else {\n                // Trigger inactive -> destroy canvas and clear ROIWindows\n                removeRoiWindow()\n                clearRoi();\n            }\n        }\n\n        function removeRoiWindow() {\n            /*  Remove RoiWindow canvas infrastructure\n            */\n            var cnvEl = document.getElementById(\"canvasforroi\");\n            if (cnvEl) {\n                // Unregister event listeners\n                document.getElementById('canvasforroi').removeEventListener('mousedown', function (e) {\n                    handleMouseDown(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mousemove', function (e) {\n                    handleMouseMove(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mouseup', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('mouseout', function (e) {\n                    handleMouseOut(e);\n                });\n\n                document.getElementById('canvasforroi').removeEventListener('touchstart', function (e) {\n                    handleTouchStart(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('touchmove', function (e) {\n                    handleTouchMove(e);\n                }, true);\n                document.getElementById('canvasforroi').removeEventListener('touchend', function (e) {\n                    handleMouseUp(e);\n                });\n                document.getElementById('canvasforroi').removeEventListener('touchcancel', function (e) {\n                    handleMouseOut(e);\n                });                \n                roiCanvas = null;\n                roiCanvasCtx = null;\n            }\n        }\n    </script>\n    <style>\n        canvas {\n            border: 1px solid yellow;\n        }\n    </style>\n    <script>\n        function openMedia(src) {\n            window.open(\n                '/media-viewer?src=' + encodeURIComponent(src),\n                '_blank',\n                'noopener'\n            );\n        }\n\n        function openVideo(src) {\n            window.open(\n                '/media-viewer?type=video&src=' + encodeURIComponent(src),\n                '_blank',\n                'noopener'\n            );\n        }\n    </script>\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/templates/webcam/webcam.html",
    "content": "{% extends 'base.html' %}\n\n{% block header %}\n{% block title %}Camera Access{% endblock %}\n{% endblock %}\n\n{% block content %}\n    <div class=\"w3-bar w3-green\">\n        <!-- Cam menue -->\n        {% if sc.lastCamTab == \"webcam\" %}\n        <button class=\"w3-bar-item w3-button cammenu w3-light-green\" id=\"webcambtn\"\n            onclick=\"openCamTab('webcam', 'webcambtn')\">Web Cam</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button cammenu\" id=\"webcambtn\"\n            onclick=\"openCamTab('webcam', 'webcambtn')\">Web Cam</button>\n        {% endif %}\n        {% if sc.supportedCameras|length() > 1 %}\n        {% if sc.lastCamTab == \"multicam\" %}\n        <button class=\"w3-bar-item w3-button cammenu w3-light-green\" id=\"multicambtn\"\n            onclick=\"openCamTab('multicam', 'multicambtn')\">Multi-Cam</button>\n        {% else %}\n        {% if sc.isLiveStream2 != None %}\n        <button class=\"w3-bar-item w3-button cammenu\" id=\"multicambtn\"\n            onclick=\"openCamTab('multicam', 'multicambtn')\">Multi-Cam</button>\n        {% endif %}\n        {% endif %}\n        {% endif %}\n        {% if sc.useStereo == True %}\n        {% if sc.lastCamTab == \"calibcam\" %}\n        <button class=\"w3-bar-item w3-button cammenu w3-light-green\" id=\"calibcambtn\"\n            onclick=\"openCamTab('calibcam', 'calibcambtn')\">Camera Calibration</button>\n        {% else %}\n        <button class=\"w3-bar-item w3-button cammenu\" id=\"calibcambtn\"\n            onclick=\"openCamTab('calibcam', 'calibcambtn')\">Camera Calibration</button>\n        {% endif %}\n        {% if sc.lastCamTab == \"stereocam\" %}\n        <button class=\"w3-bar-item w3-button cammenu w3-light-green\" id=\"stereocambtn\"\n            onclick=\"openCamTab('stereocam', 'stereocambtn')\">Stereo-Cam</button>\n        {% else %}\n        {% if sc.isLiveStream2 != None %}\n        <button class=\"w3-bar-item w3-button cammenu\" id=\"stereocambtn\"\n            onclick=\"openCamTab('stereocam', 'stereocambtn')\">Stereo-Cam</button>\n        {% endif %}\n        {% endif %}\n        {% endif %}\n        <div class=\"w3-bar-item w3-right\" style=\"padding-top:2px; padding-bottom:0\">\n            <div class=\"w3-tooltip\">\n                <span style=\"position:absolute;right:45px;top:5px;width:200px\"\n                    class=\"w3-text w3-tag\">Online help from GitHub\n                </span>\n                <img src=\"{{ url_for('static', filename='onlineHelp.png') }}\" class=\"w3-image\" id=\"onlinehelp\"\n                    alt=\"Online help\" style=\"height:34px; width:34px\"\n                    onclick=\"onlineHelp()\">\n            </div>\n        </div>\n    </div>\n    {% if sc.lastCamTab == \"webcam\" %}\n    <div id=\"webcam\" class=\"camgroup\">\n    {% else %}\n    <div id=\"webcam\" class=\"camgroup\" style=\"display:none\">\n    {% endif %}\n        <h2>Web Cam</h2>\n        <table>\n            <tr>\n                <td style=\"width: 50%;\">\n                    {% if sc.isLiveStream %}\n                    <h3 style=\"margin: 0px;\">{{ sc.activeCameraInfo}}</h3>\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;\">\n                    {% if str2 != None %}\n                    <h3 style=\"margin: 0px;\">{{ str2[\"camerainfo\"]}}</h3>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    <h4 style=\"margin: 0px;\">Video Stream:</h4>\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    <h4 style=\"margin: 0px;\">Video Stream:</h4>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    Using Camera Configuration <b>LIVE</b>\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    Using Camera Configuration <b>LIVE</b>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                <!-- Live stream -->\n                    {% if sc.isLiveStream %}\n                    <img src=\"{{ url_for('home.video_feed') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    {% if sc.isVideoRecording %}\n                    {% if sc.recordAudio == False %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo_sound.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% elif sc.isPhotoSeriesRecording %}\n                    <img src=\"{{ url_for('static', filename='recordingphotoseries.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                <!-- Live stream 2 -->\n                    {% if str2 != None %}\n                    <img src=\"{{ url_for('home.video_feed2') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    MJPEG Streaming URL: <a href=\"http://{{ g.hostname }}/video_feed\" target=\"_blank\">http://{{ g.hostname }}/video_feed</a>\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    MJPEG Streaming URL: <a href=\"http://{{ g.hostname }}/video_feed2\" target=\"_blank\">http://{{ g.hostname }}/video_feed2</a>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    <h4 style=\"margin: 0px;\">Photo:</h4>\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    <h4 style=\"margin: 0px;\">Photo:</h4>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    {% if sc.activeCameraIsUsb == False %}\n                    <p style=\"margin: 0; display: inline-block;\">Using Camera Configuration</p> \n                    <form style=\"display: inline-block;\" id=\"activecameraphotocfgform\" method=\"post\" action=\"{{ url_for('webcam.active_camera_photo_cfg') }}\">\n                        <select style=\"display: inline-block;\" name=\"activecameraphotocfg\" id=\"activecameraphotocfg\" onchange=\"doSubmit('activecameraphotocfgform')\">\n                            {% if sc.webCamActiveCamPhotoCfg == \"LIVE\" %}\n                                <option value=\"LIVE\" selected>LIVE</option>\n                            {% else %}\n                                <option value=\"LIVE\">LIVE</option>\n                            {% endif %}\n                            {% if sc.webCamActiveCamPhotoCfg == \"FOTO\" %}\n                                <option value=\"FOTO\" selected>FOTO</option>\n                            {% else %}\n                                <option value=\"FOTO\">FOTO</option>\n                            {% endif %}\n                        </select>\n                    </form>\n                    {% else %}\n                    Using Camera Configuration <b>LIVE</b>\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    {% if sc.secondCameraIsUsb == False %}\n                    <p style=\"margin: 0; display: inline-block;\">Using Camera Configuration</p> \n                    <form style=\"display: inline-block;\" id=\"secondcameraphotocfgform\" method=\"post\" action=\"{{ url_for('webcam.second_camera_photo_cfg') }}\">\n                        <select style=\"display: inline-block;\" name=\"secondcameraphotocfg\" id=\"secondcameraphotocfg\" onchange=\"doSubmit('secondcameraphotocfgform')\">\n                            {% if sc.webCamSecondCamPhotoCfg == \"LIVE\" %}\n                                <option value=\"LIVE\" selected>LIVE</option>\n                            {% else %}\n                                <option value=\"LIVE\">LIVE</option>\n                            {% endif %}\n                            {% if sc.webCamSecondCamPhotoCfg == \"FOTO\" %}\n                                <option value=\"FOTO\" selected>FOTO</option>\n                            {% else %}\n                                <option value=\"FOTO\">FOTO</option>\n                            {% endif %}\n                        </select>\n                    </form>\n                    {% else %}\n                    Using Camera Configuration <b>LIVE</b>\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    {% if sc.webCamActiveCamPhotoCfg == \"LIVE\" %}\n                    <img src=\"{{ url_for('webcam.photo_feed') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    <img src=\"{{ url_for('webcam.photo_feed_hr') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    {% if sc.webCamSecondCamPhotoCfg == \"LIVE\" %}\n                    <img src=\"{{ url_for('webcam.photo_feed2') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% else %}\n                    <img src=\"{{ url_for('webcam.photo_feed2_hr') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if sc.isLiveStream %}\n                    {% if sc.webCamActiveCamPhotoCfg == \"LIVE\" %}\n                    Photo URL: <a href=\"http://{{ g.hostname }}/photo_feed\" target=\"_blank\">http://{{ g.hostname }}/photo_feed</a>\n                    {% else %}\n                    Photo URL: <a href=\"http://{{ g.hostname }}/photo_feed_hr\" target=\"_blank\">http://{{ g.hostname }}/photo_feed_hr</a>\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    {% if sc.webCamSecondCamPhotoCfg == \"LIVE\" %}\n                    Photo URL: <a href=\"http://{{ g.hostname }}/photo_feed2\" target=\"_blank\">http://{{ g.hostname }}/photo_feed2</a>\n                    {% else %}\n                    Photo URL: <a href=\"http://{{ g.hostname }}/photo_feed2_hr\" target=\"_blank\">http://{{ g.hostname }}/photo_feed2_hr</a>\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n        </table>\n    </div>\n    {% if sc.lastCamTab == \"multicam\" %}\n    <div id=\"multicam\" class=\"camgroup\">\n    {% else %}\n    <div id=\"multicam\" class=\"camgroup\" style=\"display:none\">\n    {% endif %}\n        <h2>Multi Cam</h2>\n        <table>\n            <tr>\n                {% if sc.supportedCameras|length <= 2 %}\n                <td style=\"width: 50%;\">\n                    {% if sc.isLiveStream %}\n                    <h3 style=\"margin: 0px;\">Active: {{ sc.activeCameraInfo}}</h3>\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;\">\n                    {% if str2 != None %}\n                    <h3 style=\"margin: 0px;\">{{ str2[\"camerainfo\"]}}</h3>\n                    {% endif %}\n                </td>\n                {% else %}\n                <td style=\"width: 50%;\">\n                    {% if sc.isLiveStream %}\n                    <form id=\"activecameraform\" method=\"post\" action=\"{{ url_for('webcam.change_active_camera') }}\">\n                        <h3 style=\"margin: 0px;\">Active: Camera&nbsp;\n                            {% if cfg.streamingCfgInvalid == True %}\n                            <select name=\"activecamera\" id=\"activecamera\" onchange=\"confirmSwitchCamera('activecameraform')\">\n                            {% else %}\n                            <select name=\"activecamera\" id=\"activecamera\" onchange=\"doSubmit('activecameraform')\">\n                            {% endif %}\n                            {% for cam in cs %}\n                                {% if cam.usbDev != \"UNKNOWN\" %}\n                                {% if sc.secondCamera != cam.num %}\n                                {% if sc.activeCamera == cam.num %}\n                                <option value=\"{{ cam.num }}\" selected>{{ cam.num }} ({{ cam.model }})</option>\n                                {% else %}\n                                <option value=\"{{ cam.num }}\">{{ cam.num }} ({{ cam.model }})</option>\n                                {% endif %}\n                                {% endif %}\n                                {% endif %}\n                            {% endfor %}\n                            </select>\n                        </h3>\n                    </form>\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;\">\n                    {% if str2 != None %}\n                    <form id=\"secondcameraform\" method=\"post\" action=\"{{ url_for('webcam.change_second_camera') }}\">\n                        <h3 style=\"margin: 0px;\">Camera&nbsp;\n                            <select name=\"secondcamera\" id=\"secondcamera\" onchange=\"doSubmit('secondcameraform')\">\n                            {% for cam in cs %}\n                                {% if cam.usbDev != \"UNKNOWN\" %}\n                                {% if sc.activeCamera != cam.num %}\n                                {% if sc.secondCamera == cam.num %}\n                                <option value=\"{{ cam.num }}\" selected>{{ cam.num }} ({{ cam.model }})</option>\n                                {% else %}\n                                <option value=\"{{ cam.num }}\">{{ cam.num }} ({{ cam.model }})</option>\n                                {% endif %}\n                                {% endif %}\n                                {% endif %}\n                            {% endfor %}\n                            </select>\n                        </h3>\n                    </form>\n                    {% endif %}\n                </td>\n                {% endif %}\n            </tr>\n            <tr>\n                <td>\n                <!-- Live stream -->\n                    {% if sc.isLiveStream %}\n                    <img src=\"{{ url_for('home.video_feed') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    {% if sc.isVideoRecording %}\n                    {% if sc.recordAudio == False %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo_sound.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% elif sc.isPhotoSeriesRecording %}\n                    <img src=\"{{ url_for('static', filename='recordingphotoseries.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                <!-- Live stream 2 -->\n                    {% if str2 != None %}\n                    {% if sc.isLiveStream2 %}\n                    <img src=\"{{ url_for('home.video_feed2') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <!-- Photo button -->\n                    {% if sc.isVideoRecording or sc.isPhotoSeriesRecording %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_photo') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo\" disabled>\n                    </form>\n                    {% else %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_photo') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo\">\n                    </form>\n                    {% endif %}\n\n                    <!-- raw button -->\n                    {% if sc.isVideoRecording or sc.isPhotoSeriesRecording %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_raw_photo') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw\" disabled>\n                    </form>\n                    {% else %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_raw_photo') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw\">\n                    </form>\n                    {% endif %}\n                    {% if sc.isPhotoSeriesRecording %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_record_video') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Video\" disabled>\n                    </form>\n                    {% else %}\n                    {% if sc.isVideoRecording == false %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_record_video') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Video\">\n                    </form>\n                    {% endif %}\n                    {% if sc.isVideoRecording == true %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_stop_recording') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop\">\n                    </form>\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                    {% if str2 != None %}\n                    <!-- Photo button -->\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.take_photo2') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo\">\n                    </form>\n\n                    <!-- Raw button -->\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_raw_photo2') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw\">\n                    </form>\n\n                    {% if sc.isVideoRecording2 == false %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_record_video2') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Video\">\n                    </form>\n                    {% endif %}\n                    {% if sc.isVideoRecording2 == true %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_stop_recording2') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop\">\n                    </form>\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    &nbsp;\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if str2 != None %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.take_photo_both') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Photo - Both\">\n                    </form>\n\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_take_raw_photo_both') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Raw - Both\">\n                    </form>\n\n                    {% if sc.isVideoRecording == false and sc.isVideoRecording2 == false %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_record_video_both') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Video - Both\">\n                    </form>\n                    {% endif %}\n                    {% if sc.isVideoRecording == true or sc.isVideoRecording2 == true %}\n                    <form style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.cam_stop_recording_both') }}\">\n                        <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop - Both\">\n                    </form>\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    &nbsp;\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if str2 != None and sc.isLiveStream == True %}\n                    <form method=\"post\" action=\"{{ url_for('webcam.store_streaming_config') }}\">\n                        <p style=\"margin-bottom: 0\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Save Active Camera Settings for Camera Switch\">\n                    </form>\n                    {% endif %}\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if str2 != None and sc.isLiveStream == True %}\n                    {% if sc.activeCameraInfo[8:] == str2[\"camerainfo\"][8:] %}\n                    <form method=\"post\" action=\"{{ url_for('webcam.sync_settings') }}\">\n                        <p style=\"margin-bottom: 0\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Synchronize Configurations\">\n                    </form>\n                    {% else %}\n                    <form method=\"post\" action=\"{{ url_for('webcam.sync_settings') }}\">\n                        <p style=\"margin-bottom: 0\"></p>\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Synchronize Configurations\" disabled>\n                    </form>\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    {% if str2 != None and sc.isLiveStream == True %}\n                    <form id=\"switchcameraform\" method=\"post\" action=\"{{ url_for('webcam.switch_cameras') }}\">\n                        <p style=\"margin-bottom: 0\"></p>\n                        {% if cfg.streamingCfgInvalid == True %}\n                        <input class=\"w3-button w3-black\" type=\"submit\" onclick=\"confirmSwitchCamera('switchcameraform')\" value=\"<<< Switch Cameras >>>\">\n                        {% else %}\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"<<< Switch Cameras >>>\">\n                        {% endif %}\n                    </form>\n                    {% endif %}\n                </td>\n                <td>\n                </td>\n            </tr>\n        </table>\n    </div>\n    {% if sc.lastCamTab == \"stereocam\" %}\n    <div id=\"stereocam\" class=\"camgroup\">\n    {% else %}\n    <div id=\"stereocam\" class=\"camgroup\" style=\"display:none\">\n    {% endif %}\n        <h2>Stereo Cam</h2>\n        <table style=\"layout:fixed; width: 100%;\">\n            <tr>\n                <td colspan=2 style=\"width: 50%; text-align:center;\">\n                    {% if sc.isLiveStream %}\n                    <h3 style=\"margin: 0px;\">Left</h3>\n                    {% endif %}\n                </td>\n                <td colspan=2 style=\"width: 50%; text-align:center;\">\n                    {% if str2 != None %}\n                    <h3 style=\"margin: 0px;\">Right</h3>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td colspan=2 style=\"width: 50%;\">\n                    {% if sc.isLiveStream %}\n                    <img src=\"{{ url_for('home.video_feed') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    {% if sc.isVideoRecording %}\n                    {% if sc.recordAudio == False %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo_sound.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% elif sc.isPhotoSeriesRecording %}\n                    <img src=\"{{ url_for('static', filename='recordingphotoseries.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td colspan=2 style=\"width: 50%;\">\n                    {% if str2 != None %}\n                    <img src=\"{{ url_for('home.video_feed2') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width: 25%;\">\n                    &nbsp;\n                </td>\n                <td colspan=2 style=\"width: 50%; text-align:center;\">\n                    <h3 style=\"margin: 0px;\">Stereo</h3>\n                </td>\n                <td style=\"width: 25%;\">\n                    &nbsp;\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width: 25%;\">\n                    <table>\n                        <form id=\"stereodispform\" method=\"post\" action=\"{{ url_for('webcam.stereo_display') }}\">\n                            <tr>\n                                <td class=\"w3-tooltip\" style=\"width:15%\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        Apply calibration and rectification to camera images\n                                    </span>\n                                    <label for=\"applycalibrectify\">Rectified Images:</label>\n                                </td>\n                                <td style=\"width:10%\">\n                                    {% if ster.stereoRectifyOK == True %}\n                                    {% if ster.applyCalibRectify == True %}\n                                    <input type=\"checkbox\" id=\"applycalibrectify\" name=\"applycalibrectify\" onchange=\"dosubmitApplyCalibRectify()\" value=\"1\" checked>\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"applycalibrectify\" name=\"applycalibrectify\" onchange=\"dosubmitApplyCalibRectify()\" value=\"1\">\n                                    {% endif %}\n                                    {% else %}\n                                    <input type=\"checkbox\" id=\"applycalibrectify\" name=\"applycalibrectify\" value=\"1\" disabled>\n                                    {% endif %}\n                                </td>\n                            </tr>\n                            <tr>\n                                <td>&nbsp;</td>\n                                <td></td>\n                            </tr>\n                        </form>\n                        {% set intent = \"\" %}\n                        {% set stereoAlgo = \"\" %}\n                        {% if tmp|length() > 0 %}\n                        {% if \"intent\" in tmp %}\n                        {% set intent = tmp.intent %}\n                        {% set intentInt = tmp.intent|int %}\n                        {% endif %}\n                        {% if \"stereoAlgo\" in tmp %}\n                        {% set stereoAlgo = tmp.stereoAlgo %}\n                        {% set stereoAlgoInt = tmp.stereoAlgo|int %}\n                        {% endif %}\n                        {% endif %}\n                        <form id=\"stereoconfigform\" method=\"post\" action=\"{{ url_for('webcam.stereo_config') }}\">\n                            <tr>\n                                <td class=\"w3-tooltip\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The intended stereo processing.\n                                    </span>\n                                    <label for=\"intent\">Intent:</label>\n                                </td>\n                                <td>\n                                    <select name=\"intent\" id=\"intent\"\n                                        onchange=\"dosubmitIntent()\">\n                                        {% if intent == \"\" %}\n                                        <option value=\"\" selected></option>\n                                        {% endif %}\n                                        {% for intent in ster.intents %}\n                                        {% if ster.intentIdx == loop.index -1 %}\n                                        <option value=\"{{ loop.index -1 }}\" selected>{{ ster.intentNames[loop.index-1] }}</option>\n                                        {% else %}\n                                        <option value=\"{{ loop.index -1 }}\">{{ ster.intentNames[loop.index -1] }}</option>\n                                        {% endif %}\n                                        {% endfor %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% if ster.intentAlgoNames[intentInt]|length > 0 %}\n                            <tr>\n                                <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                        The stereo algorithm to apply.\n                                    </span>\n                                    <label for=\"stereoalgo\">Algorithm:</label>\n                                </td>\n                                <td style=\"width: 50%;\">\n                                    {% if intent == \"\" %}\n                                    <select name=\"stereoalgo\" id=\"stereoalgo\"\n                                        onchange=\"dosubmitIntentAlgo()\" disabled>\n                                    {% else %}\n                                    <select name=\"stereoalgo\" id=\"stereoalgo\"\n                                        onchange=\"dosubmitIntentAlgo()\">\n                                    {% endif %}\n                                        {% if stereoAlgo == \"\" %}\n                                        <option value=\"\" selected></option>\n                                        {% endif %}\n                                        {% for algo in ster.intentAlgos[0] %}\n                                        {% if ster.intentAlgoIdx == loop.index -1 %}\n                                        <option value=\"{{ loop.index -1 }}\" selected>{{ ster.intentAlgoNames[intentInt][loop.index -1] }}</option>\n                                        {% else %}\n                                        <option value=\"{{ loop.index -1 }}\">{{ ster.intentAlgoNames[intentInt][loop.index -1] }}</option>\n                                        {% endif %}\n                                        {% endfor %}\n                                    </select>\n                                </td>\n                            </tr>\n                            {% endif %}\n                            <div id=\"stereoconfigparams\">\n                                {% if intent == \"0\" %} <!-- Depth Map -->\n                                {% if stereoAlgo == \"0\" %} <!-- StereoBM -->\n                                {% if ster.intentAlgoLinks[0][0]|length > 0 %}\n                                <tr>\n                                    <td colspan=\"2\">\n                                        <a href=\"{{ ster.intentAlgoLinks[0][0] }}\" target=\"_blank\">Algorithm Reference</a>\n                                    </td>\n                                </tr>\n                                {% endif %}\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Factor for the number of disparities.\n                                        </span>\n                                        <label for=\"bmnumdisparitiesfactor\">Num Disparities Factor:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"bmnumdisparitiesfactor\" name=\"bmnumdisparitiesfactor\" min=\"0\" max=\"9999\" step=\"1\" value={{ ster.bm_numDisparitiesFactor }} onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Block size.\n                                        </span>\n                                        <label for=\"bmblocksize\">Block Size:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"bmblocksize\" name=\"bmblocksize\" min=\"3\" max=\"255\" step=\"2\" value=\"{{ ster.bm_blockSize }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                {% endif %}\n                                {% if stereoAlgo == \"1\" %} <!-- StereoSGBM -->\n                                {% if ster.intentAlgoLinks[0][1]|length > 0 %}\n                                <tr>\n                                    <td colspan=\"2\">\n                                        <a href=\"{{ ster.intentAlgoLinks[0][1] }}\" target=\"_blank\">Algorithm Reference</a>\n                                    </td>\n                                </tr>\n                                {% endif %}\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Minimum possible disparity value.\n                                        </span>\n                                        <label for=\"sgbmmindisparity\">Min. Disparity:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmmindisparity\" name=\"sgbmmindisparity\" min=\"0\" max=\"9999\" step=\"1\" value=\"{{ ster.sgbm_minDisparity }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Num Disparities Factor. Multiplied by 16 to get Num Disparities\n                                        </span>\n                                        <label for=\"sgbmnumdisparitiesfactor\">Num Disparities Factor:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmnumdisparitiesfactor\" name=\"sgbmnumdisparitiesfactor\" min=\"3\" max=\"255\" step=\"1\" value=\"{{ ster.sgbm_numDisparitiesFactor }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Matched block size.\n                                        </span>\n                                        <label for=\"sgbmblocksize\">Block Size:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmblocksize\" name=\"sgbmblocksize\" min=\"3\" max=\"11\" step=\"2\" value=\"{{ ster.sgbm_blockSize }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            The first parameter controlling the disparity smoothness.\n                                        </span>\n                                        <label for=\"sgbmp1\">P1:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmp1\" name=\"sgbmp1\" min=\"0\" max=\"999\" step=\"1\" value=\"{{ ster.sgbm_P1 }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            The second parameter controlling the disparity smoothness.\n                                        </span>\n                                        <label for=\"sgbmp2\">P2:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmp2\" name=\"sgbmp2\" min=\"0\" max=\"999\" step=\"1\" value=\"{{ ster.sgbm_P2 }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Maximum allowed difference (in integer pixel units) in the left-right disparity check\n                                        </span>\n                                        <label for=\"sgbmdisp12maxdiff\">Disp12 Max Diff:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmdisp12maxdiff\" name=\"sgbmdisp12maxdiff\" min=\"-1\" max=\"255\" step=\"1\" value=\"{{ ster.sgbm_disp12MaxDiff }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Truncation value for the prefiltered image pixels.\n                                        </span>\n                                        <label for=\"sgbmprefiltercap\">PreFilter Cap:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmprefiltercap\" name=\"sgbmprefiltercap\" min=\"0\" max=\"255\" step=\"1\" value=\"{{ ster.sgbm_preFilterCap }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Margin in percentage by which the best (minimum) computed cost function value should \"win\" the second best value to consider the found match correct.\n                                        </span>\n                                        <label for=\"sgbmuniquenessratio\">Uniqueness Ratio:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmuniquenessratio\" name=\"sgbmuniquenessratio\" min=\"0\" max=\"255\" step=\"1\" value=\"{{ ster.sgbm_uniquenessRatio }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Maximum size of smooth disparity regions to consider their noise speckles and invalidate.\n                                        </span>\n                                        <label for=\"sgbmspecklewindowsize\">Speckle Window Size:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmspecklewindowsize\" name=\"sgbmspecklewindowsize\" min=\"0\" max=\"200\" step=\"1\" value=\"{{ ster.sgbm_speckleWindowSize }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Maximum disparity variation within each connected component.\n                                        </span>\n                                        <label for=\"sgbmspecklerange\">Speckle Range:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <input type=\"number\" id=\"sgbmspecklerange\" name=\"sgbmspecklerange\" min=\"3\" max=\"255\" step=\"1\" value=\"{{ ster.sgbm_speckleRange }}\" onchange=\"dosubmitStereoParam()\">\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"w3-tooltip\" style=\"width: 50%;\">\n                                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                                            Set it to MODE_HH to run the full-scale two-pass dynamic programming algorithm.\n                                        </span>\n                                        <label for=\"sgbmmode\">Mode:</label>\n                                    </td>\n                                    <td style=\"width: 50%;\">\n                                        <select name=\"sgbmmode\" id=\"sgbmmode\"\n                                            onchange=\"dosubmitStereoParam()\">\n                                            {% if ster.sgbm_mode == 0 %}\n                                            <option value=\"0\" selected>MODE_SGBM</option>\n                                            {% else %}\n                                            <option value=\"0\">MODE_SGBM</option>\n                                            {% endif %}\n                                            {% if ster.sgbm_mode == 1 %}\n                                            <option value=\"1\" selected>MODE_HH</option>\n                                            {% else %}\n                                            <option value=\"1\">MODE_HH</option>\n                                            {% endif %}\n                                            {% if ster.sgbm_mode == 2 %}\n                                            <option value=\"2\" selected>MODE_SGBM_3WAY</option>\n                                            {% else %}\n                                            <option value=\"2\">MODE_SGBM_3WAY</option>\n                                            {% endif %}\n                                            {% if ster.sgbm_mode == 3 %}\n                                            <option value=\"3\" selected>MODE_HH4</option>\n                                            {% else %}\n                                            <option value=\"3\">MODE_HH4</option>\n                                            {% endif %}\n                                        </select>\n                                    </td>\n                                </tr>\n                                {% endif %}\n                                {% endif %}\n                            </div>\n                        </form>\n                    </table>\n                </td>\n                <td colspan=2 style=\"width: 50%;\">\n                    {% if sc.isStereoCamActive %}\n                    <img src=\"{{ url_for('webcam.stereo_cam_feed') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recording_stereo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                </td>\n                <td style=\"width: 25%;\">\n                    <table>\n                        <tr>\n                            <td>\n                                {% if sc.isStereoCamActive == False %}\n                                <form method=\"post\" action=\"{{ url_for('webcam.start_stereo_cam') }}\">\n                                    <p style=\"margin-bottom: 0\"></p>\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Start\">\n                                </form>\n                                {% else %}\n                                <form method=\"post\" action=\"{{ url_for('webcam.stop_stereo_cam') }}\">\n                                    <p style=\"margin-bottom: 0\"></p>\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop\">\n                                </form>\n                                {% endif %}\n                            </td>\n                        </tr>\n                        <tr>\n                            <td>\n                                {% if sc.isStereoCamActive == False or ster.intentIdx != 1 %}\n                                <form method=\"post\" action=\"{{ url_for('webcam.start_record_stereo') }}\">\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Record\" disabled>\n                                </form>\n                                {% else %}\n                                {% if sc.isStereoCamRecording == False %}\n                                <form method=\"post\" action=\"{{ url_for('webcam.start_record_stereo') }}\">\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Record\">\n                                </form>\n                                {% else %}\n                                <form method=\"post\" action=\"{{ url_for('webcam.stop_record_stereo') }}\">\n                                    <input class=\"w3-button w3-black w3-round-xxlarge\" type=\"submit\" value=\"Stop Recording\">\n                                    <input class=\"w3-button w3-red w3-circle\" type=\"submit\" value=\" \">\n                                </form>\n                                {% endif %}\n                                {% endif %}\n                            </td>\n                        </tr>\n                    </table>\n                </td>            </tr>\n            <tr>\n                <td style=\"width: 25%;\">\n                    &nbsp;\n                </td>\n                <td colspan=2 style=\"width: 50%; text-align:center;\">\n                    Streaming URL: <a href=\"http://{{ g.hostname }}/stereo_feed\" target=\"_blank\">http://{{ g.hostname }}/stereo_feed</a>\n                </td>\n                <td style=\"width: 25%;\">\n                    &nbsp;\n                </td>\n            </tr>\n\n        </table>\n    </div>\n    {% if sc.lastCamTab == \"calibcam\" %}\n    <div id=\"calibcam\" class=\"camgroup\">\n    {% else %}\n    <div id=\"calibcam\" class=\"camgroup\" style=\"display:none\">\n    {% endif %}\n        <h2>Camera Calibration and Rectification</h2>\n        {% set camNumL = sc.activeCamera|string() %}\n        {% if str2 != None %}\n        {% set camNumR = str2.camnum|string() %}\n        {% else %}\n        {% set camNumR = None %}\n        {% endif %}\n        <table style=\"layout:fixed; width: 100%;\">\n            <tr>\n                <td style=\"width: 50%;\">\n                    {% if sc.isLiveStream %}\n                    <h3 style=\"margin: 0px;\">Active: {{ sc.activeCameraInfo}}</h3>\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;\">\n                    {% if str2 != None %}\n                    <h3 style=\"margin: 0px;\">{{ str2[\"camerainfo\"]}}</h3>\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width: 50%;\">\n                    {% if ster.calibPhotosOK[camNumL] == True or (ster.calibPhotosCount[camNumL] > 0 and ster.calibPhotoRecording == False) %}\n                    {% if ster.calibShowCorners == False %}\n                    <img src=\"{{ url_for('static', filename=ster.calibPhotos[camNumL][ster.calibPhotosIdx[camNumL]]) }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename=ster.calibPhotosCrn[camNumL][ster.calibPhotosIdx[camNumL]]) }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% else %}\n                    {% if sc.isLiveStream %}\n                    <img src=\"{{ url_for('home.video_feed') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Camera Live View\">\n                    {% else %}\n                    {% if sc.isVideoRecording %}\n                    {% if sc.recordAudio == False %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename='recordingvideo_sound.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% elif sc.isPhotoSeriesRecording %}\n                    <img src=\"{{ url_for('static', filename='recordingphotoseries.jpg') }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;\">\n                    {% if camNumR != None %}\n                    {% if ster.calibPhotosOK[camNumL] == True or (ster.calibPhotosCount[camNumL] > 0 and ster.calibPhotoRecording == False) %}\n                    {% if ster.calibShowCorners == False %}\n                    <img src=\"{{ url_for('static', filename=ster.calibPhotos[camNumR][ster.calibPhotosIdx[camNumR]]) }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% else %}\n                    <img src=\"{{ url_for('static', filename=ster.calibPhotosCrn[camNumR][ster.calibPhotosIdx[camNumR]]) }}\" class=\"w3-image\" style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage\" alt=\"Placeholder for video recording\">\n                    {% endif %}\n                    {% else %}\n                    {% if str2 != None %}\n                    <img src=\"{{ url_for('home.video_feed2') }}\" class=\"w3-image\"style=\"width: 100%; height: 350px; object-fit: scale-down\" id=\"liveviewimage2\" alt=\"Camera 2 Live View\">\n                    {% endif %}\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                {% if ster.calibPhotosOK[camNumL] == True or (ster.calibPhotosCount[camNumL] > 0 and ster.calibPhotoRecording == False) %}\n                <td style=\"width: 50%; text-align: center;\">\n                    {{ (ster.getCalibPhotosIdx(camNumL) + 1)|string() }} of {{ ster.calibPhotosTarget|string() }}  ({{ ster.calibPhotos[camNumL][ster.calibPhotosIdx[camNumL]] }})\n                </td>\n                <td style=\"width: 50%; text-align: center;\">\n                    {% if str2 != None %}\n                    {{ (ster.getCalibPhotosIdx(camNumR) + 1)|string() }} of {{ ster.calibPhotosTarget|string() }}  ({{ ster.calibPhotos[camNumR][ster.calibPhotosIdx[camNumR]] }})\n                    {% endif %}\n                </td>\n                {% else %}\n                <td style=\"width: 50%; text-align: center;\">\n                    &nbsp;\n                </td>\n                <td style=\"width: 50%; text-align: center;\">\n                    &nbsp;\n                </td>\n                {% endif %}\n            </tr>\n            <tr>\n                {% if ster.calibPhotosOK[camNumL] == True or (ster.calibPhotosCount[camNumL] > 0 and ster.calibPhotoRecording == False) %}\n                <td colspan=\"2\" style=\"width: 100%;\">\n                    <div style=\"display:inline-block\">\n                        <a href=\"{{ url_for('webcam.first_calib_photo') }}\" class=\"w3-button w3-sand\">&laquo;</a>\n                        <a href=\"{{ url_for('webcam.prev_calib_photo') }}\" class=\"w3-button w3-sand\">&lt;</a>\n                        <a href=\"{{ url_for('webcam.next_calib_photo') }}\" class=\"w3-button w3-sand\">&gt;</a>\n                        <a href=\"{{ url_for('webcam.last_calib_photo') }}\" class=\"w3-button w3-sand\">&raquo;</a>\n                        <p style=\"display:inline-block\">&nbsp;&nbsp;&nbsp;</p>\n                        <a href=\"{{ url_for('webcam.remove_calib_photo') }}\" class=\"w3-button w3-sand\">Remove</a>\n                        <p style=\"display:inline-block\">&nbsp;&nbsp;&nbsp;</p>\n                        <form id=\"displaycornersform\" style=\"display:inline-block\" method=\"post\" action=\"{{ url_for('webcam.display_corners') }}\">\n                            <label style=\"display:inline-block\" for=\"displaycorners\">Show corners:</label>\n                            {% if ster.calibShowCorners == True %}\n                            <input style=\"display:inline-block\" type=\"checkbox\" id=\"displaycorners\" name=\"displaycorners\" onchange=\"dosubmitDisplayCorners()\" value=\"1\" checked>\n                            {% else %}\n                            <input style=\"display:inline-block\" type=\"checkbox\" id=\"displaycorners\" name=\"displaycorners\" onchange=\"dosubmitDisplayCorners()\" value=\"1\">\n                            {% endif %}\n                        </form>\n                    </div>\n                </td>\n                {% else %}\n                <td colspan=\"2\" style=\"width: 100%;\">\n                    <div style=\"display:inline-block; height:52.5px\">\n                        &nbsp;\n                    </div>\n                </td>\n                {% endif %}\n            </tr>\n        </table>\n        <form id=\"calibsettingsform\" method=\"post\" action=\"{{ url_for('webcam.calib_settings') }}\">\n            <table>\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Type of pattern to be used for calibration.\n                        </span>\n                        <label for=\"calibpattern\">Calibration Pattern:</label>\n                    </td>\n                    <td>\n                        <select name=\"calibpattern\" id=\"calibpattern\"\n                            onchange=\"confirmCalibSettings('calibsettingsform')\">\n                            {% for pattern in ster.calibrationPatterns %}\n                            {% if ster.calibPatternIdx == loop.index -1 %}\n                            <option value=\"{{ loop.index -1 }}\" selected>{{ ster.calibrationPatterns[loop.index-1] }}</option>\n                            {% else %}\n                            <option value=\"{{ loop.index -1 }}\">{{ ster.calibrationPatterns[loop.index -1] }}</option>\n                            {% endif %}\n                            {% endfor %}\n                        </select>\n                    </td>\n                    <td>\n                        <a href=\"{{ ster.calibrationPatternRefs[ster.calibPatternIdx] }}\" target=\"_blank\">Pattern Reference</a>\n                    </td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            Number of intersections in x and y direction of the calibration pattern.\n                        </span>\n                        <label for=\"calibpatternsize\">Pattern Size:</label>\n                    </td>\n                    <td>\n                        <div style=\"display:inline-block\">\n                            <input type=\"number\" id=\"calibpatternsize\" name=\"calibpatternsize\" min=\"2\" max=\"99\" step=\"1\" value=\"{{ ster.calibPatternSize[0] }}\" onchange=\"confirmCalibSettings('calibsettingsform')\">\n                            &nbsp;x&nbsp;\n                            <input type=\"number\" id=\"calibpatternsizey\" name=\"calibpatternsizey\" min=\"2\" max=\"99\" step=\"1\" value=\"{{ ster.calibPatternSize[1] }}\" onchange=\"confirmCalibSettings('calibsettingsform')\">\n                        </div>\n                    </td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            The number of pattern photos required for calibration.\n                        </span>\n                        <label for=\"calibphotostarget\">Number of Photos required:</label>\n                    </td>\n                    <td>\n                        <input type=\"number\" id=\"calibphotostarget\" name=\"calibphotostarget\" min=\"5\" max=\"99\" step=\"1\" value=\"{{ ster.calibPhotosTarget }}\" onchange=\"doSubmit('calibsettingsform')\">\n                    </td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                            The number of pattern photos taken.\n                        </span>\n                        <label for=\"calibphotostaken\">Number of Photos taken:</label>\n                    </td>\n                    <td>\n                        <input type=\"number\" id=\"calibphotostaken\" name=\"calibphotostaken\" min=\"0\" max=\"99\" step=\"1\" value=\"{{ ster.getCalibPhotosCount(camNumL) }}\" disabled>\n                    </td>\n                </tr>\n                <tr>\n                    <td class=\"w3-tooltip\">\n                        <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                           Controls scaling of the rectified image.<br>\n                           \"Valid Pixels Only\": Rectified image will show only valid pixels (no black areas)<br>\n                           \"All Pixels\": Rectified image will show all pixels (may result in black areas)<br>\n                        </span>\n                        <label for=\"rectifyscale\">Rectify Scale:</label>\n                    </td>\n                    <td>\n                        <select name=\"rectifyscale\" id=\"rectifyscale\"\n                            onchange=\"doSubmit('calibsettingsform')\">\n                            {% if ster.rectifyScale == 0 %}\n                            <option value=\"0\" selected>Valid Pixels Only</option>\n                            <option value=\"1\">All Pixels</option>\n                            {% else %}\n                            <option value=\"0\">Valid Pixels Only</option>\n                            <option value=\"1\" selected>All Pixels</option>\n                            {% endif %}\n                        </select>\n                    </td>\n                </tr>\n            </table>\n        </form>\n        <table>\n            <tr>\n                <td>\n                    <p style=\"margin-bottom: 0\"></p>\n                    <form id=\"resetcalibphotos\" method=\"post\" action=\"{{ url_for('webcam.reset_calib_photos') }}\">\n                        {% if ster.hasCalibPhotos(camNumL, camNumR) == True and ster.calibPhotoRecording == False %}\n                        <input class=\"w3-button w3-black\" type=\"submit\" onclick=\"confirmReset('resetcalibphotos')\" value=\"Reset Calibration Photos\">\n                        {% else %}\n                        <input class=\"w3-button w3-black\" type=\"submit\" onclick=\"confirmReset('resetcalibphotos')\" value=\"Reset Calibration Photos\" disabled>\n                        {% endif %}\n                    </form>\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <p style=\"margin-bottom: 0\"></p>\n                    {% if ster.calibPhotoRecording == True %}\n                    <form id=\"stoptakecalibphotos\" method=\"post\" action=\"{{ url_for('webcam.stop_take_calib_photos') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Stop taking Pattern Photos\">\n                    </form>\n                    {% else %}\n                    {% if ster.isCalibPhotosOK(camNumL, camNumR) == True %}\n                    <form id=\"starttakecalibphotos\" method=\"post\" action=\"{{ url_for('webcam.start_take_calib_photos') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Start taking Pattern Photos\" disabled>\n                    </form>\n                    {% else %}\n                    {% if ster.getCalibPhotosCount(camNumL) > 0 %}\n                    <form id=\"starttakecalibphotos\" method=\"post\" action=\"{{ url_for('webcam.start_take_calib_photos') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Continue taking Pattern Photos\">\n                    </form>\n                    {% else %}\n                    <form id=\"starttakecalibphotos\" method=\"post\" action=\"{{ url_for('webcam.start_take_calib_photos') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" onclick=\"confirmCalibPhotoTaking('starttakecalibphotos')\" value=\"Start taking Pattern Photos\">\n                    </form>\n                    {% endif %}\n                    {% endif %}\n                    {% endif %}\n                </td>\n                <td>\n                    <p style=\"margin-bottom: 0\"></p>\n                    {% if ster.calibPhotoRecording == True %}\n                    {% if ster.calibPhotoRecordingMsg.startswith(\"Taking\") == True %}\n                    <p class=\"w3-button w3-yellow\">{{ ster.calibPhotoRecordingMsg }}</p>\n                    {% elif ster.calibPhotoRecordingMsg.startswith(\"No\") == True %}\n                    <p class=\"w3-button w3-red\">{{ ster.calibPhotoRecordingMsg }}</p>\n                    {% else %}\n                    <p class=\"w3-button w3-green\">{{ ster.calibPhotoRecordingMsg }}</p>\n                    {% endif %}\n                    {% endif %}\n                </td>\n            </tr>\n            <tr>\n                <td>\n                    <p>&nbsp;</p>\n                    {% if ster.isCalibPhotosOK(camNumL, camNumR) == True and camNumR != None %}\n                    <form id=\"calibrate\" method=\"post\" action=\"{{ url_for('webcam.calibrate_cameras') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" onclick=\"confirmCalibration('calibratecameras')\" value=\"Calibrate Cameras\">\n                    </form>\n                    {% else %}\n                    <form id=\"calibratecameras\" method=\"post\" action=\"{{ url_for('webcam.calibrate_cameras') }}\">\n                        <input class=\"w3-button w3-black\" type=\"submit\" value=\"Calibrate Cameras\" disabled>\n                    </form>\n                    {% endif %}\n                    <p style=\"margin-bottom:0\">&nbsp;</p>\n                </td>\n                <td>\n                    <p>&nbsp;</p>\n                    {% if ster.calibDate %}\n                    Last successful calibration: {{ ster.calibDate }}\n                    {% else %}\n                    No successful calibration yet.\n                    {% endif %}\n                    <p style=\"margin-bottom:0\">&nbsp;</p>\n                </td>\n            </tr>\n        </table>\n        {% if ster.isCalibCamerasOK(camNumL, camNumR) == True %}\n        <table style=\"layout:fixed; width: 100%;\">\n            <tr>\n                <td class=\"w3-tooltip\">\n                    <span style=\"position:absolute;left:0;bottom:30px\" class=\"w3-text w3-tag\">\n                        This is a measure for the calibration quality.<br>\n                        &lt; 0.5 px: excellent<br>\n                        0.5 - 1.0 px: good<br>\n                        &gt; 1.0 px: poor\n                    </span>\n                    <label for=\"calibrmsreproerrorl\">Overall RMS Re-Projection Error:</label>\n                </td>\n                <td>\n                </td>\n            </tr>\n            <tr>\n                <td style=\"width: 50%;text-align:center;\">\n                    {% if ster.calibRmsReproError[camNumL] < 0.5 %}\n                    <p class=\"w3-button w3-green\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumL] }}</p>\n                    {% elif ster.calibRmsReproError[camNumL] <= 1.0 %}\n                    <p class=\"w3-button w3-yellow\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumL] }}</p>\n                    {% else %}\n                    <p class=\"w3-button w3-red\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumL] }}</p>\n                    {% endif %}\n                </td>\n                <td style=\"width: 50%;text-align:center;\">\n                    {% if ster.calibRmsReproError[camNumR] < 0.5 %}\n                    <p class=\"w3-button w3-green\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumR] }}</p>\n                    {% elif ster.calibRmsReproError[camNumR] <= 1.0 %}\n                    <p class=\"w3-button w3-yellow\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumR] }}</p>\n                    {% else %}\n                    <p class=\"w3-button w3-red\" style=\"margin-top:0\">{{ ster.calibRmsReproError[camNumR] }}</p>\n                    {% endif %}\n                </td>\n            </tr>\n        </table>\n        {% endif %}\n    </div>\n    <script>\n        function openCamTab(infoTabName, infoTabButton) {\n            var i;\n            var x = document.getElementsByClassName(\"camgroup\");\n            for (i = 0; i < x.length; i++) {\n                x[i].style.display = \"none\";\n            }\n            document.getElementById(infoTabName).style.display = \"block\";\n\n            var b = document.getElementsByClassName(\"cammenu\");\n            for (i = 0; i < b.length; i++) {\n                b[i].classList = \"w3-bar-item w3-button cammenu\";\n            }\n            document.getElementById(infoTabButton).classList = \"w3-bar-item w3-button cammenu w3-light-green\";\n        }\n        function doSubmit(form) {\n            document.getElementById(form).submit();\n        }\n        function confirmAction(form, action, needsConfirm) {\n            //console.log(\"confirmExecution - needsConfirm=\", needsConfirm);\n            if (needsConfirm == \"True\") {\n                if (confirm(\"Do you want to execute the following action?\\n\" + action)) {\n                    document.getElementById(form).method = \"post\";\n                    document.getElementById(form).submit();\n                } else {\n                    document.getElementById(form).method = \"get\";\n                }\n            }\n        }\n        function dosubmitApplyCalibRectify(){\n            document.getElementById(\"stereodispform\").submit();\n        }\n        function dosubmitDisplayCorners(){\n            document.getElementById(\"displaycornersform\").submit();\n        }\n        function dosubmitIntent(){\n            sta = document.getElementById(\"stereoalgo\");\n            if (sta != null) {\n                sta.value = \"\";\n                sta.disabled = true;\n            }\n            stc = document.getElementById(\"stereoconfigparams\");\n            stc.display = \"none\";\n            document.getElementById(\"stereoconfigform\").submit();\n        }\n        function dosubmitIntentAlgo(){\n            tt = document.getElementById(\"stereoconfigparams\");\n            tt.display = \"none\";\n            document.getElementById(\"stereoconfigform\").submit();\n        }\n        function dosubmitStereoParam(){\n            document.getElementById(\"stereoconfigform\").submit();\n        }\n        function confirmSwitchCamera(form) {\n            if (confirm(\"You have made changes to the active camera configuration\\nThese will get lost when switching cameras, unless you Save Active Camera Settings.\\n\\nDo you want still switch cameras?\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmReset(form) {\n            if (confirm(\"Do you want to reset camara calibration?\\nThis will remove all available calibration photos!\")) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmCalibPhotoTaking(form) {\n            if (confirm(\n                \"Show the calibration pattern to your camera(s)!\\n\" + \n                \"It must be fully visible in the camera (or in both cameras) frame.\\n\\n\" +\n                \"When the pattern is recognized, a photo will be taken\\n\" +\n                \"about every 2 seconds, until the required number is reached.\\n\\n\" +\n                \"Change the orientation from time to time \\n\" +\n                \"and wait until all photos have been taken,\"\n               )) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmCalibration(form) {\n            if (confirm(\n                \"Do you want to run the camera calibration?\\n\\n\" +\n                \"Existing calibration data will be overwritten.\"\n               )) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function confirmCalibSettings(form) {\n            if (confirm(\n                \"Changing the Calibration Pattern or the Pattern Size will reset the calibration data\\n\" +\n                \"and remove all existing calibration photos.\"\n               )) {\n                document.getElementById(form).method = \"post\";\n                document.getElementById(form).submit();\n            } else {\n                document.getElementById(form).method = \"get\";\n            }\n        }\n        function onlineHelp() {\n            window.open(\"{{ sc.getBaseHelpUrl() }}/Cam/\");\n        }\n    </script>\n    {% if sc.curMenu == \"webcam\" and sc.lastCamTab == \"calibcam\"  and ster.calibPhotoRecording == True %}\n    <script>\n        setTimeout(() => {\n            window.location.reload();\n        }, 2000); \n    </script>\n    {% endif %}\n{% endblock %}"
  },
  {
    "path": "raspiCamSrv/trigger.py",
    "content": "from flask import Blueprint, Response, flash, g, redirect, render_template, request, url_for\nfrom flask import send_file, send_from_directory\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg, Trigger, Action, ServerConfig, TriggerConfig\nfrom raspiCamSrv.motionDetector import MotionDetector\nfrom raspiCamSrv.triggerHandler import TriggerHandler\nfrom raspiCamSrv.version import version\nfrom _thread import get_ident\nfrom datetime import datetime\nfrom datetime import timedelta\nimport ast\nimport copy\nimport os\n\nfrom raspiCamSrv.auth import login_required\nimport logging\n\nbp = Blueprint(\"trigger\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n@bp.route(\"/trigger\")\n@login_required\ndef trigger():\n    logger.debug(\"In trigger\")\n    cam = Camera().cam\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    if tc.evStart == None:\n        tc.evStart = datetime.now()\n    if tc.calStart == None:\n        tc.calStart = datetime.now()\n    sc.curMenu = \"trigger\"\n    if sc.noCamera == True:\n        if sc.lastTriggerTab == \"trgmotion\" \\\n        or sc.lastTriggerTab == \"trgaction\":\n            sc.lastTriggerTab = \"trgcontrol\"\n    tmp = {}\n    # logger.debug(\"event list: %s\", tc.eventList)\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n\ndef trg_gen(camera):\n    \"\"\"Video streaming generator function.\"\"\"\n    # logger.debug(\"Thread %s: In trg_gen\", get_ident())\n    yield b\"--frame\\r\\n\"\n    while True:\n        frame, frameRaw = camera.get_frame()\n        if frame is not None:\n            # logger.debug(\"Thread %s: trg_gen - Got frame of length %s\", get_ident(), len(frame))\n            yield b\"Content-Type: image/jpeg\\r\\n\\r\\n\" + frame + b\"\\r\\n--frame\\r\\n\"\n\n@bp.route(\"/live_view_feed\")\n@login_required\ndef trg_live_view_feed():\n    logger.debug(\n        \"Thread %s: In trg_live_view_feed - client IP: %s\", get_ident(), request.remote_addr\n    )\n    sc = CameraCfg().serverConfig\n    sc.registerStreamingClient(request.remote_addr, \"live_view\", get_ident())\n    Camera().startLiveStream()\n    return Response(trg_gen(Camera()), mimetype=\"multipart/x-mixed-replace; boundary=frame\")\n\n@bp.route(\"/trgcontrol\", methods=(\"GET\", \"POST\"))\n@login_required\ndef trgcontrol():\n    logger.debug(\"In trgcontrol\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcontrol\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = None\n        if request.form.get(\"triggerbymotion\") is None:\n            tc.triggeredByMotion = False\n        else:\n            tc.triggeredByMotion = True\n        if request.form.get(\"triggerbyevents\") is None:\n            tc.triggeredByEvents = False\n        else:\n            tc.triggeredByEvents = True\n        if request.form.get(\"triggervideo\") is None:\n            tc.actionVideo = False\n        else:\n            tc.actionVideo = True\n        if request.form.get(\"triggerphoto\") is None:\n            tc.actionPhoto = False\n        else:\n            tc.actionPhoto = True\n        if tc.actionVideo:\n            if not tc.actionPhoto:\n                err = \"Together with videos, you must capture at least one photo.\"\n                tc.actionPhoto = True\n        if request.form.get(\"triggernotify\") is None:\n            tc.actionNotify = False\n        else:\n            tc.actionNotify = True\n        for key, value in tc.operationWeekdays.items():\n            if request.form.get(\"opweekday\" + key) is None:\n                tc.operationWeekdays[key] = False\n            else:\n                tc.operationWeekdays[key] = True\n        opStartStr = request.form[\"opstart\"]\n        tc.operationStartStr = opStartStr\n        opEndStr = request.form[\"opend\"]\n        tc.operationEndStr = opEndStr\n        if request.form.get(\"opautostart\") is None:\n            tc.operationAutoStart = False\n        else:\n            tc.operationAutoStart = True\n        if sc.noCamera == False:\n            detectDelay = int(request.form[\"opdelay\"])\n            tc.detectionDelaySec = detectDelay\n            detectPause = int(request.form[\"oppause\"])\n            tc.detectionPauseSec = detectPause\n        retPeriod = int(request.form[\"retentionperiod\"])\n        tc.retentionPeriod = retPeriod\n\n        if sc.noCamera == False:\n            if tc.triggeredByEvents == True \\\n            and tc.triggeredByMotion == False:\n                tc.actionVideo = False\n                tc.actionPhoto = False\n                tc.actionNotify = False\n                err = \"Actions have been deactivated because they are currently supported only in combination with Motion Detection\"\n        if err:\n            flash(err)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger Control changed\")\n        logger.debug(\"In trgcontrol - done\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/motion\", methods=(\"GET\", \"POST\"))\n@login_required\ndef motion():\n    logger.debug(\"In motion\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    logger.debug(\"tc.motionDetectAlgo: %s\", tc.motionDetectAlgo)\n    sc.lastTriggerTab = \"trgmotion\"\n    tmp = {}\n    if request.method == \"POST\":\n        algo = int(request.form[\"motiondetectionalgo\"])\n        tc.motionDetectAlgo = algo\n        if not request.form.get(\"msdthreshold\") is None:\n            msdThreshold = int(request.form[\"msdthreshold\"])\n            tc.msdThreshold = msdThreshold\n        if not request.form.get(\"bboxthreshold\") is None:\n            bboxThreshold = int(request.form[\"bboxthreshold\"])\n            tc.bboxThreshold = bboxThreshold\n        if not request.form.get(\"nmsthreshold\") is None:\n            nmsThreshold = float(request.form[\"nmsthreshold\"])\n            tc.nmsThreshold = nmsThreshold\n        if not request.form.get(\"motionthreshold\") is None:\n            motionThreshold = int(request.form[\"motionthreshold\"])\n            tc.motionThreshold = motionThreshold\n        if not request.form.get(\"backsubmodel\") is None:\n            backSubModel = int(request.form[\"backsubmodel\"])\n            tc.backSubModel = backSubModel\n        if request.form.get(\"videobboxes\") is None:\n            tc.videoBboxes = False\n        else:\n            tc.videoBboxes = True\n        if request.form.get(\"photorois\") is None:\n            tc.photoRois = False\n        else:\n            tc.photoRois = True\n        if request.form.get(\"useroi\") is None:\n            tc.useRoI = False\n            roiWindowsStr = \"()\"\n            tc.regionOfInterestStr = roiWindowsStr\n            roniWindowsStr = \"()\"\n            tc.regionOfNoInterestStr = roniWindowsStr\n        else:\n            Camera().startLiveStream()\n            tc.useRoI = True\n            roiWindowsStr = request.form[\"regionofinterest\"]\n            tc.regionOfInterestStr = roiWindowsStr\n            roniWindowsStr = request.form[\"regionofnointerest\"]\n            tc.regionOfNoInterestStr = roniWindowsStr\n            if len(tc.regionOfInterestStr) == 0 \\\n            and len(tc.regionOfNoInterestStr) == 0:\n                tc.useRoI = False\n        if sc.isTriggerTesting == True:\n            msg = \"Please restart Motion Detection test to use the changed parameters!\"\n            flash(msg)\n        else:\n            if sc.isTriggerRecording == True:\n                msg = \"Please restart motion detection to use the changed parameters!\"\n                flash(msg)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger Motion settings changed\")\n        cfg.streamingCfgInvalid = True\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/test_motion_detection\", methods=(\"GET\", \"POST\"))\n@login_required\ndef test_motion_detection():\n    logger.debug(\"In test_motion_detection\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgmotion\"\n    tmp = {}\n    if request.method == \"POST\":\n        if tc.motionDetectAlgo != 1:\n            if tc.triggeredByMotion:\n                if sc.isTriggerRecording == True:\n                    MotionDetector().stopMotionDetection()\n                    sc.isTriggerRecording = False\n                    sc.isTriggerWaiting = False\n                err = None\n                sc.isTriggerTesting = True\n                MotionDetector().setAlgorithm()\n                MotionDetector().startMotionDetection()\n                if sc.error:\n                    logger.debug(\"In motion detection - test not started because of error\")\n                    msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n                    flash(msg)\n                    if sc.error2:\n                        flash(sc.error2)\n                    err = None\n                elif tc.error:\n                    logger.debug(\"In motion detection - test not started because of error\")\n                    msg = \"Error in \" + tc.errorSource + \": \" + tc.error\n                    flash(msg)\n                    if tc.error2:\n                        flash(tc.error2)\n                    err = None\n                else:\n                    sc.isTriggerRecording = True\n                    logger.debug(\"In motion detection - test started\")\n            else:\n                err = \"Motion detection is not activated activated\"\n            if err:\n                flash(err)\n        else:\n            msg = f\"For this Motion Detection Algoritm there is no test. Current framerate is {round(tc.motionTestFramerate, 1)} fps\"\n            flash(msg)\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/stop_test_motion_detection\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_test_motion_detection():\n    logger.debug(\"In stop_test_motion_detection\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgmotion\"\n    tmp = {}\n    if request.method == \"POST\":\n        if sc.isTriggerRecording:\n            MotionDetector().stopMotionDetection()\n            sc.isTriggerTesting = False\n            sc.isTriggerRecording = False\n            logger.debug(\"In motion - detection stopped\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/test_frame1_feed\")\n# @login_required\ndef test_frame1_feed():\n    #logger.debug(\"Thread %s: In test_frame1_feed\", get_ident())\n    Camera().startLiveStream()\n    md = MotionDetector()\n    return Response(gen_testFrame1(md),\n                    mimetype='multipart/x-mixed-replace; boundary=frame')\n\ndef gen_testFrame1(motionDetector):\n    \"\"\"Video streaming generator function.\"\"\"\n    #logger.debug(\"Thread %s: In gen_testFrame1\", get_ident())\n    yield b'--frame\\r\\n'\n    while True:\n        frame = motionDetector.get_testFrame1()\n        if frame is not None:\n            #logger.debug(\"Thread %s: gen_gray - Got frame of length %s\", get_ident(), len(frame))\n            yield b'Content-Type: image/jpeg\\r\\n\\r\\n' + frame + b'\\r\\n--frame\\r\\n'\n\n@bp.route(\"/test_frame2_feed\")\n# @login_required\ndef test_frame2_feed():\n    #logger.debug(\"Thread %s: In test_frame2_feed\", get_ident())\n    Camera().startLiveStream()\n    md = MotionDetector()\n    return Response(gen_testFrame2(md),\n                    mimetype='multipart/x-mixed-replace; boundary=frame')\n\ndef gen_testFrame2(motionDetector):\n    \"\"\"Video streaming generator function.\"\"\"\n    #logger.debug(\"Thread %s: In gen_testFrame2\", get_ident())\n    yield b'--frame\\r\\n'\n    while True:\n        frame = motionDetector.get_testFrame2()\n        if frame is not None:\n            #logger.debug(\"Thread %s: gen_gray - Got frame of length %s\", get_ident(), len(frame))\n            yield b'Content-Type: image/jpeg\\r\\n\\r\\n' + frame + b'\\r\\n--frame\\r\\n'\n\n@bp.route(\"/test_frame3_feed\")\n# @login_required\ndef test_frame3_feed():\n    #logger.debug(\"Thread %s: In test_frame3_feed\", get_ident())\n    Camera().startLiveStream()\n    md = MotionDetector()\n    return Response(gen_testFrame3(md),\n                    mimetype='multipart/x-mixed-replace; boundary=frame')\n\ndef gen_testFrame3(motionDetector):\n    \"\"\"Video streaming generator function.\"\"\"\n    #logger.debug(\"Thread %s: In gen_testFrame3\", get_ident())\n    yield b'--frame\\r\\n'\n    while True:\n        frame = motionDetector.get_testFrame3()\n        if frame is not None:\n            #logger.debug(\"Thread %s: gen_gray - Got frame of length %s\", get_ident(), len(frame))\n            yield b'Content-Type: image/jpeg\\r\\n\\r\\n' + frame + b'\\r\\n--frame\\r\\n'\n\n@bp.route(\"/test_frame4_feed\")\n# @login_required\ndef test_frame4_feed():\n    #logger.debug(\"Thread %s: In test_frame4_feed\", get_ident())\n    Camera().startLiveStream()\n    md = MotionDetector()\n    return Response(gen_testFrame4(md),\n                    mimetype='multipart/x-mixed-replace; boundary=frame')\n\ndef gen_testFrame4(motionDetector):\n    \"\"\"Video streaming generator function.\"\"\"\n    #logger.debug(\"Thread %s: In gen_testFrame4\", get_ident())\n    yield b'--frame\\r\\n'\n    while True:\n        frame = motionDetector.get_testFrame4()\n        if frame is not None:\n            #logger.debug(\"Thread %s: gen_gray - Got frame of length %s\", get_ident(), len(frame))\n            yield b'Content-Type: image/jpeg\\r\\n\\r\\n' + frame + b'\\r\\n--frame\\r\\n'\n\n@bp.route(\"/action\", methods=(\"GET\", \"POST\"))\n@login_required\ndef action():\n    logger.debug(\"In action\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    logger.debug(\"tc.actionVR: %s\", tc.actionVR)\n    sc.lastTriggerTab = \"trgaction\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = None\n        vr = int(request.form[\"actionvr\"])\n        logger.debug(\"vr: %s\", vr)\n        if vr == 1:\n            tc.actionVR = vr\n        else:\n            err = \"Circular output is currently not supported\"\n        cbs = int(request.form[\"actioncircsize\"])\n        tc.actionCircSize = cbs\n        dur = int(request.form[\"actionvideoduration\"])\n        tc.actionVideoDuration = dur\n        pb = int(request.form[\"actionphotoburst\"])\n        tc.actionPhotoBurst = pb\n        pbd = int(request.form[\"actionphotoburstdelaysec\"])\n        tc.actionPhotoBurstDelaySec = pbd\n        if err:\n            flash(err)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger Camera Actions changed\")\n        cfg.streamingCfgInvalid = True\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/notify\", methods=(\"GET\", \"POST\"))\n@login_required\ndef notify():\n    logger.debug(\"In notify\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgnotify\"\n    scr = cfg.secrets\n    tmp = {}\n    if request.method == \"POST\":\n        err = \"\"\n        tc.notifyHost = request.form[\"notifyhost\"]\n        tc.notifyPort = int(request.form[\"notifyport\"])\n        tc.notifyFrom = request.form[\"notifyfrom\"]\n        tc.notifyTo = request.form[\"notifyto\"]\n        tc.notifySubject = request.form[\"notifysubject\"]\n        if tc.notifySubject == \"\":\n            err = \"Please enter 'Subject'\"\n        if tc.notifyTo == \"\":\n            err = \"Please enter 'To e-Mail'\"\n        if tc.notifyFrom == \"\":\n            err = \"Please enter 'From e-Mail'\"\n        if tc.notifyHost == \"\":\n            err = \"Please enter 'SMTP Server'\"\n        if request.form.get(\"notifyauthenticate\") is None:\n            tc.notifyAuthenticate = False\n        else:\n            tc.notifyAuthenticate = True\n        if tc.notifyAuthenticate == True:\n            user = request.form[\"notifyuser\"]\n            pwd = request.form[\"notifypassword\"]\n            if user == \"\" or pwd == \"\":\n                if tc.notifyConOK:\n                    if user == \"\":\n                        user = scr.notifyUser\n                    if pwd == \"\":\n                        pwd = scr.notifyPwd\n            if user == \"\" or pwd == \"\":\n                err = \"Please provide 'User' and 'Password'\"\n            else:\n                if request.form.get(\"notifysavepwd\") is None:\n                    tc.notifySavePwd = False\n                else:\n                    tc.notifySavePwd = True\n                tc.notifyPwdPath = request.form[\"notifypwdpath\"]\n                if tc.notifySavePwd == True:\n                    if tc.notifyPwdPath == \"\":\n                        err = \"Please provide 'Credentials File Path'\"\n                else:\n                    tc.notifyPwdPath = \"\"\n\n        if err != \"\":\n            tc.notifyConOK = False\n            flash(err)\n        else:\n            if request.form.get(\"notifyusessl\") is None:\n                tc.notifyUseSSL = False\n            else:\n                tc.notifyUseSSL = True\n            if tc.notifyAuthenticate == False:\n                user = \"\"\n                pwd = \"\"\n                tc.notifySavePwd = False\n                tc.notifyPwdPath = \"\"\n            tc.notifyPause = int(request.form[\"notifypause\"])\n            if request.form.get(\"notifyincludevideo\") is None:\n                tc.notifyIncludeVideo = False\n            else:\n                tc.notifyIncludeVideo = True\n            if request.form.get(\"notifyincludephoto\") is None:\n                tc.notifyIncludePhoto = False\n            else:\n                tc.notifyIncludePhoto = True\n            if tc.notifyConOK:\n                if user == \"\":\n                    user = scr.notifyUser\n                if pwd == \"\":\n                    pwd = scr.notifyPwd\n            (user, pwd, err) = tc.checkNotificationRecipient(user=user, pwd=pwd)\n            if err != \"\":\n                flash(err)\n            else:\n                scr.notifyUser = user\n                scr.notifyPwd = pwd\n                flash(\"Connection test successful\")\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger Notification settings changed\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/start_triggered_capture\", methods=(\"GET\", \"POST\"))\n@login_required\ndef start_triggered_capture():\n    logger.debug(\"In start_triggered_capture\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcontrol\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = None\n        if tc.triggeredByMotion \\\n        or tc.triggeredByEvents:\n            if tc.triggeredByMotion:\n                MotionDetector().setAlgorithm()\n                MotionDetector().startMotionDetection()\n            if tc.triggeredByEvents:\n                TriggerHandler().start()\n            if sc.error:\n                logger.debug(\"In motion detection not started because of error\")\n                msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n                flash(msg)\n                if sc.error2:\n                    flash(sc.error2)\n                err = None\n            elif tc.error:\n                logger.debug(\"In motion detection not started because of error\")\n                msg = \"Error in \" + tc.errorSource + \": \" + tc.error\n                flash(msg)\n                if tc.error2:\n                    flash(tc.error2)\n                err = None\n            else:\n                if tc.triggeredByMotion:\n                    sc.isTriggerRecording = True\n                if tc.triggeredByEvents:\n                    sc.isEventhandling = True\n                logger.debug(\"In motion detection started\")\n        else:\n            err = \"There is no trigger activated\"\n        if err:\n            flash(err)\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/stop_triggered_capture\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_triggered_capture():\n    logger.debug(\"In stop_triggered_capture\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcontrol\"\n    tmp = {}\n    if request.method == \"POST\":\n        if sc.isTriggerRecording:\n            MotionDetector().stopMotionDetection()\n            sc.isTriggerRecording = False\n            sc.isTriggerWaiting = False\n            logger.debug(\"In motion - detection stopped\")\n        if sc.isEventhandling:\n            TriggerHandler().stop()\n            sc.isEventhandling = False\n            logger.debug(\"In Eventhandling stopped\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/prev_month\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_month():\n    logger.debug(\"In prev_month\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=-168)\n    tc.evStartMidnight()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/prev_day\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_day():\n    logger.debug(\"In prev_day\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=-24)\n    tc.evStartMidnight()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/set_date\", methods=(\"GET\", \"POST\"))\n@login_required\ndef set_date():\n    logger.debug(\"In set_date\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    if request.method == \"POST\":\n        tc.evStartDateStr = request.form.get(\"evstartdate\")\n        tc.evStartMidnight()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/next_day\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_day():\n    logger.debug(\"In next_day\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=24)\n    tc.evStartMidnight()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/next_month\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_month():\n    logger.debug(\"In next_month\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=168)\n    tc.evStartMidnight()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/prev_hor\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_hor():\n    logger.debug(\"In prev_hor\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=-1)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/prev_quarter\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_quarter():\n    logger.debug(\"In prev_quarter\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(minutes=-15)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/set_time\", methods=(\"GET\", \"POST\"))\n@login_required\ndef set_time():\n    logger.debug(\"In set_time\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    if request.method == \"POST\":\n        tc.evStartTimeStr = request.form.get(\"evstarttime\")\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/next_quarter\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_quarter():\n    logger.debug(\"In next_quarter\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(minutes=15)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/next_hour\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_hour():\n    logger.debug(\"In next_hour\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = tc.evStart + timedelta(hours=1)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/events_now\", methods=(\"GET\", \"POST\"))\n@login_required\ndef events_now():\n    logger.debug(\"In events_now\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tc.evStart = datetime.now()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/event_include_video\", methods=(\"GET\", \"POST\"))\n@login_required\ndef event_include_video():\n    logger.debug(\"In event_include_video\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tmp = {}\n    if request.method == \"POST\":\n        if request.form.get(\"evincludevideo\") is None:\n            tc.evIncludeVideo = False\n        else:\n            tc.evIncludeVideo = True\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/event_include_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef event_include_photo():\n    logger.debug(\"In event_include_photo\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    tmp = {}\n    if request.method == \"POST\":\n        if request.form.get(\"evincludephoto\") is None:\n            tc.evIncludePhoto = False\n        else:\n            tc.evIncludePhoto = True\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/do_refresh\", methods=(\"GET\", \"POST\"))\n@login_required\ndef do_refresh():\n    logger.debug(\"In do_refresh\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgevents\"\n    if request.method == \"POST\":\n        pass\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/prev_cal_month\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_cal_month():\n    logger.debug(\"In prev_cal_month\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    sc.lastTriggerTab = \"trgcalendar\"\n    tc.calStart = tc.calStart + timedelta(hours=-24)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/set_cal_month\", methods=(\"GET\", \"POST\"))\n@login_required\ndef set_cal_month():\n    logger.debug(\"In set_cal_month\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    if request.method == \"POST\":\n        tc.calStartDateStr = request.form.get(\"setcalmonth\")\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/next_cal_month\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_cal_month():\n    logger.debug(\"In next_cal_month\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    tc.calStart = tc.calStart + timedelta(hours=750)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/calendar_now\", methods=(\"GET\", \"POST\"))\n@login_required\ndef calendar_now():\n    logger.debug(\"In calendar_now\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    tc.calStart = datetime.now()\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/do_refresh_calendar\", methods=(\"GET\", \"POST\"))\n@login_required\ndef do_refresh_calendar():\n    logger.debug(\"In do_refresh_calendar\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    if request.method == \"POST\":\n        pass\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/calendar_goto\", methods=(\"GET\", \"POST\"))\n@login_required\ndef calendar_goto():\n    logger.debug(\"In calendar_goto\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    if request.method == \"POST\":\n        day = request.form.get(\"selectedday\")\n        logger.debug(\"selected day: %s\", day)\n        tc.evStartDateStr = day\n        tc.evStartTimeStr = \"00:00:00\"\n        logger.debug(\"evStart: %s\", tc.evStart)\n        sc.lastTriggerTab = \"trgevents\"\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/do_cleanup\", methods=(\"GET\", \"POST\"))\n@login_required\ndef do_cleanup():\n    logger.debug(\"In do_cleanup\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    if request.method == \"POST\":\n        err = None\n        if sc.isTriggerRecording:\n            err = \"You need to stop trigger recording before cleanup!\"\n        if not err:\n            try:\n                tc.cleanupEvents()\n                err = \"Cleanup successfull\"\n            except Exception as e:\n                err = \"Cleanup error: \" + str(e)\n        flash(err)\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/do_download_log\", methods=(\"GET\", \"POST\"))\n@login_required\ndef do_download_log():\n    logger.debug(\"In do_download_log\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgcalendar\"\n    if request.method == \"POST\":\n        err = None\n        fp = tc.logFilePath\n        (path, file) = os.path.split(fp)\n        msg = f\"Downloading {file}\"\n        flash(msg)\n        return send_file(\n            fp,\n            as_attachment=True,\n            download_name=file\n        )\n    return redirect(url_for(\"trigger.trigger\"))\n\n@bp.route(\"/new_trigger\", methods=(\"GET\", \"POST\"))\n@login_required\ndef new_trigger():\n    logger.debug(\"In new_trigger\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgtriggers\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = \"\"\n        triggerId = None\n        if err == \"\":\n            triggerSource = request.form[\"triggersource\"]\n            logger.debug(\"trigger.new_trigger - triggerSource=%s\", triggerSource)\n            tmp[\"triggerSource\"] = triggerSource\n        if err == \"\":\n            if not request.form.get(\"triggerdevice\") is None:\n                triggerDevice = request.form[\"triggerdevice\"]\n                logger.debug(\"trigger.new_trigger - triggerDevice=%s\", triggerDevice)\n                tmp[\"triggerDevice\"] = triggerDevice\n            else:\n                err = \" \"\n        triggerEventSettings = {}\n        triggerControl = {}\n        if err == \"\":\n            if not request.form.get(\"triggerevent\") is None:\n                triggerEvent = request.form[\"triggerevent\"]\n                logger.debug(\"trigger.new_trigger - triggerEvent=%s\", triggerEvent)\n                tmp[\"triggerEvent\"] = triggerEvent\n            else:\n                err = \" \"\n        if err == \"\":\n            events, eventSettings, control = tc.triggerEvents(triggerSource, triggerDevice)\n            if len(eventSettings) > 0:\n                triggerEventSettings = copy.deepcopy(eventSettings)\n                tmp[\"triggerEventSettings\"] = triggerEventSettings\n                for param, val in triggerEventSettings.items():\n                    elmtId = f\"trigger_{ param }_value\"\n                    if not request.form.get(elmtId) is None:\n                        value = request.form.get(elmtId)\n                        err, triggerEventSettings[param] = castType(value, val)\n                        tmp[\"triggerEventSettings\"] = triggerEventSettings\n                    else:\n                        err = \" \"\n                        break\n                logger.debug(\"trigger.new_trigger - eventsettings=%s\", triggerEventSettings)\n\n            logger.debug(\"trigger.new_trigger - control=%s\", control)\n            if len(control) > 0:\n                triggerControl = copy.deepcopy(control)\n                tmp[\"triggerControl\"] = triggerControl\n                for cparam, cval in triggerControl.items():\n                    elmtId = f\"triggerctrl{ cparam }_value\"\n                    if not request.form.get(elmtId) is None:\n                        cvalue = request.form.get(elmtId)\n                        err, triggerControl[cparam] = castType(cvalue, cval)\n                        tmp[\"triggerControl\"] = triggerControl\n                    else:\n                        err = \" \"\n                        break\n                logger.debug(\"trigger.new_trigger - control=%s\", triggerControl)\n\n        if err == \"\":\n            if not request.form.get(\"triggerid\") is None:\n                triggerId = request.form[\"triggerid\"]\n                logger.debug(\"trigger.new_trigger - triggerId=%s\", triggerId)\n                if triggerId != \"\":\n                    if tc.getTrigger(triggerId) is not None:\n                        err = f\"A trigger with ID {triggerId} exists already.\"\n                    else:\n                        tmp[\"triggerId\"] = triggerId\n                else:\n                    err = \"Please enter a unique trigger ID\"\n            else:\n                err = \" \"\n        if err == \"\":\n            trigger = Trigger()\n            trigger.id = triggerId\n            trigger.source = triggerSource\n            trigger.device = triggerDevice\n            trigger.event = triggerEvent\n            trigger.params = triggerEventSettings\n            trigger.control = triggerControl\n            if sc.isEventhandling:\n                trigger.isActive = False\n                err = \"New trigger was not activated because event detection is active\"\n            else:\n                if countEvent(triggerSource, triggerDevice, triggerEvent, tc) > 0:\n                    trigger.isActive = False\n                    err = \"New trigger was not activated because another active trigger uses the same event\"\n                else:\n                    trigger.isActive = True\n            tc.triggers.append(trigger)\n            tmp = {}\n\n        if err.strip() != \"\":\n            flash(err)\n        if not triggerId is None:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Trigger created: {triggerId}\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\ndef countEvent(source:str, device:str, event:str, tc:TriggerConfig) -> int:\n    \"\"\" Check the number of triggers with the same event\n\n    Args:\n        sourve (str): Source\n        device (str): Device\n        event (str): Event\n\n    Returns:\n        int: _description_Number of triggers with the same event\n    \"\"\"\n    cnt = 0\n    for trigger in tc.triggers:\n        if  trigger.source == source \\\n        and trigger.device == device \\\n        and trigger.event == event \\\n        and trigger.isActive == True:\n            cnt += 1\n    return cnt\n\ndef checkTrigger(tc:TriggerConfig) -> bool:\n    \"\"\" Check triggers for duplicate event\n\n    Sterting from first to last trigger:\n    If another trigger withthe same event is found, the candidate trigger is deactivated    \n\n    Returns:\n        bool: True: No status changes made\n    \"\"\"\n    res = True\n    for trigger in tc.triggers:\n        if countEvent(trigger.source, trigger.device, trigger.event, tc) > 1:\n            trigger.isActive = False\n            res = False\n    return res\n\n@bp.route(\"/trigger_activation\", methods=(\"GET\", \"POST\"))\n@login_required\ndef trigger_activation():\n    logger.debug(\"In trigger_activation\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgtriggers\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = \"\"\n        cnt = 0\n        newTriggers = []\n        for trigger in tc.triggers:\n            elmtid = f\"trigger{trigger.id}_isactive\"\n            trigger.isActive = not request.form.get(elmtid) is None\n            elmtiddel = f\"trigger{trigger.id}_delete\"\n            delete = not request.form.get(elmtiddel) is None\n            if delete == False:\n                newTriggers.append(trigger)\n            else:\n                cnt += 1\n        if cnt > 0:\n            tc.triggers = newTriggers\n\n        if checkTrigger(tc) == False:\n            err = \"Some triggers werde deactivated because they used the same event\"\n        if err.strip() != \"\":\n            flash(err)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger activation changed\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\ndef parseTuple(stuple: str) -> tuple[str, tuple]:\n    \"\"\" Parse a string which is assumed to be a tuple\n\n    Args:\n        stuple (str): string to be tuplelized\n\n    Returns:\n        tuple[str, tuple]: \n            - error\n            - tuplelized string    \n    \"\"\"\n    rest = stuple\n    err = \"\"\n    try:\n        tpl = ast.literal_eval(str(stuple))\n        if type(tpl) is tuple:\n            rest = tpl\n        else:\n            err = f\"{stuple} could not be cast to type of tuple!\"\n    except Exception as e:\n        err = f\"Error parsing {stuple} to tuple: {type(e):{e}}\"\n    return err, rest\n\ndef castType(val:str, tpl:object) ->tuple[str, object]:\n    \"\"\" Cast the given value to the type of the given template\n\n    Args:\n        val (str)   : Value to be casted\n        tpl (object): template\n\n    Returns:\n        tuple[str, object]: \n            - Error message\n            - type-converted value\n    \"\"\"\n    logger.debug(\"castType - val=%s, tpl=%s\", val, tpl)\n    err = \"\"\n    res = val\n    if type(tpl) is dict:\n        if \"type\" in tpl:\n            typ = tpl[\"type\"]\n            if type(typ) is str:\n                if typ.casefold().endswith(\"ornone\") == True:\n                    if val.casefold() == \"none\":\n                        res = None\n                        logger.debug(\"castType - err=%s, res=%s\", err, res)\n                        return err, res\n                if typ.casefold().startswith(\"bool\"):\n                    if val == \"0\":\n                        res = False\n                    elif val == \"1\":\n                        res = True\n                    elif val.casefold() == \"false\":\n                        res = False\n                    elif val.casefold() == \"true\":\n                        res = True\n                    else:\n                        err = \"String does not represent boolean.\"\n                    logger.debug(\"castType - err=%s, res=%s\", err, res)\n                    return err, res\n                if typ.casefold().startswith(\"int\"):\n                    try:\n                        value = int(val)\n                    except Exception as e:\n                        err = f\"Error parsing {val} to int: {type(e)}: {e}\"\n                        logger.debug(\"castType - err=%s, res=%s\", err, res)\n                        return err, res\n                if typ.casefold().startswith(\"float\"):\n                    try:\n                        value = float(val)\n                    except Exception as e:\n                        err = f\"Error parsing {val} to float: {type(e)}: {e}\"\n                        logger.debug(\"castType - err=%s, res=%s\", err, res)\n                        return err, res\n                if typ.casefold().startswith(\"str\"):\n                    res = val\n                    logger.debug(\"castType - err=%s, res=%s\", err, res)\n                    return err, res\n                if typ.casefold().startswith(\"tuple\"):\n                    err, res = parseTuple(val)\n                    logger.debug(\"castType - err=%s, res=%s\", err, res)\n                    return err, res\n            else:\n                value = castType(val, typ)\n            if type(value) == int \\\n            or type(value) == float:\n                if \"min\" in tpl:\n                    if value < tpl[\"min\"]:\n                        err = f\"Value {val} is smaller than minimum {tpl['min']}\"\n                if \"max\" in tpl:\n                    if value > tpl[\"max\"]:\n                        err = f\"Value {val} is greater than maximum {tpl['max']}\"\n                if err == \"\":\n                    res = value\n                logger.debug(\"castType - err=%s, res=%s\", err, res)\n                return err, res\n            else:\n                res = value\n                logger.debug(\"castType - err=%s, res=%s\", err, res)\n                return err, res\n\n    if type(val) is str:\n        try:\n            if type(tpl) is str:\n                pass\n            elif type(tpl) is int:\n                res = int(val)\n            elif type(tpl) is float:\n                res = float(val)\n            elif type(tpl) is bool:\n                if val == \"0\":\n                    res = False\n                elif val == \"1\":\n                    res = True\n                elif val.casefold() == \"false\":\n                    res = False\n                elif val.casefold() == \"true\":\n                    res = True\n                else:\n                    err = \"String does not represent boolean.\"            \n            elif type(tpl) is tuple:\n                l = len(tpl)\n                err, valt = parseTuple(val)\n                if err == \"\":\n                    ll = len(valt)\n                    if ll != l:\n                        err = f\"{val} should be a tuple of length {l}\"\n                    else:\n                        for n in range(0, l):\n                            if type(valt[n]) != type(tpl[n]):\n                                err = f\"{val} : elements of tuple do not have the expected type\"\n                                break\n                        if err == \"\":\n                            res = valt\n        except TypeError as e:\n            err = f\"Type error for {val}: {e}\"\n        except Exception as e:\n            err = f\"{type(e)} error for {val}: {e}\"\n    else:\n        err = f\"{val} should be a string rather than {type(val)}\"\n    logger.debug(\"castType - err=%s, res=%s\", err, res)\n    return err, res\n\n@bp.route(\"/new_action\", methods=(\"GET\", \"POST\"))\n@login_required\ndef new_action():\n    logger.debug(\"In new_action\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgactions\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = \"\"\n        action = None\n        if err == \"\":\n            actionSource = request.form[\"actionsource\"]\n            logger.debug(\"action.new_action - actionSource=%s\", actionSource)\n            tmp[\"actionSource\"] = actionSource\n        if err == \"\":\n            if not request.form.get(\"actiondevice\") is None:\n                actionDevice = request.form[\"actiondevice\"]\n                logger.debug(\"action.new_action - actionDevice=%s\", actionDevice)\n                tmp[\"actionDevice\"] = actionDevice\n            else:\n                err = \" \"\n        actionTarget = {}\n        if err == \"\":\n            if not request.form.get(\"actionmethod\") is None:\n                method = request.form[\"actionmethod\"]\n                logger.debug(\"action.new_action - method=%s\", method)\n                actionTarget[\"method\"] = method\n                tmp[\"actionTarget\"] = actionTarget\n            else:\n                err = \" \"\n        if err == \"\":\n            actionTargets = tc.actionTargets(actionSource, actionDevice)\n            if len(actionTargets) > 0:\n                for target in actionTargets:\n                    if target[\"method\"] == method:\n                        ctarget = copy.deepcopy(target)\n                        if \"params\" in ctarget:\n                            params = ctarget[\"params\"]\n                            if len(params) > 0:\n                                for param, paramVal in params.items():\n                                    elmtId = f\"action_{method}_param_{param}_value\"\n                                    if not request.form.get(elmtId) is None:\n                                        value = request.form.get(elmtId)\n                                        errt, params[param] = castType(value, paramVal)\n                                        if errt != \"\":\n                                            if err == \"\":\n                                                err = f\"Parameter {param}: {errt}\"\n                                        tmp[\"actionTarget\"] = ctarget\n                                    else:\n                                        err = \" \"\n                                        break\n                        else:\n                            params = {}\n                        if err == \"\":\n                            if \"control\" in ctarget:\n                                control = ctarget[\"control\"]\n                                if len(control) > 0:\n                                    for ctrl, ctrlVal in control.items():\n                                        elmtId = f\"action_{method}_control_{ctrl}_value\"\n                                        if not request.form.get(elmtId) is None:\n                                            value = request.form.get(elmtId)\n                                            errt, control[ctrl] = castType(value, ctrlVal)\n                                            if errt != \"\":\n                                                if err == \"\":\n                                                    err = f\"Control {ctrl}: {errt}\"\n                                            tmp[\"actionTarget\"] = ctarget\n                                        else:\n                                            err = \" \"\n                                            break\n                            else:\n                                control = {}\n\n                logger.debug(\"action.new_action - tmp=%s\", tmp)\n\n        if err == \"\":\n            if not request.form.get(\"actionid\") is None:\n                actionId = request.form[\"actionid\"]\n                logger.debug(\"action.new_action - actionId=%s\", actionId)\n                if actionId != \"\":\n                    if tc.getAction(actionId) is not None:\n                        err = f\"An action with ID {actionId} exists already.\"\n                    else:\n                        tmp[\"actionId\"] = actionId\n                else:\n                    err = \"Please enter a unique action ID\"\n            else:\n                err = \" \"\n\n        if err == \"\":\n            action = Action()\n            action.id = actionId\n            action.source = actionSource\n            action.device = actionDevice\n            action.method = method\n            action.params = params\n            action.control = control\n            action.isActive = True\n            tc.actions.append(action)\n            tmp = {}\n\n        if err == \"\":\n            if \"steps\" in control \\\n            and \"duration\" in control:\n                if control[\"steps\"] > 1 \\\n                and control[\"duration\"] > 0.0:\n                    err = \"Graduall approach in steps is currently not yet supported.\"\n                    control[\"steps\"] = 1\n                    control[\"duration\"] = 0.0\n\n        if err.strip() != \"\":\n            flash(err)\n        if not action is None:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Action created: {action.id}\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\ndef checkActionUsage(actionId:str, actionUsage: list, sc:ServerConfig) -> tuple[bool, list]:\n    \"\"\" Check whether an action is still in use in an action button\n\n    Args:\n        actionId (str): Action ID to be checked\n\n    Returns:\n        bool: True when action is in use\n        list: Action buttons where action is used\n    \"\"\"\n    used = False\n    for row in sc.aButtons:\n        for button in row:\n            if button.buttonAction == actionId:\n                used = True\n                actionUsage.append(button.buttonText)\n    return used, actionUsage\n\n@bp.route(\"/action_activation\", methods=(\"GET\", \"POST\"))\n@login_required\ndef action_activation():\n    logger.debug(\"In action_activation\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgactions\"\n    tmp = {}\n    actionUsage = []\n    nondelete = []\n    if request.method == \"POST\":\n        err = \"\"\n        cnt = 0\n        activationChanged = False\n        newActions = []\n        testaction = None\n        for action in tc.actions:\n            elmtid = f\"action{action.id}_isactive\"\n            isActive = not request.form.get(elmtid) is None\n            if action.isActive != isActive:\n                activationChanged = True\n            action.isActive = isActive\n            elmtiddel = f\"action{action.id}_delete\"\n            elmtidtest = f\"action{action.id}_test\"\n            delete = not request.form.get(elmtiddel) is None\n            if delete == False:\n                newActions.append(action)\n                if not request.form.get(elmtidtest) is None:\n                    testaction = action\n            else:\n                use, actionUsage = checkActionUsage(action.id, actionUsage, sc)\n                if use == False:\n                    for trg in tc.triggers:\n                        if action.id in trg.actions:\n                            trg.actions.pop(action.id)\n                    cnt += 1\n                else:\n                    nondelete.append(action.id)\n                    newActions.append(action)\n        if cnt > 0:\n            tc.actions = newActions\n        if len(nondelete) > 0:\n            err = f\"Actions {nondelete} could not be deleted because they are used in action buttons {actionUsage}\"\n\n        if err.strip() == \"\":\n            if not testaction is None:\n                err = TriggerHandler.doAction(testaction.id)\n\n        if err.strip() != \"\":\n            flash(err)\n        if activationChanged:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Action activation changed\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/trigger_action\", methods=(\"GET\", \"POST\"))\n@login_required\ndef trigger_action():\n    logger.debug(\"In trigger_action\")\n    cfg = CameraCfg()\n    g.hostname = request.host\n    g.version = version\n    sc = cfg.serverConfig\n    tc = cfg._triggerConfig\n    if tc.useRoI == True:\n        Camera().startLiveStream()\n    sc.lastTriggerTab = \"trgtriggeractions\"\n    tmp = {}\n    if request.method == \"POST\":\n        err = \"\"\n        for trigger in tc.triggers:\n            tid = trigger.id\n            for action in tc.actions:\n                aid = action.id\n                elmId = f\"triggeraction_{tid}_{aid}\"\n                if request.form.get(elmId) is None:\n                    trigger.actions[aid] = False\n                else:\n                    trigger.actions[aid] = True\n        for trigger in tc.triggers:\n            if \"event_log\" in trigger.control:\n                if trigger.control[\"event_log\"] == True:\n                    # Check for \"start_video\" or \"record_video\" actions which do not have a \"take_photo\" action\n                    has_photo = False\n                    has_video = False\n                    for action in tc.actions:\n                        if trigger.actions[action.id] == True:\n                            if action.method == \"take_photo\":\n                                has_photo = True\n                            elif action.method == \"start_video\" \\\n                            or action.method == \"record_video\":\n                                has_video = True\n                    if has_video == True \\\n                    and has_photo == False:\n                        err = f\"Trigger {trigger.id} has 'event_log' set. Therefore you should add a 'take_photo' action in addition to the video action!\"\n                        break\n            if trigger.source == \"MotionDetector\":\n                # Check that no Camera actions are assigned\n                for action in tc.actions:\n                    if trigger.actions[action.id] == True:\n                        if action.source == \"Camera\":\n                            err = f\"Trigger {trigger.id}: You cannot assign Camera actions here. These are covered by settings in 'Camera' dialog\"\n                            trigger.actions[action.id] = False\n                        if action.source == \"SMTP\":\n                            err = f\"Trigger {trigger.id}: You cannot assign SMTP actions here. These are covered by settings in 'Notification' dialog\"\n                            trigger.actions[action.id] = False\n        if err.strip() != \"\":\n            flash(err)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Trigger-Action settings changed\")\n    return render_template(\"trigger/trigger.html\", tc=tc, sc=sc, tmp=tmp)\n\n@bp.route(\"/media-viewer\")\n@login_required\ndef media_viewer():\n    src = request.args.get(\"src\")\n    media_type = request.args.get(\"type\", \"image\")\n\n    filename = os.path.basename(src) if src else \"\"\n\n    return render_template(\n        \"media_viewer.html\",\n        src=src,\n        media_type=media_type,\n        filename=filename\n    )\n"
  },
  {
    "path": "raspiCamSrv/triggerHandler.py",
    "content": "from gpiozero import Button, LineSensor, MotionSensor, LightSensor, DistanceSensor, RotaryEncoder, DigitalInputDevice\nfrom gpiozero import LED, PWMLED, RGBLED, Buzzer,TonalBuzzer, Motor,PhaseEnableMotor, Servo, AngularServo, DigitalOutputDevice, OutputDevice\nfrom raspiCamSrv.gpioDevices import StepperMotor, ServoPWM\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.motionDetector import MotionDetector\nfrom raspiCamSrv.camCfg import CameraCfg, TriggerConfig, ServerConfig, GPIODevice, Trigger, Action\nfrom _thread import get_ident\nfrom datetime import datetime, timedelta\nfrom raspiCamSrv.dbx import get_dbx\nfrom sqlite3 import Connection\nfrom uuid import uuid4, UUID\nimport smtplib\nimport json\nfrom email.message import EmailMessage\nimport mimetypes\nimport time\nimport threading\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nclass TriggerHandler():\n    \"\"\" Class for trigger and event handling\n    \n    \"\"\"\n    logger.debug(\"Thread %s: TriggerHandler - setting class variables\", get_ident())\n    _instance = None\n    _registry = {}\n    _registry_lock = threading.Lock()\n    _sub_threads = []\n    _list_lock = threading.Lock()\n    _event_contexts = []\n    _context_lock = threading.Lock()\n    _livestream_lock = threading.Lock()\n    triggerThread = None\n    triggerThreadStop = False\n\n    def __new__(cls):\n        logger.debug(\"Thread %s: TriggerHandler.__new__\", get_ident())\n        if cls._instance is None:\n            logger.debug(\"Thread %s: TriggerHandler.__new__ - Instantiating Class\", get_ident())\n            cls._instance = super(TriggerHandler, cls).__new__(cls)\n        return cls._instance\n                    \n    @staticmethod\n    def _isActive() -> bool:\n        \"\"\" Check whether trigger is supposed to be active\n\n        This is controlled by operation times specified in CameraCfg().triggerConfig\n\n        Returns:\n            bool: true when triggering is active\n        \"\"\"\n        active = True\n        cfg = CameraCfg()\n        tc = cfg.triggerConfig\n        \n        now = datetime.now()\n        wd = str(now.isoweekday())\n        if tc.operationWeekdays[wd] == True:\n            h = now.hour\n            m = now.minute\n            dm = 60 * h + m\n            if dm >= tc.operationStartMinute \\\n            and dm <= tc.operationEndMinute:\n                active = True\n            else:\n                active = False\n        else:\n            active = False\n        if active:\n            cfg.serverConfig.isEventsWaiting = False\n        else:\n            cfg.serverConfig.isEventsWaiting = True\n        return active\n    \n    @classmethod\n    def _findDeviceInRegistry(cls, source: str, deviceId: str, sc:ServerConfig, busy:bool) -> tuple[bool, dict]:\n        \"\"\" Find the registry entry for the specified device class\n        \n        A specific device can be used in multiple triggers or actions.\n        Every device must be instantiated only once. \n        For GPIO devices, a second try would result in a \"Device in use\" error.\n        Therefore instantiated devices are stored in the registry for later access\n        Camera and MotionDetector devices are singletons, \n        but their class is stored in the registry for unregistration.\n        \n        The registry structure is:\n        - source (\"GPIO\")\n            +- deviceId\n                 +- \"deviceClass\" : device type (class name)\n                 +- \"deviceObject\": reference to instantiated device object\n                 +- \"busy\"        : true if the device is currently busy  \n                 +- \"lastAccess   : Time of last access to a device\n                 +- \"methods\"     : Methods to which callbacks are assigned\n                 +- ...\n        - source (\"Camera\" or \"MotionDetector\")\n            +- deviceId\n                 +- \"deviceClass\" : device type (class name)\n                 +- \"deviceObject\": reference to instantiated device object\n                 +- \"methods\"     : Methods to which callbacks are assigned\n\n        Args:\n            source (str)      : source of device, e.g. \"GPIO\"\n            deviceId (str)    : ID of the device (key element)\n            sc (ServerConfig) : Server Configuration object which holds device information\n            busy (bool)       : Busy state to be set\n\n        Raises:\n\n        Returns:\n            bool: True if device was not busy and busy state was set\n                  or if the device was not busy and busy state was not requested\n            dict: Dictionary entry for the device - level deviceId.\n                  If an entry for the device does not yet exist, it will be created\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._findDeviceInRegistry - source=%s deviceId=%s\", get_ident(), source, deviceId)\n\n        res = {}\n        busyAcquired = False\n        with cls._registry_lock:\n            if not source in cls._registry:\n                cls._registry[source] = {}\n\n            if not deviceId in cls._registry[source]:\n                cls._registry[source][deviceId] = {}\n                \n            if not \"deviceClass\" in cls._registry[source][deviceId]:\n                if source == \"GPIO\":\n                    device = sc.getDevice(deviceId)\n                    deviceClass = device.type\n                    deviceArgs = device.params\n                    cls._registry[source][deviceId][\"deviceClass\"] = deviceClass\n\n                    # Instantiate device object\n                    deviceObj = globals()[deviceClass](**deviceArgs)\n                    device.setState(deviceObj)\n                    logger.debug(\"Thread %s: TriggerHandler._findDeviceInRegistry - instantiated: %s(%s)\", get_ident(), deviceClass, deviceArgs)\n                    cls._registry[source][deviceId][\"deviceObject\"] = deviceObj\n                elif source == \"Camera\":\n                    device = deviceId\n                    deviceClass = \"Camera\"\n                    deviceArgs = {}\n                    cls._registry[source][deviceId][\"deviceClass\"] = deviceClass\n\n                    # Nothing to instantiate for Camera\n                    deviceObj = Camera()\n                    logger.debug(\"Thread %s: TriggerHandler._findDeviceInRegistry - instantiated: %s(%s)\", get_ident(), deviceClass, deviceArgs)\n                    cls._registry[source][deviceId][\"deviceObject\"] = deviceObj\n                elif source == \"MotionDetector\":\n                    device = deviceId\n                    deviceClass = \"MotionDetector\"\n                    deviceArgs = {}\n                    cls._registry[source][deviceId][\"deviceClass\"] = deviceClass\n\n                    # Nothing to instantiate for Camera\n                    deviceObj = MotionDetector()\n                    logger.debug(\"Thread %s: TriggerHandler._findDeviceInRegistry - instantiated: %s(%s)\", get_ident(), deviceClass, deviceArgs)\n                    cls._registry[source][deviceId][\"deviceObject\"] = deviceObj\n                \n            if not \"methods\" in cls._registry[source][deviceId]:\n                cls._registry[source][deviceId][\"methods\"] = []\n\n            if source == \"GPIO\":\n                if not \"busy\" in cls._registry[source][deviceId]:\n                    cls._registry[source][deviceId][\"busy\"] = busy\n                    if busy == True:\n                        busyAcquired = True\n                else:\n                    if busy == True:\n                        if cls._registry[source][deviceId][\"busy\"] == True:\n                            busyAcquired = False\n                        else:\n                            cls._registry[source][deviceId][\"busy\"] = True\n                            busyAcquired = True\n                    else:\n                        if cls._registry[source][deviceId][\"busy\"] == True:\n                            cls._registry[source][deviceId][\"busy\"] = False\n                            busyAcquired = False\n                        else:\n                            busyAcquired = True\n            if source == \"GPIO\":\n                if busyAcquired == True:\n                    cls._registry[source][deviceId][\"last_access\"] = datetime.now()\n\n            res = cls._registry[source][deviceId]\n            logger.debug(\"Thread %s: TriggerHandler._findDeviceInRegistry - Returning:busyAcquired=%s res=%s\", get_ident(), busyAcquired, res)\n        return busyAcquired, res\n    \n    @classmethod\n    def _bouncing(cls, trg:Trigger, sc:ServerConfig) -> bool:\n        \"\"\" Check for bouncing\n\n        If the time difference between the current time and the time of last device access\n        is larger than the bouncing time for the trigger (if defined), bouncing is assumed.\n        \n        Returns:\n            bool: True if bouncing\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._bouncing - trigger.id=%s\", get_ident(), trg.id)\n        res = False\n        \n        with cls._registry_lock:\n            control = trg.control\n            if \"bounce_time\" in control:\n                bounce_time = control[\"bounce_time\"]\n                logger.debug(\"Thread %s: TriggerHandler._bouncing - bounce time=%s\", get_ident(), bounce_time)\n                if bounce_time > 0.0:\n                    source = trg.source\n                    device = sc.getDevice(trg.device)\n                    deviceId = device.id\n                    logger.debug(\"Thread %s: TriggerHandler._bouncing - Checking reg for: source:%s, deviceId:%s\", get_ident(), source, deviceId)\n                    if source in cls._registry:\n                        if deviceId in cls._registry[source]:\n                            reg = cls._registry[source][deviceId]\n                            logger.debug(\"Thread %s: TriggerHandler._bouncing - found reg: %s\", get_ident(), reg)\n                            if \"last_access\" in reg:\n                                lastAccess = reg[\"last_access\"]\n                                logger.debug(\"Thread %s: TriggerHandler._bouncing - last_access: %s\", get_ident(), lastAccess)\n                                now = datetime.now()\n                                diff = now - lastAccess\n                                secs = diff.total_seconds()\n                                if secs < bounce_time:\n                                    res = True\n                                    logger.debug(\"Thread %s: TriggerHandler._bouncing - bouncing - timediff: %s s\", get_ident(), secs)\n                                else:\n                                    logger.debug(\"Thread %s: TriggerHandler._bouncing - Nobouncing - timediff: %s s\", get_ident(), secs)\n                                    reg[\"last_access\"] = datetime.now()\n                            else:\n                                reg[\"last_access\"] = datetime.now()\n            logger.debug(\"Thread %s: TriggerHandler._bouncing - Result: %s\", get_ident(), res)\n        return res\n\n    @classmethod    \n    def _doGpioAction(cls, action:Action, trigger:Trigger=None, eventId:UUID=None, last:bool=False, threadRegistered:bool=True, wait:bool=True) -> bool:\n        \"\"\" Execute an action with a GPIO device\n\n        Args:\n            action (Action): The action to execute\n            trigger (Trigger): Trigger on behalf of which the action is executed\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n            threadRegistered (bool): if True, the thread is registered in the sub_threads list and must be unregistered\n                                    after completion\n        \"\"\"\n        if trigger:\n            triggerDisp = trigger.id\n        else:\n            triggerDisp = \"No Trigger\"\n        logger.debug(\"Thread %s: TriggerHandler._doGpioAction - action=%s trigger.id=%s - starting - wait=%s last=%s\", get_ident(), action.id, triggerDisp, wait, last)\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n        \n        isEvent = False\n        if trigger:\n            triggerCtrl = trigger.control\n            if \"event_log\" in triggerCtrl:\n                event_log = triggerCtrl[\"event_log\"]\n                if event_log == True:\n                    isEvent = True\n                    db = get_dbx()\n\n        deviceId = action.device\n        \n        done = False\n        \n        acquired, reg = cls._findDeviceInRegistry(\"GPIO\", deviceId, sc, busy=True)\n        if wait == True:\n            while acquired == False:\n                time.sleep(0.1)\n                acquired, reg = cls._findDeviceInRegistry(\"GPIO\", deviceId, sc, busy=True)\n\n        if acquired:\n            if isEvent == True:\n                db = get_dbx()\n            deviceClass = reg[\"deviceClass\"]\n            deviceObj = reg[\"deviceObject\"]\n            method = action.method\n            methodParams = action.params\n            actionCtrl = action.control\n            logger.debug(\"Thread %s: TriggerHandler._doGpioAction - deviceClass=%s method=%s params=%s\", get_ident(), deviceClass, method, methodParams)\n\n            # Update action context\n            eCtx = cls._getEventContext(eventId)\n            ctx = cls._getActionContext(eventId, action.id)\n            ctx[\"action_start\"] = datetime.now()\n\n            try:\n                # Apply action method\n                if hasattr(deviceObj, method):\n                    logger.debug(\"Thread %s: TriggerHandler._doGpioAction - class: %s has method: %s\", get_ident(), deviceClass, method)\n                    attr = getattr(deviceObj, method)\n                    if callable(attr) == True:\n                        logger.debug(\"Thread %s: TriggerHandler._doGpioAction - method: %s - is callable\", get_ident(), method)\n                        if len(methodParams) > 0:\n                            call = f\"{deviceClass}.{method}({methodParams})\"\n                            logger.debug(\"Thread %s: TriggerHandler._doGpioAction - calling: %s\", get_ident(), call)\n                            result = attr(**methodParams)\n                            logger.debug(\"Thread %s: TriggerHandler._doGpioAction Action:%s - %s=%s\", get_ident(),  action.id, call, result)\n                        else:\n                            call = f\"{deviceClass}.{method}()\"\n                            logger.debug(\"Thread %s: TriggerHandler._doGpioAction - calling: %s\", get_ident(), call)\n                            result = attr()\n                            logger.debug(\"Thread %s: TriggerHandler._doGpioAction Action:%s - %s=%s\", get_ident(),  action.id, call, result)\n                    else:\n                        logger.debug(\"Thread %s: TriggerHandler._doGpioAction - method: %s - is not callable\", get_ident(), method)\n                        if len(methodParams) > 0:\n                            for key, value in methodParams.items():\n                                if value != \"\":\n                                    assignment = f\"{deviceClass}.{method}={value}\"\n                                    setattr(deviceObj, method, value)\n                                    logger.debug(\"Thread %s: TriggerHandler._doGpioAction Action: %s - %s\", get_ident(),  action.id, assignment)\n                                else:\n                                    call = f\"{deviceClass}.{method}\"\n                                    result = attr\n                                    logger.debug(\"Thread %s: TriggerHandler._doGpioAction Action: %s - %s\", get_ident(),  action.id, call)\n                                break\n                        else:\n                            call = f\"{deviceClass}.{method}\"\n                            result = attr\n                            logger.debug(\"Thread %s: TriggerHandler._doGpioAction Action: %s - %s\", get_ident(),  action.id, call)\n                else:\n                    logger.debug(\"TriggerHandler._doGpioAction - Action %s - Method %s not found in %s\", action.id, method, deviceClass)\n\n                # Log\n                if isEvent == True:\n                    cls._logEvent(db, \"gpio_action\", tc, eCtx, ctx)\n            except Exception as e:\n                logger.error(\"TriggerHandler._doGpioAction - Error %s: %s\", type(e), e)\n                err = f\"Error {type(e)} while executing action: {e}\"\n                if isEvent == True:\n                    cls._logEvent(db, \"gpio_action_error\", tc, eCtx, ctx, err=err)\n                tc.error = f\"Error {type(e)} while executing action {action.id}: {e}\"\n                \n            # Wait for the specified duration\n            if \"duration\" in actionCtrl:\n                duration = actionCtrl[\"duration\"]\n                if duration > 0.0:\n                    time.sleep(duration)\n                    # Then try off() or stop()\n                    method = \"\"\n                    if hasattr(deviceObj, \"off\"):\n                        method = \"off\"\n                    else:\n                        if hasattr(deviceObj, \"stop\"):\n                            method = \"stop\"\n                    if method != \"\":\n                        attr = getattr(deviceObj, method)\n                        logger.debug(\"Thread %s: TriggerHandler._doGpioAction - Trying to stop device with method %s\", get_ident(), method)\n                        try:\n                            attr()\n                        except Exception as e:\n                            logger.error(\"TriggerHandler._doGpioAction - Error while stopping %s with %s: %s: %s\", deviceId, method, type(e), e)\n            # Track state\n            device = sc.getDevice(deviceId)\n            device.trackState(deviceObj)\n\n            # Release busy state\n            cls._findDeviceInRegistry(\"GPIO\", deviceId, sc, busy=False)\n            ctx[\"action_stop\"] = datetime.now()\n            # Log stop\n            if isEvent == True:\n                cls._logEvent(db, \"gpio_action_finished\", tc, eCtx, ctx)\n            done = True\n        else:\n            logger.debug(\"Thread %s: TriggerHandler._doGpioAction - action=%s trigger.id=%s - Not executing. Device busy\", get_ident(), action.id, triggerDisp)\n\n        if last == True:\n            # Finalize event\n            cls._finalizeEvent(eventId)\n            \n        # Remove sub_thread from the list of threads\n        if threadRegistered == True:\n            thread = threading.current_thread()\n            with cls._list_lock:\n                if thread in cls._sub_threads:\n                    cls._sub_threads.remove(thread)\n\n        logger.debug(\"Thread %s: TriggerHandler._doGpioAction - action=%s trigger.id=%s terminated\", get_ident(), action.id, triggerDisp)\n        return done\n    \n    @classmethod\n    def _videoTimer(cls, action:Action, isEvent:bool, sc:ServerConfig, tc:TriggerConfig, eventId:UUID):\n        \"\"\" Record video with duration according to action control. Then stop\n\n        Args:\n            action (Action): Action\n            sc (ServerConfig): Server configuration\n\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._videoTimer\", get_ident())\n\n        actionCtrl = action.control\n        if \"duration\" in actionCtrl:\n            duration = actionCtrl[\"duration\"]\n            time.sleep(duration)\n        \n        if isEvent:\n            db = get_dbx()\n        else:\n            db = None\n        \n        cls._doStopVideo(action, isEvent, sc, tc, eventId, db)\n\n        thread = threading.current_thread()\n        with cls._list_lock:\n            if thread in cls._sub_threads:\n                cls._sub_threads.remove(thread)\n\n        logger.debug(\"Thread %s: TriggerHandler._videoTimer - action=%s terminated\", get_ident(), action.id)\n\n    \n    @classmethod\n    def _doRecordVideo(cls, action:Action, isEvent:bool, sc:ServerConfig, tc:TriggerConfig, eventId:UUID, db:Connection) -> tuple[bool, str]:\n        \"\"\" Record video according to action details\n\n        Args:\n            action (Action): Action\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n\n        Returns:\n            tuple[bool, str]:\n                bool: True when recording was started\n                str:  Error message in case of errors\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._doRecordVideo\", get_ident())\n        \n        done = False\n        \n        done, msg = cls._doStartVideo(action, isEvent, sc, tc, eventId, db)\n        \n        recordingThread = threading.Thread(target=cls._videoTimer, args=(action, isEvent, sc, tc, eventId))\n        ctx = cls._getActionContext(eventId, action.id)\n        ctx[\"thread\"] = recordingThread\n        recordingThread.start()\n        cls._sub_threads.append(recordingThread)\n            \n        return done,msg\n    \n    @classmethod\n    def _doStartVideo(cls, action:Action, isEvent:bool, sc:ServerConfig, tc:TriggerConfig, eventId:UUID, db:Connection) -> tuple[bool, str]:\n        \"\"\" Start video recording according to action details\n\n        Args:\n            action (Action): Action\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n\n        Returns:\n            tuple[bool, str]:\n                bool: True when recording was started\n                str:  Error message in case of errors\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._doStartVideo\", get_ident())\n        # Update action context\n        eCtx = cls._getEventContext(eventId)\n        ctx = cls._getActionContext(eventId, action.id)\n        ctx[\"action_start\"] = datetime.now()\n        \n        msg = \"\"\n        done = False\n        methodParams = action.params\n\n        typ = sc.videoType\n        if \"type\" in methodParams:\n            typ = methodParams[\"type\"]\n            if typ == \"mp4\" \\\n            or typ == \"h264\":\n                pass\n            else:\n                logger.error(\"TriggerHandler._doStartVideo - Action %s - Invalid 'type': %s\", action.id, typ)\n                msg = f\"Action {action.id} - 'type': {typ} invalid. \"\n                typ = sc.videoType\n\n        timeImg = datetime.now()\n        if isEvent == True:\n            path = tc.actionPath\n            filenameVid = timeImg.strftime(\"%Y-%m-%dT%H-%M-%S\") + \".\" + typ\n            filename = \"\"\n        else:\n            path = \"\"\n            filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + typ\n            filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n\n        if not \"video\" in ctx:\n            ctx[\"video\"] = {}\n        video = ctx[\"video\"]\n        video[\"video_start\"] = timeImg\n        video[\"video_file\"] = filenameVid\n        logger.debug(\"Thread %s: TriggerHandler._doStartVideo - Recording a video %s\", get_ident(), filenameVid)\n        try:\n            fp = Camera().recordVideo(filenameVid, filename, alternatePath=path, noEvents=True)\n            video[\"video_path\"] = fp\n            time.sleep(2)\n            if not sc.error:\n                if Camera.isVideoRecording():\n                    logger.debug(\"Thread %s: TriggerHandler._doStartVideo - Video recording started\", get_ident())\n                    sc.isVideoRecording = True\n                    if sc.recordAudio:\n                        sc.isAudioRecording = True\n                    msg = f\"Video recording started {fp}\"\n\n                    if isEvent == True:\n                        cls._logEvent(db, \"video_start\",tc, eCtx, ctx)\n                    done = True\n                else:\n                    logger.debug(\"Thread %s: TriggerHandler._doStartVideo - Video recording did not start\", get_ident())\n                    sc.isVideoRecording = False\n                    sc.isAudioRecording = False\n                    msg = \"Start of video recording failed. Probably the requested resolution too high\"\n            else:\n                msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        except Exception as e:\n            logger.error(\"TriggerHandler._doStartVideo - Error %s: %s\", type(e), e)\n            sc.isVideoRecording = False\n            sc.isAudioRecording = False\n            msg = f\"Error {type(e)} while starting video recording: {e}\"\n            tc.error = f\"Error {type(e)} while starting video recording: {e}\"\n\n        if done == False:\n            if isEvent == True:\n                cls._logEvent(db, \"video_start_err\", tc, eCtx, ctx, err=msg)\n            \n        return done, msg\n        \n    @classmethod\n    def _doStopVideo(cls, action:Action, isEvent:bool, sc:ServerConfig, tc:TriggerConfig, eventId:UUID, db:Connection) -> tuple[bool, str]:\n        \"\"\" Stop video recording according to action details\n\n        Args:\n            action (Action): Action\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n\n        Returns:\n            tuple[bool, str]:\n                bool: True when photo(s) were taken\n                str:  Error message in case of errors\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._doStopVideo\", get_ident())\n        msg = \"\"\n        done = False\n\n        # Get action context\n        eCtx = cls._getEventContext(eventId)\n        ctx = cls._getActionContext(eventId, action.id)\n        if not \"video\" in ctx:\n            ctx[\"video\"] = {}\n        video = ctx[\"video\"]\n        try:\n            Camera().stopVideoRecording(noEvents=True)\n            video[\"video_stop\"] = datetime.now()\n            time.sleep(2)\n            if Camera.isVideoRecording() == False:\n                sc.isVideoRecording = False\n                sc.isAudioRecording = False\n                if isEvent == True:\n                    cls._logEvent(db, \"video_stop\", tc, eCtx, ctx)\n                done = True\n                logger.debug(\"Thread %s: TriggerHandler._doStopVideo - Video recording stopped\", get_ident())\n                msg=\"Video recording stopped\"\n            else:\n                logger.debug(\"Thread %s: TriggerHandler._doStopVideo - Video recording did not stop\", get_ident())\n                msg=\"Video recording did not stop\"\n        except Exception as e:\n            logger.error(\"TriggerHandler._doStopVideo - Error %s: %s\", type(e), e)\n            msg = f\"Error {type(e)} while stopping video recording: {e}\"\n            tc.error = f\"Error {type(e)} while stopping video recording: {e}\"\n            \n        if done == False:\n            if isEvent == True:\n                cls._logEvent(db, \"video_stop_err\", tc, eCtx, ctx, err=msg)\n\n        ctx[\"action_stop\"] = datetime.now()\n\n        return done, msg\n    \n    @classmethod\n    def _doTakePhoto(cls, action:Action, isEvent:bool, sc:ServerConfig, tc:TriggerConfig, eventId:UUID, db:Connection) -> tuple[bool, str]:\n        \"\"\" Take photo(s) according to action details\n\n        Args:\n            action (Action): Action\n            isEvent (bool): True if the action is handled as event\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n\n        Returns:\n            tuple[bool, str]:\n                bool: True when photo(s) were taken\n                str:  Error message in case of errors\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._doTakePhoto - entry\", get_ident())\n        \n        msg = \"\"\n        done = False\n        methodParams = action.params\n        actionCtrl = action.control\n        # Get action context\n        eCtx = cls._getEventContext(eventId)\n        ctx = cls._getActionContext(eventId, action.id)\n        ctx[\"action_start\"] = datetime.now()\n\n        typ = sc.photoType\n        if \"type\" in methodParams:\n            typ = methodParams[\"type\"]\n            if typ == \"jpg\" \\\n            or typ == \"jpeg\" \\\n            or typ == \"bmp\" \\\n            or typ == \"png\" \\\n            or typ == \"gif\" \\\n            or typ == \"dng\":\n                pass\n            else:\n                logger.error(\"TriggerHandler._doTakePhoto - Action %s - Invalid 'type': %s\", action.id, typ)\n                msg = f\"Action {action.id} - 'type': {typ} invalid. \"\n                typ = sc.photoType\n        burstCount = 1\n        burstIntvl = 0\n        photoCtx = {}\n        if \"burst_count\" in actionCtrl:\n            burstCount = actionCtrl[\"burst_count\"]\n        if \"burst_intvl\" in actionCtrl:\n            burstIntvl = actionCtrl[\"burst_intvl\"]\n        try:\n            ctx[\"photos\"] = []\n            for count in range(0, burstCount):\n                timeImg = datetime.now()\n                if isEvent == True:\n                    path = tc.actionPath\n                    file = timeImg.strftime(\"%Y-%m-%dT%H-%M-%S\")\n                else:\n                    path = \"\"\n                    file = timeImg.strftime(\"%Y%m%d_%H%M%S\")\n                if typ == \"dng\":\n                    filename = file + \".\" + sc.photoType\n                    filenameRaw = file + \".\" + typ\n                    fp = Camera().takeRawImage(filenameRaw, filename, alternatePath=path, noEvents=True)\n                else:\n                    filename = file + \".\" + typ\n                    fp = Camera().takeImage(filename, alternatePath=path, noEvents=True)\n                    \n                photoCtx = {}\n                photoCtx[\"photo_time\"] = timeImg\n                photoCtx[\"photo_file\"] = filename\n                photoCtx[\"photo_path\"] = fp\n                ctx[\"photos\"].append(photoCtx)\n                logger.debug(\"Thread %s: TriggerHandler._doTakePhoto - Image saved as %s\", get_ident(), fp)\n                time.sleep(burstIntvl)\n                if burstCount == 1:\n                    msg = f\"{msg}Photo taken: {fp}\"\n                else:\n                    if count == 0:\n                        msg = f\"{msg}{burstCount} photos taken: {fp} ...\"\n                # log\n                if isEvent == True:\n                    cls._logEvent(db, \"photo_taken\", tc, eCtx, ctx, photoCtx)                     \n                done = True\n        except Exception as e:\n            logger.error(\"TriggerHandler._doTakePhoto - error %s: %s\", type(e), e)\n            msg = f\"Error {type(e)} while taking a photo: {e}\"\n            tc.error = f\"Error {type(e)} while taking a photo: {e}\"\n        \n        if done == False:\n            if isEvent == True:\n                cls._logEvent(db, \"photo_error\", tc, eCtx, ctx, photoCtx, err=msg)\n\n        ctx[\"action_stop\"] = datetime.now()\n\n        logger.debug(\"Thread %s: TriggerHandler._doTakePhoto - exit - done=%s msg=%s\", get_ident(), done, msg)\n        return done, msg\n    \n    @classmethod\n    def _doCameraAction(cls, action:Action, trigger:Trigger=None, eventId:UUID=None, last:bool=False, threadRegistered:bool=True, wait:bool=True) -> tuple[bool, str]:\n        \"\"\" Execution of a camera action\n\n        Args:\n            action (Action): Action to be executed\n            trigger (Trigger, optional): Trigger on behalf of which the action is executed\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n            threadRegistered (bool, optional): If true (default), the active thread must be unregistered\n            wait (bool, optional): If true (default), the thread needs to wait for the resource\n\n        Raises:\n\n        Returns:\n            bool: True if action was executed\n        \"\"\"\n        if trigger:\n            triggerDisp = trigger.id\n        else:\n            triggerDisp = \"No Trigger\"\n        logger.debug(\"Thread %s: TriggerHandler._doCameraAction - action=%s trigger.id=%s - starting - wait=%s last=%s\", get_ident(), action.id, triggerDisp, wait, last)\n\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n        \n        done = False\n        msg = \"\"\n\n        db = None\n        isEvent = False\n        if trigger:\n            triggerCtrl = trigger.control\n            if \"event_log\" in triggerCtrl:\n                event_log = triggerCtrl[\"event_log\"]\n                if event_log == True:\n                    isEvent = True\n                    db = get_dbx()\n        logger.debug(\"Thread %s: TriggerHandler._doCameraAction - isEvent=%s\", get_ident(), isEvent)\n\n        acquired = True\n        \n        if action.device == \"CAM-1\":\n            if sc.isVideoRecording == True:\n                if action.method == \"stop_video\":\n                    acquired = True\n                else:\n                    acquired = False\n            else:\n                if action.method == \"stop_video\":\n                    acquired = False\n                    msg = f\"Video recording not active. Stopping not required\"\n        logger.debug(\"Thread %s: TriggerHandler._doCameraAction - acquired=%s\", get_ident(), acquired)\n        \n        # Start the live stream to avoid issues with concurrent camera access\n        streamActive = sc.isLiveStream\n        logger.debug(\"Thread %s: TriggerHandler._doCameraAction - Initially: sc.isLivestream=%s\", get_ident(), streamActive)\n        with cls._livestream_lock:\n            Camera().startLiveStream()\n            while sc.isLiveStream == False:\n                time.sleep(0.1)\n            logger.debug(\"Thread %s: TriggerHandler._doCameraAction - Finally: sc.isLivestream=%s\", get_ident(), sc.isLiveStream)\n            if streamActive == False and sc.isLiveStream == True:\n                # If camera has been started give time to collect data for AE and AWB\n                if Camera().requiresTimeForAutoAlgos() == True:\n                    logger.debug(\"Thread %s: TriggerHandler._doCameraAction - Camera requires time for auto algorithms\", get_ident())\n                    time.sleep(2)\n            \n        if acquired:\n            if action.device == \"CAM-1\":\n                method = action.method\n                if method == \"take_photo\":\n                    done, msg = cls._doTakePhoto(action, isEvent, sc, tc, eventId, db)\n                elif action.method == \"record_video\":\n                    done, msg = cls._doRecordVideo(action, isEvent, sc, tc, eventId, db)\n                elif action.method == \"start_video\":\n                    done, msg = cls._doStartVideo(action, isEvent, sc, tc, eventId, db)\n                elif action.method == \"stop_video\":\n                    done, msg = cls._doStopVideo(action, isEvent, sc, tc, eventId, db)\n                else:\n                    logger.error(\"TriggerHandler._doCameraAction - Method %s not supported for device %s/%s\", action.method, action.source, action.device)\n                    msg = f\"Method {action.method} not supported for device {action.source}/{action.device}\"\n            else:\n                logger.error(\"TriggerHandler._doCameraAction - Device %s not supported for source %s\", action.device, action.source)\n                msg = f\"Device {action.device} not supported for source {action.source}\"\n        else:\n            logger.debug(\"Thread %s: TriggerHandler._doCameraAction - action=%s trigger.id=%s - Not executing. Device busy\", get_ident(), action.id, triggerDisp)\n            if msg == \"\":\n                msg = f\"Dev:ice {action.device} busy. Please try later.\"\n\n\n        if last == True:\n            # Finalize event\n            cls._finalizeEvent(eventId)\n            \n        # Remove sub_thread from the list of threads\n        if threadRegistered == True:\n            thread = threading.current_thread()\n            with cls._list_lock:\n                if thread in cls._sub_threads:\n                    cls._sub_threads.remove(thread)\n\n        logger.debug(\"Thread %s: TriggerHandler._doCameraAction - action=%s trigger.id=%s terminated\", get_ident(), action.id, triggerDisp)\n        return done, msg\n    \n    @classmethod            \n    def _initNotificationMessage(cls, eventId:UUID) -> EmailMessage:\n        \"\"\" Set up an eMail Message for notification\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._initNotificationMessage\", get_ident())\n        tc = CameraCfg().triggerConfig\n        ctx = cls._getEventContext(eventId)\n        if \"event_id\" in ctx:\n            logTS = ctx[\"event_TS\"]\n        else:\n            logTS = datetime.now().strftime(\"%Y-%m-%dT%H:%M:%S\")\n        if \"trigger\" in ctx:\n            triggerId = ctx[\"trigger\"]\n            trg = tc.getTrigger(triggerId)\n            triggerType = f\"{trg.source}/{trg.device}\"\n            triggerParams =f\"{trg.params}\"\n        else:\n            triggerId = \"Unknown\"\n        \n\n        msg = EmailMessage()\n        msg[\"From\"] = tc.notifyFrom\n        msg[\"To\"] = tc.notifyTo\n        msg[\"Subject\"] = tc.notifySubject\n        trgContent = [\n            (\"Time:\", logTS),\n            (\"Trigger:\", triggerId),\n            (\"Type:\", triggerType),\n            (\"Parameter:\", str(triggerParams))\n        ]\n    \n        actContent = []\n        actLen = 0\n        attContent = []\n        attLen = 0\n        if \"actions\" in ctx:\n            actions = ctx[\"actions\"]\n            for actionCtx in actions:\n                actLine = []\n                actLine.append(f\"- {actionCtx['action']}\")\n                if len(actionCtx[\"action\"]) > actLen:\n                    actLen = len(actionCtx[\"action\"])\n                if \"action_start\" in actionCtx:\n                    actLine.append(actionCtx[\"action_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\"))\n                else:\n                    actLine.append(\"\")\n                if \"action_stop\" in actionCtx:\n                    actLine.append(\"-\")\n                    actLine.append(actionCtx[\"action_stop\"].strftime(\"%Y-%m-%dT%H:%M:%S\"))\n                else:\n                    actLine.append(\"\")\n                    actLine.append(\"\")\n                actContent.append(tuple(actLine))\n                if \"photos\" in actionCtx:\n                    photos = actionCtx[\"photos\"]\n                    for photo in photos:\n                        attLine = []\n                        if \"photo_file\" in photo:\n                            attLine.append(photo[\"photo_file\"])\n                            if len(photo[\"photo_file\"]) > attLen:\n                                attLen = len(photo[\"photo_file\"])\n                            if \"photo_time\" in photo:\n                                attLine.append(photo[\"photo_time\"].strftime(\"%Y-%m-%dT%H:%M:%S\"))\n                            else:\n                                attLine.append(\"\")\n                            attLine.append(\"\")\n                            attLine.append(\"\")\n                            attContent.append(tuple(attLine))\n                if \"video\" in actionCtx:\n                    video = actionCtx[\"video\"]\n                    if \"video_file\" in video:\n                        attLine = []\n                        filename = video[\"video_file\"]\n                        if len(filename) > attLen:\n                            attLen = len(filename)\n                        attLine.append(filename)\n                        if \"video_start\" in video:\n                            attLine.append(video[\"video_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\"))\n                        else:\n                            attLine.append(\"\")\n                        if \"video_stop\" in video:\n                            attLine.append(\"-\")\n                            attLine.append(video[\"video_stop\"].strftime(\"%Y-%m-%dT%H:%M:%S\"))\n                        else:\n                            attLine.append(\"\")\n                            attLine.append(\"\")\n                        attContent.append(tuple(attLine))\n                        \n        # Assemble content\n        leftLen = max(actLen + 3, attLen)\n        content = \"Notification on an event\\n\\n\"\n        for left, right in trgContent:\n            content += f\"{left:<12} {right}\\n\"\n        content += \"\\nActions:\\n\"\n        for action, start, dash, stop in actContent:\n            content += f\"{action:<{leftLen}} {start:<20} {dash:<2} {stop}\\n\"\n        content += \"\\nAttachments:\\n\"\n        for filename, start, dash, stop in attContent:\n            content += f\"{filename:<{leftLen}} {start:<20} {dash:<2} {stop}\\n\"\n        \n        # Plain text for fallback\n        msg.set_content(content)\n        \n        # HTML content\n        html = \"<html><body><div style='font-family: monospace; white-space: pre;'>\"\n        html += content.replace(\"\\n\", \"<br>\")\n        html += \"</div></body></html>\"\n        msg.add_alternative(html, subtype=\"html\")\n        \n        logger.debug(\"Thread %s: MotionDetector._initNotificationMessage - done\", get_ident())\n        return msg\n        \n    @classmethod\n    def _attachMedia(cls, mail:EmailMessage, eventId:UUID) -> bool:\n        \"\"\" Attach media to the mail message\n        \n        This method is called when the action is executed in the context of a trigger.\n        The media is attached from the event context.\n\n        Args:\n            mail (EmailMessage): Email message to which the media should be attached\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n\n        Returns:\n            bool: True if media was attached\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._attachMedia\", get_ident())\n        done = False\n        ctx = cls._getEventContext(eventId)\n        if \"actions\" in ctx:\n            actions = ctx[\"actions\"]\n            for actionCtx in actions:\n                if \"photos\" in actionCtx:\n                    photos = actionCtx[\"photos\"]\n                    for photo in photos:\n                        if \"photo_file\" in photo:\n                            filename = photo[\"photo_file\"]\n                            filepath = photo[\"photo_path\"]\n                            logger.debug(\"Thread %s: TriggerHandler._attachMedia - Adding attachment %s\", get_ident(), filename)\n                            with open(filepath, \"rb\") as f:\n                                filetype, _ = mimetypes.guess_type(filename)\n                                mail.add_attachment(f.read(), maintype=filetype, subtype=filetype.split(\"/\")[1], filename=filename)\n                if \"video\" in actionCtx:\n                    video = actionCtx[\"video\"]\n                    if \"video_file\" in video:\n                        filename = video[\"video_file\"]\n                        filepath = video[\"video_path\"]\n                        logger.debug(\"Thread %s: TriggerHandler._attachMedia - Adding attachment %s\", get_ident(), filename)\n                        # Wait a second for the file to be closed\n                        time.sleep(1)\n                        with open(filepath, \"rb\") as f:\n                            filetype, _ = mimetypes.guess_type(filename)\n                            mail.add_attachment(f.read(), maintype=filetype, subtype=filetype.split(\"/\")[1], filename=filename)\n            done = True\n        return done\n\n    @classmethod\n    def _doSMTPaAction(cls, action:Action, trigger:Trigger=None, eventId:UUID=None, last:bool=False, threadRegistered:bool=True, wait:bool=True) -> tuple[bool, str]:\n        \"\"\" Send a mail\n\n        Args:\n            action (Action): Action to be executed\n            trigger (Trigger, optional): Trigger on behalf of which the action is executed\n            eventId (UUID): Unique ID of the event in the context of which the action is executed\n            threadRegistered (bool, optional): If true (default), the active thread must be unregistered\n            wait (bool, optional): If true (default), the thread needs to wait for the resource\n\n        Raises:\n\n        Returns:\n            bool: True if action was executed\n        \"\"\"\n        if trigger:\n            triggerDisp = trigger.id\n        else:\n            triggerDisp = \"No Trigger\"\n        logger.debug(\"Thread %s: TriggerHandler._doSMTPaAction - action=%s trigger.id=%s - starting - wait=%s last=%s\", get_ident(), action.id, triggerDisp, wait, last)\n        cfg = CameraCfg()\n        \n        done = False\n        msg = \"\"\n        ok = True\n    \n        if last == True:\n            # Wait for other actions to complete\n            cls._waitForCompletion(eventId)\n        \n        ctx = cls._getActionContext(eventId, action.id)\n        ctx[\"action_start\"] = datetime.now()\n        \n        try:\n            # Prepare email\n            mail = cls._initNotificationMessage(eventId)\n\n            #Attach media\n            cls._attachMedia(mail, eventId)\n            \n            # Send email\n            tc = cfg.triggerConfig\n            scr =CameraCfg().secrets\n            if tc.notifyUseSSL == True:\n                server = smtplib.SMTP_SSL(host=tc.notifyHost, port=tc.notifyPort)\n            else:\n                server = smtplib.SMTP(host=tc.notifyHost, port=tc.notifyPort)\n            server.connect(tc.notifyHost)\n            \n            if tc.notifyAuthenticate == True:\n                logger.debug(\"Thread %s: TriggerHandler._doSMTPaAction - Authentication with user/pwd\", get_ident())\n                server.login(scr.notifyUser, scr.notifyPwd)\n            else:\n                logger.debug(\"Thread %s: TriggerHandler._doSMTPaAction - Authentication skipped\", get_ident())\n            server.ehlo()\n            server.send_message(mail)\n            server.quit()\n            logger.debug(\"Thread %s: TriggerHandler._doSMTPaAction - Mail sent\", get_ident())\n        except Exception as e:\n            logger.error(\"TriggerHandler._doSMTPaAction - Error %s: %s\", type(e), e)\n            tc.error = f\"Error {type(e)} while sending email: {e}\"\n\n        if last == True:\n            # Finalize event\n            cls._finalizeEvent(eventId)\n            \n        # Remove sub_thread from the list of threads\n        if threadRegistered == True:\n            thread = threading.current_thread()\n            with cls._list_lock:\n                if thread in cls._sub_threads:\n                    cls._sub_threads.remove(thread)\n\n        ctx[\"action_stop\"] = datetime.now()\n\n        logger.debug(\"Thread %s: TriggerHandler._doSMTPaAction - action=%s trigger.id=%s terminated\", get_ident(), action.id, triggerDisp)\n        return done, msg\n                \n\n    @classmethod\n    def doAction(cls, actionId: str) ->str:\n        \"\"\" Execute an action outside the triggering context\n        \n        This method is designed to be executed outside the triggering context.\n        It is assumed that it is not run within the trigger handling thread, for example through an action button.\n\n        The function will wait for completion of the action.\n                \n        Action execution initiated through this method will share registered devices.\n        Therefore, 'manual' execution of actions can be done while the trigger handling thread is active.\n\n        Args:\n            actionId (str): ID of the action to be executed\n\n        Returns:\n            (str) : Information / Error message\n        \"\"\"\n        msg = \"\"\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n\n        action = tc.getAction(actionId)\n        if action is None:\n            msg = f\"There is no action with ID {actionId}\"\n        else:\n            if action.isActive:\n                if action.source == \"GPIO\":\n                    done = cls._doGpioAction(action, threadRegistered=False, wait=False)\n                    if not done:\n                        msg = \"Device busy. Please repeat the action later.\"\n                    else:\n                        msg = \"Action completed\"\n                elif action.source == \"Camera\":\n                    done, msg = cls._doCameraAction(action, threadRegistered=False, wait=False)\n                    if not done:\n                        if msg == \"\":\n                            msg = \"Device busy. Please repeat the action later.\"\n                    else:\n                        if msg == \"\":\n                            msg = \"Action completed\"\n                else:\n                    msg = f\"Actions from source {action.source} are not (yet) supported.\"\n            else:\n                msg = f\"Action {actionId} is not activated.\"\n        return msg\n    \n    @classmethod\n    def _getEventContext(cls, eventId:UUID) -> dict:\n        \"\"\" Return the event context for a given event ID\n        \n        Args:\n            eventId (UUID): event ID to be searched\n\n        Returns:\n            dict: event context\n        \"\"\"\n        ctx = {}\n        for eventCtx in cls._event_contexts:\n            if eventCtx[\"event_id\"] == eventId:\n                ctx = eventCtx\n                break\n        return ctx\n    \n    @classmethod\n    def _getActionContext(cls, eventId:UUID, actionId:str) -> dict:\n        \"\"\" Find the action context in the event context\n\n        Args:\n            eventId (UUID): Unique ID of the ecent\n            actionId (str): Action ID\n\n        Returns:\n            dict: dictionary for action context\n        \"\"\"\n        ctx = {}\n        if eventId is not None:\n            eventCtx = cls._getEventContext(eventId)\n            if \"actions\" in eventCtx:\n                actions = eventCtx[\"actions\"]\n                for action in actions:\n                    if action[\"action\"] == actionId:\n                        ctx = action\n                        break\n        return ctx\n    \n    @classmethod\n    def _finalizeEvent(cls, eventId:UUID):\n        \"\"\" Finalize an event\n\n            - Handle event log\n            - Remove event from event contexts\n        Args:\n            eventId (UUID): ID of invent to be finalized\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler.finalizeEvent - eventId=%s\", get_ident(), eventId)\n        \n        # Wait for event completion\n        cls._waitForCompletion(eventId)\n        \n        ctx = cls._getEventContext(eventId)\n        if ctx in cls._event_contexts:\n            with cls._context_lock:\n                cls._event_contexts.remove(ctx)\n                logger.debug(\"Thread %s: TriggerHandler.finalizeEvent - context removed\", get_ident())\n\n    @classmethod\n    def _waitForCompletion(cls, eventId:UUID):\n        \"\"\" wait for compeltion of tasks for an event\n        \n            Iterate through the action contexts for an event.\n            As long as an action is found, which is not marked as last action,\n            for which the thread is still in the thread table, wait\n\n        Args:\n            eventId (UUID): Unique ID of event\n        \"\"\"\n        time.sleep(1.0)\n        logger.debug(\"Thread %s: TriggerHandler._waitForCompletion - entry\", get_ident())\n        ctxActions = None\n        if eventId:\n            ctx = cls._getEventContext(eventId)\n            if ctx in cls._event_contexts:\n                logger.debug(\"Thread %s: TriggerHandler._waitForCompletion - context=%s\", get_ident(), ctx)\n                if \"actions\" in ctx:\n                    ctxActions = ctx[\"actions\"]\n        if ctxActions is not None:\n            if len(ctxActions) > 0:\n                wait = True\n                while wait == True:\n                    wait = False\n                    for actionCtx in ctxActions:\n                        if \"is_last\" in actionCtx:\n                            isLast = actionCtx[\"is_last\"]\n                            if isLast == False:\n                                if \"thread\" in actionCtx:\n                                    thread = actionCtx[\"thread\"]\n                                    if thread in cls._sub_threads:\n                                        wait = True\n                    if wait == True:\n                        time.sleep(0.1)\n        if eventId:\n            ctx = cls._getEventContext(eventId)\n            logger.debug(\"Thread %s: TriggerHandler._waitForCompletion - context=%s\", get_ident(), ctx)\n        logger.debug(\"Thread %s: TriggerHandler._waitForCompletion - exit\", get_ident())\n        \n    @classmethod\n    def _logEvent(cls, db:Connection, logType:str, tc:TriggerConfig, eventCtx:dict, actionCtx:dict=None, photoCtx:dict=None, err:str=\"\"):\n        logger.debug(\"Thread %s: TriggerHandler._logEvent - entry\", get_ident())\n        \n        triggerId = eventCtx[\"trigger\"]\n        trigger = tc.getTrigger(triggerId)\n        eventTS = eventCtx[\"event_TS\"]\n        \n        if logType == \"start\":\n            # Event start\n            logTS = eventTS\n            key = eventTS\n            with open(tc.logFilePath, \"a\") as f:\n                f.write(eventCtx[\"event_TS\"] + \" Event  detected       Trigger: \" + trigger.id + \" - '\" + trigger.source + \"' \" + str(trigger.params) + \"\\n\")\n            db.execute(\n                \"INSERT INTO events (timestamp, date, minute, time, type, trigger, triggertype, triggerparam) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                (key, key[:10], key[11:16], key[11:19], \"Motion\", trigger.id, trigger.source, str(trigger.params))\n            )\n            db.commit()\n            \n        elif logType == \"gpio_action\":\n            # GPIO action\n            if \"action\" in actionCtx:\n                actionId = actionCtx[\"action\"]\n                action = tc.getAction(actionId)\n                hasDuration = False\n                duration = 0\n                if \"duration\" in action.control:\n                    duration = action.control[\"duration\"]\n                    if duration > 0:\n                        hasDuration = True\n                logTS = actionCtx[\"action_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    if hasDuration == True:\n                        f.write(logTS + \"  GPIO: \" + actionId + \" started\\n\")\n                    else:\n                        f.write(logTS + \"  GPIO: \" + actionId + \"\\n\")\n                db.execute(\n                    \"INSERT INTO eventactions (event, timestamp, date, time, actiontype, actionduration, filename, fullpath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (eventTS, logTS, logTS[:10], logTS[11:19], actionId, duration, \"\", \"\")\n                )\n                db.commit()\n            \n        elif logType == \"gpio_action_finished\":\n            # GPIO action finished\n            if \"action\" in actionCtx:\n                actionId = actionCtx[\"action\"]\n                action = tc.getAction(actionId)\n                actionTS = actionCtx[\"action_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                logTS = actionCtx[\"action_stop\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                duration = actionCtx[\"action_stop\"] - actionCtx[\"action_start\"]\n                actionDuration = duration.total_seconds()\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \"  GPIO: \" + actionId + \" stopped\\n\")\n                db.execute(\n                    \"UPDATE eventactions set actionduration = ? WHERE event = ? AND timestamp = ? AND actiontype = ?\",\n                    (actionDuration, eventTS, actionTS, actionId)\n                )\n                db.commit()\n        \n        elif logType == \"gpio_action_error\":\n            # GPIO Error\n            if \"action\" in actionCtx:\n                actionId = actionCtx[\"action\"]\n                logTS = datetime.now().strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \"  GPIO: \" + actionId + \" Error:  \" + err + \"\\n\")\n            \n        elif logType == \"photo_taken\":\n            # Photo taken\n            if \"photo_file\" in photoCtx:\n                fnPhoto = photoCtx[\"photo_file\"]\n                fpPhoto = photoCtx[\"photo_path\"]\n                logTS = photoCtx[\"photo_time\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Photo: \" + fnPhoto + \"\\n\")\n                db.execute(\n                    \"INSERT INTO eventactions (event, timestamp, date, time, actiontype, actionduration, filename, fullpath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (eventTS, logTS, logTS[:10], logTS[11:19], \"Photo\", 0, fnPhoto, fpPhoto)\n                )\n                db.commit()\n        \n        elif logType == \"photo_error\":\n            # Photo Error\n            if \"photo_file\" in photoCtx:\n                fnPhoto = photoCtx[\"photo_file\"]\n                logTS = datetime.now().strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Photo: \" + fnPhoto + \" Error:  \" + err + \"\\n\")\n\n        elif logType == \"video_start\":\n            # Video Start\n            if \"video\" in actionCtx:\n                fnVideo = actionCtx[\"video\"][\"video_file\"]\n                fpVideo = actionCtx[\"video\"][\"video_path\"]\n                logTS = actionCtx[\"video\"][\"video_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                actionId = actionCtx[\"action\"]\n                action = tc.getAction(actionId)\n                if \"duration\" in action.control:\n                    actionDuration = action.control[\"duration\"]\n                else:\n                    actionDuration = 0\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" started\" + \"\\n\")\n                db.execute(\n                    \"INSERT INTO eventactions (event, timestamp, date, time, actiontype, actionduration, filename, fullpath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n                    (eventTS, logTS, logTS[:10], logTS[11:19], \"Video\", actionDuration, fnVideo, fpVideo)\n                )\n                db.commit()\n        \n        elif logType == \"video_start_err\":\n            # Video Start error\n            if \"video\" in actionCtx:\n                fnVideo = actionCtx[\"video\"][\"video_file\"]\n                logTS = datetime.now().strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" Start   Error: \" + err + \"\\n\")\n            \n        elif logType == \"video_stop\":\n            # Video stopped\n            if \"video\" in actionCtx:\n                fnVideo = actionCtx[\"video\"][\"video_file\"]\n                fpVideo = actionCtx[\"video\"][\"video_path\"]\n                videoKey = actionCtx[\"video\"][\"video_start\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                logTS = actionCtx[\"video\"][\"video_stop\"].strftime(\"%Y-%m-%dT%H:%M:%S\")\n                actionId = actionCtx[\"action\"]\n                action = tc.getAction(actionId)\n                if \"duration\" in action.control:\n                    actionDuration = action.control[\"duration\"]\n                else:\n                    actionDuration = 0\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" stopped\" + \"\\n\")\n                logger.debug(\"Thread %s: MotionDetector._stopAction - UPDATE eventactions\", get_ident())\n                db.execute(\n                    \"UPDATE eventactions set actionduration = ? WHERE event = ? AND timestamp = ? AND actiontype = ?\",\n                    (round(actionDuration,0), eventTS, videoKey, \"Video\")\n                )\n                db.commit()\n        \n        elif logType == \"video_stop_err\":\n            # Video stop error\n            if \"video\" in actionCtx:\n                fnVideo = actionCtx[\"video\"][\"video_file\"]\n                logTS = datetime.now().strftime(\"%Y-%m-%dT%H:%M:%S\")\n                with open(tc.logFilePath, \"a\") as f:\n                    f.write(logTS + \" Video: \" + fnVideo + \" Stop     Error\" + err + \"\\n\")\n        else:\n            logger.error(\"TriggerHandler._logEvent - Unknown logType %s\", logType)\n        \n\n    @classmethod    \n    def _actionDispatcher(cls, gpioDevice, trigger:Trigger):\n        \"\"\" Dispatch actions configured for the given trigger\n        \n            Every active action associated with the given trigger, which initiated the event,\n            will be started in an own sub-thread.\n            All sub_threads are listed in the cls._sub_threads list.\n            When a thread terminates, its last action will be to remove itself from the list.\n            \n            A context is maintained for the event fired by the given trigger:\n\n            eventCtx = {\n                \"event_id\": eventId,\n                \"trigger\": trigger,\n                \"event_time\": eventTime,\n                \"event_TS\": eventTimestamp,\n                \"actions]:[\n                    {\n                        \"action\": action,\n                        \"is_last\": is_last,\n                        \"thread\": thread,\n                        \"action_start\": actionStart,\n                        \"action_stop\": actionStop,\n                        \"video\": [\n                            {\n                                \"video_start\": videoStart,\n                                \"video_file\": filename,\n                                \"video_path\": filepath\n                            }\n                        ],\n                        \"photos\": [\n                            {\n                                \"photo_time\": photoTime,\n                                \"photo_file\": filename,\n                                \"photo_path\": filepath\n                            },\n                            ...\n                        ]\n                    }\n                ]\n            }\n            \n        Args:\n            gpioDevice (_type_): gpiozero device opject which activated the function\n            trigger (Trigger): Trigger on behalf of which the function was activated\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._actionDispatcher - gpioDevice=%s trigger.id=%s\", get_ident(), gpioDevice, trigger.id)\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n\n        isEvent = False\n        if trigger:\n            triggerCtrl = trigger.control\n            if \"event_log\" in triggerCtrl:\n                event_log = triggerCtrl[\"event_log\"]\n                if event_log == True:\n                    isEvent = True\n        \n        if cls._isActive() == True:\n            trg = tc.getTrigger(trigger.id)\n            if cls._bouncing(trg, sc) == False:\n                # Initialize the trigger context\n                eventId = uuid4()\n                eventCtx = {}\n                eventCtx[\"event_id\"] = eventId\n                eventCtx[\"trigger\"] = trigger.id\n                now = datetime.now()\n                eventCtx[\"event_time\"] = now\n                eventCtx[\"event_TS\"] = now.strftime(\"%Y-%m-%dT%H:%M:%S\")\n                eventCtx[\"actions\"] = []\n                with cls._context_lock:\n                    cls._event_contexts.append(eventCtx)\n\n                # log event start\n                if isEvent == True:\n                    db = get_dbx()\n                    cls._logEvent(db, \"start\", tc, eventCtx)\n                \n                # Search SMTP action in order to set it as last one\n                setAsLast = True\n                for act, status in trg.actions.items():\n                    if status == True:\n                        action = tc.getAction(act)\n                        if action.isActive:\n                            if action.source == \"SMTP\":\n                                action = tc.getAction(act)\n                                actionCtx = {}\n                                actionCtx[\"action\"] = action.id\n                                actionCtx[\"is_last\"] = setAsLast\n                                # For SMTP action, do not wait.\n                                # Mails can always be sent\n                                smtpActionThread = threading.Thread(target=cls._doSMTPaAction, args=(action, trigger, eventId, setAsLast, True, False))\n                                actionCtx[\"thread\"] = smtpActionThread\n                                smtpActionThread.start()\n                                cls._sub_threads.append(smtpActionThread)\n                                setAsLast = False\n                \n                # Iterate actions\n                for act, status in trg.actions.items():\n                    if status == True:\n                        action = tc.getAction(act)\n                        if action.isActive:\n                            if action.source != \"SMTP\":\n                                actionCtx = {}\n                                actionCtx[\"action\"] = action.id\n                                actionCtx[\"is_last\"] = setAsLast\n                                eventCtx[\"actions\"]. append(actionCtx)\n                                if action.source == \"GPIO\":\n                                    gpioActionThread = threading.Thread(target=cls._doGpioAction, args=(action, trigger, eventId, setAsLast))\n                                    actionCtx[\"thread\"] = gpioActionThread\n                                    gpioActionThread.start()\n                                    cls._sub_threads.append(gpioActionThread)\n                                if action.source == \"Camera\":\n                                    # For camera action, do not wait for the camera.\n                                    # Waits would mainly occur with video recording. There, it does no make sense\n                                    # to wait until current recording is finished and start new recording afterwards.\n                                    camActionThread = threading.Thread(target=cls._doCameraAction, args=(action, trigger, eventId, setAsLast, True, False))\n                                    actionCtx[\"thread\"] = camActionThread\n                                    camActionThread.start()\n                                    cls._sub_threads.append(camActionThread)\n                                setAsLast = False\n                logger.debug(\"Thread %s: TriggerHandler._actionDispatcher - cls._event_contexts: %s\", get_ident(), cls._event_contexts)\n\n    @classmethod\n    def _registerGpioTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Register callback function for gpiozero device\n\n        Args:\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger\", get_ident())\n        \n        triggerId = trg.id\n        deviceId = trg.device\n        _, reg = cls._findDeviceInRegistry(\"GPIO\", deviceId, sc, False)\n        deviceClass = reg[\"deviceClass\"]\n        deviceObj = reg[\"deviceObject\"]\n\n        try:\n            # Apply event settings\n            params = trg.params\n            if len(params) > 0:\n                for param, value in params.items():\n                    if hasattr(deviceObj, param):\n                        attr = getattr(deviceObj, param)\n                        if callable(attr) == True:\n                            call = f\"{deviceClass}.{param}()\"\n                            logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger Trigger: %s - %s=%s\", get_ident(),  triggerId, call, result)\n                            result = attr()\n                        else:\n                            if value != \"\":\n                                assignment = f\"{deviceClass}.{param}={value}\"\n                                logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger Trigger: %s - %s\", get_ident(),  triggerId, assignment)\n                                setattr(deviceObj, param, value)\n                            else:\n                                call = f\"{deviceClass}.{param}\"\n                                logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger - Trigger: %s - %s=%s\", get_ident(),  triggerId, call, result)\n                                result = attr\n    \n            # register event handler\n            event = trg.event\n            if hasattr(deviceObj, event):\n                attr = getattr(deviceObj, event)\n                assignment = f\"{deviceClass}.{event}\"\n                if callable(attr) == False:\n                    setattr(deviceObj, event, lambda deviceObj=deviceObj, trg=trg: cls._actionDispatcher(deviceObj, trigger=trg))\n                    logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger - Trigger: %s - callback assigned to %s\", get_ident(),  triggerId, assignment)\n                    with cls._registry_lock:\n                        reg[\"methods\"].append(event)\n                else:\n                    raise ValueError(f\"{assignment} is callable and not suitable for assignment of callback\")\n            else:\n                raise ValueError(f\"Class {deviceClass} has no element {event}\")\n        except Exception as e:\n            logger.error(\"TriggerHandler._registerGpioTrigger - Trigger: %s - Error %s : %s\",  triggerId, type(e), e)\n            tc.error = f\"Trigger: {triggerId} - Error: {type(e)} - {e}\"\n\n    @classmethod\n    def _registerCameraTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Register callback function for Camera\n\n        Args:\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._registerCameraTrigger\", get_ident())\n\n        triggerId = trg.id\n        deviceId = trg.device\n        _, reg = cls._findDeviceInRegistry(\"Camera\", deviceId, sc, False)\n        deviceClass = reg[\"deviceClass\"]\n        deviceObj = reg[\"deviceObject\"]\n\n        try:\n            # Get Camera instance\n            deviceObj = Camera()\n    \n            # register event handler\n            event = trg.event\n            if hasattr(deviceObj, event):\n                attr = getattr(deviceObj, event)\n                assignment = f\"{deviceClass}.{event}\"\n                setattr(deviceObj, event, lambda deviceObj=deviceObj, trg=trg: cls._actionDispatcher(deviceObj, trigger=trg))\n                logger.debug(\"Thread %s: TriggerHandler._registerGpioTrigger - Trigger: %s - callback assigned to %s\", get_ident(),  triggerId, assignment)\n                with cls._registry_lock:\n                    reg[\"methods\"].append(event)\n            else:\n                raise ValueError(f\"Class {deviceClass} has no element {event}\")\n        except Exception as e:\n            logger.error(\"TriggerHandler._registerCameraTrigger - Trigger: %s - Error %s : %s\",  triggerId, type(e), e)\n            tc.error = f\"Trigger: {triggerId} - Error: {type(e)} - {e}\"\n\n    @classmethod\n    def _registerMotionDetectorTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Register callback function for MotionDetector\n\n        Args:\n            sc (ServerConfig): Server configuration\n            tc (TriggerConfig): Trigger configuration\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._registerMotionDetectorTrigger\", get_ident())\n        \n        # Register trigger only if motion detection is enabled\n        if tc.triggeredByMotion == True:\n            triggerId = trg.id\n            deviceId = trg.device\n            _, reg = cls._findDeviceInRegistry(\"MotionDetector\", deviceId, sc, False)\n            deviceClass = reg[\"deviceClass\"]\n            deviceObj = reg[\"deviceObject\"]\n\n            try:\n                # Get MotionDetector instance\n                deviceObj = MotionDetector()\n        \n                # register event handler\n                event = trg.event\n                if hasattr(deviceObj, event):\n                    attr = getattr(deviceObj, event)\n                    assignment = f\"{deviceClass}.{event}\"\n                    setattr(deviceObj, event, lambda deviceObj=deviceObj, trg=trg: cls._actionDispatcher(deviceObj, trigger=trg))\n                    logger.debug(\"Thread %s: TriggerHandler._registerMotionDetectorTrigger - Trigger: %s - callback assigned to %s\", get_ident(),  triggerId, assignment)\n                    with cls._registry_lock:\n                        reg[\"methods\"].append(event)\n                else:\n                    raise ValueError(f\"Class {deviceClass} has no element {event}\")\n            except Exception as e:\n                logger.error(\"TriggerHandler._registerMotionDetectorTrigger - Trigger: %s - Error %s : %s\",  triggerId, type(e), e)\n                tc.error = f\"Trigger: {triggerId} - Error: {type(e)} - {e}\"\n        \n    @classmethod        \n    def _registerTriggers(cls):\n        \"\"\" Register callback functions for methods specified for the configured triggers\n\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._registerTriggers\", get_ident())\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n        \n        try:\n            for trg in tc.triggers:\n                if trg.isActive == True:\n                    if trg.source == \"GPIO\":\n                        cls._registerGpioTrigger(sc, tc, trg)\n                    elif trg.source == \"Camera\":\n                        cls._registerCameraTrigger(sc, tc, trg)\n                    elif trg.source == \"MotionDetector\":\n                        cls._registerMotionDetectorTrigger(sc, tc, trg)\n            \n        except Exception as e:\n            logger.error(\"TriggerHandler._registerTriggers - Error %s : %s\", type(e), e)\n            tc.error = f\"registerTriggers - Error: {type(e)} - {e}\"\n\n    @classmethod\n    def _unregisterGpioTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Unregister callback function for gpiozero device\n\n        Args:\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._unregisterGpioTrigger\", get_ident())\n\n        triggerId = trg.id\n        deviceId = trg.device\n        device = sc.getDevice(deviceId)\n        deviceClass = device.type\n        method = trg.event\n        \n        unregister = False\n        if \"GPIO\" in cls._registry:\n            regGPIO = cls._registry[\"GPIO\"]\n            if deviceId in regGPIO:\n                regGpioDevice = regGPIO[deviceId]\n                if \"deviceObject\" in regGpioDevice:\n                    deviceObj = regGpioDevice[\"deviceObject\"]\n                    if \"methods\" in regGpioDevice:\n                        regGpioMethods = regGpioDevice[\"methods\"]\n                        if method in regGpioMethods:\n                            unregister = True\n        if unregister:\n            assignment = f\"{deviceClass}.{method}=None\"\n            setattr(deviceObj, method, None)\n            logger.debug(\"Thread %s: TriggerHandler._unregisterGpioTrigger - assignment: %s\", get_ident(), assignment)\n            regGpioMethods.remove(method)\n\n    @classmethod\n    def _unregisterCameraTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Unregister callback function for Camera device\n\n        Args:\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._unregisterCameraTrigger\", get_ident())\n\n        triggerId = trg.id\n        deviceId = trg.device\n        deviceClass = \"Camera\"\n        method = trg.event\n        \n        unregister = False\n        if \"Camera\" in cls._registry:\n            regCamera = cls._registry[\"Camera\"]\n            if deviceId in regCamera:\n                regCameraDevice = regCamera[deviceId]\n                if \"deviceObject\" in regCameraDevice:\n                    deviceObj = regCameraDevice[\"deviceObject\"]\n                    if \"methods\" in regCameraDevice:\n                        regCameraMethods = regCameraDevice[\"methods\"]\n                        if method in regCameraMethods:\n                            unregister = True\n        if unregister:\n            assignment = f\"{deviceClass}.{method}=None\"\n            setattr(deviceObj, method, None)\n            logger.debug(\"Thread %s: TriggerHandler._unregisterCameraTrigger - assignment: %s\", get_ident(), assignment)\n            regCameraMethods.remove(method)\n\n    @classmethod\n    def _unregisterMotionDetectorTrigger(cls, sc:ServerConfig, tc:TriggerConfig, trg:Trigger):\n        \"\"\" Unregister callback function for MotionDetector device\n\n        Args:\n            trg (Trigger): Trigger which specifies device and method to be registered\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._unregisterMotionDetectorTrigger\", get_ident())\n\n        triggerId = trg.id\n        deviceId = trg.device\n        deviceClass = \"MotionDetector\"\n        method = trg.event\n        \n        unregister = False\n        if \"MotionDetector\" in cls._registry:\n            regMotionDetector = cls._registry[\"MotionDetector\"]\n            if deviceId in regMotionDetector:\n                regMotionDetectorDevice = regMotionDetector[deviceId]\n                if \"deviceObject\" in regMotionDetectorDevice:\n                    deviceObj = regMotionDetectorDevice[\"deviceObject\"]\n                    if \"methods\" in regMotionDetectorDevice:\n                        regMotionDetectorMethods = regMotionDetectorDevice[\"methods\"]\n                        if method in regMotionDetectorMethods:\n                            unregister = True\n        if unregister:\n            assignment = f\"{deviceClass}.{method}=None\"\n            setattr(deviceObj, method, None)\n            logger.debug(\"Thread %s: TriggerHandler._unregisterMotionDetectorTrigger - assignment: %s\", get_ident(), assignment)\n            regMotionDetectorMethods.remove(method)\n        \n    @classmethod\n    def _unregisterTriggers(cls):\n        \"\"\" Unregister callback functions for methods specified for the configured triggers\n\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._unregisterTriggers\", get_ident())\n        cfg = CameraCfg()\n        sc = cfg.serverConfig\n        tc = cfg.triggerConfig\n        \n        try:\n            for trg in tc.triggers:\n                if trg.isActive == True:\n                    if trg.source == \"GPIO\":\n                        cls._unregisterGpioTrigger(sc, tc, trg)\n                    if trg.source == \"Camera\":\n                        cls._unregisterCameraTrigger(sc, tc, trg)\n                    if trg.source == \"MotionDetector\":\n                        cls._unregisterMotionDetectorTrigger(sc, tc, trg)\n            \n        except Exception as e:\n            logger.error(\"TriggerHandler._unregisterTriggers - Error %s : %s\", type(e), e)\n        \n    @classmethod\n    def _closeGpioDevices(cls):\n        \"\"\" Close devices found in the registry\n\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._closeGpioDevices\", get_ident())\n\n        close = False\n        closed = []\n        if \"GPIO\" in cls._registry:\n            regGPIO = cls._registry[\"GPIO\"]\n            for deviceClass in regGPIO:\n                regGpioDevice = regGPIO[deviceClass]\n                if \"deviceObject\" in regGpioDevice:\n                    deviceObj = regGpioDevice[\"deviceObject\"]\n                    if deviceObj:\n                        if hasattr(deviceObj, \"close\"):\n                            try:\n                                attr = getattr(deviceObj, \"close\")\n                                if callable(attr) == True:\n                                    attr()\n                                    logger.debug(\"Thread %s: TriggerHandler._closeDevices - %s.close()\", get_ident(), deviceClass)\n                                    closed.append(deviceClass)\n                            except Exception as e:\n                                logger.error(\"TriggerHandler._closeDevices - Error while closing %s: %s - %s\", deviceClass, type(e), e)                        \n        for dev in closed:\n            cls._registry[\"GPIO\"].pop(dev)\n        if \"GPIO\" in cls._registry:\n            if len(cls._registry[\"GPIO\"]) > 0:\n                logger.error(\"TriggerHandler._closeDevices - Rgegistry not empty, not all devices closed. Resetting aniway\")                        \n        cls._registry[\"GPIO\"] = {}            \n\n    @classmethod\n    def _triggerThread(cls):\n        \"\"\" Thread for lifecycle of trigger registration\n\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler._triggerThread\", get_ident())\n        \n        # Set active status\n        cls._isActive()\n        \n        stop = False\n        cls._registerTriggers()\n        while not stop:\n            time.sleep(0.5)\n            if cls.triggerThreadStop == True:\n                logger.debug(\"Thread %s: TriggerHandler._triggerThread - stop requested\", get_ident())\n                stop = True\n\n        logger.debug(\"Thread %s: TriggerHandler._triggerThread - stopped\", get_ident())\n        \n        # Wait for sub_threds to terminate\n        logger.debug(\"Thread %s: TriggerHandler._triggerThread - There are %s subthreads to wait for termination\", get_ident(), len(cls._sub_threads))\n        cnt = 0\n        while cls._sub_threads:\n            time.sleep(0.1)\n            cnt += 1\n            if cnt > 200:\n                logger.warning(\"TriggerHandler._triggerThread - %s sub_threads not yet terminated. Stopping anyway\")\n                break\n        cls._sub_threads = []\n        cls._event_contexts = []\n        cls._unregisterTriggers()\n        cls.triggerThread = None\n\n    @classmethod\n    def start(cls) -> bool:\n        \"\"\" Start trigger operation\n\n        Returns:\n            bool: true if operation was successfully started\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler.start\", get_ident())\n        sc = CameraCfg().serverConfig\n        tc = CameraCfg().triggerConfig\n        \n        tc.error = None\n        tc.error2 = None\n\n        if cls.triggerThread is None:\n            sc.error = None\n            tc.error = None\n            if not sc.error:\n                logger.debug(\"Thread %s: TriggerHandler.start - starting new thread\", get_ident())\n                cls.triggerThread = threading.Thread(target=cls._triggerThread, daemon=True)\n                cls.triggerThread.start()\n                logger.debug(\"Thread %s: TriggerHandler.start - thread started\", get_ident())\n            else:\n                logger.debug(\"Thread %s: TriggerHandler.start - not started\", get_ident())\n\n    @classmethod\n    def stop(cls) -> bool:\n        \"\"\" Stop trigger operation\n\n        Returns:\n            bool: true if operation was successfully stopped\n        \"\"\"\n        logger.debug(\"Thread %s: TriggerHandler.stop\", get_ident())\n\n        if cls.triggerThread is None:\n            logger.debug(\"Thread %s: TriggerHandler.stop - thread was not active\", get_ident())\n        else:\n            logger.debug(\"Thread %s: TriggerHandler.stop - stopping thread\", get_ident())\n            cls.triggerThreadStop = True\n            cnt = 0\n            while cls.triggerThread:\n                time.sleep(0.01)\n                cnt += 1\n                if cnt > 500:\n                    logger.error(\"Trigger thread did not stop within 5 sec\")\n                    if cls.triggerThread.is_alive():\n                        cnt = 0\n                    else:\n                        cls.triggerThread = None\n            cls._closeGpioDevices()\n            cls.triggerThreadStop = False\n        logger.debug(\"Thread %s: TriggerHandler.stop: Thread has stopped\", get_ident())\n        "
  },
  {
    "path": "raspiCamSrv/version.py",
    "content": "version=\"V4.10.0\""
  },
  {
    "path": "raspiCamSrv/versionDoc.py",
    "content": "docversion=\"4.10\""
  },
  {
    "path": "raspiCamSrv/webcam.py",
    "content": "from flask import (\n    Blueprint,\n    Response,\n    flash,\n    g,\n    redirect,\n    render_template,\n    request,\n    url_for,\n)\nfrom werkzeug.exceptions import abort\nfrom raspiCamSrv.camera_pi import Camera\nfrom raspiCamSrv.camCfg import CameraCfg, TuningConfig, StereoConfig, ServerConfig, AiConfig\nfrom raspiCamSrv.version import version\nfrom raspiCamSrv.home import generateHistogram\nfrom _thread import get_ident\nfrom pathlib import Path\nimport os\nimport datetime\nimport time\nimport copy\n\nfrom raspiCamSrv.auth import login_required, login_for_streaming\nimport logging\n\nbp = Blueprint(\"webcam\", __name__)\n\nlogger = logging.getLogger(__name__)\n\n# Try to import StereoCam\ntry:\n    from raspiCamSrv.stereoCam import StereoCam\nexcept ImportError:\n    pass\n\n\n@bp.route(\"/webcam\")\n@login_required\ndef webcam():\n    logger.debug(\"In webcam\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    sc.error = None\n    sc.errorc2 = None\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    if sc.useStereo == False:\n        if sc.lastCamTab == \"calibcam\" or sc.lastCamTab == \"stereocam\":\n            sc.lastCamTab = \"webcam\"\n    if len(sc.supportedCameras) < 2:\n        sc.lastCamTab = \"webcam\"\n\n    if sc.error:\n        msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n        flash(msg)\n        if sc.error2:\n            flash(sc.error2)\n    if sc.errorc2:\n        msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n        flash(msg)\n        if sc.errorc22:\n            flash(sc.errorc22)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n@bp.route(\"/active_camera_photo_cfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef active_camera_photo_cfg():\n    logger.debug(\"In active_camera_photo_cfg\")\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"webcam\"\n    if request.method == \"POST\":\n        sc.webCamActiveCamPhotoCfg = request.form[\"activecameraphotocfg\"]\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n@bp.route(\"/second_camera_photo_cfg\", methods=(\"GET\", \"POST\"))\n@login_required\ndef second_camera_photo_cfg():\n    logger.debug(\"In second_camera_photo_cfg\")\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"webcam\"\n    if request.method == \"POST\":\n        sc.webCamSecondCamPhotoCfg = request.form[\"secondcameraphotocfg\"]\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n@bp.route(\"/store_streaming_config\", methods=(\"GET\", \"POST\"))\n@login_required\ndef store_streaming_config():\n    logger.debug(\"In store_streaming_config\")\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        tc = cfg.triggerConfig\n        scfg = cfg.streamingCfg[str(sc.activeCamera)]\n        scfg[\"cameraproperties\"] = copy.deepcopy(cfg.cameraProperties)\n        if sc.activeCameraIsUsb == False:\n            scfg[\"tuningconfig\"] = copy.deepcopy(cfg.tuningConfig)\n        scfg[\"liveconfig\"] = copy.deepcopy(cfg.liveViewConfig)\n        scfg[\"photoconfig\"] = copy.deepcopy(cfg.photoConfig)\n        scfg[\"rawconfig\"] = copy.deepcopy(cfg.rawConfig)\n        scfg[\"videoconfig\"] = copy.deepcopy(cfg.videoConfig)\n        scfg[\"controls\"] = copy.deepcopy(cfg.controls)\n        scfg[\"triggercamera\"] = copy.deepcopy(tc.cameraSettings)\n        scfg[\"aiconfig\"] = copy.deepcopy(cfg.aiConfig)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(\n            f\"Camera settings for {sc.activeCameraInfo} saved for camera switch and streaming\"\n        )\n        cfg.streamingCfgInvalid = False\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/sync_settings\", methods=(\"GET\", \"POST\"))\n@login_required\ndef sync_settings():\n    logger.debug(\"In sync_settings\")\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    tc = cfg.triggerConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        if sc.activeCameraInfo[8:] == str2[\"camerainfo\"][8:]:\n            scfg = cfg.streamingCfg[str(Camera().camNum2)]\n            scfg[\"cameraproperties\"] = copy.deepcopy(cfg.cameraProperties)\n            if sc.activeCameraIsUsb == False:\n                scfg[\"tuningconfig\"] = copy.deepcopy(cfg.tuningConfig)\n            scfg[\"liveconfig\"] = copy.deepcopy(cfg.liveViewConfig)\n            scfg[\"photoconfig\"] = copy.deepcopy(cfg.photoConfig)\n            scfg[\"rawconfig\"] = copy.deepcopy(cfg.rawConfig)\n            scfg[\"videoconfig\"] = copy.deepcopy(cfg.videoConfig)\n            scfg[\"controls\"] = copy.deepcopy(cfg.controls)\n            scfg[\"triggercamera\"] = copy.deepcopy(tc.cameraSettings)\n            scfg[\"aiconfig\"] = copy.deepcopy(cfg.aiConfig)\n            Camera().restartLiveStream2()\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(\n                f\"Camera settings for {sc.activeCameraInfo} synced with camera {str2['camerainfo']}\"\n            )\n        else:\n            flash(\"Camera settings can only be synced for the same camera model\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/switch_cameras\", methods=(\"GET\", \"POST\"))\n@login_required\ndef switch_cameras():\n    logger.debug(\"In switch_cameras\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        msg = None\n        activeCam = sc.activeCamera\n        newCam = activeCam\n        if sc.secondCamera is None:\n            for cm in cs:\n                if activeCam != cm.num:\n                    newCam = cm.num\n                    newCamInfo = \"Camera \" + str(cm.num) + \" (\" + cm.model + \")\"\n                    newCamModel = cm.model\n                    newCamIsUsb = cm.isUsb\n                    newCamUsbDev = cm.usbDev\n                    newCamHasAi = cm.hasAi\n                    break\n        else:\n            newCam = sc.secondCamera\n            newCamInfo = sc.secondCameraInfo\n            newCamModel = sc.secondCameraModel\n            newCamIsUsb = sc.secondCameraIsUsb\n            newCamUsbDev = sc.secondCameraUsbDev\n            newCamHasAi = sc.secondCameraHasAi\n\n        if newCam != sc.activeCamera:\n            if sc.isTriggerRecording:\n                msg = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n            if sc.isVideoRecording == True:\n                msg = \"Please stop video recording before changing the tuning configuration\"\n            if sc.isPhotoSeriesRecording:\n                msg = \"Please go to 'Photo Series' and stop the active process before changing the tuning configuration\"\n            if not msg:\n                sc.secondCamera = sc.activeCamera\n                sc.secondCameraInfo = sc.activeCameraInfo\n                sc.secondCameraModel = sc.activeCameraModel\n                sc.secondCameraIsUsb = sc.activeCameraIsUsb\n                sc.secondCameraUsbDev = sc.activeCameraUsbDev\n                sc.secondCameraHasAi = sc.activeCameraHasAi\n                sc.activeCamera = newCam\n                sc.activeCameraInfo = newCamInfo\n                sc.activeCameraModel = newCamModel\n                sc.activeCameraIsUsb = newCamIsUsb\n                sc.activeCameraUsbDev = newCamUsbDev\n                sc.activeCameraHasAi = newCamHasAi\n                cfg.liveViewConfig.stream_size = None\n                cfg.photoConfig.stream_size = None\n                cfg.rawConfig.stream_size = None\n                cfg.videoConfig.stream_size = None\n                strCfg = cfg.streamingCfg\n                newCamStr = str(newCam)\n                if newCamStr in strCfg:\n                    ncfg = strCfg[newCamStr]\n                    if \"tuningconfig\" in ncfg:\n                        cfg.tuningConfig = ncfg[\"tuningconfig\"]\n                    else:\n                        cfg.tuningConfig = TuningConfig()\n                        \n                    if \"aiconfig\" in ncfg:\n                        cfg.aiConfig = copy.deepcopy(ncfg[\"aiconfig\"])\n                    else:\n                        cfg.aiConfig = AiConfig()\n                else:\n                    cfg.tuningConfig = TuningConfig()\n                    cfg.aiConfig = AiConfig()\n                Camera().switchCamera()\n                if sc.isLiveStream2:\n                    str2 = cfg.streamingCfg[str(Camera().camNum2)]\n                logger.debug(\n                    \"switch_cameras - active camera set to %s\", sc.activeCamera\n                )\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(\n                    f\"Cameras switched: Active camera now: {sc.activeCameraInfo}\"\n                )\n                cfg.streamingCfgInvalid = False\n        if msg:\n            flash(msg)\n        return render_template(\"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs)\n    else:\n        return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/change_active_camera\", methods=(\"GET\", \"POST\"))\n@login_required\ndef change_active_camera():\n    logger.debug(\"In change_active_camera\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        msg = None\n        newCam = int(request.form[\"activecamera\"])\n        secondCamera = -1\n        if not sc.secondCamera is None:\n            secondCamera = sc.secondCamera\n        for cm in cs:\n            if newCam == cm.num:\n                newCamInfo = \"Camera \" + str(cm.num) + \" (\" + cm.model + \")\"\n                newCamModel = cm.model\n                newCamIsUsb = cm.isUsb\n                newCamUsbDev = cm.usbDev\n                newCamHasAi = cm.hasAi\n                break\n\n        if newCam != sc.activeCamera:\n            if sc.isTriggerRecording:\n                msg = \"Please go to 'Trigger' and stop the active process before changing the configuration\"\n            if sc.isVideoRecording == True:\n                msg = \"Please stop video recording before changing the camera\"\n            if sc.isPhotoSeriesRecording:\n                msg = \"Please go to 'Photo Series' and stop the active process before changing the camera\"\n            if newCam == secondCamera:\n                msg = \"Active camera must be different from second camera. Use 'Switch Cameras' to swap the cameras.\"\n            if not msg:\n                sc.activeCamera = newCam\n                sc.activeCameraInfo = newCamInfo\n                sc.activeCameraModel = newCamModel\n                sc.activeCameraIsUsb = newCamIsUsb\n                sc.activeCameraUsbDev = newCamUsbDev\n                sc.activeCameraHasAi = newCamHasAi\n                cfg.liveViewConfig.stream_size = None\n                cfg.photoConfig.stream_size = None\n                cfg.rawConfig.stream_size = None\n                cfg.videoConfig.stream_size = None\n                strCfg = cfg.streamingCfg\n                newCamStr = str(newCam)\n                if newCamStr in strCfg:\n                    ncfg = strCfg[newCamStr]\n                    if \"tuningconfig\" in ncfg:\n                        cfg.tuningConfig = ncfg[\"tuningconfig\"]\n                    else:\n                        cfg.tuningConfig = TuningConfig()\n                else:\n                    cfg.tuningConfig = TuningConfig()\n                Camera().switchCamera()\n                if sc.isLiveStream2:\n                    str2 = cfg.streamingCfg[str(Camera().camNum2)]\n                logger.debug(\n                    \"switch_cameras - active camera set to %s\", sc.activeCamera\n                )\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(f\"Active camera changed to: {sc.activeCameraInfo}\")\n                camL, camR = getStereoCameras()\n                doInitCalibration(camL, camR)\n        if msg:\n            flash(msg)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/change_second_camera\", methods=(\"GET\", \"POST\"))\n@login_required\ndef change_second_camera():\n    logger.debug(\"In change_second_camera\")\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        msg = None\n        secondCam = int(request.form[\"secondcamera\"])\n        activeCam = sc.activeCamera\n        newCam = secondCam\n        for cm in cs:\n            if newCam == cm.num:\n                newCamInfo = \"Camera \" + str(cm.num) + \" (\" + cm.model + \")\"\n                newCamModel = cm.model\n                newCamIsUsb = cm.isUsb\n                newCamUsbDev = cm.usbDev\n                newCamHasAi = cm.hasAi\n                break\n\n        if newCam != sc.secondCamera:\n            if newCam == activeCam:\n                msg = \"Second camera must be different from active camera. Use 'Switch Cameras' to swap the cameras.\"\n            # if not msg:\n                #if newCamIsUsb == True:\n                #    # Switching the second camera to a USB camera requires existance of a streaming config\n                #    newCamStr = str(newCam)\n                #    if newCamStr not in cfg.streamingCfg:\n                #        msg = f\"This selected USB camera {newCamInfo} is not yet properly configured. Please open it as active camera first.\"\n            if not msg:\n                sc.secondCamera = newCam\n                sc.secondCameraInfo = newCamInfo\n                sc.secondCameraModel = newCamModel\n                sc.secondCameraIsUsb = newCamIsUsb\n                sc.secondCameraUsbDev = newCamUsbDev\n                sc.secondCameraHasAi = newCamHasAi\n                Camera().switchCamera()\n                if sc.isLiveStream2:\n                    str2 = cfg.streamingCfg[str(Camera().camNum2)]\n                logger.debug(\n                    \"switch_cameras - second camera set to %s\", sc.secondCamera\n                )\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(f\"Second camera now: {sc.secondCameraInfo}\")\n        if msg:\n            flash(msg)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/photo_feed\")\n@login_for_streaming\ndef photo_feed():\n    # logger.debug(\"Thread %s: In photo_feed\", get_ident())\n    Camera().startLiveStream()\n    return Response(Camera().get_photoFrame(), mimetype=\"image/jpeg\")\n\n\n@bp.route(\"/photo_feed_hr\")\n@login_for_streaming\ndef photo_feed_hr():\n    # logger.debug(\"Thread %s: In photo_feed_hr\", get_ident())\n    Camera().startLiveStream()\n    return Response(Camera().get_photoFrame_hr(), mimetype=\"image/jpeg\")\n\n\n@bp.route(\"/photo_feed2\")\n@login_for_streaming\ndef photo_feed2():\n    # logger.debug(\"Thread %s: In photo_feed2\", get_ident())\n    Camera().startLiveStream2()\n    return Response(Camera().get_photoFrame2(), mimetype=\"image/jpeg\")\n\n\n@bp.route(\"/photo_feed2_hr\")\n@login_for_streaming\ndef photo_feed2_hr():\n    # logger.debug(\"Thread %s: In photo_feed2_hr\", get_ident())\n    Camera().startLiveStream2()\n    return Response(Camera().get_photoFrame2_hr(), mimetype=\"image/jpeg\")\n\n\n@bp.route(\"/cam_take_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_take_photo():\n    logger.debug(\"Thread %s: In cam_take_photo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image %s\", filename)\n        fp = Camera().takeImage(filename)\n        if not sc.error:\n            logger.debug(\"take_photo - sc.displayContent: %s\", sc.displayContent)\n            if sc.displayContent == \"hist\":\n                logger.debug(\n                    \"take_photo - sc.displayHistogram: %s\", sc.displayHistogram\n                )\n                if sc.displayHistogram is None:\n                    logger.debug(\"take_photo - sc.displayPhoto: %s\", sc.displayPhoto)\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_take_raw_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_take_raw_photo():\n    logger.debug(\"Thread %s: In cam_take_raw_photo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        if sc.activeCameraIsUsb == False:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n        else:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".tiff\"\n        logger.debug(\"Saving raw image %s\", filenameRaw)\n        fp = Camera().takeRawImage(filenameRaw, filename)\n        if not sc.error:\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_record_video\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_record_video():\n    logger.debug(\"Thread %s: In cam_record_video\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording a video %s\", filenameVid)\n        fp = Camera().recordVideo(filenameVid, filename)\n        time.sleep(4)\n        if not sc.error:\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            # Check whether video is being recorded\n            if Camera().isVideoRecording():\n                logger.debug(\"Video recording started\")\n                sc.isVideoRecording = True\n                if sc.recordAudio:\n                    sc.isAudioRecording = True\n                msg = \"Video saved as \" + fp\n                flash(msg)\n            else:\n                logger.debug(\"Video recording did not start\")\n                sc.isVideoRecording = False\n                sc.isAudioRecording = False\n                msg = \"Video recording failed. Requested resolution too high \"\n                flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_stop_recording\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_stop_recording():\n    logger.debug(\"Thread %s: In cam_stop_recording\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        logger.debug(\"Requesting video recording to stop\")\n        Camera().stopVideoRecording()\n        sc.isVideoRecording = False\n        sc.isAudioRecording = False\n        # sleep a little bit to avoid race condition with restoreLiveStream in video thread\n        time.sleep(2)\n        msg = \"Video recording stopped\"\n        flash(msg)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/take_photo2\", methods=(\"GET\", \"POST\"))\n@login_required\ndef take_photo2():\n    logger.debug(\"Thread %s: In take_photo2\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image %s\", filename)\n        fp = Camera().takeImage2(filename)\n        if not sc.errorc2:\n            logger.debug(\"take_photo - success\")\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            flash(msg)\n            if sc.errorc22:\n                flash(sc.errorc22)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_take_raw_photo2\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_take_raw_photo2():\n    logger.debug(\"Thread %s: In cam_take_raw_photo2\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        if sc.secondCameraIsUsb == False:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.rawPhotoType\n        else:\n            filenameRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".tiff\"\n        logger.debug(\"Saving raw image %s\", filenameRaw)\n        fp = Camera().takeRawImage2(filenameRaw, filename)\n        if not sc.errorc2:\n            msg = \"Image saved as \" + fp\n            flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            flash(msg)\n            if sc.errorc22:\n                flash(sc.errorc22)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_record_video2\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_record_video2():\n    logger.debug(\"Thread %s: In cam_record_video2\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording a video %s\", filenameVid)\n        fp = Camera().recordVideo2(filenameVid, filename)\n        time.sleep(4)\n        if not sc.errorc2:\n            # Check whether video is being recorded\n            if Camera().isVideoRecording2():\n                logger.debug(\"Video recording started\")\n                sc.isVideoRecording2 = True\n                msg = \"Video saved as \" + fp\n                flash(msg)\n            else:\n                logger.debug(\"Video recording did not start\")\n                sc.isVideoRecording2 = False\n                msg = \"Video recording failed. Requested resolution too high \"\n                flash(msg)\n        else:\n            msg = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n            flash(msg)\n            if sc.errorc22:\n                flash(sc.errorc22)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_stop_recording2\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_stop_recording2():\n    logger.debug(\"Thread %s: In cam_stop_recording2\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        logger.debug(\"Requesting video recording to stop\")\n        Camera().stopVideoRecording2()\n        sc.isVideoRecording2 = False\n        # sleep a little bit to avoid race condition with restoreLiveStream in video thread\n        time.sleep(2)\n        msg = \"Video recording stopped\"\n        flash(msg)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/take_photo_both\", methods=(\"GET\", \"POST\"))\n@login_required\ndef take_photo_both():\n    logger.debug(\"Thread %s: In take_photo_both\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Saving image %s\", filename)\n        fp1 = Camera().takeImage(filename)\n        fp2 = Camera().takeImage2(filename)\n        msg1 = \"\"\n        msg2 = \"\"\n        if not sc.error:\n            logger.debug(\"takeImage - success\")\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg1 = f\"Photo saved as {fp1}\"\n        else:\n            msg1 = \"Error in \" + sc.errorcSource + \": \" + sc.errorc\n        if not sc.errorc2:\n            logger.debug(\"takeImage2 - success\")\n            msg2 = f\"Photo saved as {fp2}\"\n        else:\n            msg2 = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n        flash(msg1)\n        flash(msg2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n@bp.route(\"/cam_take_raw_photo_both\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_take_raw_photo_both():\n    logger.debug(\"Thread %s: In cam_take_raw_photo_both\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        file = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\"\n        filename1 = file + sc.photoType\n        if sc.activeCameraIsUsb == False:\n            filename1Raw = file + sc.rawPhotoType\n        else:\n            filename1Raw = file + \"tiff\"\n        filename2 = file + sc.photoType\n        if sc.secondCameraIsUsb == False:\n            filename2Raw = file + sc.rawPhotoType\n        else:\n            filename2Raw = file + \"tiff\"\n        logger.debug(\"Saving raw images as %s and %s\", filename1Raw, filename2Raw)\n        fp1 = Camera().takeRawImage(filename1Raw, filename1)\n        fp2 = Camera().takeRawImage2(filename2Raw, filename2)\n        msg1 = \"\"\n        msg2 = \"\"\n        if not sc.error:\n            logger.debug(\"takeRawImage - success\")\n            if sc.displayContent == \"hist\":\n                if sc.displayHistogram is None:\n                    if sc.displayPhoto:\n                        generateHistogram(sc)\n            msg1 = f\"Raw Photo saved as {fp1}\"\n        else:\n            msg1 = \"Error in \" + sc.errorcSource + \": \" + sc.errorc\n        if not sc.errorc2:\n            logger.debug(\"takeRawImage2 - success\")\n            msg2 = f\"Raw Photo saved as {fp2}\"\n        else:\n            msg2 = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n        flash(msg1)\n        flash(msg2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_record_video_both\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_record_video_both():\n    logger.debug(\"Thread %s: In cam_record_video_both\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        timeImg = datetime.datetime.now()\n        filenameVid = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.videoType\n        filename = timeImg.strftime(\"%Y%m%d_%H%M%S\") + \".\" + sc.photoType\n        logger.debug(\"Recording a video %s\", filenameVid)\n        fp1 = Camera().recordVideo(filenameVid, filename)\n        fp2 = Camera().recordVideo2(filenameVid, filename)\n        time.sleep(4)\n        msg1 = \"\"\n        msg2 = \"\"\n        if not sc.error:\n            # Check whether video is being recorded\n            if Camera().isVideoRecording():\n                logger.debug(\"Video recording 1 started\")\n                sc.isVideoRecording = True\n                if sc.recordAudio:\n                    sc.isAudioRecording = True\n                if sc.displayContent == \"hist\":\n                    if sc.displayHistogram is None:\n                        if sc.displayPhoto:\n                            generateHistogram(sc)\n                msg1 = f\"Video saved as {fp1}\"\n            else:\n                logger.debug(\"Video recording 1 did not start\")\n                sc.isVideoRecording = False\n                msg1 = \"Video recording failed\"\n        else:\n            err = True\n            msg1 = \"Error in \" + sc.errorSource + \": \" + sc.error\n        if not sc.errorc2:\n            # Check whether video is being recorded\n            if Camera().isVideoRecording2():\n                logger.debug(\"Video recording 2 started\")\n                sc.isVideoRecording2 = True\n                msg2 = f\"Video saved as {fp2}\"\n            else:\n                logger.debug(\"Video recording 2 did not start\")\n                sc.isVideoRecording2 = False\n                msg2 = \"Video recording failed\"\n        else:\n            msg2 = \"Error in \" + sc.errorc2Source + \": \" + sc.errorc2\n        flash(msg1)\n        flash(msg2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/cam_stop_recording_both\", methods=(\"GET\", \"POST\"))\n@login_required\ndef cam_stop_recording_both():\n    logger.debug(\"Thread %s: In cam_stop_recording_both\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"multicam\"\n    if request.method == \"POST\":\n        logger.debug(\"Requesting video recording to stop\")\n        msg1 = \"\"\n        msg2 = \"\"\n        if sc.isVideoRecording == False:\n            msg1 = \"No video recording in progress for camera 1\"\n        else:\n            fp1 = Camera().videoOutput\n            Camera().stopVideoRecording()\n            sc.isVideoRecording = False\n            mag1 = f\"Stopped Video: {fp1}\"\n\n        if sc.isVideoRecording2 == False:\n            msg2 = \"No video recording in progress for camera 2\"\n        else:\n            fp2 = Camera().videoOutput2\n            Camera().stopVideoRecording2()\n            sc.isVideoRecording2 = False\n            mag2 = f\"Stopped Video: {fp2}\"\n\n        time.sleep(2)\n        flash(msg1)\n        flash(msg2)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/start_stereo_cam\", methods=(\"GET\", \"POST\"))\n@login_required\ndef start_stereo_cam():\n    logger.debug(\"Thread %s: In start_stereo_cam\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        err = None\n        StereoCam().startStereoCam()\n        if sc.error:\n            logger.debug(\"In start_stereo_cam - StereoCam not started because of error\")\n            msg = \"Error in \" + sc.errorSource + \": \" + sc.error\n            flash(msg)\n            if sc.error2:\n                flash(sc.error2)\n            err = None\n        else:\n            sc.isStereoCamActive = True\n            logger.debug(\"In start_stereo_cam - StereoCam started\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/stop_stereo_cam\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_stereo_cam():\n    logger.debug(\"Thread %s: In stop_stereo_cam\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        if sc.isStereoCamActive == True:\n            scam = StereoCam()\n            if sc.isStereoCamRecording == True:\n                scam.stopRecordStereo()\n                time.sleep(1)\n            scam.stopStereoCam()\n            sc.isStereoCamActive = False\n            logger.debug(\"In stop_stereo_cam - StereoCam stopped\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/stereo_cam_feed\")\n# @login_required\ndef stereo_cam_feed():\n    # logger.debug(\"Thread %s: In stereo_cam_feed\", get_ident())\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    scam = StereoCam()\n    return Response(\n        gen_stereoCamFrame(scam), mimetype=\"multipart/x-mixed-replace; boundary=frame\"\n    )\n\n\ndef gen_stereoCamFrame(stereoCam):\n    \"\"\"Stereo camera streaming generator function.\"\"\"\n    # logger.debug(\"Thread %s: In gen_stereoCamFrame\", get_ident())\n    yield b\"--frame\\r\\n\"\n    while True:\n        frame = stereoCam.get_stereoFrame()\n        if frame:\n            # logger.debug(\"Thread %s: gen_stereoCamFrame - Got frame of length %s: %s\", get_ident(), len(frame), frame)\n            yield b\"Content-Type: image/jpeg\\r\\n\\r\\n\" + frame + b\"\\r\\n--frame\\r\\n\"\n\n\n@bp.route(\"/stereo_feed\")\n@login_for_streaming\ndef stereo_feed():\n    logger.debug(\n        \"Thread %s: In stereo_feed - client IP: %s\", get_ident(), request.remote_addr\n    )\n    sc = CameraCfg().serverConfig\n    sc.registerStreamingClient(request.remote_addr, \"stereo_feed\", get_ident())\n    Camera().startLiveStream()\n    Camera().startLiveStream2()\n    StereoCam().startStereoCam()\n    sc.isStereoCamActive = True\n    return Response(\n        gen_stereoCamFrame(StereoCam()),\n        mimetype=\"multipart/x-mixed-replace; boundary=frame\",\n    )\n\n\n@bp.route(\"/stereo_display\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stereo_display():\n    logger.debug(\"Thread %s: In stereo_display\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        if not request.form.get(\"applycalibrectify\") is None:\n            ster.applyCalibRectify = True\n        else:\n            ster.applyCalibRectify = False\n        logger.debug(\n            \"Thread %s: In stereo_display applyCalibRectify set to %s\",\n            get_ident(),\n            ster.applyCalibRectify,\n        )\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/stereo_config\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stereo_config():\n    logger.debug(\"Thread %s: In stereo_config\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        err = \"\"\n        done = False\n        intent = None\n        stereoAlgo = None\n        if err == \"\":\n            if not request.form.get(\"intent\") is None:\n                intent = request.form[\"intent\"]\n                logger.debug(\"webcam.stereo_config - intent=%s\", intent)\n                tmp[\"intent\"] = intent\n\n                if int(intent) != ster.intentIdx:\n                    ster.intentIdx = int(intent)\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Intent for Stereo Cam changed to {ster.intent}\"\n                    )\n            else:\n                err = \" \"\n        if err == \"\":\n            if not request.form.get(\"stereoalgo\") is None:\n                stereoAlgo = request.form[\"stereoalgo\"]\n                logger.debug(\"webcam.stereo_config - stereoAlgo=%s\", stereoAlgo)\n                tmp[\"stereoAlgo\"] = stereoAlgo\n\n                if int(stereoAlgo) != ster.intentAlgoIdx:\n                    ster.intentAlgoIdx = int(stereoAlgo)\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Algorithm for Stereo Cam changed to {ster.intentAlgo}\"\n                    )\n            else:\n                err = \" \"\n        if err == \"\":\n            if not intent is None:\n                if intent == \"0\":\n                    if not stereoAlgo is None:\n                        if stereoAlgo == \"0\":\n                            if not request.form.get(\"bmnumdisparitiesfactor\") is None:\n                                bm_numDisparitiesFactor = request.form.get(\n                                    \"bmnumdisparitiesfactor\"\n                                )\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"bmblocksize\") is None:\n                                bm_blockSize = request.form.get(\"bmblocksize\")\n                            else:\n                                err = \" \"\n                            if err == \"\":\n                                try:\n                                    ster.bm_numDisparitiesFactor = int(\n                                        bm_numDisparitiesFactor\n                                    )\n                                    ster.bm_blockSize = int(bm_blockSize)\n                                    done = True\n                                except ValueError as e:\n                                    err = e.args[0]\n                        if stereoAlgo == \"1\":\n                            if not request.form.get(\"sgbmmindisparity\") is None:\n                                sgbm_minDisparity = request.form.get(\"sgbmmindisparity\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmnumdisparitiesfactor\") is None:\n                                sgbm_numDisparitiesFactor = request.form.get(\n                                    \"sgbmnumdisparitiesfactor\"\n                                )\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmblocksize\") is None:\n                                sgbm_blockSize = request.form.get(\"sgbmblocksize\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmp1\") is None:\n                                sgbm_P1 = request.form.get(\"sgbmp1\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmp2\") is None:\n                                sgbm_P2 = request.form.get(\"sgbmp2\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmdisp12maxdiff\") is None:\n                                sgbm_disp12MaxDiff = request.form.get(\n                                    \"sgbmdisp12maxdiff\"\n                                )\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmprefiltercap\") is None:\n                                sgbm_preFilterCap = request.form.get(\"sgbmprefiltercap\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmuniquenessratio\") is None:\n                                sgbm_uniquenessRatio = request.form.get(\n                                    \"sgbmuniquenessratio\"\n                                )\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmspecklewindowsize\") is None:\n                                sgbm_speckleWindowSize = request.form.get(\n                                    \"sgbmspecklewindowsize\"\n                                )\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmspecklerange\") is None:\n                                sgbm_speckleRange = request.form.get(\"sgbmspecklerange\")\n                            else:\n                                err = \" \"\n                            if not request.form.get(\"sgbmmode\") is None:\n                                sgbm_mode = request.form.get(\"sgbmmode\")\n                            else:\n                                err = \" \"\n\n                            if err == \"\":\n                                try:\n                                    ster.sgbm_minDisparity = int(sgbm_minDisparity)\n                                    ster.sgbm_numDisparitiesFactor = int(\n                                        sgbm_numDisparitiesFactor\n                                    )\n                                    ster.sgbm_blockSize = int(sgbm_blockSize)\n                                    ster.sgbm_P1 = int(sgbm_P1)\n                                    ster.sgbm_P2 = int(sgbm_P2)\n                                    ster.sgbm_disp12MaxDiff = int(sgbm_disp12MaxDiff)\n                                    ster.sgbm_preFilterCap = int(sgbm_preFilterCap)\n                                    ster.sgbm_uniquenessRatio = int(\n                                        sgbm_uniquenessRatio\n                                    )\n                                    ster.sgbm_speckleWindowSize = int(\n                                        sgbm_speckleWindowSize\n                                    )\n                                    ster.sgbm_speckleRange = int(sgbm_speckleRange)\n                                    ster.sgbm_mode = int(sgbm_mode)\n                                    done = True\n                                except ValueError as e:\n                                    err = e.args[0]\n        if err.strip() != \"\":\n            flash(err)\n        if done == True:\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Stereo cam settings changed\")\n\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp\n    )\n\n\n@bp.route(\"/first_calib_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef first_calib_photo():\n    logger.debug(\"Thread %s: In first_calib_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    for cam in ster.calibPhotosIdx:\n        if ster.calibPhotosIdx[cam] > 0:\n            ster.calibPhotosIdx[cam] = 0\n    return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/prev_calib_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef prev_calib_photo():\n    logger.debug(\"Thread %s: In prev_calib_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    for cam in ster.calibPhotosIdx:\n        if ster.calibPhotosIdx[cam] > 0:\n            ster.calibPhotosIdx[cam] -= 1\n    return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/next_calib_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef next_calib_photo():\n    logger.debug(\"Thread %s: In next_calib_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    for cam in ster.calibPhotosIdx:\n        if (\n            ster.calibPhotosIdx[cam] >= 0\n            and ster.calibPhotosIdx[cam] < ster.calibPhotosCount[cam] - 1\n        ):\n            ster.calibPhotosIdx[cam] += 1\n    return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/last_calib_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef last_calib_photo():\n    logger.debug(\"Thread %s: In last_calib_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    for cam in ster.calibPhotosIdx:\n        ster.calibPhotosIdx[cam] = ster.calibPhotosCount[cam] - 1\n    return redirect(url_for(\"webcam.webcam\"))\n\n\ndef doRemoveCalibPhoto(sc: ServerConfig, ster: StereoConfig, idx: int) -> bool:\n    \"\"\"Remove a calibration photo with the given index for all cameras\"\"\"\n    logger.debug(\"Thread %s: In doRemoveCalibPhoto - idx = %s\", get_ident(), idx)\n    res = True\n    for cam in ster.calibPhotosIdx:\n        sp = ster.calibPhotos[cam][idx]\n        spC = ster.calibPhotosCrn[cam][idx]\n        fp = sc.photoRoot + \"/\" + sp\n        fpC = sc.photoRoot + \"/\" + spC\n        logger.debug(\n            \"Thread %s: In doRemoveCalibPhoto - Trying to remove %s\", get_ident(), fp\n        )\n        if os.path.exists(fp):\n            try:\n                os.remove(fp)\n                os.remove(fpC)\n                logger.debug(\n                    \"Thread %s: In doRemoveCalibPhoto - removed Photo %s\",\n                    get_ident(),\n                    fp,\n                )\n            except Exception as e:\n                logger.error(\n                    \"Thread %s: Error removing calibration photo %s: %s\",\n                    get_ident(),\n                    fp,\n                    e,\n                )\n                res = False\n        else:\n            logger.debug(\n                \"Thread %s: In doRemoveCalibPhoto - Photo %s does not exist\",\n                get_ident(),\n                fp,\n            )\n        ster.calibPhotos[cam].pop(idx)\n        ster.calibPhotosCrn[cam].pop(idx)\n        ster.calibPhotosCount[cam] = len(ster.calibPhotos[cam])\n        if ster.calibPhotosIdx[cam] >= ster.calibPhotosCount[cam]:\n            ster.calibPhotosIdx[cam] = ster.calibPhotosCount[cam] - 1\n        if ster.calibPhotosCount[cam] < ster.calibPhotosTarget:\n            ster.calibPhotosOK[cam] = False\n    return res\n\n\ndef getStereoCameras():\n    \"\"\"Get the two cameras to be involved in stereo processing\"\"\"\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL = str(cfg.serverConfig.activeCamera)\n    if Camera().camNum2 is not None:\n        camR = str(Camera().camNum2)\n    else:\n        camR = None\n    return camL, camR\n\n\n@bp.route(\"/remove_calib_photo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef remove_calib_photo():\n    logger.debug(\"Thread %s: In remove_calib_photo\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    camL, camR = getStereoCameras()\n    if doRemoveCalibPhoto(sc, ster, ster.calibPhotosIdx[camL]) == False:\n        flash(\"Not all photos could be removed\")\n    else:\n        doResetCalibration(camL, camR, keepPhotos=True)\n    sc.unsavedChanges = True\n    sc.addChangeLogEntry(f\"Calibration photo removed\")\n    return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/display_corners\", methods=(\"GET\", \"POST\"))\n@login_required\ndef display_corners():\n    logger.debug(\"Thread %s: In display_corners\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if not request.form.get(\"displaycorners\") is None:\n        ster.calibShowCorners = True\n    else:\n        ster.calibShowCorners = False\n    return redirect(url_for(\"webcam.webcam\"))\n\n\n@bp.route(\"/calib_settings\", methods=(\"GET\", \"POST\"))\n@login_required\ndef calib_settings():\n    logger.debug(\"Thread %s: In calib_settings\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if request.method == \"POST\":\n        logger.debug(\"Thread %s: In calib_settings - POST\", get_ident())\n        camL, camR = getStereoCameras()\n        calibPatternIdx = ster.calibPatternIdx\n        if not request.form.get(\"calibpattern\") is None:\n            calibPatternIdx = int(request.form.get(\"calibpattern\"))\n        if calibPatternIdx != ster.calibPatternIdx:\n            logger.debug(\n                \"Thread %s: In calib_settings - calibPatternIdx changed\", get_ident()\n            )\n            doResetCalibration(camL, camR)\n            ster.calibPatternIdx = calibPatternIdx\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(\n                f\"Calibration pattern changed to {ster.calibPattern[calibPatternIdx]}\"\n            )\n        else:\n            (calibPatternSizeX, calibPatternSizeY) = ster.calibPatternSize\n            if not request.form.get(\"calibpatternsize\") is None:\n                calibPatternSizeX = int(request.form.get(\"calibpatternsize\"))\n            if not request.form.get(\"calibpatternsizey\") is None:\n                calibPatternSizeY = int(request.form.get(\"calibpatternsizey\"))\n            if (calibPatternSizeX, calibPatternSizeY) != ster.calibPatternSize:\n                logger.debug(\n                    \"Thread %s: In calib_settings - calibPatternSize changed %s -> %s\",\n                    get_ident(),\n                    ster.calibPatternSize,\n                    (calibPatternSizeX, calibPatternSizeY),\n                )\n                doResetCalibration(camL, camR)\n                ster.calibPatternSize = (calibPatternSizeX, calibPatternSizeY)\n                sc.unsavedChanges = True\n                sc.addChangeLogEntry(\n                    f\"Calibration pattern size changed to {ster.calibPatternSize}\"\n                )\n            else:\n                calibPhotosTarget = ster.calibPhotosTarget\n                if not request.form.get(\"calibphotostarget\") is None:\n                    calibPhotosTarget = int(request.form.get(\"calibphotostarget\"))\n                if calibPhotosTarget != ster.calibPhotosTarget:\n                    logger.debug(\n                        \"Thread %s: In calib_settings - calibPhotosTarget changed from %s to %s\",\n                        get_ident(),\n                        ster.calibPhotosTarget,\n                        calibPhotosTarget,\n                    )\n                    ster.calibPhotosTarget = calibPhotosTarget\n                    if ster.calibPhotosTarget > len(ster.calibPhotos[camL]):\n                        doResetCalibration(camL, camR, keepPhotos=True)\n                    elif ster.calibPhotosTarget < len(ster.calibPhotos[camL]):\n                        doResetCalibration(camL, camR, keepPhotos=True)\n                        ster.calibPhotosOK[camL] = True\n                        ster.calibPhotosOK[camR] = True\n                    else:\n                        ster.calibPhotosOK[camL] = True\n                        ster.calibPhotosOK[camR] = True\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Calibration photos target changed to {ster.calibPhotosTarget}\"\n                    )\n                rectifyScale = ster.rectifyScale\n                if not request.form.get(\"rectifyscale\") is None:\n                    rectifyScale = int(request.form.get(\"rectifyscale\"))\n                if rectifyScale != ster.rectifyScale:\n                    logger.debug(\n                        \"Thread %s: In calib_settings - rectifyScale changed from %s to %s\",\n                        get_ident(),\n                        ster.rectifyScale,\n                        rectifyScale,\n                    )\n                    ster.rectifyScale = rectifyScale\n                    ster.calibCameraOK[camL] = False\n                    if not camR is None:\n                        ster.calibCameraOK[camR] = False\n                    ster.calibStereoOK = False\n                    ster.stereoRectifyOK = False\n                    sc.unsavedChanges = True\n                    sc.addChangeLogEntry(\n                        f\"Rectify scale changed to {ster.rectifyScale}\"\n                    )\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/reset_calib_photos\", methods=(\"GET\", \"POST\"))\n@login_required\ndef reset_calib_photos():\n    logger.debug(\"Thread %s: In reset_calib_photos\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if request.method == \"POST\":\n        camL, camR = getStereoCameras()\n        doResetCalibration(camL, camR)\n        sc.unsavedChanges = True\n        sc.addChangeLogEntry(f\"Calibration was reset\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/start_take_calib_photos\", methods=(\"GET\", \"POST\"))\n@login_required\ndef start_take_calib_photos():\n    logger.debug(\"Thread %s: In start_take_calib_photos\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if request.method == \"POST\":\n        msg = \"\"\n        if ster.calibPhotosPath == \"\":\n            msg = \"Calibration photos path is not set.\"\n        if msg == \"\":\n            camL, camR = getStereoCameras()\n            doInitCalibration(camL, camR)\n\n            if camR is None:\n                ster.calibPhotoRecordingMsg = f\"Taking calibration photos... Place chessboard pattern in view of camera {caml}.\"\n            else:\n                ster.calibPhotoRecordingMsg = f\"Taking calibration photos... Place chessboard pattern in view of cameras {camL} and {camR}.\"\n            StereoCam().takeCalibrationPhotos(camL, camR)\n        if msg != \"\":\n            flash(msg)\n    return redirect(url_for(\"webcam.webcam\"))\n    # return render_template(\"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs)\n\n\n@bp.route(\"/stop_take_calib_photos\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_take_calib_photos():\n    logger.debug(\"Thread %s: In stop_take_calib_photos\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if request.method == \"POST\":\n        StereoCam().stoptakeCalibrationPhotos()\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\ndef doResetCalibration(camL: str, camR: str, keepPhotos: bool = False):\n    \"\"\"Reset the  camera calibration.\"\"\"\n    logger.debug(\"Thread %s: In doResetCalibration\", get_ident())\n    cfg = CameraCfg()\n    sc = cfg.serverConfig\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n\n    # Remove existing photos, if not excluded\n    if not keepPhotos == True:\n        while len(ster.calibPhotos[camL]) > 0:\n            doRemoveCalibPhoto(sc, ster, 0)\n\n    # Remove surplus photos\n    while len(ster.calibPhotos[camL]) > ster.calibPhotosTarget:\n        doRemoveCalibPhoto(sc, ster, len(ster.calibPhotos[camL]) - 1)\n\n    doInitCalibration(camL, camR)\n    doCleanup(sc, ster)\n\n    ster.calibPhotosOK[camL] = False\n    if not camR is None:\n        ster.calibPhotosOK[camR] = False\n    ster.calibCameraOK[camL] = False\n    if not camR is None:\n        ster.calibCameraOK[camR] = False\n    ster.calibDate = None\n    ster.calibDataOK = False\n    ster.calibStereoOK = False\n    ster.stereoRectifyOK = False\n    ster.applyCalibRectify = False\n\n    sc.unsavedChanges = True\n    sc.addChangeLogEntry(f\"Calibration photos were reset\")\n\n\ndef doCleanup(sc: ServerConfig, ster: StereoConfig):\n    \"\"\"Clean up the calibration photo path\"\"\"\n    logger.debug(\"Thread %s: In doCleanup\", get_ident())\n\n    # Create list of existing files\n    root = sc.photoRoot + \"/\" + ster.calibPhotosSubPath\n    rootPath = Path(root)\n\n    # List all sub-directories\n    camDirs = [a.name for a in rootPath.iterdir() if a.is_dir()]\n\n    for cam in camDirs:\n        logger.debug(\n            \"Thread %s: In doCleanup  - Searching in cam: %s\", get_ident(), cam\n        )\n        if cam in ster.calibPhotos:\n            camFiles = [f.name for f in (rootPath / cam).iterdir() if f.is_file()]\n            for f in camFiles:\n                logger.debug(\n                    \"Thread %s: In doCleanup  - Found file: %s\", get_ident(), f\n                )\n                sp = ster.calibPhotosSubPath + cam + \"/\" + f\n                if (\n                    not sp in ster.calibPhotos[cam]\n                    and not sp in ster.calibPhotosCrn[cam]\n                ):\n                    try:\n                        fp = sc.photoRoot + \"/\" + sp\n                        logger.debug(\n                            \"Thread %s: In doCleanup  - Removing file: %s\",\n                            get_ident(),\n                            fp,\n                        )\n                        os.remove(fp)\n                        logger.debug(\n                            \"Thread %s: In doRemoveCalibPhoto - removed Photo %s\",\n                            get_ident(),\n                            fp,\n                        )\n                    except Exception as e:\n                        logger.error(\n                            \"Thread %s: Error removing unused calibration photo %s: %s\",\n                            get_ident(),\n                            fp,\n                            e,\n                        )\n        else:\n            logger.debug(\n                \"Thread %s: In doCleanup  - Ignoring subdirectory: %s\", get_ident(), cam\n            )\n\n\ndef doInitCalibration(camL: str, camR: str):\n    \"\"\"Initialize the  camera calibration, if required.\"\"\"\n    logger.debug(\"Thread %s: In doInitCalibration\", get_ident())\n    sc = CameraCfg().serverConfig\n    ster = CameraCfg().stereoCfg\n\n    if not camL in ster.calibPhotosOK:\n        ster.calibPhotosOK[camL] = False\n    if not camR is None:\n        if not camR in ster.calibPhotosOK:\n            ster.calibPhotosOK[camR] = False\n\n    if not camL in ster.calibPhotos:\n        ster.calibPhotos[camL] = []\n        ster.calibPhotosCrn[camL] = []\n    if not camR is None:\n        if not camR in ster.calibPhotos:\n            ster.calibPhotos[camR] = []\n            ster.calibPhotosCrn[camR] = []\n\n    if not camL in ster.calibPhotosCount:\n        ster.calibPhotosCount[camL] = 0\n    if not camR is None:\n        if not camR in ster.calibPhotosCount:\n            ster.calibPhotosCount[camR] = 0\n\n    if not camL in ster.calibPhotosIdx:\n        ster.calibPhotosIdx[camL] = -1\n    if not camR is None:\n        if not camR in ster.calibPhotosIdx:\n            ster.calibPhotosIdx[camR] = -1\n\n    if not camL in ster.calibCameraOK:\n        ster.calibCameraOK[camL] = False\n    if not camR is None:\n        if not camR in ster.calibCameraOK:\n            ster.calibCameraOK[camR] = False\n\n    if not camL in ster.calibRmsReproError:\n        ster.calibRmsReproError[camL] = 1.0\n    if not camR is None:\n        if not camR in ster.calibRmsReproError:\n            ster.calibRmsReproError[camR] = 1.0\n\n    calibRootPath = ster.calibPhotosPath\n    os.makedirs(calibRootPath, exist_ok=True)\n    calibCamPathL = calibRootPath + \"/\" + camL\n    os.makedirs(calibCamPathL, exist_ok=True)\n    if not camR is None:\n        calibCamPathR = calibRootPath + \"/\" + camR\n        os.makedirs(calibCamPathR, exist_ok=True)\n    calibDataPath = sc.photoRoot + \"/\" + ster.calibDataSubPath\n    os.makedirs(calibDataPath, exist_ok=True)\n\n\n@bp.route(\"/calibrate_cameras\", methods=(\"GET\", \"POST\"))\n@login_required\ndef calibrate_cameras():\n    logger.debug(\"Thread %s: In calibrate_cameras\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"calibcam\"\n    if request.method == \"POST\":\n        msg = \"\"\n        camL, camR = getStereoCameras()\n        try:\n            StereoCam().calibrateCameras(camL, camR)\n            sc.unsavedChanges = True\n            sc.addChangeLogEntry(f\"Camera(s) calibrated\")\n            msg = f\"Cameras calibrated successfully. Calibration data in {ster.calibDataSubPath + ster.calibDataFile}\"\n        except Exception as e:\n            logger.error(\"Thread %s: Error calibrating cameras: %s\", get_ident(), e)\n            msg = \"Error calibrating cameras: {}\".format(e)\n        if msg != \"\":\n            flash(msg)\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/start_record_stereo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef start_record_stereo():\n    logger.debug(\"Thread %s: In start_record_stereo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        scam = StereoCam()\n        if sc.isStereoCamRecording == False:\n            timeImg = datetime.datetime.now()\n            filenameVidRaw = timeImg.strftime(\"%Y%m%d_%H%M%S\")\n            done, fp, err = scam.startRecordStereo(filenameVidRaw)\n            msg = \"\"\n            if done == True:\n                msg = f\"Recording started successfully: {fp}\"\n            else:\n                msg = f\"Error starting recording: {err}\"\n            flash(msg)\n        else:\n            flash(\"Recording is already active.\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n\n\n@bp.route(\"/stop_record_stereo\", methods=(\"GET\", \"POST\"))\n@login_required\ndef stop_record_stereo():\n    logger.debug(\"Thread %s: In stop_record_stereo\", get_ident())\n    g.hostname = request.host\n    g.version = version\n    cfg = CameraCfg()\n    ster = cfg.stereoCfg\n    cs = cfg.cameras\n    camL, camR = getStereoCameras()\n    doInitCalibration(camL, camR)\n    tmp = {}\n    tmp[\"intent\"] = str(ster.intentIdx)\n    tmp[\"stereoAlgo\"] = str(ster.intentAlgoIdx)\n    sc = cfg.serverConfig\n    str2 = None\n    if sc.isLiveStream2:\n        str2 = cfg.streamingCfg[str(Camera().camNum2)]\n    sc.curMenu = \"webcam\"\n    sc.lastCamTab = \"stereocam\"\n    if request.method == \"POST\":\n        scam = StereoCam()\n        if sc.isStereoCamRecording == True:\n            scam.stopRecordStereo()\n        else:\n            flash(\"Recording is not active.\")\n    return render_template(\n        \"webcam/webcam.html\", sc=sc, cfg=cfg, str2=str2, ster=ster, tmp=tmp, cs=cs\n    )\n"
  },
  {
    "path": "requirements.txt",
    "content": "RPi.GPIO\ngpiozero\nFlask>=3,<4\nnumpy\nmatplotlib<3.8\nflask-jwt-extended\npsutil\nrequests\nmunkres\ngunicorn\nrpi_hardware_pwm"
  },
  {
    "path": "scripts/install_raspiCamSrv.sh",
    "content": "#!/bin/bash\nset -e\n\n############################################\n# raspiCamSrv Installer + systemd setup\n############################################\necho\necho \"==========================================\"\necho \"=== raspiCamSrv Automated Installer    ===\"\necho \"===                                    ===\"\necho \"=== Exit at any step with Ctrl+C       ===\"\necho \"==========================================\"\n\nUSER_NAME=\"$USER\"\nINSTALL_ROOT=\"$HOME/prg\"\nINSTALL_DIR=\"$INSTALL_ROOT/raspi-cam-srv\"\nBACKUP_SAV=\"$INSTALL_ROOT/raspi-cam-srv_backups\"\nBACKUP_DIR=\"$INSTALL_DIR/backups\"\nREPO_URL=\"https://github.com/signag/raspi-cam-srv.git\"\nSERVICE_PORT=5000\nHOSTNAME=\"$(hostname)\"\nIS_LITE=false\nENABLE_AUDIO=false\nENABLE_AI=false\nENABLE_HW_PWM=false\nENABLE_ADVANCED=true\nUPDATE_INSTALL=true\n\n# Parameters for repetitive initialization of DB for RPI Zero\n# To avoid issues with DMA allocation\nMAX_TRIES=5\nWAIT_SEC=5\n\n##############################################\n# Detect Raspberry Pi Model\n##############################################\nRPI_MODEL=$(tr -d '\\0' < /proc/device-tree/model)\nif [[ \"$RPI_MODEL\" == \"Raspberry Pi Zero\"* ]]; then\n    RPI_MODEL_ZERO=true\n    if [[ \"$RPI_MODEL\" == \"Raspberry Pi Zero 2\"* ]]; then\n        RPI_MODEL_ZERO_1=false\n    else\n        RPI_MODEL_ZERO_1=true\n    fi\nelse\n    RPI_MODEL_ZERO=false\n    RPI_MODEL_ZERO_1=false\nfi\n\n##############################################\n# Detect OS version and check for full version\n##############################################\nOS_CODENAME=$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)\n\nif dpkg -l | grep -q raspberrypi-ui-mods || \\\n   dpkg -l | grep -q lxsession || \\\n   [[ -d /usr/share/xsessions ]]; then\n    OS_VARIANT=\"full\"\nelse\n    OS_VARIANT=\"lite\"\n    IS_LITE=true\nfi\necho\necho \"RPI Model           : $RPI_MODEL\"\necho \"Detected OS codename: $OS_CODENAME $OS_VARIANT\"\necho \"Hostname            : $HOSTNAME\"\necho\necho \"Running as user     : $USER_NAME\"\necho \"Installing at       : $INSTALL_ROOT\"\n\n# No longer excluding lite versions. Installations also successful on lite\nif [[ \"$OS_VARIANT\" == \"XXlite\" ]]; then\n    echo \"It seems that the lite variant of $OS_CODENAME is installed.\"\n    echo \"It is strongly recommended to install the full version of $OS_CODENAME!\"\n    echo \"With the installed lite version, there may be issues running raspiCamSrv!\"\n    echo\n    read -rp \"Do you want to continue with the installation, nevertheless? [y/N]: \" INSTALL_CHOICE\n    INSTALL_CHOICE=${INSTALL_CHOICE,,}   # normalize to lowercase\n    if [[ \"$INSTALL_CHOICE\" == \"y\" ]]; then\n        DO_INSTALL=true\n    else\n        echo\n        echo \"=======================================\"\n        echo \"=== raspiCamSrv not installed       ===\"\n        echo \"=======================================\"\n        exit 1\n    fi\nfi\n\n##############################################\n# Check raspiCamSrv service\n##############################################\nSERVICE=\"raspiCamSrv.service\"\nSERVICE_CONFIGURED=false\nSERVICE_RUNNING=false\nSERVICE_USER=false\nSERVICE_ENABLED=false\n\nif systemctl --user cat \"$SERVICE\" >/dev/null 2>&1; then\n    SERVICE_USER=true\n    SERVICE_CONFIGURED=true\n\n    if systemctl --user is-enabled --quiet raspiCamSrv.service; then\n        SERVICE_ENABLED=true\n    fi\n\n    if systemctl --user is-active --quiet \"$SERVICE\"; then\n        SERVICE_RUNNING=true\n    fi\nfi\n\nif [[ \"$SERVICE_USER\" == false ]]; then\n    if systemctl cat \"$SERVICE\" >/dev/null 2>&1; then\n        SERVICE_CONFIGURED=true\n\n        if systemctl is-enabled --quiet \"$SERVICE\"; then\n            SERVICE_ENABLED=true\n        fi\n\n        if systemctl is-active --quiet \"$SERVICE\"; then\n            SERVICE_RUNNING=true\n        fi\n    fi\nfi\n\n############################################\n# Ask user about Installation Mode\n############################################\nif [[ -d \"$INSTALL_DIR\" ]]; then\n    echo\n    echo \"=====================\"\n    echo \"Installation Mode\"\n    echo \"=====================\"\n    echo \"Installation Path   : $INSTALL_DIR (exists)\"\n    if [[ \"$SERVICE_RUNNING\" == true ]]; then\n        echo \"Service Status      : $SERVICE (running, will be stopped)\"\n    elif [[ \"$SERVICE_CONFIGURED\" == true ]]; then\n        echo \"Service Status      : $SERVICE (configured, not running)\"\n    else\n        echo \"Service Status      : $SERVICE (not configured)\"\n    fi\n    echo\n    echo \"A raspiCamSrv installation exists already.\"\n    echo\n    read -rp \"Do you want to skip update of raspiCamSrv and software stack and only reconfigure the service[Y/n]: \" MODE_CHOICE\n    echo\n\n    MODE_CHOICE=${MODE_CHOICE,,}   # normalize to lowercase\n\n    if [[ \"$MODE_CHOICE\" == \"n\" ]]; then\n        UPDATE_INSTALL=true\n        echo\n        echo \"Updating repository and software for existing installation\"\n    else\n        UPDATE_INSTALL=false\n        echo\n        echo \"Only installing/replacing service for existing installation\"\n    fi\nfi\n\n############################################\n# Ask user about defaults\n############################################\necho\necho \"=====================\"\necho \"Installation Defaults\"\necho \"=====================\"\nif [[ ! -d \"$INSTALL_DIR\" ]]; then\n    echo \"Installation Path   : $INSTALL_DIR\"\n    if [[ -d \"$BACKUP_SAV\" ]]; then\n        echo \"Backup              : Restoring backup from a previous installation\"\n    fi\nfi\necho \"WSGI Server         : Gunicorn\"\necho \"Gunicorn Threads    : 6\"\necho \"Service Port        : $SERVICE_PORT (default, will be adjusted if already in use)\"\nif [[ \"$SERVICE_USER\" == true ]]; then\n    echo \"Audio Recording     : Enabled (Installing user service)\"\nelse\n    echo \"Audio Recording     : Disabled (Installing system service)\"\nfi\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    if [[ \"$OS_CODENAME\" == \"bullseye\" ]]; then\n        echo \"Advanced Features   : Disabled (Bullseye detected)\"\n    else\n        echo \"Advanced Features   : Enabled\"\n        echo \"                      USB Cams, Histograms, Stereo Vision, extended Motion Detection\"\n        echo \"                      (Requires OpenCV, numpy, matplotlib)\"\n    fi\n    echo \"AI Camera Support   : Disabled\"\n    echo \"Hardware PWM Support: Disabled\"\n    echo \"                      Hardware PWM is required for jitter-free servo control\"\nfi\necho\nread -rp \"Do you want to install with these settings? [Y/n]: \" INSTALL_CHOICE\necho\n\nINSTALL_CHOICE=${INSTALL_CHOICE,,}   # normalize to lowercase\n\nif [[ \"$INSTALL_CHOICE\" == \"n\" ]]; then\n    USE_DEFAULTS=false\nelse\n    USE_DEFAULTS=true\nfi\n\n############################################\n# Ask user about WSGI server choice\n############################################\nif [[ \"$USE_DEFAULTS\" == false ]]; then\n    echo\n    echo \"Available WSGI servers:\"\n    echo \"1) Gunicorn (recommended for publicly accessible systems) - default\"\n    echo \"2) Flask built-in server (OK for testing and private networks)\"\n    read -rp \"Choose WSGI server [1/2]: \" WSGI_CHOICE\n    echo\n\n    WSGI_CHOICE=${WSGI_CHOICE,,}   # normalize to lowercase\n\n    if [[ \"$WSGI_CHOICE\" == \"2\" ]]; then\n        WSGI_SERVER=\"werkzeug\"\n    else\n        WSGI_SERVER=\"gunicorn\"\n    fi\n    echo \"Using WSGI server: $WSGI_SERVER\"\nelse\n    WSGI_SERVER=\"gunicorn\"\nfi\n\n############################################\n# Ask user about number of threads for Gunicorn\n############################################\nif [[ \"$WSGI_SERVER\" == \"gunicorn\" ]]; then\n    if [[ \"$USE_DEFAULTS\" == false ]]; then\n        echo\n        read -rp \"How many parallel video streams do you require? [default: 6]: \" THREAD_CHOICE\n        echo\n\n        THREAD_CHOICE=${THREAD_CHOICE,,}   # normalize to lowercase\n\n        if [[ \"$THREAD_CHOICE\" == \"\" ]]; then\n            THREAD_COUNT=6\n        else\n            if [[ \"$THREAD_CHOICE\" =~ ^-?[0-9]+$ ]]; then\n                THREAD_COUNT=$THREAD_CHOICE\n            else\n                THREAD_COUNT=6\n            fi\n        fi\n        echo \"Using $THREAD_COUNT threads for Gunicorn worker process\"\n    else\n        THREAD_COUNT=6\n    fi\nelse\n    THREAD_COUNT=1\nfi\n\n############################################\n# Ask user about audio recording\n############################################\nif [[ \"$USE_DEFAULTS\" == false ]]; then\n    echo\n    read -rp \"Do you need to record audio along with videos? [y/N]: \" AUDIO_CHOICE\n    echo\n\n    AUDIO_CHOICE=${AUDIO_CHOICE,,}   # normalize to lowercase\n\n    if [[ \"$AUDIO_CHOICE\" == \"y\" ]]; then\n        ENABLE_AUDIO=true\n    else\n        ENABLE_AUDIO=false\n    fi\n    echo \"Audio recording enabled: $ENABLE_AUDIO\"\nelse\n    ENABLE_AUDIO=false\nfi\n\n############################################\n# Ask user about advanced features\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    if [[ \"$OS_CODENAME\" != \"bullseye\" ]]; then\n        if [[ \"$USE_DEFAULTS\" == false ]]; then\n            echo\n            read -rp \"Do you want to enable advanced features (USB Cams, Histograms, Stereo Vision, extended Motion Detection)? [Y/n]: \" ADVANCED_CHOICE\n            echo\n\n            ADVANCED_CHOICE=${ADVANCED_CHOICE,,}   # normalize to lowercase\n\n            if [[ \"$ADVANCED_CHOICE\" == \"n\" ]]; then\n                ENABLE_ADVANCED=false\n            else\n                ENABLE_ADVANCED=true\n            fi\n            echo \"Advanced features enabled: $ENABLE_ADVANCED\"\n        else\n            ENABLE_ADVANCED=true\n        fi\n    else\n        ENABLE_ADVANCED=false\n    fi\nfi\n\n############################################\n# Ask user about imx500 Camera support\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    if [[ \"$OS_CODENAME\" != \"bullseye\" ]]; then\n        if [[ \"$USE_DEFAULTS\" == false ]]; then\n            echo\n            read -rp \"Do you intend to use the Raspberry Pi AI Camera (imx500)? [y/N]: \" AI_CHOICE\n            echo\n\n            AI_CHOICE=${AI_CHOICE,,}   # normalize to lowercase\n\n            if [[ \"$AI_CHOICE\" == \"y\" ]]; then\n                ENABLE_AI=true\n            else\n                ENABLE_AI=false\n            fi\n            echo \"AI Camera support enabled: $ENABLE_AI\"\n        else\n            ENABLE_AI=false\n        fi\n    else\n        ENABLE_AI=false\n    fi\nfi\n\n############################################\n# Ask user about Hardware PWM support\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    if [[ \"$USE_DEFAULTS\" == false ]]; then\n        echo\n        read -rp \"Do you intend to use Hardware PWM for jitter-free servo control? [y/N]: \" HW_PWM_CHOICE\n        echo\n\n        HW_PWM_CHOICE=${HW_PWM_CHOICE,,}   # normalize to lowercase\n\n        if [[ \"$HW_PWM_CHOICE\" == \"y\" ]]; then\n            ENABLE_HW_PWM=true\n        else\n            ENABLE_HW_PWM=false\n        fi\n        echo \"Hardware PWM support enabled: $ENABLE_HW_PWM\"\n    else\n        ENABLE_HW_PWM=false\n    fi\nfi\n\n############################################\n# Request start confirmation\n############################################\necho\nread -rp \"No more questions! Ready to start installation? [Y/n]: \" START_CHOICE\nSTART_CHOICE=${START_CHOICE,,}\nif [[ \"$START_CHOICE\" == \"n\" ]]; then\n    echo\n    echo \"========================================\"\n    echo \"=== raspiCamSrv installation aborted ===\"\n    echo \"========================================\"\n    exit 1\nfi\n\n############################################\n# Stop a running service before proceeding\n############################################\nif [[ \"$SERVICE_RUNNING\" == true ]]; then\n    echo\n    echo \"Stopping running service '$SERVICE'...\"\n    if [[ \"$SERVICE_USER\" == true ]]; then\n        systemctl --user stop \"$SERVICE\"\n        if systemctl --user is-active --quiet \"$SERVICE\"; then\n            echo\n            echo \"Failed to stop user service '$SERVICE'.\" \n            echo \"Please check the service status and stop it manually before running the installer again.\"\n            exit 1\n        else\n            echo\n            echo \"User service '$SERVICE' stopped successfully.\"\n        fi\n    else\n        sudo systemctl stop \"$SERVICE\"\n        if systemctl is-active --quiet \"$SERVICE\"; then\n            echo\n            echo \"Failed to stop system service '$SERVICE'.\" \n            echo \"Please check the service status and stop it manually before running the installer again.\"\n            exit 1\n        else\n            echo\n            echo \"System service '$SERVICE' stopped successfully.\"\n        fi\n    fi\nfi\n\n############################################\n# Check that ffmpeg is installed\n############################################\necho\necho \"Step 3: Checking ffmpeg ...\"\nif command -v ffmpeg >/dev/null 2>&1; then\n    echo \"ffmpeg is installed\"\nelse\n    echo \"Installing ffmpeg\"\n    sudo apt install -y ffmpeg    \nfi\n\n############################################\n# Create root directory\n############################################\necho\necho \"Step 4: Creating root directory ...\"\nif [[ -d \"$INSTALL_ROOT\" ]]; then\n    echo \"Root directory exists already: $INSTALL_ROOT\"\nelse\n    mkdir -p \"$INSTALL_ROOT\"\n    echo \"Created: $INSTALL_ROOT\"\nfi\ncd \"$INSTALL_ROOT\"\n\n############################################\n# Check that git is installed\n############################################\necho\necho \"Step 5: Checking git ...\"\nif command -v git >/dev/null 2>&1; then\n    echo \"git is installed\"\nelse\n    echo \"Installing git ...\"\n    sudo apt install -y git\nfi\n\n############################################\n# Clone/update repository\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    echo\n    if [[ ! -d \"raspi-cam-srv\" ]]; then\n        echo \"Step 6: Cloning raspi-cam-srv ...\"\n        git clone --branch main --single-branch --depth 1 \"$REPO_URL\"\n    else\n        echo \"Step 6: Repository already exists — updating ...\"\n        cd raspi-cam-srv\n        git fetch origin main --depth=1\n        git reset --hard origin/main\n        cd ..\n    fi\nelse\n    echo\n    echo \"Step 6: Skipping repository setup for existing installation\"\nfi\n\n############################################\n# Python virtual environment\n############################################\necho\necho \"Step 7: Creating virtual environment ...\"\ncd raspi-cam-srv\nif [[ -d \".venv\" ]]; then\n    echo \"Virtual environment exists already\"\nelse\n    python3 -m venv --system-site-packages .venv\n    echo \"Created: .venv\"\nfi\n\n############################################\n# Activate virtual environment\n############################################\necho\necho \"Step 8: Activating virtual environment ...\"\nsource .venv/bin/activate\necho \"Virtual environment activated\"\necho \"$PS1\"\nif [[ \"$OS_CODENAME\" == \"bullseye\" ]]; then\n    if [[ \"$UPDATE_INSTALL\" == true ]]; then\n        echo\n        echo \"On bullseye updating pip ...\"\n        python -m pip install --upgrade pip setuptools wheel\n    fi\nfi\n\n\n############################################\n# Check that Picamera2 is installed\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    echo\n    echo \"Step 9: Checking Picamera2 ...\"\n    if python3 -c \"import picamera2\" 2>/dev/null; then\n        echo \"Picamera2 is installed\"\n    else\n        echo\n        echo \"Installing Picamera2 ...\"\n        if [[ \"$OS_CODENAME\" == \"bullseye\" ]]; then\n            sudo apt install -y python3-picamera2 --no-install-recommends\n        fi\n        if [[ \"$OS_CODENAME\" == \"bookworm\" ]]; then\n            sudo apt install -y python3-picamera2 --no-install-recommends\n        fi\n        if [[ \"$OS_CODENAME\" == \"trixie\" ]]; then\n            sudo apt install -y python3-libcamera python3-picamera2 --no-install-recommends\n        fi\n    fi\nelse\n    echo\n    echo \"Step 9: Skipping Picamera2 installation check for existing installation\"\nfi\n\n############################################\n# Install Flask\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    echo\n    echo \"Step 10: Installing Flask ...\"\n    pip install --ignore-installed \"Flask>=3,<4\"\nelse\n    echo\n    echo \"Step 10: Skipping Flask installation for existing installation\"\nfi\n\n############################################\n# Optional installations\n############################################\nif [[ \"$UPDATE_INSTALL\" == true ]]; then\n    ############################################\n    # OpenCV\n    ############################################\n    echo \n    echo \"Step 11.1: Installing OpenCV ...\"\n    if [[ \"$OS_CODENAME\" != \"bullseye\" ]]; then\n        if [[ \"$ENABLE_ADVANCED\" == true ]]; then\n        sudo apt-get install -y python3-opencv\n        else\n            echo \"Step 11.1: OpenCV not installed (advanced features disabled)\"\n        fi\n    else\n        echo \"Step 11.1: OpenCV not installed on bullseye system\"\n    fi\n    ############################################\n    # numpy\n    ############################################\n    echo \n    echo \"Step 11.2: Installing numpy ...\"\n    if [[ \"$OS_CODENAME\" != \"bullseye\" ]]; then\n        if [[ \"$ENABLE_ADVANCED\" == true ]]; then\n            if [[ \"$RPI_MODEL_ZERO_1\" == false ]]; then\n                pip install --ignore-installed numpy\n            else\n                # For RPI Zero use apt instead of pip to avoid issues with numpy on ARMv6 architecture\n                sudo apt-get install -y python3-numpy\n            fi\n        else\n            echo \"Step 11.2: numpy not installed (advanced features disabled)\"\n        fi\n    else\n        echo \"Step 11.2: numpy not installed on bullseye system\"\n    fi\n    ############################################\n    # matplotlib\n    ############################################\n    echo \n    echo \"Step 11.3: Installing matplotlib ...\"\n    if [[ \"$OS_CODENAME\" != \"bullseye\" ]]; then\n        if [[ \"$ENABLE_ADVANCED\" == true ]]; then\n            if [[ \"$RPI_MODEL_ZERO_1\" == false ]]; then\n                if [[ \"$OS_CODENAME\" == \"bookworm\" ]]; then\n                    pip install --ignore-installed \"matplotlib<3.8\"\n                else\n                    pip install --ignore-installed matplotlib\n                fi\n            else\n                # For RPI Zero use apt instead of pip to avoid issues with matplotlib on ARMv6 architecture\n                sudo apt-get install -y python3-matplotlib\n            fi\n        else\n            echo \"Step 11.3: matplotlib not installed (advanced features disabled)\"\n        fi\n    else\n        echo \"Step 11.3: matplotlib not installed for bullseye system\"\n    fi\n    ############################################\n    # flask-jwt-extended\n    ############################################\n    echo \n    echo \"Step 11.4: Installing flask-jwt-extended ...\"\n    pip install --ignore-installed flask-jwt-extended\n\n    if [[ \"$IS_LITE\" == true ]]; then\n        echo \n        echo \"Step 11.5: Installing psutil ...\"\n        if [[ \"$RPI_MODEL_ZERO_1\" == false ]]; then\n            pip install --ignore-installed psutil\n        else\n            # For RPI Zero use apt instead of pip to avoid issues with psutil on ARMv6 architecture\n            sudo apt-get install -y python3-psutil\n        fi\n    fi\n    ############################################\n    # imx500-all\n    ############################################\n    if [[ \"$ENABLE_AI\" == true ]]; then\n        echo \n        PACKAGE=\"imx500-all\"\n        echo \"Step 11.6: Installing $PACKAGE ...\"\n\n        if dpkg -s \"$PACKAGE\" >/dev/null 2>&1; then\n            echo \"Package '$PACKAGE' is already installed.\"\n        else\n            echo \"Package '$PACKAGE' is not installed. Installing...\"\n            sudo apt update\n            sudo apt install -y \"$PACKAGE\"\n        fi\n    fi\n    ############################################\n    # munkres\n    ############################################\n    if [[ \"$ENABLE_AI\" == true ]]; then\n        echo \n        echo \"Step 11.7: Installing munkres ...\"\n        pip install --break-system-packages munkres\n    fi\n    ############################################\n    # rpi-hardware-pwm\n    ############################################\n    if [[ \"$ENABLE_HW_PWM\" == true ]]; then\n        echo \n        echo \"Step 11.8: Installing rpi-hardware-pwm ...\"\n        pip install --ignore-installed rpi-hardware-pwm\n    fi\nelse\n    echo\n    echo \"Step 11: Skipping optional installations\"\nfi\n\nif [[ \"$WSGI_SERVER\" == \"gunicorn\" ]]; then\n    if [[ \"$UPDATE_INSTALL\" == true ]]; then\n        echo \n        echo \"Step 11.9: Installing gunicorn ...\"\n        pip install --break-system-packages gunicorn\n    else\n        if command -v gunicorn >/dev/null 2>&1; then\n            : \n        else\n            echo \n            echo \"Step 11.9: Installing gunicorn ...\"\n            pip install --break-system-packages gunicorn\n        fi\n    fi\nfi\n\n############################################\n# Initialize database\n############################################\necho\necho \"Step 12: Initializing database ...\"\nif [[ -f \"$INSTALL_ROOT/raspi-cam-srv/instance/raspiCamSrv.sqlite\" ]]; then\n    echo \"Existing database found. Initialization skipped.\"\n    echo \"If you need to reset the database, activate the virtual environment and run\"\n    echo \"python3 -m flask --app raspiCamSrv init-db\"\nelse\n    if [[ \"$RPI_MODEL_ZERO\" == true ]]; then\n        SUCCESS=false\n\n        for i in $(seq 1 $MAX_TRIES); do\n            echo \"Attempt $i of $MAX_TRIES...\"\n            if python3 -m flask --app raspiCamSrv init-db; then\n                SUCCESS=true\n                break\n            fi\n            if [ $i -lt $MAX_TRIES ]; then\n                echo \"Failed. Waiting ${WAIT_SEC}s before retry...\"\n                sleep $WAIT_SEC\n            fi\n        done\n        if [ \"$SUCCESS\" = false ]; then\n            echo\n            echo \"ERROR: All $MAX_TRIES attempts failed. Aborting.\"\n            echo \"Please run the installer again later.\"\n            exit 1\n        fi\n    else\n        python3 -m flask --app raspiCamSrv init-db\n    fi\nfi\n\n############################################\n# Leaving venv\n############################################\necho\ndeactivate\necho \"Virtual environment deactivated\"\necho \"$PS1\"\n\n############################################\n# Checking port\n############################################\necho\necho \"Step 13: Checking Flask service port ...\"\n\nok=false\nwhile [[ \"$ok\" != true ]]; do\n    echo \"Trying port $SERVICE_PORT ...\"\n    if ss -tulpn | grep -q \":$SERVICE_PORT\\b\"; then\n        SERVICE_PORT=$((SERVICE_PORT + 1))\n    else\n        ok=true\n    fi\ndone\necho \"Using port $SERVICE_PORT\"\n\n############################################\n# Cleanup existing services\n############################################\nif [[ \"$SERVICE_CONFIGURED\" == true ]]; then\n    echo\n    echo \"Cleaning up existing service before reinstalling ...\"\nfi\n\nif [[ \"$SERVICE_USER\" == true ]]; then\n    if [[ \"$SERVICE_ENABLED\" == true ]]; then\n        systemctl --user disable \"$SERVICE\" >/dev/null 2>&1\n        echo \"User service '$SERVICE' disabled.\"\n    fi\n    if [[ \"$SERVICE_CONFIGURED\" == true ]]; then\n        SERVICE_FILE=\"$HOME/.config/systemd/user/raspiCamSrv.service\"\n        rm \"$SERVICE_FILE\"\n        echo \"User service '$SERVICE' configuration removed.\"\n    fi\nfi\n\nif [[ \"$SERVICE_USER\" == false ]]; then\n    if [[ \"$SERVICE_ENABLED\" == true ]]; then\n        sudo systemctl disable \"$SERVICE\" >/dev/null 2>&1\n        echo \"System service '$SERVICE' disabled.\"\n    fi\n    if [[ \"$SERVICE_CONFIGURED\" == true ]]; then\n        SERVICE_FILE=\"/etc/systemd/system/raspiCamSrv.service\"\n        sudo rm \"$SERVICE_FILE\"\n        echo \"System service '$SERVICE' configuration removed.\"\n    fi\nfi\n\n############################################\n# Systemd System Unit No Audio / werkzeug\n############################################\necho\nif [[ \"$ENABLE_AUDIO\" == false && \"$WSGI_SERVER\" == \"werkzeug\" ]]; then\n    echo\n    echo \"Installing '$SERVICE' as system service for WSGI Server werkzeug ...\"\n\n    SERVICE_FILE=\"/etc/systemd/system/raspiCamSrv.service\"\n\n    if [[ -f \"$SERVICE_FILE\" ]]; then\n        sudo rm \"$SERVICE_FILE\"\n        echo \"Existing service file removed: $SERVICE_FILE\"\n    fi\n    sudo tee \"$SERVICE_FILE\" >/dev/null <<EOF\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=$INSTALL_ROOT/raspi-cam-srv/.venv/bin/python -m flask --app raspiCamSrv run --port=$SERVICE_PORT --host=0.0.0.0\nEnvironment=\"PATH=$INSTALL_ROOT/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nWorkingDirectory=$INSTALL_ROOT/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\nUser=$USER_NAME\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n    sudo systemctl daemon-reload\n    sudo systemctl enable raspiCamSrv.service >/dev/null 2>&1\n    sudo systemctl start raspiCamSrv.service\n\n    echo \"System service '$SERVICE' installed and started.\"\nfi\n\n############################################\n# Systemd System Unit No Audio / gunicorn\n############################################\nif [[ \"$ENABLE_AUDIO\" == false && \"$WSGI_SERVER\" == \"gunicorn\" ]]; then\n    echo\n    echo \"Installing '$SERVICE' as system service for WSGI Server gunicorn ...\"\n\n    SERVICE_FILE=\"/etc/systemd/system/raspiCamSrv.service\"\n\n    if [[ -f \"$SERVICE_FILE\" ]]; then\n        sudo rm \"$SERVICE_FILE\"\n        echo \"Existing service file removed: $SERVICE_FILE\"\n    fi\n    sudo tee \"$SERVICE_FILE\" >/dev/null <<EOF\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=$INSTALL_ROOT/raspi-cam-srv/.venv/bin/gunicorn -b 0.0.0.0:$SERVICE_PORT -w 1 -k gthread --threads $THREAD_COUNT --timeout 0 --log-level info 'raspiCamSrv:create_app()'\nEnvironment=\"PATH=$INSTALL_ROOT/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nEnvironment=\"GUNICORN_THREADS=$THREAD_COUNT\"\nWorkingDirectory=$INSTALL_ROOT/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\nUser=$USER_NAME\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n    sudo systemctl daemon-reload\n    sudo systemctl enable raspiCamSrv.service >/dev/null 2>&1\n    sudo systemctl start raspiCamSrv.service\n\n    echo \"System service '$SERVICE' installed and started.\"\nfi\n\n############################################\n# Systemd User Unit Audio / werkzeug\n############################################\nif [[ \"$ENABLE_AUDIO\" == true && \"$WSGI_SERVER\" == \"werkzeug\" ]]; then\n    echo \"Installing '$SERVICE' as user unit for WSGI Server werkzeug ...\"\n\n    mkdir -p \"$HOME/.config/systemd/user\"\n    SERVICE_FILE=\"$HOME/.config/systemd/user/raspiCamSrv.service\"\n\n    if [[ -f \"$SERVICE_FILE\" ]]; then\n        rm \"$SERVICE_FILE\"\n        echo \"Existing service file removed: $SERVICE_FILE\"\n    fi\n    tee \"$SERVICE_FILE\" >/dev/null <<EOF\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=$INSTALL_ROOT/raspi-cam-srv/.venv/bin/python -m flask --app raspiCamSrv run --port=$SERVICE_PORT --host=0.0.0.0\nEnvironment=\"PATH=$INSTALL_ROOT/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nWorkingDirectory=$INSTALL_ROOT/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n    systemctl --user daemon-reload\n\n    # Enable lingering so the user service can run without login\n    sudo loginctl enable-linger \"$USER_NAME\"\n\n    systemctl --user enable raspiCamSrv.service >/dev/null 2>&1\n    systemctl --user start raspiCamSrv.service\n\n    echo \"User service installed and started.\"\nfi\n\n############################################\n# Systemd User Unit Audio / gunicorn\n############################################\nif [[ \"$ENABLE_AUDIO\" == true && \"$WSGI_SERVER\" == \"gunicorn\" ]]; then\n    echo \"Installing '$SERVICE' as user unit for WSGI Server gunicorn ...\"\n\n    mkdir -p \"$HOME/.config/systemd/user\"\n    SERVICE_FILE=\"$HOME/.config/systemd/user/raspiCamSrv.service\"\n\n    if [[ -f \"$SERVICE_FILE\" ]]; then\n        rm \"$SERVICE_FILE\"\n        echo \"Existing service file removed: $SERVICE_FILE\"\n    fi\n    tee \"$SERVICE_FILE\" >/dev/null <<EOF\n[Unit]\nDescription=raspiCamSrv\nAfter=network.target\n\n[Service]\nExecStart=$INSTALL_ROOT/raspi-cam-srv/.venv/bin/gunicorn -b 0.0.0.0:$SERVICE_PORT -w 1 -k gthread --threads $THREAD_COUNT --timeout 0 --log-level info 'raspiCamSrv:create_app()'\nEnvironment=\"PATH=$INSTALL_ROOT/raspi-cam-srv/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\nEnvironment=\"GUNICORN_THREADS=$THREAD_COUNT\"\nWorkingDirectory=$INSTALL_ROOT/raspi-cam-srv\nStandardOutput=inherit\nStandardError=inherit\nRestart=always\n\n[Install]\nWantedBy=default.target\nEOF\n    systemctl --user daemon-reload\n\n    # Enable lingering so the user service can run without login\n    sudo loginctl enable-linger \"$USER_NAME\"\n\n    systemctl --user enable raspiCamSrv.service >/dev/null 2>&1\n    systemctl --user start raspiCamSrv.service\n\n    echo \"User service installed and started.\"\nfi\n\n############################################\n# Restore backup, if available\n############################################\nif [ ! -d \"$BACKUP_DIR\" ] || [ -z \"$(find \"$BACKUP_DIR\" -mindepth 1 -print -quit)\" ]; then\n    if [ -d \"$BACKUP_SAV\" ]; then\n        mv \"$BACKUP_SAV\" \"$BACKUP_DIR\"\n        echo\n        echo \"Backup restored from $BACKUP_SAV to $BACKUP_DIR\"\n        echo \"You can activate a backup in dialog Settings/Configuration\"\n    fi\nfi\n\n############################################\n# Check Hardware PWM support\n############################################\nif [[ \"$ENABLE_HW_PWM\" == true ]]; then\n    echo\n    echo \"=============================================================================================================\"\n    echo \"Checking for Hardware PWM support on GPIO pins 12, 13, 18, 19 ...\"\n    echo \"pinctrl get 12,13,18,19\"\n    pinctrl get 12,13,18,19\n    echo \"\"\n    echo \"If you see 'PWM0' or 'PWM1' in the output above, Hardware PWM support is available for the indicated pins.\"\n    echo \"Otherwise, you need to specify device tree overlays in /boot/firmware/config.txt and reboot your Raspberry Pi.\"\n    echo \"Depending on your RPI model and the required pins, add the following lines to /boot/firmware/config.txt:\"\n    echo \"[all]\"\n    echo \"dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\"\n    echo \"[pi5]\"\n    echo \"dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4\"\n    echo \"dtoverlay=pwm-2chan,pin=18,func=2,pin2=19,func2=2\"\n    echo \"=============================================================================================================\"\nfi\n\n\n############################################\n# Finish\n############################################\necho\necho \"==========================================\"\necho \"=== raspiCamSrv installation completed ===\"\necho \"===                                    ===\"\necho \"=== Access via http://$HOSTNAME:$SERVICE_PORT\"\necho \"==========================================\"\n"
  },
  {
    "path": "scripts/uninstall_raspiCamSrv.sh",
    "content": "#!/bin/bash\nset -e\n\n############################################\n# raspiCamSrv Uninstaller\n############################################\necho\necho \"==========================================\"\necho \"=== raspiCamSrv Automated Uninstaller  ===\"\necho \"===                                    ===\"\necho \"=== Exit at any step with Ctrl+C       ===\"\necho \"==========================================\"\n\nUSER_NAME=\"$USER\"\nINSTALL_ROOT=\"$HOME/prg\"\nINSTALL_DIR=\"$INSTALL_ROOT/raspi-cam-srv\"\nHOSTNAME=\"$(hostname)\"\n\n##############################################\n# Detect Raspberry Pi Model\n##############################################\nRPI_MODEL=$(tr -d '\\0' < /proc/device-tree/model)\nif [[ \"$RPI_MODEL\" == \"Raspberry Pi Zero\"* ]]; then\n    RPI_MODEL_ZERO=true\nelse\n    RPI_MODEL_ZERO=false\nfi\n\n##############################################\n# Detect OS version and check for full version\n##############################################\nOS_CODENAME=$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)\n\nif dpkg -l | grep -q raspberrypi-ui-mods || \\\n   dpkg -l | grep -q lxsession || \\\n   [ -d /usr/share/xsessions ]; then\n    OS_VARIANT=\"full\"\nelse\n    OS_VARIANT=\"lite\"\nfi\necho\necho \"RPI Model           : $RPI_MODEL\"\necho \"Detected OS codename: $OS_CODENAME $OS_VARIANT\"\necho \"Hostname            : $HOSTNAME\"\necho\necho \"Running as user     : $USER_NAME\"\necho \"Uninstalling from   : $INSTALL_DIR\"\nif [[ ! -d \"$INSTALL_DIR\" ]]; then\n    echo\n    echo \"No raspiCamSrv installation found at $INSTALL_DIR.\"\n    echo \"Nothing to uninstall.\"\n    exit 0\nfi\n\n############################################\n# Request confirmation\n############################################\necho\nread -rp \"raspiCamSrv will be completely removed from $HOSTNAME. Continue? [yes/NO]: \" UNINST_CHOICE\necho\n\nUNINST_CHOICE=${UNINST_CHOICE,,}   # normalize to lowercase\n\nif [[ \"$UNINST_CHOICE\" != \"yes\" ]]; then\n    echo\n    echo \"=======================================\"\n    echo \"=== raspiCamSrv uninstall cancelled ===\"\n    echo \"=======================================\"\n    exit 0\nfi\n\n############################################\n# Check for backups\n############################################\nBACKUP_DIR=\"$INSTALL_DIR/backups\"\nBACKUP_SAV=\"$INSTALL_ROOT/raspi-cam-srv_backups\"\nif [ -d \"$BACKUP_DIR\" ] && [ ! -z \"$(find \"$BACKUP_DIR\" -mindepth 1 -print -quit)\" ]; then\n    echo \"Backups found in $BACKUP_DIR:\"\n    ls -l \"$BACKUP_DIR\"\n    echo\n    read -rp \"Do you want to keep these backups? [y/N]: \" BACKUP_CHOICE\n\n    BACKUP_CHOICE=${BACKUP_CHOICE,,}   # normalize to lowercase\n\n    if [[ \"$BACKUP_CHOICE\" == \"y\" ]]; then\n        if [ ! -d \"$BACKUP_SAV\" ]; then\n            mv \"$BACKUP_DIR\" \"$BACKUP_SAV\"\n            echo\n            echo \"Backups saved at $BACKUP_SAV\"\n        else\n            echo\n            echo \"Backup save directory $BACKUP_SAV already exists.\"\n            echo \"Please remove or rename it before running the uninstaller again.\"\n            echo\n            echo \"=======================================\"\n            echo \"=== raspiCamSrv uninstall cancelled ===\"\n            echo \"=======================================\"\n            exit 0\n        fi\n    fi\nfi\n\n############################################\n# Uninstalling raspiCamSrv service\n############################################\necho\necho \"Uninstalling raspiCamSrv service ...\"\n\nSERVICE_FILE_SYS=\"/etc/systemd/system/raspiCamSrv.service\"\nSERVICE_FILE_USR=\"$HOME/.config/systemd/user/raspiCamSrv.service\"\nif [ -f \"$SERVICE_FILE_SYS\" ]; then\n    sudo systemctl disable raspiCamSrv.service\n    echo \"raspiCamSrv system unit disabled\"\n    sudo systemctl stop raspiCamSrv.service\n    echo \"raspiCamSrv system unit stopped\"\n    sudo rm \"$SERVICE_FILE_SYS\"\n    echo \"Service file removed $SERVICE_FILE_SYS\"\n    sudo systemctl daemon-reload\nelif [ -f \"$SERVICE_FILE_USR\" ]; then\n    # Disable lingering\n    sudo loginctl disable-linger \"$USER_NAME\"\n    echo \"lingering disabled for user $USER_NAME\"\n    systemctl --user disable raspiCamSrv.service\n    echo \"raspiCamSrv user unit disabled\"\n    systemctl --user stop raspiCamSrv.service\n    echo \"raspiCamSrv user unit stopped\"\n    rm \"$SERVICE_FILE_USR\"\n    echo \"Service file removed $SERVICE_FILE_USR\"\n    systemctl --user daemon-reload   \nelse\n    echo \"No raspiCamSrv service files found\"\nfi\n\n############################################\n# Removing installation\n############################################\necho\necho \"Uninstalling raspiCamSrv ...\"\nif [ -d \"$INSTALL_DIR\" ]; then\n    rm -fdr \"$INSTALL_DIR\"\n    echo \"$INSTALL_DIR removed\"\nelse\n    echo \"$INSTALL_DIR does not exist\"\nfi\n\n############################################\n# Removing Install root\n############################################\necho\necho \"Removing empty install root ...\"\nif [ -d \"$INSTALL_ROOT\" ]; then\n    if [ -d \"$INSTALL_ROOT\" ] && [ -z \"$(find \"$INSTALL_ROOT\" -mindepth 1 -print -quit)\" ]; then\n        rm -d \"$INSTALL_ROOT\"\n        echo \"$INSTALL_ROOT removed\"\n    else\n        echo \"$INSTALL_ROOT is not empty. Not removed\"\n    fi\nelse\n    echo \"$INSTALL_ROOT does not exist\"\nfi\n\n############################################\n# Finish\n############################################\necho\necho \"==========================================\"\necho \"=== raspiCamSrv uninstall completed    ===\"\necho \"==========================================\"\n"
  }
]