[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig <https://EditorConfig.org>\nroot = true\n\n# elementary defaults\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = tab\nindent_style = space\ninsert_final_newline = true\nmax_line_length = 80\ntab_width = 4\n\n# Markup files\n[{*.html,*.xml,*.yaml,*.yml}]\ntab_width = 2"
  },
  {
    "path": ".github/workflows/pages.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches: [master]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/configure-pages@v4\n      - uses: actions/upload-pages-artifact@v3\n        with:\n          path: public\n      - id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/python-tests.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions\n\nname: Python tests\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version:\n          - \"3.9\"\n          - \"3.11\"\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v4\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install system dependencies\n      run: |\n        sudo apt-get update && sudo apt-get install -y libcap-dev libturbojpeg\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install -U -r requirements.txt\n        pip install -U -r requirements-multi.txt\n        pip install pylint~=2.17.7\n        pip install -U ruff pytest pytest-doctestplus pytest-pylint pytest-mypy mock\n        pip install --force-reinstall git+https://github.com/prusa3d/prusa-connect-sdk-printer.git\n        pip install --force-reinstall git+https://github.com/prusa3d/gcode-metadata.git\n\n    - name: Lint with ruff\n      run: |\n        ruff check .\n    - name: Lint with pylit\n      run: |\n        PYTHONPATH=`pwd` pytest -v --mypy --pylint --doctest-plus --doctest-rst prusa/link\n    - name: Tests\n      run: |\n        PYTHONPATH=`pwd` pytest -v --mypy --pylint --doctest-plus --doctest-rst tests\n"
  },
  {
    "path": ".gitignore",
    "content": "venv\n.idea\n.env\n__pycache__\n*.pyc\nbuild\ndist\n*.egg-info\n*.orig\n.hypothesis\n*.coverage\nhtmlcov\ndocs/*.png\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"prusa-link-web\"]\n\tpath = Prusa-Link-Web\n\turl = https://github.com/prusa3d/Prusa-Link-Web.git\n[submodule \"PiShrink\"]\n\tpath = PiShrink\n\turl = https://github.com/Drewsif/PiShrink\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "---\nrepos:\n  # - repo: https://github.com/pre-commit/mirrors-yapf\n  #   rev: 'v0.32.0'  # Use the sha / tag you want to point at\n  #   hooks:\n  #     - id: yapf\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.6.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n  # - repo: https://github.com/pre-commit/mirrors-pylint\n  #  rev: v2.4.4\n  #  hooks:\n  #    - id: pylint\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: 'v0.3.5'\n    hooks:\n    - id: ruff\n      args: [--fix]\n  - repo: https://github.com/pycqa/flake8\n    rev: 7.0.0\n    hooks:\n      - id: flake8\n"
  },
  {
    "path": ".pylintrc",
    "content": "[BASIC]\n\ngood-names=i,l,f,sn,ip\n\n[TYPECHECK]\n\ngenerated-members=prctl\n\n[MASTER]\nextension-pkg-whitelist=pydantic\nignore-patterns=.*/v4l2.py\n\n[MESSAGES CONTROL]\ndisable=\n    fixme,\n    unsubscriptable-object,\n    too-few-public-methods,\n    too-many-instance-attributes,\n    too-many-public-methods,\n    wrong-import-order\n"
  },
  {
    "path": "CONTRIBUTION.md",
    "content": "# Contribution\n\n## Developement\n\n**On Rasbian**:\n\nRunning on foreground without install:\n\n```sh\npython3 -m prusa.link -f\n```\n\n**From desktop**:\n\n`/dev/ttyACM0` can be USB <-> UART printer port\n\n```sh\npython3 -m prusa.link -f -s /dev/ttyACM0\n```\n\nWhen you install `socat` tool to RaspberryPi Zero, you can create use\nvirtual TCP <-> UART port.\n\n```\nsocat PTY,link=$HOME/ttyAMA0,raw,wait-slave EXEC:'\"ssh pi@prusa-link socat - /dev/ttyAMA0,nonblock,raw\"'\n# in another terminal\npython3 -m prusa.link -f -s $HOME/ttyAMA0\n```\n\n**Own static files**:\n\n```sh\nPRUSA_LINK_STATIC=./my_static python3 -m prusa.link -f\n```\n\n**Communication debug**:\nprusalink -f -I -i -l urllib3.connectionpool=DEBUG -l connect-printer=DEBUG\n"
  },
  {
    "path": "ChangeLog",
    "content": "# ChangeLog\n\n0.8.2 (2024-12-18)\n    * Fix crc issue\n\n0.8.1 (2024-06-28)\n    * Add a v4l2 workaround so broken camera handles are disqualified from scan\n\n0.8.0 (2024-06-27)\n    * Rpi5 first boot fix\n    * Fix power panic stuck at Resend\n    * Fix startup issues on RPi 5\n    * Wait a bit for printer to finish moves after waking up from power panic\n    * Sync files upon Upload finish\n    * Support newer \"//action\" without the space\n    * Fix the new image missing the libcamera dependency\n    * Improve message shown when preparing a power panic recovery\n    * Fix print stats not ignoring the skipped part of gcode after PP\n    * Ensure power panic info is written to persistent storage\n    * Fix active tool not re-setting to None when told to\n    * Use printer mid-movement power panic recovery trick\n    * Fix the multi-instance proxy. Again\n    * Stop showing the tune menu after connecting to the printer\n    * Use the 2023-10-10 Raspberry Pi OS base image for the image builder\n    * Decrease the severity of web server access log messages\n    * Add a way to download logs even on systemd journal based systems\n    * Bump the API version so it is the same as the xBuddy reported one\n    * Support the new multi-level telemetry data structure\n    * Don't send over serial when temperature calibration is running\n    * Periodically send a keepalive gcode to keep the printer in PrusaLink mode\n    * Support the set ready and cancel ready LCD menu toggle\n    * Add gcodes that flag the state of a usb print for the printer statistics to get saved\n    * Handle the new re-print LCD menu item\n    * Add the initial support for the MMU\n    * Add Power Panic support\n    * Make ->Ready to recover dissapear sooner from the LCD\n    * The minimum firmwre version has been increased to 3.14.0\n    * Fix the multi-instance proxy\n\n0.7.2 (2023-10-11)\n    * Add an automatic PrusaLink image builder script\n    * Add multi-instance documentation\n    * Telemetry improvement\n    * Attempt to turn the RPi wifi power management off in the images\n\n0.7.1rc1 (2023-08-10)\n    * Fixed HTTP response status on HEAD request for non-existing files\n    * Attribute ro renamed to read_only\n    * Fix printer returning to READY instead of IDLE\n    * Respect the X-Forwarded-Prefix header in Wizard\n    * Add focus support for the v4l2 cameras\n    * Add a multi-instance web waypoint, use a reverse proxy to navigate to the correct instances\n    * Automatically redirect from the multi-instance waypoint if exactly one printer is configured\n    * Fix missing error detail pages\n    * Fix an error when dosconnecting from a cammera that failed to connect\n    * Fix an intermittent error that was killing instances for like two years\n    * Differentiate between multi instance printers by showing the printer number on the LCD screen\n    * Static Web update\n        - Fix behavior on SD card ejection\n        - Add simple camera focus control if supported\n        - Add default value for connect hostname\n        - Update translations\n        - Add tooltips for error status texts in telemetry\n        - Add confirmation modal to overwrite a file by upload\n        - Fix undefined printer states displaying\n        - Hide zero temperatures in telemetry\n        - Add support to be running behind the proxy\n        - Fix folder deletion button\n        - Fix prusa connect port handling in URL builder\n    * Hold the STOPPED and FINISHED state for at least 11s\n    * Fix MK2(.5)S SN being broken on multi-instance images\n    * Implemented UPGRADE high lvl Connect command\n\n0.7.0 (2023-05-19)\n    * Fixed printer sends info about api key change to Connect after change\n    * Added the network error beep setting to the web API\n    * Support renaming gcodes directory (cfg and API)\n    * Added a multi-instace auto config and starter utility\n    * Disable error beeps during prints\n    * Static Web update\n        - Using v1 endpoints for job and transfer\n        - Creating and Deleting folders\n        - Translation update\n        - Network error chime control\n        - Default storage names\n        - Upgrade procedure rework\n\n0.7.0rc3 (2023-03-09)\n    * Added v1 endpoints for flat filesystem structure, old struct is moved to\n      files_legacy file\n    * Added api/v1/update/<env> GET endpoint\n    * Printer name and location are added to register url as query parameters,\n      if available\n    * Static Web update\n        - Apply UI/UX refactoring\n        - Add support for max-age cache control\n        - Add control of last-modified header for snapshots\n        - Add drop zone to files storage\n        - Remove manual camera connection dialog\n        - Migrate to files api v1\n        - PrusaLink update\n    * Added Force header to api/v1/files/<storage>/<path> DELETE endpoint for\n      deletion of non-empty folder\n    * Changed Print-After-Upload header value check for PUT\n    * Added endpoint to start printing file\n    * Removed the original picamera driver\n    * Raspberry Pi Camera support utilizing libcamera directly\n    * Hardware encoding support. (Pi Zero W manages FullHD snapshots without issues)\n    * Fix \"unicam\" appearing when a Raspberry Pi camera is connected\n    * Fix not following the configured resolution\n    * Added api/v1/status endpoint\n    * Added new endpoint for updating prusalink python package\n    * Added api/v1/transfer endpoint\n\n0.7.0rc2 (2022-12-09)\n    * Support thermal model errors (FW 3.12)\n    * USB Camera\n    * SD Card fixes\n    * Fix MBL data for MK2.5(S)\n    * Wizard refactoring\n    * Static web update\n        - Cameras\n        - File sorting\n        - Stop dialog fixed\n        - Connect status\n    * API Settings moved from Wizard to Settings\n    * Raspberry Pi Camera support\n    * Added cache control headers for cameras snap endpoints\n    * Fixed PUT upload when folders within the path does not exist\n    * Cameras! Support for:\n        - V4L2 cameras (webcams - MJPEG and YUYV formats supported)\n        - picamera2 (libcamera stack) (slow)\n        - Changing the resolution\n        - Camera auto-detection\n        - Triggering on layer change (PrusaSlicer sliced files only!)\n    * Fix files with uppercase extensions not showing up locally\n    * Support \"hotend fan\" = \"extruder fan\"\n    * Re-send the complete telemetry every five minutes\n    * Fix stats missing for prints of gcodes without M73\n    * Fix pause being able to double print time reported\n    * Fixed error when trying to get space info of SD Card\n\n\n0.7.0rc1 (2022-09-13)\n    * Work around a bug: printer in serial print mode while wizard is shown\n    * READY state changed to IDLE, PREPARED state changed to READY\n    * New status display\n       - notifies about setup wizard,\n       - shows upload progress\n       - shows the name of a file being printed\n       - notifies about errors\n       - shows an idle screen with the IP address after 30min\n       - add idle screen and show transfers during print pauses\n    * Name and location of printer value validation\n    * Fix negative timeout being possible in serial read\n    * Additional Connect (un)registration support\n    * File and Directory name validation refactoring\n    * Fixed transfer and print in ATTENTION error\n    * New Connect API support\n    * Fix PrusaLink IP not getting reset from printer on shutdown\n    * Fix the serial_number step in wizard\n    * Fix unicode characters in file names breaking lcd printer\n    * Make RESET_PRINTER clear the command queue and have priority that way\n    * Made the app stop itself faster\n    * Use M400 instead of G4 for printer queue syncing\n    * Reworked validation of correct S/N write\n    * Modified username length and password length and format validation\n    * Use \"Sync|->:\" and \"Sync->|:\" to signify which way is the current transfer going\n    * Add DNS service discovery compatible with PrusaSlicer\n    * Support file upload cancels from PrusaSlicer\n    * Static web update:\n       - Fix big log files displaying\n       - Decrease display log file size limit to 1M\n       - Change temperature controls widget number format to display integers\n       - Add stop/resume print button\n       - Add protection from steppers disabling when printing\n       - Fix sidebar width\n       - Replace PNG icons with SVG\n       - Fix router, telemetry graph dinmensions and page layout\n       - Update error handling to avoid duplicates of popups\n       - Add support for file extensions provided by printer profiles from API\n       - Fix display names of origins\n       - New application design\n       - New field to rename project file uploaded by URL\n       - New widget displaying used/free size (not-connected to printer yet)\n       - New Rename and Copy actions (hidden)\n       - New tool to unify icons colors\n       - Updated free space logic\n       - Fixed storage tabs behavior\n       - Avoid unnecessary requests to BE for file metadata\n       - Hardcode storages list to printers\n       - Removed page `Temperatures`\n       - Fix formatting of percentages\n       - Project preview is now not dependent on `/api/job` endpoint\n       - Confirm dialog after uploade via drag zone\n       - Nozzle diameter\n       - Offline mode\n       - Connect Like icons\n       - Translaction fallbacks\n     * Differentiate between FW and ID errors in the wizard, update texts\n     * Fixed download ref, added total storage space info\n     * Added storage space info to api/printer\n     * Added function for save file with custom name\n     * Add dynamic download throttling when printing\n     * Added caching for thumbnail images\n     * Send printer info on printer reset / info invalidation event\n     * Fixed error handling for PrusaLink Web\n     * Reset print stats after a print ends\n     * Fix print fail from a unchecked print buffer underflow\n     * Report mesh bed levelling data\n     * Use the print mode to report the right print stats row to connect\n     * Make sure fan errors send reason, improve their behavior a little\n     * Fix SD Card module race conditions\n     * Make it possible to hide certain loggers from interesting log\n     * Filter telemetry, send only what's \"significantly\" changed\n     * Fixed maximum temperature check for nozzle and heatbed\n     * Api-Key is implicitly None, can be set in wizard or using endpoint\n     * Start PrusaLink even without a connection to the printer\n     * Start sending telemetry slowly after a period of inactivity\n     * Files can be printed without selecting first, fixed job printTime info in api/job\n     * Don't wait for a printer to boot when running through the EINSY pins\n     * Added api/v1/info endpoint\n     * Add printer statistics tracking\n     * Add time to filament change tracking\n     * Add sheet settings tracking\n     * Return a better reason when print of a non existent file is requested\n     * Make printer settings reflect the actual printer type\n     * Fixed doubled gcode extensions when custom name is used\n     * Added nozzle diameter info to api/v1/info\n     * TLS is changed from int to bool\n     * Added endpoint for capture an image from a camera\n     * Fixed check for negative temperatures of nozzle and bed\n     * Add a special SD menu to set the printer to READY from the LCD\n     * Add boot partition config copy script (for RPis)\n     * Added endpoint api/v1/storage with storage info\n     * Round auto guessed preheat temps to the nearest five\n     * Remove any irrelevant telemetry right on state change\n     * Added endpoint api/v1/<storage>/<path>\n     * Add automatic serial port scan\n     * Use USB S/N if available (fixes MK2.5 SN issues)\n     * Added endpoint with a list of available ports\n     * Added capabilities flag to api/version\n     * Added min extrusion temp to api/v1/info endpoint, fixed value\n     * Added optional ro parameter to api/files and api/v1/{storage}/{path} endpoints\n     * Added link_state parameter to api/printer endpoint\n     * Fixed item updater allowing invalidation of a disabled item\n     * Fixed upload PUT Print-After-Upload if already printing error\n     * Added api/v1/<storage>/<path> delete endpoint\n     * Fixed a semicolon in a filename being printed breaking everything\n     * Fixed a bronken RESET_PRINTER for raspis connected through USB\n     * API key option removed from wizard\n     * Added endpoint for deletion of API key\n\n0.6.0 (2021-12-17)\n    * Added endpoint for control of extruder\n    * Added endpoint for heatbed temperature control\n    * Static web update\n      - Add debug outputs to investigate project picture collision\n      - Removed unnecessary colon after hostname in Dashboard\n      - Switched from data.link to data.printer for settings end point\n      - Add advanced upload widget\n      - Printer Control Page\n      - Add target temperatures to the left sidebar\n      - Add possibility to send control values by Enter key press\n      - Add serial number setting\n      - Prevent api polling when previous requests were not handled\n      - Prevent error messages flood in case of a connection problem\n      - Optimize application loop\n      - Add serial, CONNECT and communication state to the left side bar\n    * Added size and date attributes to api/logs GET endpoint\n    * Removed m_time file attribute\n    * Added restriction for forbidden characters in uploaded file name\n    * Added download and basic upload info to link-info page\n    * Added and implemented JSON file with HW limits\n    * Added api/printer/printhead GET endpoint\n    * Changed variable firmware_version to firmware\n    * Added LOAD/UNLOAD filament commands\n    * Added disable_steppers command to api/printer/printhead\n    * Implementation of farm_mode into api/settings endpoints\n    * New Upload errors\n      - check Content-Length header\n      - check if file is uploaded complete\n      - check storage free space first\n      - errors refactoring\n      - simple html errors\n    * Changed args to kwargs for high level commands\n    * HTTP Request handling improvement\n    * own Serial class implementation (speed improvement)\n    * Changed args to kwargs for execute_gcode command\n    * log thread stack on interesting events\n    * move job_id into the EEPROM\n    * make STOP_PRINT wait for any of the READY, STOPPED or FINISHED states\n    * Implementation of new Transfer object from SDK\n    * Make a centralised wizard activation condition\n    * Download finished callback implementation\n    * Fix SD initialising always as PRESENT even when ABSENT\n\n0.5.1 (2021-07-16)\n    * Implementation of print after upload endpoint\n    * Minimal suported firmware is 3.10.0\n    * Sort files directory first, newest first\n    * Faster checking and processing when uploading gcode\n    * Static web update\n      - Upload gcode fixes\n      - File browser is available when printer printes\n      - Files are sort by printers API\n      - Printed file widget rework\n      - Fixed progress bar behaviour when printing is finished\n      - Fixed error handling for periodic requests\n      - Page heading is sticky now\n      - Telemetry sidebar is sticky now\n      - Added frontend version to the Settings page\n      - Fixed 'undefined' error pop up heading in some cases\n      - Log viewer\n      - Login and password can be changed in settings\n      - All not available project properties are hidden\n      - Toaster messages are now sticky to window bottom\n      - Fixed printing time estimations missmatching\n      - Printer name and location can be changed in settings\n      - Files with size above 100MB won't be loaded into textarea\n    * React to thermal runaway by going into the error state\n    * Use daemon type WSGI threads\n    * Removed temporary gcode copy for printing\n    * Support the new M20 attributes and their order\n    * Fix progress equal to -1 not being supported\n    * Fixed upload from local web\n    * SEND_INFO hostname fixed\n    * Fix SD Card file selection\n    * Log HTTP requests and errors over Python Logger\n    * Improve FW error message support\n    * Work around print head returning to the print after Stop print\n    * Added endpoint for download file from url\n    * Password in plain text form is not stored in memory\n    * Added endpoint for gettting info about the file currently being downloaded\n    * Added endpoint for abort current download process\n    * Require user attention after each print, even failed ones (if enabled)\n    * Added checked and finished flags to api/printer\n    * Added states structure to api/connection endpoint\n    * Added Connect configuration info to api/connection endpoint\n    * Added connection.py with api/connection GET, POST endpoints\n    * Added api/settings GET endpoint\n    * Added m_timestamp to SDCard files properties\n    * Added api/settings POST endpoint, fix settings.py name\n    * Fixed /api/printer flags\n    * Implementation of gcode download endpoint\n    * Added api/logs endpoint\n    * Added api/logs/<log_file> endpoint\n    * Added wizard/serial endpoints and page for setup S/N of the printer\n    * Updated metadata for selected file\n    * Require two \"Not SD printing\" to work around a SD printing bug\n    * Added username and password change functionality to api/settings POST, fixed ChangeLog\n    * Fixed SD Card metadata read\n    * Go into the ERROR state when the printer stops responding for aprox. one minute\n    * Added endpoint for regenerate api-key\n    * Added api/settings/sn endpoint for setup S/N of the printer\n    * Wizard is locked after successful configuration\n    * Added endpoint for control of printhead movement\n    * Added endpoint GET api/settings/sn\n\n0.4.0 (2021-04-13)\n    * getting IP refactoring\n    * fix firmware version reading\n    * working download gcode endpoint\n    * command argument for profiling application\n    * connection over VPN fix\n    * Added additional network info\n\n0.3.0 (2021-03-30)\n\n    * Fixed broken command resends\n    * Fixed state changed handler\n    * Added new endpoint /api/connections with JSON response\n    * Skipped pidfile when process is not alive\n    * Added new endpoint /api/printer with JSON response\n    * Fixed complaint in wizard about `api-key` when `username` was too short\n    * Fix printer.sn being unset in the wizard by waiting for it\n    * Fixed some telemetry being sent basically at random\n    * Enabled the RESET_PRINTER command\n    * Fixed printer resetting multiple times when it gets reset mid-print\n    * Fixed accidentally hogging CPU when displaying LCD messages\n    * Set log levels by module name in config or as command arguments\n    * Report whether the current job is from the SD or not\n    * Support long file names in the upcoming 3.10 release (file explorer only)\n    * Added new endpoint /api/files with JSON response\n    * Added new endpoint /api/job with JSON response\n    * Added support for the new C parameter in M155\n    * Modification of /api/connection, files, printer and job endpoints\n    * All files in data_dir (user's home by default)\n    * Parse print info from the file name (for SD files)\n    * Introduce ErrorState(s) from SDK\n    * Modify `LCDPrinter` to show IP and status based on SDK error states\n    * Support the nem M27 P\n    * Fix not being able to print from root of SD when in a folder in LCD menu\n    * Send 0% when a new print start is observed\n    * Fix no progress being sent when SD print has no stats in its gcode\n    * Support fan errors. Send reason for ATTENTION state in state change data\n    * Support showing the IP address in the support menu using \"M552 P<IP>\"\n    * /link-info debug page\n    * SN is obtained always through the FW and isn't stored in a file anymore\n    * Ensure M20 won't be sent during print. Ever\n    * Start faster when already printing from SD\n    * Don't store password in plaintext but use digest\n    * Stop Wizard on printer errors.\n    * Support the new STOPPED state\n    * Use X-Api-Key or HTTP Digest for /api endpoints\n    * hostname in /api/version\n    * Fix /api/endpoints\n    * Fix /api/files and /api/job endpoints\n    * Nicer messages for Wlan errors; LCDPrinter now accesses the model for IP\n    * Statics generated from submodule\n    * Support pausing, resuming and stopping of serial prints from the LCD\n    * Implementation of metadata into /api/files endpoint\n    * Process all commands in a single thread -> racing avoided\n    * Uploading from local web\n    * Prusa Link version in INFO event\n    * G-code preview and download endpoints\n    * Thread names via prctl - can be show in htop\n    * Shutdown fix\n    * Report build number alongside firmware version\n    * Added api commands for pause, stop and resume print job\n    * If-Modified-Since and If-None-Match headers support for /api/files\n    * Additional version info\n    * Files in hidden folder are ignored\n    * Report file names of SD prints better\n    * Added endpoint for start print\n    * LCD message modification (GO: <IP>)\n    * Fix connection errors causing the printer to report being in ERROR state\n    * Add the possibility to log at debug level around interesting events\n    * Distinguish wifi from lan\n    * Implementation of select/print file functions from local web\n    * File resource endpoint\n    * working `start / stop / pause / resume` job\n    * job info refactoring\n    * endpoint for deleting file\n    * fix job get / set\n\n0.2.0 (2020-12-14)\n\n    * JOB_INFO fix\n    * Service must be start using daemon script prusa-link\n    * Implicit config path is /etc/Prusa-Link/prusa-link.ini\n    * Implicit settings path is {HOME}/prusa_printer_settings.ini\n    * Wizard - part II\n    * Api-Key in INFO event\n    * Wizard redesign\n\n\n0.1.3 (2020-12-01)\n\n    * Report at least a file name for SD prints\n    * Wizard - part I\n    * Fix command handling and re-undo fw double-ok workarounds\n      (FW commit gd167b3bd or newer is required)\n\n0.1.2 (2020-11-23)\n\n    * New FW (3.9.2.3566) required\n    * local web service on http://IP-address:8080\n    * file upload from Prusa Slicer (use `PrusaSlicer` Api-Key)\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include *.py\ninclude MANIFEST.in\ninclude requirements.txt\ninclude requirements-pi.txt\ngraft prusa/link/data\ngraft prusa/link/templates\ngraft prusa/link/static\ngraft image_builder\nglobal-exclude *~\nglobal-exclude *.swp\nprune venv\n"
  },
  {
    "path": "MULTIINSTANCE.md",
    "content": "# PrusaLink multi instance\nIn this mode, an instance of PrusaLink is created for each new printer\ndetected on any of the USB ports of the host system, allowing the user\nto connect multiple printers using a single Raspberry Pi.\n\n## Setup\nThe multi instance image requires the same setup as the regular one,\nbut there are some differences\n\n1) The multi-instance manager does not connect to printers on the GPIO\npins as the device udev auto-detection in Linux does not work on those\n2) Cameras automatically connect to the first instance only. If you wish\nto use for example a camera for each printer, you'll need to manually\ncopy over relevant configuration\n3) In this image, the manager of these PrusaLink instances is run as root.\nHowever web interface of the instance manager is run under the user account.\n\n### Cameras\nThe temporary process of connecting multiple cameras is not user friendly\nand requires manual work. This will change in the future.\nThe process is as follows:\n1) Connect all cameras you wish to use and let them connect to the first\ninstance\n2) Open the web interface of the first instance and under cameras, save\nevery camera manually. This will create a configuration section for each\ncamera in `prusa_printer_settings.ini` of the first instance\n3) Using ssh, navigate to `/etc/prusalink/prusalink1.ini` and open it\n4) Turn off the camera auto-detection in the first instance by adding\nthis section into the file\n    ```\n    [cameras]\n    auto_detect = False\n    ```\n5) Navigate to `/home/<username>/PrusaLink1` and open\n`prusa_printer_settings.ini`\n6) Move the section corresponding to each camera over to the instance in which\nyou wish to use it. The camera sections have hashes as names,\nthe order of which is noted in the section `[camera_order]`\n7) Move the camera order entry for each camera as well.\nA camera order section with a single camera in it looks like this\n    ```\n    [camera_order]\n    1 = asdfghjkl\n    ```\n8) After a reboot, the cameras should be connected to the correct instances\n\n## Running the manager\nTo run PrusaLink in the multi-instance mode run `prusalink-manager start`\nas root. There are other options allowing you to specify which user to run the\ninstances and web under. The default is UID = 1000\n\nHere's the help output of prusalink-manager\n\n```\nMulti-instance suite for PrusaLink\n\npositional arguments:\n  {start,stop,clean,rescan}\n                        Available commands\n    start               Start the instance managing daemon (needs root\n                        privileges)\n    stop                Stop any manager daemon running (needs root\n                        privileges)\n    clean               Danger! cleans all PrusaLink multi-instance\n                        configuration\n    rescan              Notify the daemon a printer has been connected\n\noptions:\n  -h, --help            show this help message and exit\n  -i, --info            include log messages up to the INFO level\n  -d, --debug           include log messages up to the INFO level\n  -u USERNAME, --username USERNAME\n                        Which users to use for running and storing everything\n  -p PREPEND_EXECUTABLES_WITH, --prepend-executables-with PREPEND_EXECUTABLES_WITH\n                        Environment variables and path to the executables\n                        directory\n```\n"
  },
  {
    "path": "README.md",
    "content": "# PrusaLink\n\nPrusaLink is a compatibility layer between 8-bit Prusa 3D printers\n(MK2.5, MK2.5S, MK3, MK3S and MK3S+) and PrusaConnect, which lets you\ncontrol and monitor your 3D printer from anywhere.\nGet more info at [connect.prusa3d.com](https://connect.prusa3d.com/)\n\nPrusaLink also provides a local web interface:\n[Prusa-Link-Web](https://github.com/prusa3d/Prusa-Link-Web)\n\n\n## Setup\nTo use PrusaLink please follow our\n[Setup Guide](https://help.prusa3d.com/guide/prusalink-and-prusa-connect-mk3-s-_221744)\n\n### Login\nIf you wish to log into the console environment and haven't changed the\ncredentials, you'll need these default ones:\n\n```\nusername: jo\npassword: raspberry\n```\n\n## Dev Setup\nIf using the Raspberry Pi pins, follow the guide above for the hardware\npreparation. Pins can be used even on regular (non-Zero) Pis\nthrough Dupont jumper cables. Just make sure those make proper contact\nwith the Einsy board. A connection over USB is also possible,\nmaking PrusaLink compatible with pretty much any Linux system,\nbut since the RPi has been used as a reference, please excuse the Debian\nspecific instructions.\n\nIf using the Pi, create your micro SD card the usual way,\na Lite image will do nicely.\nJust in case, here's a guide: https://www.youtube.com/watch?v=ntaXWS8Lk34\n\n### UART over GPIO pins\nOn some RPis, the main UART is handling Bluetooth, so the printer\ncommunication would get handled by a miniUART, which doesn't work for us.\nTo disable Bluetooth, add these lines into `config.txt` which is located in\nthe Pi's boot partition.\n```ini\n[all]\nenable_uart=1\ndtoverlay=disable-bt\n```\n\n### Installation\nPrusaLink needs libpcap headers installed to name its OS threads.\nGit and Pip are needed for installation, while pigpio is only needed if the\nRPi GPIO pins are to be used.\n\n```bash\nsudo apt install git python3-pip pigpio libcap-dev libmagic1 libturbojpeg0 libatlas-base-dev python3-numpy libffi-dev libopenblas0\n\n# If you are using different distro (e.g. Ubuntu), use libturbojpeg library\n# instead of libturbojpeg0\n\n# for the Raspberry Pi camera module support\n# pre-installed on the newer Raspberry Pi OS images post September 2022\nsudo apt install -y python3-libcamera --no-install-recommends\n\npip install PrusaLink\n\n# Or install straight from GitHub\npip install git+https://github.com/prusa3d/gcode-metadata.git\npip install git+https://github.com/prusa3d/Prusa-Connect-SDK-Printer.git\npip install git+https://github.com/prusa3d/Prusa-Link.git\n```\n\n## Config\nPrusaLink behavior can be altered using command arguments and configuration\nfiles. The default configuration path is `/etc/prusalink/prusalink.ini` and\ndoes not get created automatically. The configuration documentation can be\nfound under `prusa/link/data/prusalink.ini`. The executable argument\ndocumentation is provided in the standard help text screen shown after\nrunning `prusalink --help`\n\nThe `prusa_printer_settings.ini` file is created by the PrusaLink wizard,\nand can be downloaded from the PrusaConnect settings page once you\n register your printer.\n\n### Configuring PrusaLink on the SD card\nIf you need to manually configure PrusaLink on the SD created from our image,\nit now comes with an auto-copy script. Put your `prusalink.ini` or\n`prusa_printer_settings.ini` files into the boot portion of the SD,\n*That's the only one that shows up under Windows or Mac,*\nand they will get copied over to their default locations on the next boot.\n\n### Permission denied\nMake sure the user you're running PrusaLink under is a member of the group\n**dialout**. To add it, run\n\n```sudo usermod -a -G dialout <username>```\n\nthen log out and in with that user.\n\n### Access on port 80\nPrusaLink has a local web interface, to make it accessible\non the default port 80, either start it as root and configure the user to which\nit should de-elevate itself after the web server is up, or start it as a normal\nuser on port 8080 - or any other, then redirect the port 80 to the port\nPrusaLink is listening on using these commands.\n\n### Running behind a reverse-proxy\nIf you got a proxy that changes the URI path, add the\nX-Forwarded-Prefix header. PrusaLink will use it to construct the correct\nURLs for the web interface.\n\n```bash\n# use -i to specify the interface affected\niptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n```\nPrusaLink advertises itself on the local network. This makes it visible\nin PrusaSlicer under Physical Printers -> Browse. To advertise port 80,\nthe instance has to be able to ping itself. This can be done by setting up a\nsimilar redirect on the loopback interface\n```bash\niptables -t nat -I OUTPUT -p tcp -o <loopback device name> -d localhost --dport 80 -j REDIRECT --to-ports 8080\n```\n\n### Multi-instance\nIf you want to connect multiple printers to a single pi, have a look at\n[MULTIINSTANCE.md](MULTIINSTANCE.md)\n\n## Usage\nBy default, the executable starts the daemon process and exits.\nThe executable is called `prusalink` and can be used to control the daemon,\nif you want to run it in your terminal instead, use the `-f` option\nTo get the most recent help screen use `prusalink --help`, here's\nwhat it says in 0.7.0\n```\nusage: prusalink [-h] [-f] [-c <file>] [-p <FILE>] [-a <ADDRESS>] [-t <PORT>]\n                 [-I] [-s <PORT>] [-i] [-d] [-l MODULE_LOG_LEVEL] [--profile]\n                 [command]\n\nPrusaLink daemon.\n\npositional arguments:\n  command               daemon action (start|stop|restart|status) (default:\n                        start)\n\noptions:\n  -h, --help            show this help message and exit\n  -f, --foreground      run as script on foreground\n  -c <file>, --config <file>\n                        path to config file (default:\n                        /etc/prusalink/prusalink.ini)\n  -p <FILE>, --pidfile <FILE>\n                        path to pid file\n  -a <ADDRESS>, --address <ADDRESS>\n                        IP listening address (host or IP)\n  -t <PORT>, --tcp-port <PORT>\n                        TCP/IP listening port\n  -I, --link-info       /link-info debug page\n  -s <PORT>, --serial-port <PORT>\n                        Serial (printer's) port or 'auto'\n  -i, --info            more verbose logging level INFO is set\n  -d, --debug           DEBUG logging level is set\n  -l MODULE_LOG_LEVEL, --module-log-level MODULE_LOG_LEVEL\n                        sets the log level of any submodule(s). use\n                        <module_path>=<log_level>\n  --profile             Use cProfile for profiling application.\n```\n"
  },
  {
    "path": "config.custom.js",
    "content": "const webpackConfig = require(\"./webpack.config\");\n\nmodule.exports = (env, args) => {\n    const config = {\n        PRINTER_NAME: \"Original Prusa i3\",\n        PRINTER_TYPE: \"fdm\",\n\n        WITH_SETTINGS: true,\n        WITH_CONTROLS: true,\n        WITH_PROJECTS: true,\n        WITH_LOGS: true,\n        WITH_FONT: false,\n        WITH_PRINT_BUTTON: true,\n        WITH_V1_API: true,\n        WITH_CAMERAS: true,\n        WITH_DOWNLOAD_BUTTON: true,\n        WITH_TELEMETRY_NOZZLE_DIAMETER: true,\n        WITH_API_KEY_AUTH: false,\n        WITH_API_KEY_SETTING: true,\n        WITH_NAME_SORTING_ONLY: false,\n        WITH_SYSTEM_UPDATES: true,\n\n        WITH_SYSTEM_VERSION: true,\n        WITH_PRINTER_SETTINGS: true,\n        WITH_USER_SETTINGS: true,\n        WITH_SERIAL: true,\n        ...env,\n    };\n    return webpackConfig(config, args);\n}\n"
  },
  {
    "path": "docs/Makefile",
    "content": "#\n# Makefile\n# Martin Užák, 2021-01-14 14:55\n#\n\nUMLFILES = prusalink_states.txt wizard.txt\n\numl: $(UMLFILES)\n\tplantuml $(UMLFILES)\n\nclean:\n\trm -fv *png\n\n\n# vim:ft=make\n#\n"
  },
  {
    "path": "docs/prusalink_states.txt",
    "content": "@startuml\n\nSerial --> RPIenabled\nSerial: Serial Port exists\n\nRPIenabled --> IDPrinter\nRPIenabled: RPI Port is enabled / Device on serial port\n\nRPIenabled --> LCD\nLCD: Messages on LCD\n\nIDPrinter --> GoodFW\nIDPrinter: Identify allowed Prusa Printer\n\nGoodFW --> ReadSN\nGoodFW: Firwmare is up-to-date\n\nReadSN --> ValidSN\nReadSN: SN can be read\n\nValidSN --> PrinterOk\nValidSN: Obtained SN is valid\nPrinterOk: Printer detected right\n\nDevice --> Phy\nDevice: Ethernet or WiFi device exists\n\nPhy --> Lan\nPhy:  Eth|Wifi device connected\n\nLan --> Internet\nLan: Device has assigned IP\n\nLan --> Local\nLocal: Messages on printer Web\n\nInternet --> HTTP\nInternet: DNS is working.\nInternet: There is no problem communicating\nInternet: to other hosts in the internet.\n\nHTTP --> Token\nHTTP: HTTP traffic to Connect is OK, no 5XX statuses\n\nToken --> API\nToken: Token is set and valid\n\nAPI: There are no 4XX problems while communicating to Connect\n\nAPI --> Connect\nConnect: Messages on Connect (with printer token)\n\nnote \"Error output to: Connect, Printer Display and Printer Web\" as ErrorOutput\n\nstate Internet #white\nstate HTTP #white\nstate Token #white\n\nstate \"Printer OK\" as PrinterOk #lightgreen\nstate \"API OK\" as API #lightgreen\n\nstate \"Printer Web\" as Local #lightblue\nstate LCD #lightblue\nstate Connect #lightblue\n\n\n@enduml\n"
  },
  {
    "path": "docs/wizard.txt",
    "content": "@startuml\n\n\nstate \"Add Printer\" as Add\nstate \"PrusaLink Wizard\" as Wizard #lavender\nstate \"Use config\" as WConfig #lavender\nstate \"Network settings\" as WiFi #lavender\nstate Services #lavender\nstate Printer #lavender\nstate \"Name\" as PName #lavender\nstate \"Connect\" as PConnect #lavender\nstate Recapitulation #lavender\nstate \"Add Printer Form\" as AConnect\nstate \"PrusaLink Web\" as LBoard #grey\nstate \"Printer Overview\" as Overview\nstate \"PrusaLink Web\" as Link #gray\nstate Settings #gray\nstate Code #salmon\nstate \"Network settings\" as Network\nstate Overview\nstate Config #lightgreen\n\n[*] -> Connect\nConnect: Printers\nConnect -> Add\n\nAdd: Select Printer\nAdd -up-> Wizard: Go to Printer\nAdd -> Name\n\nName: Name and Location\nName: Team\nName -> Network\n\nNetwork: WiFi Settings\nNetwork: IP Settings\nNetwork: Link Username and Password\nNetwork: SSH\nNetwork: NTP server\nNetwork-> Code\n\n[*] -up-> Wizard\n\nWizard -up-> WiFi\nWizard -up-> WConfig\nWizard -> Printer\n\nWConfig: Use downloaded\nWConfig: prusa-printer-settings.ini\nWConfig --> LBoard\n\nWiFi: WiFi setting\nWiFi: IP Settings\nWiFi --> Wizard\n\nPrinter: Printer Detection\nPrinter: FW Check\nPrinter -> PName\n\nPName -> Services\nPName: Name and Location\n\nServices: Username and Password\nServices: SSH\nServices: NTP server\nServices -> PConnect\nServices -> Recapitulation\n\nPConnect: SN Check\nPConnect: Connect Registration\nPConnect -[dotted]-> Code\n\nCode -up[dotted]-> PConnect\nCode: NO UI FORM\nCode: Generate Code\nCode -> Overview\n\nPConnect -> Recapitulation\n\nRecapitulation -> LBoard\nRecapitulation --> AConnect\n\nAConnect: Name and Location\nAConnect: Team\nAConnect: Registration Code\nAConnect -> Overview\n\nOverview -up-> LBoard\nOverview: Detect Link on LAN\nOverview -> Config\n\nConfig: prusa-printer-settings.ini\nConfig: Download and add to medium\nConfig: Download and add to Wizard\nConfig -up-> WConfig\nConfig --> Settings\n\n[*] --> Link\n\nLink: Already configured\nLink -> Settings\n\nSettings: Additional (Re)registration\nSettings: Unregistration\nSettings -up[dotted]-> Code\nCode -[dotted]-> Settings\n\nlegend right\n    | Color | Type |\n    |<#lavender>| Wizard on PrusaLink |\n    |<#fefece>| Wizard on PrusaConnect |\n    |<#gray>| PrusaLink |\n    |<#salmon>| Hidden action on PrusaConnect |\n    |<#lightgreen>| File download |\nendlegend\n\n@enduml\n"
  },
  {
    "path": "image_builder/__init__.py",
    "content": ""
  },
  {
    "path": "image_builder/image_builder.py",
    "content": "\"\"\"Following a writeup from here:\nhttps://blog.grandtrunk.net/2023/03/raspberry-pi-4-emulation-with-qemu/\"\"\"\nimport argparse\nimport os\nimport re\nimport shlex\nimport subprocess\nimport threading\nfrom functools import partial\nfrom importlib.resources import files\nfrom os.path import join\nfrom time import sleep\nfrom urllib.request import urlretrieve\n\nKERNEL_URL_REGEX = re.compile(\n    r\".*/(?P<file_name>linux-image-(?P<version_name>(?P<version>\"\n    r\"\\d+\\.\\d+\\.\\d+-\\d+)-armmp-lpae)_\\d+\\.\\d+\\.\\d+-\\d+_armhf.deb)\")\n\n\nKERNEL_URL = (\"http://security.debian.org/debian-security/pool/updates/main/l/\"\n              \"linux/linux-image-6.1.0-21-armmp-lpae_6.1.90-1_armhf.deb\")\nmatch = KERNEL_URL_REGEX.match(KERNEL_URL)\n\nif match is None:\n    raise RuntimeError(\"Invalid kernel URL\") from None\n\nKERNEL_VERSION = match.group(\"version\")\nKERNEL_VERSION_NAME = match.group(\"version_name\")\nKERNEL_FILE_NAME = match.group(\"file_name\")\nINITRD_NAME = f\"initrd.img-{KERNEL_VERSION_NAME}\"\nVMLINUZ_NAME = f\"vmlinuz-{KERNEL_VERSION_NAME}\"\n\nIMAGE_URL = (\"https://downloads.raspberrypi.org/raspios_lite_armhf/images/\"\n             \"raspios_lite_armhf-2024-03-15/\"\n             \"2024-03-15-raspios-bookworm-armhf-lite.img.xz\")\n\nDATA_FILE = \"data.json\"\nCOMPRESSED_IMAGE_NAME = \"source_image.img.xz\"\nSOURCE_IMAGE_NAME = \"source_image.img\"\nSACRIFICIAL_IMAGE_NAME = \"sacrificial_image.img\"\nIMAGE_NAME = \"image.img\"\nSHRUNK_IMAGE_NAME = \"shrunk_image.img\"\nOUTPUT_IMAGE_PATTERN = \"prusalink{mode}{version}.img\"\nBOOTFS_MOUNT = \"image_bootfs\"\nROOTFS_MOUNT = \"image_rootfs\"\nKERNEL_NAME = \"kernel8.img\"\nDTB_NAME = \"bcm2710-rpi-3-b-plus.dtb\"\nEMULATOR_CONNECT_RETRIES = 200\nEMULATOR_SHUTDOWN_TIMEOUT = 20\n\nBUILDER_DATA_PATH = str(files(\"prusa.link\") / \"data\" / \"image_builder\")\n\nRPI_EMULATOR_COMMAND = (\n    \"qemu-system-aarch64 \"\n    \"-machine raspi3b \"\n    \"-cpu cortex-a72 \"\n    \"-m 1G \"\n    \"-smp 4 \"\n    \"-serial stdio \"\n    f\"-dtb {DTB_NAME} \"\n    f\"-kernel {KERNEL_NAME} \"\n    \"-drive file=./{image_name},format=raw,if=sd \"\n    \"-append \\\"rw dwc_otg.lpm_enable=0 root=/dev/mmcblk0p2 rootdelay=1\\\" \"\n    \"-netdev user,id=ulan,hostfwd=tcp::2222-:22 \"\n    \"-device usb-net,netdev=ulan \"\n)\n\nVIRT_EMULATOR_COMMAND = (\n    \"qemu-system-arm \"\n    \"-nographic \"\n    \"-machine virt \"\n    \"-cpu cortex-a7 \"\n    \"-m 2G \"\n    \"-smp 4 \"\n    \"-kernel {vmlinuz} \"\n    \"-initrd {initrd} \"\n    \"-drive file={image_name},format=raw,id=hd,if=none,media=disk \"\n    \"-device virtio-scsi-device -device scsi-hd,drive=hd \"\n    \"-append \\\"root=/dev/sda2 console=ttyAMA0,115200\\\" \"\n    \"-netdev user,id=net0,hostfwd=tcp::2222-:22 \"\n    \"-device virtio-net-device,netdev=net0 \"\n)\n\nSSH_COMMAND = \"sshpass -p raspberry ssh -o StrictHostKeyChecking=no \" \\\n              \"-o UserKnownHostsFile=/dev/null -q -p 2222 jo@127.0.0.1 \"\n\nDATA_DIRECTORY = \"imager_data\"\nOUTPUT_DIRECTORY = \"generated_images\"\n\n\ndef reporthook(chunk_number, chunk_size, total_size):\n    \"\"\"A hook for urlretrieve to report progress\"\"\"\n    percent = min(int(chunk_number * chunk_size * 100 / total_size), 100)\n    print(f\"\\rDownloaded {percent}%\", end=\"\")\n\n\ndef ensure_directory(directory):\n    \"\"\"If missing, makes directories, along the supplied path\"\"\"\n    if not os.path.exists(directory):\n        os.makedirs(directory)\n\n\ndef run_emulator(command):\n    \"\"\"Runs a given command as if it is an emulator with expected settings\"\"\"\n    emulator_thread = threading.Thread(target=run_command, args=(command,))\n    emulator_thread.start()\n    print(\"Waiting for the emulator to boot\")\n\n    success = False\n    for _ in range(EMULATOR_CONNECT_RETRIES):\n        try:\n            run_over_ssh(\"echo Connected to the emulator\")\n        except subprocess.CalledProcessError:\n            sleep(1)\n            continue\n        else:\n            success = True\n            break\n\n    if not success:\n        raise RuntimeError(\"The emulator did not boot in time\")\n    return emulator_thread\n\n\ndef retry(call, retries=3, sleep_time=1):\n    \"\"\"Retry a function call a number of times\"\"\"\n    if retries < 0:\n        raise ValueError(\"Number of retries must be higher or equal zero\")\n    repetitions = retries + 1\n    for i in range(repetitions):\n        try:\n            return call()\n        except Exception:  # pylint: disable=broad-except\n            if i == repetitions - 1:\n                raise\n            sleep(sleep_time)\n    return None\n\n\ndef run_command(command, check=True, retries=1):\n    \"\"\"Run command and print output\"\"\"\n    to_run = partial(subprocess.run, shlex.split(command), check=check)\n    retry(to_run, retries=retries)\n\n\ndef run_over_ssh(command, check=True, retries=1):\n    \"\"\"Runs a command over ssh, checks for errors and retries once\"\"\"\n    run_command(SSH_COMMAND + command, check=check, retries=retries)\n\n\ndef check_binary(binary_name):\n    \"\"\"Checks if a binary is installed\"\"\"\n    print(f\"Checking if {binary_name} is installed\")\n    try:\n        subprocess.run(shlex.split(f\"which {binary_name}\"), check=True)\n    except subprocess.CalledProcessError as err:\n        raise RuntimeError(f\"{binary_name} is not installed\") from err\n\n\ndef insert_from_file_before_line(to_file, from_file, search=None, index=None):\n    \"\"\"Inserts the contents of a file into another file before a given line\"\"\"\n    if search is None and index is None:\n        raise RuntimeError(\"Either search or index must be specified\")\n\n    with open(to_file, \"r\", encoding=\"utf-8\") as file:\n        lines = file.readlines()\n\n    with open(from_file, \"r\", encoding=\"utf-8\") as file:\n        lines_to_insert = file.readlines()\n\n    split_on = 0\n    if search is not None and index is None:\n        for i, line in enumerate(lines):\n            if line.strip() == search:\n                split_on = i\n                break\n    else:\n        split_on = index\n\n    result = lines[:split_on] + lines_to_insert + lines[split_on:]\n\n    with open(to_file, \"w\", encoding=\"utf-8\") as file:\n        file.writelines(result)\n\n\ndef mount_image(image_name, expand=False):\n    \"\"\"Mounts the image and returns the loop device part\"\"\"\n    print(f\"Creating loop device for {image_name}\")\n    losetup_result = subprocess.run(\n        shlex.split(f\"sudo losetup --partscan --find --show {image_name}\"),\n        check=True,\n        capture_output=True)\n    loop_device = losetup_result.stdout.decode(\"utf-8\").strip()\n\n    if expand:\n        print(f\"Resizing {image_name}\")\n        run_command(f\"parted {loop_device} resizepart 2 100%\")\n        run_command(f\"e2fsck -f {loop_device}p2\")\n        run_command(f\"resize2fs {loop_device}p2\")\n\n    ensure_directory(BOOTFS_MOUNT)\n    ensure_directory(ROOTFS_MOUNT)\n\n    print(f\"Mounting {image_name}\")\n    run_command(f\"mount {loop_device}p1 {BOOTFS_MOUNT}\")\n    run_command(f\"mount {loop_device}p2 {ROOTFS_MOUNT}\")\n    return loop_device\n\n\ndef unmount_image(loop_device):\n    \"\"\"Unmounts the image and removes the loop device\"\"\"\n    print(\"Unmounting image\")\n    retry(partial(run_command, f\"umount {BOOTFS_MOUNT}\"))\n    retry(partial(run_command, f\"umount {ROOTFS_MOUNT}\"))\n\n    print(f\"Removing loop device {loop_device}\")\n    retry(partial(run_command, f\"losetup -d {loop_device}\"))\n\n\ndef basic_image_setup():\n    \"\"\"Sets up the image with ssh and a user jo with password raspberry\"\"\"\n    print(\"Write userconf.txt\")\n    userconf_path = join(BOOTFS_MOUNT, \"userconf.txt\")\n    with open(userconf_path, \"w\", encoding=\"utf-8\") as userconf:\n        userconf.write(\n            \"jo:$6$Jy4tV1H40VvfLZcX$hh/728SqdBocM2FTZ3fJh9Fx1u2FIJD/\"\n            \"8U075tyNewDDVEDS3e9.Miz213qujfnJ967Zs.43VRRhC4d/FDuKn0\")\n\n    print(\"Enable SSH\")\n    ssh_file_path = join(BOOTFS_MOUNT, \"ssh\")\n    with open(ssh_file_path, \"w\", encoding=\"utf-8\") as _:\n        ...\n\n\n# pylint: disable=too-many-locals, too-many-statements\ndef build_image():\n    \"\"\"Builds the requested image\"\"\"\n    ensure_directory(DATA_DIRECTORY)\n    ensure_directory(OUTPUT_DIRECTORY)\n    os.chdir(DATA_DIRECTORY)\n\n    if os.getuid() != 0:\n        raise RuntimeError(\"This script must be run as root\")\n    check_binary(\"qemu-system-aarch64\")\n    check_binary(\"sshpass\")\n    check_binary(\"ssh\")\n    check_binary(\"wget\")\n    check_binary(\"parted\")\n\n    parser = argparse.ArgumentParser(\n        description=\"PrusaLink RPi image generator\")\n\n    parser.add_argument(\"-d\", \"--dev\",\n                        action=\"store_true\",\n                        help=\"Build the image from master (for development)\")\n\n    parser.add_argument(\"-r\", \"--refresh\",\n                        action=\"store_true\",\n                        help=\"Re-do everything from scratch\")\n\n    parser.add_argument(\"-m\", \"--multi-instance\",\n                        action=\"store_true\",\n                        help=\"Build the multi-instance image\")\n\n    parser.add_argument(\"-b\", \"--branch-or-hash\",\n                        help=\"Specify a commit branch name or a hash of \"\n                             \"PrusaLink to get\")\n\n    args = parser.parse_args()\n\n    try:\n        check_binary(\"pishrink.sh\")\n    except Exception:  # pylint: disable=broad-except\n        print(\"pishrink is not installed, downloading\")\n        run_command(\"wget https://raw.githubusercontent.com/\"\n                    \"Drewsif/PiShrink/master/pishrink.sh\")\n        run_command(\"chmod +x pishrink.sh\")\n\n    # --- Get source image ---\n    if not os.path.exists(SOURCE_IMAGE_NAME) or args.refresh:\n        print(\"Cleaning up old image files\")\n        run_command(f\"rm {COMPRESSED_IMAGE_NAME}\", check=False)\n        run_command(f\"rm {SOURCE_IMAGE_NAME}\", check=False)\n        run_command(f\"rm {IMAGE_NAME}\", check=False)\n\n        print(f\"Downloading {IMAGE_URL}\")\n        urlretrieve(IMAGE_URL, COMPRESSED_IMAGE_NAME, reporthook=reporthook)\n        print(\"\")\n\n        print(\"Decompressing image\")\n        run_command(f\"xz --decompress -T0 {COMPRESSED_IMAGE_NAME}\")\n\n        print(\"Resize to 4GB\")\n        run_command(f\"qemu-img resize -f raw {SOURCE_IMAGE_NAME} 4G\")\n\n    # --- Get kernel ---\n    regenerate_initramfs = False\n    if args.refresh:\n        regenerate_initramfs = True\n    if not os.path.exists(KERNEL_VERSION_NAME):\n        regenerate_initramfs = True\n    if not os.path.exists(INITRD_NAME):\n        regenerate_initramfs = True\n    if not os.path.exists(VMLINUZ_NAME):\n        regenerate_initramfs = True\n\n    if regenerate_initramfs:\n        print(\"Cleaning up old kernel files\")\n        run_command(\"rm linux-image-*\", check=False)\n        run_command(\"rm initrd.img-*\", check=False)\n        run_command(\"rm vmlinuz-*\", check=False)\n\n        print(f\"Downloading {KERNEL_URL}\")\n        urlretrieve(KERNEL_URL, KERNEL_FILE_NAME, reporthook=reporthook)\n        print(\"\")\n\n        print(\"Copying sacrificial image\")\n        run_command(f\"cp {SOURCE_IMAGE_NAME} {SACRIFICIAL_IMAGE_NAME}\")\n\n        sacrificial_loop = mount_image(SACRIFICIAL_IMAGE_NAME, expand=True)\n\n        print(\"Copying the kernel package into the image\")\n        run_command(f\"cp {KERNEL_FILE_NAME} {ROOTFS_MOUNT}/.\")\n\n        print(\"Extracting kernel and dtb files\")\n        run_command(f\"cp {BOOTFS_MOUNT}/{KERNEL_NAME} .\")\n        run_command(f\"cp {BOOTFS_MOUNT}/{DTB_NAME} .\")\n\n        basic_image_setup()\n\n        print(\"Unmounting sacrificial image\")\n        unmount_image(sacrificial_loop)\n\n        emulator_command = RPI_EMULATOR_COMMAND.format(\n            image_name=SACRIFICIAL_IMAGE_NAME)\n\n        print(\"Run the initrd generating emulator\")\n        emulator_thread = run_emulator(emulator_command)\n        print(\"Generating vmlinuz and initrd\")\n        run_over_ssh(f\"sudo dpkg -i /{KERNEL_FILE_NAME}\")\n        run_over_ssh(\"sudo poweroff\", check=False)\n        print(\"Waiting for the initrd generating emulator to shut down\")\n        emulator_thread.join()\n\n        print(\"Copying the generated vmlinuz and initrd\")\n        initrd_loop = mount_image(SACRIFICIAL_IMAGE_NAME, expand=False)\n\n        run_command(f\"cp {ROOTFS_MOUNT}/boot/{VMLINUZ_NAME} .\")\n        run_command(f\"cp {ROOTFS_MOUNT}/boot/{INITRD_NAME} .\")\n        run_command(f\"cp -r {ROOTFS_MOUNT}/lib/modules/\"\n                    f\"{KERNEL_VERSION_NAME} .\")\n\n        unmount_image(initrd_loop)\n\n        print(\"Cleaning up\")\n        run_command(f\"rm {SACRIFICIAL_IMAGE_NAME}\")\n        run_command(f\"rm {KERNEL_NAME}\")\n        run_command(f\"rm {DTB_NAME}\")\n\n    print(\"Copying source image\")\n    run_command(f\"cp {SOURCE_IMAGE_NAME} {IMAGE_NAME}\")\n\n    raw_loop = mount_image(IMAGE_NAME, expand=True)\n\n    basic_image_setup()\n\n    print(\"Write boot-message.service\")\n    message_service_path = join(\n        ROOTFS_MOUNT, \"etc/systemd/system/boot-message.service\")\n    boot_message_path = join(BUILDER_DATA_PATH, \"boot-message.service\")\n    run_command(f\"cp {boot_message_path} {message_service_path}\")\n\n    print(\"Write additional temporary modules\")\n    run_command(f\"cp -r {KERNEL_VERSION_NAME} \"\n                f\"{ROOTFS_MOUNT}/lib/modules/{KERNEL_VERSION_NAME}\")\n\n    config_txt_path = join(BOOTFS_MOUNT, \"config.txt\")\n    with open(config_txt_path, \"a\", encoding=\"utf-8\") as config_txt:\n        config_txt.write(\"dtoverlay=disable-bt\\n\")\n\n    unmount_image(raw_loop)\n\n    print(\"Run the emulator\")\n    emulator_command = VIRT_EMULATOR_COMMAND.format(\n        image_name=IMAGE_NAME,\n        vmlinuz=VMLINUZ_NAME,\n        initrd=INITRD_NAME)\n    emulator_thread = run_emulator(emulator_command)\n\n    print(\"Enabling boot-message.service\")\n    run_over_ssh(\"sudo systemctl enable boot-message.service\")\n\n    print(\"Disabling bluetooth service\")\n    run_over_ssh(\"sudo systemctl disable hciuart.service\")\n\n    print(\"Disabling console over serial\")\n    run_over_ssh(\"sudo raspi-config nonint do_serial_hw 0\")\n    run_over_ssh(\"sudo raspi-config nonint do_serial_cons 1\")\n\n    print(\"Changing hostname to prusalink\")\n    run_over_ssh(\"sudo raspi-config nonint do_hostname prusalink\")\n\n    print(\"Waiting for NTP to sync, TODO: make this smarter\")\n    sleep(20)\n\n    print(\"Updating system\")\n    run_over_ssh(\"sudo apt-get update -y\")\n    run_over_ssh(\"sudo apt-get upgrade -y\")\n\n    print(\"Installing dependencies\")\n    # I guess we need this for the wi-fi setting to get applied normally\n    run_over_ssh(\"sudo apt-get install -y uuid\")\n    run_over_ssh(\"sudo apt-get install -y git python3-pip pigpio libcap-dev \"\n                 \"libmagic1 libturbojpeg0 libffi-dev python3-numpy \"\n                 \"cmake iptables python3-libcamera\")\n\n    print(\"Installing PrusaLink\")\n    # Caution: not tied to requirements-pi.txt\n    run_over_ssh(\"pip install --break-system-packages wiringpi\")\n    if args.multi_instance:\n        run_over_ssh(\"pip install --break-system-packages ipcqueue\")\n    if args.dev or args.branch_or_hash is not None:\n        hash_part = \"\"\n        if args.branch_or_hash is not None:\n            hash_part = f\"@{args.branch_or_hash}\"\n        run_over_ssh(\"pip install --break-system-packages git+https://\"\n                     \"github.com/prusa3d/gcode-metadata.git\")\n        run_over_ssh(\"pip install --break-system-packages git+https://\"\n                     \"github.com/prusa3d/Prusa-Connect-SDK-Printer.git\")\n        run_over_ssh(\"pip install --break-system-packages git+https://\"\n                     f\"github.com/prusa3d/Prusa-Link.git{hash_part}\")\n    else:\n        run_over_ssh(\"pip install --break-system-packages prusalink\")\n\n    output = subprocess.run(\n        shlex.split(SSH_COMMAND + \".local/bin/prusalink --version\"),\n        capture_output=True, check=False)\n    version_text = output.stdout.decode(\"utf-8\").split(\"\\n\")[0]\n    prusalink_version = version_text.split(\": \")[1]\n\n    print(\"Removing traces of the installation\")\n    run_over_ssh(\"sudo systemctl disable ssh\")\n    run_over_ssh(\"sudo logrotate -f /etc/logrotate.conf\")\n    run_over_ssh(\"sudo rm /var/log/*.1\", check=False)\n    run_over_ssh(\"sudo rm /var/log/*.gz\", check=False)\n    run_over_ssh(\"sudo cat /dev/null | sudo tee /var/log/lastlog\")\n    run_over_ssh(\"rm ~/.bash_history\", check=False)\n\n    print(\"Shutting down the emulator\")\n    run_over_ssh(\"sudo poweroff\", check=False)\n\n    emulator_thread.join(timeout=EMULATOR_SHUTDOWN_TIMEOUT)\n\n    print(\"Shrinking image\")\n    run_command(f\"pishrink.sh -p {IMAGE_NAME} {SHRUNK_IMAGE_NAME} \")\n\n    shrunk_loop = mount_image(SHRUNK_IMAGE_NAME)\n\n    print(\"Adding the first boot script\")\n    rc_local_path = join(ROOTFS_MOUNT, \"etc/rc.local\")\n    insert_from_file_before_line(\n        to_file=rc_local_path,\n        from_file=join(BUILDER_DATA_PATH, \"first-boot.sh\"),\n        index=1)\n\n    print(\"Adding the start script\")\n    if args.multi_instance:\n        rc_local_bak_path = join(ROOTFS_MOUNT, \"etc/rc.local.bak\")\n        insert_from_file_before_line(\n            to_file=rc_local_bak_path,\n            from_file=join(BUILDER_DATA_PATH, \"manager-start-script.sh\"),\n            search=\"exit 0\")\n    else:\n        rc_local_bak_path = join(ROOTFS_MOUNT, \"etc/rc.local.bak\")\n        insert_from_file_before_line(\n            to_file=rc_local_bak_path,\n            from_file=join(BUILDER_DATA_PATH, \"prusalink-start-script.sh\"),\n            search=\"exit 0\")\n\n    print(\"Removing modules needed for virtio\")\n    run_command(f\"rm -r {ROOTFS_MOUNT}/lib/modules/\"\n                f\"{KERNEL_VERSION_NAME}\")\n\n    run_command(f\"rm -r {ROOTFS_MOUNT}/var/cache/*\", check=False)\n    run_command(f\"rm -r {ROOTFS_MOUNT}/home/jo/.cache/*\", check=False)\n\n    unmount_image(shrunk_loop)\n\n    output_image_name = OUTPUT_IMAGE_PATTERN.format(\n        mode=\"-multi-instance\" if args.multi_instance else \"\",\n        version=f\"-{prusalink_version}\")\n\n    run_command(f\"mv {SHRUNK_IMAGE_NAME} {output_image_name}\")\n\n    print(\"Removing old compressed image\")\n    run_command(f\"rm {output_image_name}.xz\", check=False)\n\n    print(\"Compressing image\")\n    run_command(f\"xz --compress --keep -6 -T0 {output_image_name}\")\n\n    print(\"Cleaning up\")\n    run_command(f\"rm {IMAGE_NAME}\")\n\n    run_command(f\"mv {output_image_name}.xz ../{OUTPUT_DIRECTORY}/\")\n    run_command(f\"mv {output_image_name} ../{OUTPUT_DIRECTORY}/\")\n\n    os.chdir(\"..\")\n\n    print(\"Done\")\n\n\ndef main():\n    \"\"\"Main function, if the build fails, tries to kill the emulator\"\"\"\n    try:\n        build_image()\n    except Exception:  # pylint: disable=broad-except\n        run_command(\"killall qemu-system-aarch64\", check=False)\n        run_command(\"killall qemu-system-arm\", check=False)\n        raise\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "prusa/link/__init__.py",
    "content": "\"\"\"Original PrusaLink printer adapter.\n\n    Copyright (C) 2024 PrusaResearch\n\"\"\"\n__application__ = \"PrusaLink\"\n__vendor__ = \"Prusa Research\"\n\n__version__ = \"0.8.2\"\n__date__ = \"18 Dec 2024\"\n__copyright__ = \"(c) 2024 Prusa 3D\"\n__author_name__ = \"PrusaLink Developers\"\n__author_email__ = \"link@prusa3d.cz\"\n__author__ = f\"{__author_name__} <{__author_email__}>\"\n__description__ = f\"{__application__} for MK3 host software\"\n\n__credits__ = \"Tomáš Jozífek, Ondřej Tůma, Michal Zoubek\"\n__url__ = \"https://github.com/prusa3d/Prusa-Link\"\n"
  },
  {
    "path": "prusa/link/__main__.py",
    "content": "\"\"\"main() command line function.\"\"\"\n\nimport logging\nimport sys\nimport threading\nfrom argparse import ArgumentParser, ArgumentTypeError\nfrom cProfile import Profile\nfrom grp import getgrnam\nfrom os import chmod, geteuid, kill, mkdir, path\nfrom pwd import getpwnam\nfrom signal import SIGKILL, SIGTERM\nfrom time import sleep\n\nfrom daemon import DaemonContext  # type: ignore\nfrom lockfile.pidlockfile import PIDLockFile  # type: ignore\nfrom prusa.connect.printer import __version__ as sdk_version\n\nfrom . import __version__ as link_version\nfrom .config import Config\nfrom .const import EXIT_TIMEOUT\nfrom .interesting_logger import InterestingLogger, InterestingLogRotator\nfrom .printer_adapter.updatable import Thread\n\n# pylint: disable=wrong-import-position, wrong-import-order\n# Pop this singleton into existence before importing prusalink\nInterestingLogRotator()\nlogging.setLoggerClass(InterestingLogger)\n\nfrom .daemon import Daemon  # noqa: E402\n\nlog = logging.getLogger(__name__)\n\n# pylint: disable=too-many-return-statements\n# pylint: disable=too-many-statements\nCONFIG_FILE = '/etc/prusalink/prusalink.ini'\n\n\ndef excepthook(exception_arguments, args, argv):\n    \"\"\"If running as a daemon, restarts the app on unhandled exceptions\"\"\"\n    assert exception_arguments is not None\n    InterestingLogRotator.trigger(\"exception in a thread\")\n    log.exception(\"Caught an exception at top level!\")\n    if args is None:\n        log.fatal(\"Exception during startup, cannot restart\")\n    if args.foreground:\n        log.fatal(\"This instance is now broken. Will not restart \"\n                  \"because we're running in the foreground mode\")\n    else:\n        log.warning(\"Caught unhandled exception, restarting PrusaLink\")\n        Daemon.restart(argv)\n    # excepthook has the global exception set, besides even if we failed\n    # here, it will literally affect nothing\n    # pylint: disable=misplaced-bare-raise\n    # ruff: noqa: PLE0704\n    raise\n\n\ndef set_log_levels(config: Config):\n    \"\"\"Set log level for each defined module.\"\"\"\n    for module, level in config.log_settings.items():\n        logging.getLogger(module).setLevel(level)\n\n\nclass LogLevel(str):\n    \"\"\"Log level type with __call__ checker method.\"\"\"\n\n    def __new__(cls, level):\n        if len(level.split(\"=\")) != 2:\n            raise ArgumentTypeError(\"log level needs to be specified in format\"\n                                    \"<module_path>=<log_level>\")\n        return super().__new__(cls, level)\n\n\ndef check_process(pid):\n    \"\"\"Check if process with pid is alive.\"\"\"\n    try:\n        kill(pid, 0)\n        return True\n    except OSError:\n        return False\n\n\ndef wait_process(pid, timeout=1):\n    \"\"\"Wait for process with timeout. Return True if process was terminated.\"\"\"\n    sleep_amount = 0.1\n    for _ in range(int(timeout / sleep_amount)):\n        if not check_process(pid):\n            return True\n        sleep(sleep_amount)\n    return False\n\n\ndef stop(pid):\n    \"\"\"Tries to stop PrusaLink nicely, if it times out, uses SIGKILL\"\"\"\n    kill(pid, SIGTERM)\n    if wait_process(pid, EXIT_TIMEOUT):\n        return\n\n    log.warning(\"Failed to stop - SIGKIL will be used!\")\n    try:\n        kill(pid, SIGKILL)\n    except ProcessLookupError:\n        log.warning(\"Could not find a process with pid %s to kill\", pid)\n    wait_process(pid, EXIT_TIMEOUT)\n\n\ndef main():\n    \"\"\"Standard main function.\"\"\"\n    # pylint: disable=too-many-branches\n    parser = ArgumentParser(prog=\"prusalink\", description=\"PrusaLink daemon.\")\n    parser.add_argument(\n        \"command\",\n        nargs='?',\n        default=\"start\",\n        type=str,\n        help=\"daemon action (start|stop|restart|status) (default: start)\")\n    parser.add_argument(\"-f\",\n                        \"--foreground\",\n                        action=\"store_true\",\n                        help=\"run as script on foreground\")\n    parser.add_argument(\"-c\",\n                        \"--config\",\n                        default=CONFIG_FILE,\n                        type=str,\n                        help=f\"path to config file (default: {CONFIG_FILE})\",\n                        metavar=\"<file>\")\n    parser.add_argument(\"-p\",\n                        \"--pidfile\",\n                        type=str,\n                        help=\"path to pid file\",\n                        metavar=\"<FILE>\")\n    parser.add_argument(\"-a\",\n                        \"--address\",\n                        type=str,\n                        help=\"IP listening address (host or IP)\",\n                        metavar=\"<ADDRESS>\")\n    parser.add_argument(\"-t\",\n                        \"--tcp-port\",\n                        type=int,\n                        help=\"TCP/IP listening port\",\n                        metavar=\"<PORT>\")\n    parser.add_argument(\"-I\",\n                        \"--link-info\",\n                        action=\"store_true\",\n                        help=\"/link-info debug page\")\n    parser.add_argument(\"-s\",\n                        \"--serial-port\",\n                        type=str,\n                        help=\"Serial (printer's) port or 'auto'\",\n                        metavar=\"<PORT>\")\n    parser.add_argument(\"-n\",\n                        \"--printer-number\",\n                        type=int,\n                        help=\"Multi-instance printer number to show in wizard\")\n    parser.add_argument(\"-i\",\n                        \"--info\",\n                        action=\"store_true\",\n                        help=\"more verbose logging level INFO is set\")\n    parser.add_argument(\"-d\",\n                        \"--debug\",\n                        action=\"store_true\",\n                        help=\"DEBUG logging level is set\")\n    parser.add_argument(\"-l\",\n                        \"--module-log-level\",\n                        action=\"append\",\n                        help=\"sets the log level of any submodule(s). \"\n                        \"use <module_path>=<log_level>\",\n                        type=LogLevel)\n    parser.add_argument(\"--profile\",\n                        action=\"store_true\",\n                        help=\"Use cProfile for profiling application.\")\n    parser.add_argument(\"--version\",\n                        action=\"store_true\",\n                        help=\"Print out version info and exit\")\n\n    argv = list(arg for arg in sys.argv[1:] if arg not in ('start', 'restart'))\n    args = parser.parse_args()\n\n    if args.version:\n        print(\"PrusaLink version:\", link_version)\n        print(\"PrusaConnect-SDK version:\", sdk_version)\n        return 0\n\n    profile = None\n    if args.profile:\n        profile = Profile()\n        profile.enable()\n        Thread.enable_profiling()\n\n    # Restart on thread exceptions\n    threading.excepthook = lambda exc_args: excepthook(exc_args, args, argv)\n\n    try:\n        config = Config(args)\n\n        set_log_levels(config)\n\n        pid_file = PIDLockFile(config.daemon.pid_file)\n        pid = pid_file.read_pid() if pid_file.is_locked() else None\n\n        if args.command == \"stop\":\n            if pid and check_process(pid):\n                print(\"Stopping service with pid\", pid)\n                stop(pid)\n            else:\n                print(\"Service not running\")\n            return 0\n\n        if args.command == \"status\":\n            if pid and check_process(pid):\n                print(\"Service running with pid\", pid)\n                return 0\n            print(\"Service not running\")\n            return 1\n\n        if args.command == \"restart\":\n            if pid and check_process(pid):\n                print(\"Restarting service with pid\", pid)\n                stop(pid)\n\n        elif args.command == \"start\":\n            pass\n        elif not args.foreground:\n            parser.error(\"Unknown command %s\")\n            return 1\n\n        daemon = Daemon(config, argv)\n        if args.foreground:\n            log.info(\"Starting service on foreground.\")\n            return daemon.run(False)\n\n        if pid:\n            if not check_process(pid):\n                pid_file.break_lock()\n            else:\n                print(\"Service is already running\")\n                return 1\n\n        files_preserve = []\n        for handler in logging.root.handlers:\n            if hasattr(handler, \"socket\"):\n                files_preserve.append(handler.socket.fileno())\n        context = DaemonContext(pidfile=pid_file,\n                                files_preserve=files_preserve,\n                                signal_map={SIGTERM: daemon.sigterm})\n\n        pid_dir = path.dirname(config.daemon.pid_file)\n        if pid_dir == '/var/run/prusalink' and not path.exists(pid_dir):\n            mkdir(pid_dir)\n            chmod(pid_dir, 0o777)\n\n        if geteuid() == 0:\n            context.initgroups = True  # need only for RPi, don't know why\n            context.uid = getpwnam(config.daemon.user).pw_uid\n            context.gid = getgrnam(config.daemon.group).gr_gid\n\n        with context:\n            log.info(\"Starting service with pid %d\", pid_file.read_pid())\n            retval = daemon.run()\n            log.info(\"Shutdown\")\n            return retval\n\n    except Exception as exc:  # pylint: disable=broad-except\n        log.info(\"%s\", args)\n        log.exception(\"Unhandled exception reached the top level\")\n        parser.error(f\"{exc}\")\n        return 1\n\n    finally:\n        if profile:\n            profile.disable()\n            profile.dump_stats(\"prusalink-__main__.profile\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "prusa/link/camera_governor.py",
    "content": "\"\"\"Implements a simple loop for getting cameras unstuck\nand for auto adding them\"\"\"\nimport logging\nfrom functools import partial\nfrom threading import Event, Thread\nfrom typing import Optional\n\nfrom prusa.connect.printer.camera_configurator import CameraConfigurator\nfrom prusa.connect.printer.camera_controller import CameraController\n\nfrom .const import CAMERA_SCAN_INTERVAL\nfrom .interesting_logger import InterestingLogRotator\nfrom .util import loop_until\n\nlog = logging.getLogger(\"my_camera_configurator\")\n\n\nclass CameraGovernor:\n    \"\"\"A module for continually refreshing and adding cameras\"\"\"\n\n    def __init__(self, camera_configurator: CameraConfigurator,\n                 camera_controller: CameraController) -> None:\n        self.camera_configurator = camera_configurator\n        self.camera_controller = camera_controller\n\n        self._governance_quit_event = Event()\n        self._governance_thread: Optional[Thread] = None\n\n    def _govern(self) -> None:\n        \"\"\"Monitors the cameras re-starts failed ones,\n        optionally scans for newly connected ones\"\"\"\n        log.debug(\"Running the camera governance routine\")\n        if self.camera_controller.disconnect_stuck_cameras():\n            InterestingLogRotator.trigger(\"a stuck camera\")\n        self.camera_configurator.load_cameras()\n\n    def start(self) -> None:\n        \"\"\"Starts the camera governing loop\"\"\"\n        self._governance_quit_event.clear()\n        target = partial(\n            loop_until,\n            loop_evt=self._governance_quit_event,\n            run_every_sec=lambda: CAMERA_SCAN_INTERVAL,\n            to_run=self._govern)\n\n        self._governance_thread = Thread(\n            target=target,\n            name=\"camera_governance\",\n            daemon=True,\n        )\n        self._governance_thread.start()\n\n    def stop(self) -> None:\n        \"\"\"Stops the auto-add loop\"\"\"\n        self._governance_quit_event.set()\n\n    def wait_stopped(self) -> None:\n        \"\"\"Waits util the component's thread stops\"\"\"\n        if self._governance_thread is None:\n            return\n        if self._governance_thread.is_alive():\n            self._governance_thread.join()\n"
  },
  {
    "path": "prusa/link/cameras/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/cameras/encoders.py",
    "content": "\"\"\"This file contains encoders for the camera drivers\nEspecially the hardware conversion needs a lot of prep work\"\"\"\n\nimport abc\nimport ctypes\nimport fcntl\nimport functools\nimport mmap\nimport os\nimport select\nfrom enum import Enum\nfrom math import sqrt\nfrom types import MappingProxyType\n\nimport numpy as np\nfrom turbojpeg import TJSAMP_422, TurboJPEG  # type: ignore\n\nfrom . import v4l2\n\njpeg = TurboJPEG()\n\n\ndef fopen(path, write=False):\n    \"\"\"Opens a specified video device file\"\"\"\n    return open(path, \"rb+\" if write else \"rb\", buffering=0, opener=opener)\n\n\ndef opener(path, flags):\n    \"\"\"Adds flags for the open function\"\"\"\n    return os.open(path, flags | os.O_NONBLOCK)\n\n\nclass Quality(Enum):\n    \"\"\"A simple enum that can be easily interpreted by encoders\"\"\"\n    VERY_LOW = \"Very low\"\n    LOW = \"Low\"\n    MEDIUM = \"Medium\"\n    HIGH = \"High\"\n    VERY_HIGH = \"Very high\"\n\n\nclass BufferDetails:\n    \"\"\"A structure to encapsulate buffer info needed for encoding\"\"\"\n\n    def __init__(self, file_descriptor, length, offset):\n        self.file_descriptor = file_descriptor\n        self.length = length\n        self.offset = offset\n        self.mmap = mmap.mmap(fileno=self.file_descriptor,\n                              length=self.length,\n                              offset=self.offset)\n\n    def __del__(self):\n        try:\n            self.mmap.close()\n        except AttributeError:\n            pass\n\n\ndef get_appropriate_encoder(resolution, pixel_format, use_mmap=False):\n    \"\"\"Returns the appropriate encoder based on stream parameters\"\"\"\n    max_resolution = max(resolution.width, resolution.height)\n    if pixel_format == v4l2.V4L2_PIX_FMT_MJPEG:\n        return PassthroughEncoder()\n    if not MJPEGEncoder.is_available():\n        return JPEGEncoder()\n    if max_resolution > MJPEGEncoder.WIDTH_LIMIT:\n        return JPEGEncoder()\n    encoder = MJPEGEncoder()\n    if use_mmap:\n        # Switch to a type that copies data instead of trying to use\n        # a foreign buffer\n        encoder.ingest_buffer_memory = v4l2.V4L2_MEMORY_MMAP\n    return encoder\n\n\nclass Encoder:\n    \"\"\"A base class for encoders\"\"\"\n\n    def __init__(self):\n        \"\"\"Set all parameters encoder needs after calling init\"\"\"\n        self.width = 0\n        self.height = 0\n        self.stride = 0\n        self.fps = 30\n        self._quality = Quality.HIGH\n\n        # Information about the buffer from which to read\n        self.source_details = None\n\n    def start(self):\n        \"\"\"Initializes the encoder\"\"\"\n\n    def stop(self):\n        \"\"\"Stops the encoder\"\"\"\n\n    @property\n    def quality(self):\n        \"\"\"Gets the quality\"\"\"\n        return self._quality\n\n    @quality.setter\n    def quality(self, quality=Quality.HIGH):\n        \"\"\"An entry point for other parameters dependant on quality\"\"\"\n        self._quality = quality\n\n    @abc.abstractmethod\n    def encode(self, bytes_used: int) -> bytes:\n        \"\"\"Encode here, return bytes\"\"\"\n\n\nclass MJPEGEncoder(Encoder):\n    \"\"\"Encoder using the MJPEG Encoder on the Raspberry Pi through V4L2\n\n    Glossary:\n    SOURCE means foreign object like a buffer we copy data from\n    INGEST means our own data structure with raw data (V4L2 name: Output)\n    CODED means the structure with compressed data (V4L2 name: Capture)\n    \"\"\"\n    WIDTH_LIMIT = 1920\n    DEVICE_PATH = \"/dev/video11\"\n\n    # These are suggested bitrates for 1080p30 in Mbps\n    BITRATE_TABLE = MappingProxyType({\n        Quality.VERY_LOW: 6,\n        Quality.LOW: 12,\n        Quality.MEDIUM: 18,\n        Quality.HIGH: 27,\n        Quality.VERY_HIGH: 45,\n    })\n\n    # Use only one buffer, so no indexes need to exist\n    BUFFER_INDEX = 0\n\n    @classmethod\n    @functools.cache\n    def is_available(cls):\n        \"\"\"Figures whether we can do hardware decode or not\"\"\"\n        if not os.path.exists(cls.DEVICE_PATH):\n            return False\n\n        with open(cls.DEVICE_PATH, 'rb+', buffering=0) as file_descriptor:\n            coded_format = v4l2.v4l2_format()\n            coded_format.type = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE\n            coded_format.fmt.pix_mp.pixelformat = v4l2.V4L2_PIX_FMT_MJPEG\n            if fcntl.ioctl(file_descriptor, v4l2.VIDIOC_S_FMT, coded_format):\n                return False\n\n            ingest_format = v4l2.v4l2_format()\n            ingest_format.type = v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE\n            ingest_format.fmt.pix_mp.pixelformat = v4l2.V4L2_PIX_FMT_YUYV\n            return not fcntl.ioctl(file_descriptor, v4l2.VIDIOC_S_FMT,\n                                   ingest_format)\n\n    def __init__(self):\n        \"\"\"Initialise V4L2 encoder\"\"\"\n        super().__init__()\n\n        self._bitrate = None  # set by setting quality\n        self.coded_buffer = None\n        self.coded_mmap = None\n        self.ingest_buffer = None\n        self.ingest_mmap = None\n\n        self.controls = []\n        self.file_object = None\n\n        # This is important, it tells us if we can use the buffer given to\n        # encode as is, or if we are to copy the data (MMAP = copy)\n        self.ingest_buffer_memory = v4l2.V4L2_MEMORY_DMABUF\n\n    def _pre_fill_format(self, format_type, pixel_format):\n        format_ = v4l2.v4l2_format()\n        format_.type = format_type\n        format_.fmt.pix_mp.width = self.width\n        format_.fmt.pix_mp.height = self.height\n        format_.fmt.pix_mp.pixelformat = pixel_format\n        format_.fmt.pix_mp.plane_fmt[0].bytesperline = self.stride\n        format_.fmt.pix_mp.field = v4l2.V4L2_FIELD_ANY\n        format_.fmt.pix_mp.colorspace = v4l2.V4L2_COLORSPACE_JPEG\n        format_.fmt.pix_mp.num_planes = 1\n        return format_\n\n    def _request_buffers(self, buffer_type, memory, count=1):\n        buffer_request = v4l2.v4l2_requestbuffers()\n        buffer_request.count = count\n        buffer_request.type = buffer_type\n        buffer_request.memory = memory\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_REQBUFS, buffer_request)\n\n    def _get_buffer(self, buffer_type, memory):\n        # This is a definition of a ctype array\n        plane_proto = v4l2.v4l2_plane * 1\n        buffer = v4l2.v4l2_buffer()\n        ctypes.memset(ctypes.byref(buffer), 0, ctypes.sizeof(buffer))\n        buffer.type = buffer_type\n        buffer.memory = memory\n        buffer.index = 0\n        buffer.length = 1\n        buffer.m.planes = plane_proto()\n        return buffer\n\n    def _stream_on(self, buffer_type):\n        typev = v4l2.v4l2_buf_type(buffer_type)\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_STREAMON, typev)\n\n    def _stream_off(self, buffer_type):\n        typev = v4l2.v4l2_buf_type(buffer_type)\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_STREAMOFF, typev)\n\n    def start(self):\n        # Removed framerate calculation, we don't do those\n        reference_complexity = 1920 * 1080\n        actual_complexity = self.width * self.height\n        reference_bitrate = self.BITRATE_TABLE[self.quality] * 1000000\n        self._bitrate = int(reference_bitrate *\n                            sqrt(actual_complexity / reference_complexity))\n\n        # pylint: disable=consider-using-with\n        self.file_object = open(self.DEVICE_PATH, 'rb+', buffering=0)\n\n        capability = v4l2.v4l2_capability()\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_QUERYCAP, capability)\n\n        control = v4l2.v4l2_control()\n        control.id = v4l2.V4L2_CID_MPEG_VIDEO_BITRATE\n        control.value = self._bitrate\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_S_CTRL, control)\n\n        ingest_format = self._pre_fill_format(\n            format_type=v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,\n            pixel_format=v4l2.V4L2_PIX_FMT_YUYV,\n        )\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_S_FMT, ingest_format)\n\n        coded_format = self._pre_fill_format(\n            format_type=v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,\n            pixel_format=v4l2.V4L2_PIX_FMT_MJPEG,\n        )\n        coded_format.fmt.pix_mp.plane_fmt[0].bytesperline = 0\n        coded_format.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_S_FMT, coded_format)\n\n        self._request_buffers(\n            buffer_type=v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,\n            memory=self.ingest_buffer_memory)\n\n        self._request_buffers(\n            buffer_type=v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,\n            memory=v4l2.V4L2_MEMORY_MMAP)\n\n        # Prepare the buffer for encoded data\n        # The raw data buffer will get re-used from libcamera in this case\n        self.coded_buffer = self._get_buffer(\n            v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,\n            v4l2.V4L2_MEMORY_MMAP,\n        )\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_QUERYBUF, self.coded_buffer)\n        plane = self.coded_buffer.m.planes[0]\n        self.coded_mmap = mmap.mmap(\n            fileno=self.file_object.fileno(),\n            length=plane.length,\n            offset=plane.m.mem_offset,\n            prot=mmap.PROT_READ | mmap.PROT_WRITE,\n            flags=mmap.MAP_SHARED,\n        )\n\n        self.ingest_buffer = self._get_buffer(\n            v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,\n            self.ingest_buffer_memory,\n        )\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_QUERYBUF, self.ingest_buffer)\n        if self.ingest_buffer_memory == v4l2.V4L2_MEMORY_MMAP:\n            plane = self.ingest_buffer.m.planes[0]\n            self.ingest_mmap = mmap.mmap(\n                fileno=self.file_object.fileno(),\n                length=plane.length,\n                offset=plane.m.mem_offset,\n                prot=mmap.PROT_READ | mmap.PROT_WRITE,\n                flags=mmap.MAP_SHARED,\n            )\n\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_QBUF, self.coded_buffer)\n\n        self._stream_on(v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)\n        self._stream_on(v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)\n\n    def stop(self):\n        \"\"\"Prepares the encoder for encoding\"\"\"\n        self._stream_off(v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)\n        self._stream_off(v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)\n\n        self._request_buffers(\n            buffer_type=v4l2.V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,\n            memory=self.ingest_buffer_memory,\n            count=0)\n\n        self.coded_mmap.close()\n        self.coded_mmap = None\n\n        if self.ingest_mmap is not None:\n            self.ingest_mmap.close()\n            self.ingest_mmap = None\n\n        self._request_buffers(\n            buffer_type=v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,\n            memory=v4l2.V4L2_MEMORY_MMAP,\n            count=0)\n\n        self.file_object.close()\n        self.ingest_buffer = None\n        self.coded_buffer = None\n\n    def encode(self, bytes_used):\n        \"\"\"Encodes a frame\"\"\"\n\n        if self.file_object is None or self.file_object.closed:\n            raise RuntimeError(\"Cannot encode with a stopped encoder\")\n\n        if self.ingest_buffer_memory == v4l2.V4L2_MEMORY_DMABUF:\n            ingest_plane = self.ingest_buffer.m.planes[0]\n            ingest_plane.m.fd = self.source_details.file_descriptor\n            ingest_plane.length = self.source_details.length\n            ingest_plane.bytesused = bytes_used\n\n        elif self.ingest_buffer_memory == v4l2.V4L2_MEMORY_MMAP:\n            self.ingest_mmap.write(self.source_details.mmap.read(bytes_used))\n            self.ingest_mmap.seek(self.ingest_buffer.m.planes[0].m.mem_offset)\n            self.source_details.mmap.seek(self.source_details.offset)\n\n        fcntl.ioctl(self.file_object, v4l2.VIDIOC_QBUF, self.ingest_buffer)\n\n        select.select((self.file_object, ), (), ())\n\n        if fcntl.ioctl(self.file_object, v4l2.VIDIOC_DQBUF,\n                       self.ingest_buffer):\n            raise RuntimeError(\n                \"Encoding failed - dequeueing the ingest buffer\")\n\n        if fcntl.ioctl(self.file_object, v4l2.VIDIOC_DQBUF, self.coded_buffer):\n            raise RuntimeError(\n                \"Encoding failed - de-queueing the coded buffer\")\n\n        output = self.coded_mmap.read(self.coded_buffer.m.planes[0].bytesused)\n        self.coded_mmap.seek(0)\n\n        if fcntl.ioctl(self.file_object, v4l2.VIDIOC_QBUF, self.coded_buffer):\n            raise RuntimeError(\n                \"Encoding failed - re-queueing the coded buffer\")\n\n        return output\n\n\nclass JPEGEncoder(Encoder):\n    \"\"\"Encoder using the TurboJPEG library (CPU encoding)\"\"\"\n    QUALITY_TABLE = MappingProxyType({\n        Quality.VERY_LOW: 25,\n        Quality.LOW: 50,\n        Quality.MEDIUM: 70,\n        Quality.HIGH: 85,\n        Quality.VERY_HIGH: 95,\n    })\n\n    def __init__(self):\n        super().__init__()\n        self.quality_percent = None\n\n    def start(self):\n        \"\"\"Prepares the encoder for encoding\"\"\"\n        self.quality_percent = self.QUALITY_TABLE[self.quality]\n\n    def encode(self, bytes_used):\n        \"\"\"Extracts Y, U and V, then puts them one after another instead of\n        interweaving\"\"\"\n        array_data = np.array(self.source_details.mmap, dtype=np.uint8)\n\n        size = bytes_used\n        yuv_array = np.empty((size, ), dtype=np.uint8)\n        yuv_array[:size // 2] = array_data[0::2]\n        yuv_array[size // 2:size // 4 * 3] = array_data[1::4]\n        yuv_array[size // 4 * 3:] = array_data[3::4]\n        return jpeg.encode_from_yuv(yuv_array,\n                                    self.height,\n                                    self.width,\n                                    quality=self.quality_percent,\n                                    jpeg_subsample=TJSAMP_422)\n\n\nclass PassthroughEncoder(Encoder):\n    \"\"\"An encoder, that just transforms the data from the format accepted by\n    encode to the format returned by encoders without touching the data\"\"\"\n\n    def encode(self, bytes_used: int) -> bytes:\n        \"\"\"Reads the source data and outputs as bytes\"\"\"\n        return self.source_details.mmap[:bytes_used]\n"
  },
  {
    "path": "prusa/link/cameras/picamera_driver.py",
    "content": "\"\"\"Contains implementation of a driver for Rpi Cameras\"\"\"\nimport gc\nimport logging\nimport select\nfrom time import time\nfrom types import MappingProxyType\nfrom typing import Any, Callable, Dict, Optional\n\nfrom prusa.connect.printer.camera import Resolution\nfrom prusa.connect.printer.camera_driver import CameraDriver\nfrom prusa.connect.printer.const import (\n    CAMERA_WAIT_TIMEOUT,\n    CapabilityType,\n    NotSupported,\n)\n\nfrom ..util import is_potato_cpu, prctl_name\nfrom . import v4l2\nfrom .encoders import BufferDetails, MJPEGEncoder, get_appropriate_encoder\n\nlog = logging.getLogger(__name__)\n\nPICAMERA_SUPPORTED = False\ntry:\n    from libcamera import (  # type: ignore\n        Camera,\n        CameraManager,\n        ControlId,\n        FrameBufferAllocator,\n        PixelFormat,\n        Rectangle,\n        Request,\n        Size,\n        Stream,\n        StreamConfiguration,\n        StreamFormats,\n        StreamRole,\n        controls,\n    )\nexcept ImportError:\n    CameraManager = Camera = StreamConfiguration = Stream = StreamFormats = \\\n        StreamRole = PixelFormat = Request = Size = FrameBufferAllocator = \\\n        controls = Rectangle = ControlId = None\nelse:\n    PICAMERA_SUPPORTED = True\n\n\nPICAMERA_MODELS = {\n    \"imx219\",\n    \"imx296_mono\",\n    \"imx477_v1\",\n    \"ov5647_noir\",\n    \"imx219_noir\",\n    \"imx378\",\n    \"imx519\",\n    \"ov9281_mono\",\n    \"imx290\",\n    \"imx477\",\n    \"se327m12\",\n    \"imx296\",\n    \"imx477_noir\",\n    \"ov5647\",\n    \"imx708\",\n    \"imx708_noir\",\n    \"imx708_wide\",\n    \"imx708_wide_noir\",\n}\n\nSUPPORTED_PIXEL_FORMAT = \"YUYV\"\n\n\ndef param_change(func):\n    \"\"\"Wraps any settings change with a stop and start of the video\n    stream, so the camera driver does not return it's busy\"\"\"\n\n    def inner(self, new_param):\n        # pylint: disable=protected-access\n        self.camera.stop()\n        self.encoder.stop()\n        func(self, new_param)\n        self._start()\n\n    return inner\n\n\nclass PiCameraDriver(CameraDriver):\n    \"\"\"A camera driver for RaspberryPi cameras\"\"\"\n\n    name = \"PiCamera\"\n    supported = PICAMERA_SUPPORTED\n    REQUIRES_SETTINGS: MappingProxyType[str, str] = MappingProxyType({})\n\n    @staticmethod\n    def _scan():\n        \"\"\"Scan for Pi Cameras\"\"\"\n        available = {}\n\n        camera_manager = CameraManager.singleton()\n        for camera in camera_manager.cameras:\n            model = \"unknown\"\n            for name, value in camera.properties.items():\n                if str(name) == \"Model\":\n                    model = value\n                    break\n            log.debug(\"picamera found model: %s\", model)\n            if model in PICAMERA_MODELS:\n                available[camera.id] = {\n                    \"id_string\": camera.id,\n                    \"name\": f\"RaspberryPi Camera: {model}\"}\n\n        return available\n\n    def __init__(self, camera_id: str, config: Dict[str, str],\n                 disconnected_cb: Callable[[\"CameraDriver\"], None]) -> None:\n        # pylint: disable=duplicate-code\n        super().__init__(camera_id, config, disconnected_cb)\n\n        self.camera_manager: CameraManager = CameraManager.singleton()\n        self.camera: Optional[Camera] = None\n        self.resolution: Optional[Resolution] = None\n        self.raw_resolution = None\n        self.stream: Optional[Stream] = None\n        self.request: Optional[Request] = None\n        self.allocator: Optional[FrameBufferAllocator] = None\n        self.frame_number = 0\n        self.scaler_crop = Rectangle(Size(3200, 2400))\n\n        self.encoder = None\n\n        self.controls_to_set: Dict[ControlId, Any] = {}\n\n    @staticmethod\n    def get_resolutions(camera: Camera, stream_role: StreamRole,\n                        wanted_pixel_format: Optional[str] = None):\n        \"\"\"Gets the formats and their resolutions for any given camera\"\"\"\n        resolutions = set()\n        camera_config = camera.generate_configuration(\n            [stream_role])\n        stream_config = camera_config.at(0)\n        stream_formats: StreamFormats = stream_config.formats\n\n        for pixel_format in stream_formats.pixel_formats:\n            if wanted_pixel_format is not None:\n                if str(pixel_format) != wanted_pixel_format:\n                    continue\n            for resolution in stream_formats.sizes(pixel_format):\n                # Ignore resolutions that would need more post-processing\n                # as a result of padding to 64 bytes. Docs say 32,\n                # but that does not seem to be right. 32 here, means 64 bytes.\n                # One for brightness and one for color, two per pixel\n                if stream_role != StreamRole.Raw:\n                    if resolution.width % 32:\n                        continue\n                    # Cannot HW encode these, and we don't have the CPU\n                    # for it either\n                    if is_potato_cpu() and \\\n                            resolution.width > MJPEGEncoder.WIDTH_LIMIT:\n                        continue\n                resolutions.add(Resolution(\n                    resolution.width, resolution.height))\n        return resolutions\n\n    @staticmethod\n    def make_camera_configuration(camera, still_resolution: Resolution,\n                                  raw_resolution: Resolution,\n                                  pixel_format: str):\n        \"\"\"Creates a camera configuration for our specific use case\n\n        Sets the raw sensor resolution, the scaled down output resolution\n        and the pixel format for a specified camera\n\n        The buffer counts are hardcoded, getting more of them would\n        incentivize the camera stack to pre-fill them which would mean\n        we'd get old data from the first couple of them\n        \"\"\"\n        camera_configuration = camera.generate_configuration(\n            [StreamRole.Raw, StreamRole.StillCapture])\n\n        raw_configuration: StreamConfiguration = camera_configuration.at(0)\n        raw_configuration.size = Size(raw_resolution.width,\n                                      raw_resolution.height)\n        raw_configuration.buffer_count = 0\n\n        still_configuration: StreamConfiguration = camera_configuration.at(1)\n        still_configuration.size = Size(still_resolution.width,\n                                        still_resolution.height)\n        still_configuration.pixel_format = PixelFormat(pixel_format)\n        still_configuration.buffer_count = 1\n\n        return camera_configuration\n\n    def _connect(self):\n        \"\"\"Connects to the picamera\"\"\"\n        for camera in self.camera_manager.cameras:\n            if camera.id == self.config[\"id_string\"]:\n                self.camera = camera\n                break\n        if self.camera is None:\n            raise RuntimeError(\"Couldn't find a configured pi camera\"\n                               f\" {self.config['name']} in the connected ones\")\n        self._capabilities = ({\n            CapabilityType.TRIGGER_SCHEME,\n            CapabilityType.IMAGING,\n            CapabilityType.RESOLUTION,\n        })\n\n        if controls.LensPosition in self.camera.controls:\n            self._capabilities.add(CapabilityType.FOCUS)\n            # Defaults to infinity\n            self._config[\"focus\"] = self._config.get(\"focus\", str(0.0))\n            self.set_focus(float(self._config[\"focus\"]))\n\n        sensor_resolutions = self.get_resolutions(\n            self.camera, StreamRole.Raw)\n        self._available_resolutions = self.get_resolutions(\n            self.camera, StreamRole.StillCapture, SUPPORTED_PIXEL_FORMAT)\n\n        if not self.available_resolutions or not sensor_resolutions:\n            raise NotSupported(\n                \"Sorry, PrusaLink PiCamera module supports only YUYV 4:2:2. \"\n                \"This camera does not support either that, or something else \"\n                \"is broken\")\n        self.raw_resolution = sorted(sensor_resolutions)[-1]\n\n        self.camera.acquire()\n        self.allocator = FrameBufferAllocator(self.camera)\n\n        initial_resolution = self._get_initial_resolution(\n            self._available_resolutions, self._config)\n        self._set_resolution(initial_resolution)\n        self._config[\"resolution\"] = str(initial_resolution)\n\n        self._start()\n\n    def _start(self):\n        \"\"\"A method to start the camera and the encoder after connecting\n        or parameter change\"\"\"\n        # set controls again\n        if controls.AfMode in self.camera.controls:\n            self.controls_to_set[controls.AfMode] = \\\n                controls.AfModeEnum.Manual\n        self.controls_to_set[controls.ScalerCrop] = self.scaler_crop\n\n        self.encoder.start()\n        self.camera.start()\n\n    @staticmethod\n    def _get_scalar_crop(raw_resolution, target_resolution):\n        \"\"\"Figures out how to crop the raw sensor to get the resulting scaled\n        image in the correct aspect ratio\"\"\"\n        raw_aspect_ratio = (raw_resolution.width /\n                            raw_resolution.height)\n        still_aspect_ratio = (target_resolution.width /\n                              target_resolution.height)\n        if raw_aspect_ratio > still_aspect_ratio:\n            width = int(raw_resolution.height * still_aspect_ratio)\n            width_offset = int((raw_resolution.width - width) / 2)\n\n            cropped_size = Size(width, raw_resolution.height)\n            scaler_crop = Rectangle(width_offset, 0, cropped_size)\n\n        elif raw_aspect_ratio < still_aspect_ratio:\n            height = int(raw_resolution.width / still_aspect_ratio)\n            height_offset = int((raw_resolution.height - height) / 2)\n\n            cropped_size = Size(raw_resolution.width, height)\n            scaler_crop = Rectangle(0, height_offset, cropped_size)\n        else:\n            cropped_size = Size(raw_resolution.width, raw_resolution.height)\n            scaler_crop = Rectangle(0, 0, cropped_size)\n        return scaler_crop\n\n    @param_change\n    def set_resolution(self, resolution):\n        \"\"\"Sets the camera resolution\"\"\"\n        self._set_resolution(resolution)\n\n    def _set_resolution(self, resolution):\n        \"\"\"A way to set the resolution without @param_change\"\"\"\n        self.allocator.buffers(self.stream).clear()\n        self.allocator = None\n        self.request = None\n        self.stream = None\n        gc.collect()\n\n        camera_configuration = self.make_camera_configuration(\n            self.camera, resolution, self.raw_resolution,\n            SUPPORTED_PIXEL_FORMAT)\n        camera_configuration.validate()\n\n        self.scaler_crop = self._get_scalar_crop(\n            raw_resolution=self.raw_resolution,\n            target_resolution=resolution)\n\n        self.camera.configure(camera_configuration)\n\n        self.stream = camera_configuration.at(1).stream\n\n        # A lot of this can fail, that would hopefully result in another\n        # attempt to connect. To see what result codes to expect and stuff,\n        # look at picamera2 on github, they do it the more proper way\n        gc.collect()\n        self.allocator = FrameBufferAllocator(self.camera)\n        self.allocator.allocate(self.stream)\n\n        buffer = self.allocator.buffers(self.stream)[0]\n        self.request = self.camera.create_request()\n        self.request.add_buffer(self.stream, buffer)\n\n        self.encoder = get_appropriate_encoder(\n            resolution, v4l2.v4l2_fourcc(*SUPPORTED_PIXEL_FORMAT))\n\n        plane = buffer.planes[0]\n        self.encoder.source_details = BufferDetails(\n            file_descriptor=plane.fd,\n            length=self.stream.configuration.frame_size,\n            offset=plane.offset)\n\n        self.encoder.width = resolution.width\n        self.encoder.height = resolution.height\n        self.encoder.stride = self.stream.configuration.stride\n\n    def _focus_transform(self, value):\n        \"\"\"Transforms the focus value from 0 - 1 to the range\n        supported by the camera\"\"\"\n        min_position = self.camera.controls[controls.LensPosition].min\n        max_position = self.camera.controls[controls.LensPosition].max\n        position_range = max_position - min_position\n        return value * position_range - min_position\n\n    def set_focus(self, focus):\n        \"\"\"Sets the camera resolution\"\"\"\n        self.controls_to_set[controls.LensPosition] = \\\n            self._focus_transform(focus)\n\n    def take_a_photo(self):\n        \"\"\"Asks for eight photos but is only interested in the last one\"\"\"\n        prctl_name()\n        log.debug(\"Taking a photo!\")\n\n        self.request.reuse()\n\n        for control_id, value in self.controls_to_set.items():\n            self.request.set_control(control_id, value)\n        self.controls_to_set.clear()\n\n        self.camera.queue_request(self.request)\n\n        started_at = time()\n        while True:\n            remaining = started_at + CAMERA_WAIT_TIMEOUT - time()\n            if self.request.status == Request.Status.Complete:\n                break\n            if remaining <= 0:\n                raise TimeoutError(\"Taking a photo timed out\")\n\n            # Cannot use returned events for breaking this loop because\n            # we would need to handle a negative time remaining as well\n            select.select((self.camera_manager.event_fd,), (), (), remaining)\n\n        log.debug(\"Converting a photo\")\n        data = self.encoder.encode(self.stream.configuration.frame_size)\n        log.debug(\"Done converting a photo\")\n        return data\n\n    def _disconnect(self):\n        \"\"\"Disconnects from the camera\"\"\"\n        if self.camera is None:\n            return\n        self.camera.stop()\n        self.camera.release()\n"
  },
  {
    "path": "prusa/link/cameras/v4l2.py",
    "content": "# Python bindings for the v4l2 userspace api\n\n# Copyright (C) 1999-2009 the contributors\n\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation; either version 2 of the License, or\n# (at your option) any later version.\n\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\n# Alternatively you can redistribute this file under the terms of the\n# BSD license as stated below:\n\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n# 1. Redistributions of source code must retain the above copyright\n#    notice, this list of conditions and the following disclaimer.\n# 2. Redistributions in binary form must reproduce the above copyright\n#    notice, this list of conditions and the following disclaimer in\n#    the documentation and/or other materials provided with the\n#    distribution.\n# 3. The names of its contributors may not be used to endorse or promote\n#    products derived from this software without specific prior written\n#    permission.\n\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\n# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"\nPython bindings for the v4l2 userspace api in Linux 2.6.34\n\"\"\"\n\n# see linux/videodev2.h\n\n# flake8: noqa\n\nimport ctypes\n\n\n_IOC_NRBITS = 8\n_IOC_TYPEBITS = 8\n_IOC_SIZEBITS = 14\n_IOC_DIRBITS = 2\n\n_IOC_NRSHIFT = 0\n_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS\n_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS\n_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS\n\n_IOC_NONE = 0\n_IOC_WRITE = 1\n_IOC_READ  = 2\n\n\ndef _IOC(dir_, type_, nr, size):\n    return (\n        ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value |\n        ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value |\n        ctypes.c_int32(nr << _IOC_NRSHIFT).value |\n        ctypes.c_int32(size << _IOC_SIZESHIFT).value)\n\n\ndef _IOC_TYPECHECK(t):\n    return ctypes.sizeof(t)\n\n\ndef _IO(type_, nr):\n    return _IOC(_IOC_NONE, type_, nr, 0)\n\n\ndef _IOW(type_, nr, size):\n    return _IOC(_IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))\n\n\ndef _IOR(type_, nr, size):\n    return _IOC(_IOC_READ, type_, nr, _IOC_TYPECHECK(size))\n\n\ndef _IOWR(type_, nr, size):\n    return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))\n\n\n#\n# type alias\n#\n\nenum = ctypes.c_uint\nc_int = ctypes.c_int\n\n\n#\n# time\n#\n\nclass timeval(ctypes.Structure):\n    _fields_ = [\n        ('secs', ctypes.c_long),\n        ('usecs', ctypes.c_long),\n    ]\n\n\n#\n# v4l2\n#\n\n\nVIDEO_MAX_FRAME = 32\nVIDEO_MAX_PLANES = 8\n\nVID_TYPE_CAPTURE = 1\nVID_TYPE_TUNER = 2\nVID_TYPE_TELETEXT = 4\nVID_TYPE_OVERLAY = 8\nVID_TYPE_CHROMAKEY = 16\nVID_TYPE_CLIPPING = 32\nVID_TYPE_FRAMERAM = 64\nVID_TYPE_SCALES\t= 128\nVID_TYPE_MONOCHROME = 256\nVID_TYPE_SUBCAPTURE = 512\nVID_TYPE_MPEG_DECODER = 1024\nVID_TYPE_MPEG_ENCODER = 2048\nVID_TYPE_MJPEG_DECODER = 4096\nVID_TYPE_MJPEG_ENCODER = 8192\n\n\ndef v4l2_fourcc(a, b, c, d):\n    return ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24)\n\n\ndef v4l2_fourcc2str(fourcc):\n    a = chr(fourcc & 0xFF)\n    b = chr((fourcc >> 8) & 0xFF)\n    c = chr((fourcc >> 16) & 0xFF)\n    d = chr((fourcc >> 24) & 0xFF)\n    return ''.join([a, b, c, d])\n\n\nv4l2_field = enum\n(\n    V4L2_FIELD_ANY,\n    V4L2_FIELD_NONE,\n    V4L2_FIELD_TOP,\n    V4L2_FIELD_BOTTOM,\n    V4L2_FIELD_INTERLACED,\n    V4L2_FIELD_SEQ_TB,\n    V4L2_FIELD_SEQ_BT,\n    V4L2_FIELD_ALTERNATE,\n    V4L2_FIELD_INTERLACED_TB,\n    V4L2_FIELD_INTERLACED_BT,\n) = range(10)\n\n\ndef V4L2_FIELD_HAS_TOP(field):\n    return (\n\tfield == V4L2_FIELD_TOP or\n\tfield == V4L2_FIELD_INTERLACED or\n\tfield == V4L2_FIELD_INTERLACED_TB or\n\tfield == V4L2_FIELD_INTERLACED_BT or\n\tfield == V4L2_FIELD_SEQ_TB or\n\tfield == V4L2_FIELD_SEQ_BT)\n\n\ndef V4L2_FIELD_HAS_BOTTOM(field):\n    return (\n        field == V4L2_FIELD_BOTTOM or\n        field == V4L2_FIELD_INTERLACED or\n        field == V4L2_FIELD_INTERLACED_TB or\n        field == V4L2_FIELD_INTERLACED_BT or\n        field == V4L2_FIELD_SEQ_TB or\n        field == V4L2_FIELD_SEQ_BT)\n\n\ndef V4L2_FIELD_HAS_BOTH(field):\n    return (\n        field == V4L2_FIELD_INTERLACED or\n        field == V4L2_FIELD_INTERLACED_TB or\n        field == V4L2_FIELD_INTERLACED_BT or\n        field == V4L2_FIELD_SEQ_TB or\n        field == V4L2_FIELD_SEQ_BT)\n\n\nv4l2_buf_type = enum\n(\tV4L2_BUF_TYPE_VIDEO_CAPTURE,\n\tV4L2_BUF_TYPE_VIDEO_OUTPUT,\n\tV4L2_BUF_TYPE_VIDEO_OVERLAY,\n\tV4L2_BUF_TYPE_VBI_CAPTURE,\n\tV4L2_BUF_TYPE_VBI_OUTPUT,\n\tV4L2_BUF_TYPE_SLICED_VBI_CAPTURE,\n\tV4L2_BUF_TYPE_SLICED_VBI_OUTPUT,\n\tV4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY,\n\tV4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,\n\tV4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,\n\tV4L2_BUF_TYPE_SDR_CAPTURE,\n\tV4L2_BUF_TYPE_SDR_OUTPUT,\n\tV4L2_BUF_TYPE_META_CAPTURE,\n\tV4L2_BUF_TYPE_PRIVATE, # Deprecated, do not use.\n) = list(range(1, 14)) + [0x80]\n\n\nv4l2_ctrl_type = enum\n(\n    V4L2_CTRL_TYPE_INTEGER,\n    V4L2_CTRL_TYPE_BOOLEAN,\n    V4L2_CTRL_TYPE_MENU,\n    V4L2_CTRL_TYPE_BUTTON,\n    V4L2_CTRL_TYPE_INTEGER64,\n    V4L2_CTRL_TYPE_CTRL_CLASS,\n    V4L2_CTRL_TYPE_STRING,\n    V4L2_CTRL_TYPE_BITMASK,\n\tV4L2_CTRL_TYPE_INTEGER_MENU,\n) = range(1, 10)\n\n\n# Compound types are >= 0x0100\nV4L2_CTRL_COMPOUND_TYPES = 0x0100\nV4L2_CTRL_TYPE_U8\t     = 0x0100\nV4L2_CTRL_TYPE_U16\t     = 0x0101\nV4L2_CTRL_TYPE_U32\t     = 0x0102\n\n\nv4l2_tuner_type = enum\n(\n    V4L2_TUNER_RADIO,\n    V4L2_TUNER_ANALOG_TV,\n    V4L2_TUNER_DIGITAL_TV,\n) = range(1, 4)\n\n\nv4l2_memory = enum\n(\n    V4L2_MEMORY_MMAP,\n    V4L2_MEMORY_USERPTR,\n    V4L2_MEMORY_OVERLAY,\n    V4L2_MEMORY_DMABUF,\n) = range(1, 5)\n\n\nv4l2_colorspace = enum\n(\n    #Default colorspace, i.e. let the driver figure it out.\n    #Can only be used with video capture.\n\tV4L2_COLORSPACE_DEFAULT,\n\t# SMPTE 170M: used for broadcast NTSC/PAL SDTV\n\tV4L2_COLORSPACE_SMPTE170M,\n\t# Obsolete pre-1998 SMPTE 240M HDTV standard, superseded by Rec 709\n\tV4L2_COLORSPACE_SMPTE240M,\n\t# Rec.709: used for HDTV\n\tV4L2_COLORSPACE_REC709,\n    #Deprecated, do not use. No driver will ever return this. This was\n    #based on a misunderstanding of the bt878 datasheet.\n\tV4L2_COLORSPACE_BT878,\n    #NTSC 1953 colorspace. This only makes sense when dealing with\n    #really, really old NTSC recordings. Superseded by SMPTE 170M.\n\tV4L2_COLORSPACE_470_SYSTEM_M,\n    #EBU Tech 3213 PAL/SECAM colorspace. This only makes sense when\n    #dealing with really old PAL/SECAM recordings. Superseded by\n    #SMPTE 170M.\n\tV4L2_COLORSPACE_470_SYSTEM_BG,\n    #Effectively shorthand for V4L2_COLORSPACE_SRGB, V4L2_YCBCR_ENC_601\n    #and V4L2_QUANTIZATION_FULL_RANGE. To be used for (Motion-)JPEG.\n\tV4L2_COLORSPACE_JPEG,\n\t# For RGB colorspaces such as produces by most webcams.\n\tV4L2_COLORSPACE_SRGB,\n\t# AdobeRGB colorspace\n\tV4L2_COLORSPACE_ADOBERGB,\n\t# BT.2020 colorspace, used for UHDTV.\n\tV4L2_COLORSPACE_BT2020,\n\t# Raw colorspace: for RAW unprocessed images\n\tV4L2_COLORSPACE_RAW,\n\t# DCI-P3 colorspace, used by cinema projectors\n\tV4L2_COLORSPACE_DCI_P3,\n) = range(0, 13)\n\n\nv4l2_priority = enum\n(\n    V4L2_PRIORITY_UNSET,\n    V4L2_PRIORITY_BACKGROUND,\n    V4L2_PRIORITY_INTERACTIVE,\n    V4L2_PRIORITY_RECORD,\n    V4L2_PRIORITY_DEFAULT,\n) = list(range(0, 4)) + [2]\n\n\nclass v4l2_rect(ctypes.Structure):\n    _fields_ = [\n        ('left', ctypes.c_int32),\n        ('top', ctypes.c_int32),\n        ('width', ctypes.c_int32),\n        ('height', ctypes.c_int32),\n    ]\n\n\nclass v4l2_fract(ctypes.Structure):\n    _fields_ = [\n        ('numerator', ctypes.c_uint32),\n        ('denominator', ctypes.c_uint32),\n    ]\n\n\n#\n# Driver capabilities\n#\n\nclass v4l2_capability(ctypes.Structure):\n    _fields_ = [\n        ('driver', ctypes.c_char * 16),\n        ('card', ctypes.c_char * 32),\n        ('bus_info', ctypes.c_char * 32),\n        ('version', ctypes.c_uint32),\n        ('capabilities', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Values for 'capabilities' field\n#\n\nV4L2_CAP_VIDEO_CAPTURE        = 0x00000001  # Is a video capture device\nV4L2_CAP_VIDEO_OUTPUT         = 0x00000002  # Is a video output device\nV4L2_CAP_VIDEO_OVERLAY        = 0x00000004  # Can do video overlay\nV4L2_CAP_VBI_CAPTURE          = 0x00000010  # Is a raw VBI capture device\nV4L2_CAP_VBI_OUTPUT           = 0x00000020  # Is a raw VBI output device\nV4L2_CAP_SLICED_VBI_CAPTURE   = 0x00000040  # Is a sliced VBI capture device\nV4L2_CAP_SLICED_VBI_OUTPUT    = 0x00000080  # Is a sliced VBI output device\nV4L2_CAP_RDS_CAPTURE          = 0x00000100  # RDS data capture\nV4L2_CAP_VIDEO_OUTPUT_OVERLAY = 0x00000200  # Can do video output overlay\nV4L2_CAP_HW_FREQ_SEEK         = 0x00000400  # Can do hardware frequency seek\nV4L2_CAP_RDS_OUTPUT           = 0x00000800  # Is an RDS encoder\nV4L2_CAP_VIDEO_CAPTURE_MPLANE =\t0x00001000  # Is a video capture device that supports multiplanar formats\nV4L2_CAP_VIDEO_OUTPUT_MPLANE  = 0x00002000  # Is a video output device that supports multiplanar formats\nV4L2_CAP_VIDEO_M2M_MPLANE     = 0x00004000  # Is a video mem-to-mem device that supports multiplanar formats\nV4L2_CAP_VIDEO_M2M            = 0x00008000  # Is a video mem-to-mem device\nV4L2_CAP_TUNER                = 0x00010000  # has a tuner\nV4L2_CAP_AUDIO                = 0x00020000  # has audio support\nV4L2_CAP_RADIO                = 0x00040000  # is a radio device\nV4L2_CAP_MODULATOR            = 0x00080000  # has a modulator\nV4L2_CAP_SDR_CAPTURE\t\t  = 0x00100000  # Is a SDR capture device\nV4L2_CAP_EXT_PIX_FORMAT\t\t  = 0x00200000  # Supports the extended pixel format\nV4L2_CAP_SDR_OUTPUT\t\t      = 0x00400000  # Is a SDR output device\nV4L2_CAP_META_CAPTURE\t\t  = 0x00800000  # Is a metadata capture device\nV4L2_CAP_READWRITE            = 0x01000000  # read/write systemcalls\nV4L2_CAP_ASYNCIO              = 0x02000000  # async I/O\nV4L2_CAP_STREAMING            = 0x04000000  # streaming I/O ioctls\nV4L2_CAP_TOUCH                = 0x10000000  # Is a touch device\nV4L2_CAP_DEVICE_CAPS          = 0x80000000  # sets device capabilities field\n\n\n#\n# Video image format\n#\n\nclass v4l2_pix_format(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('pixelformat', ctypes.c_uint32),\n        ('field', v4l2_field),\n        ('bytesperline', ctypes.c_uint32),\n        ('sizeimage', ctypes.c_uint32),\n        ('colorspace', v4l2_colorspace),\n        ('priv', ctypes.c_uint32),\n    ]\n\n# RGB formats\nV4L2_PIX_FMT_RGB332 = v4l2_fourcc('R', 'G', 'B', '1')\nV4L2_PIX_FMT_RGB444 = v4l2_fourcc('R', '4', '4', '4')\nV4L2_PIX_FMT_RGB555 = v4l2_fourcc('R', 'G', 'B', 'O')\nV4L2_PIX_FMT_RGB565 = v4l2_fourcc('R', 'G', 'B', 'P')\nV4L2_PIX_FMT_RGB555X = v4l2_fourcc('R', 'G', 'B', 'Q')\nV4L2_PIX_FMT_RGB565X = v4l2_fourcc('R', 'G', 'B', 'R')\nV4L2_PIX_FMT_BGR24 = v4l2_fourcc('B', 'G', 'R', '3')\nV4L2_PIX_FMT_RGB24 = v4l2_fourcc('R', 'G', 'B', '3')\nV4L2_PIX_FMT_BGR32 = v4l2_fourcc('B', 'G', 'R', '4')\nV4L2_PIX_FMT_RGB32 = v4l2_fourcc('R', 'G', 'B', '4')\nV4L2_PIX_FMT_RGBX32 = v4l2_fourcc('X', 'B', '2', '4')\nV4L2_PIX_FMT_XRGB32 = v4l2_fourcc('B', 'X', '2', '4')\nV4L2_PIX_FMT_RGBA32 = v4l2_fourcc('A', 'B', '2', '4')\n\n# Grey formats\nV4L2_PIX_FMT_GREY = v4l2_fourcc('G', 'R', 'E', 'Y')\nV4L2_PIX_FMT_Y10 =  v4l2_fourcc('Y', '1', '0', ' ')\nV4L2_PIX_FMT_Y16 = v4l2_fourcc('Y', '1', '6', ' ')\n\n# Palette formats\nV4L2_PIX_FMT_PAL8 = v4l2_fourcc('P', 'A', 'L', '8')\n\n# Luminance+Chrominance formats\nV4L2_PIX_FMT_YVU410 = v4l2_fourcc('Y', 'V', 'U', '9')\nV4L2_PIX_FMT_YVU420 = v4l2_fourcc('Y', 'V', '1', '2')\nV4L2_PIX_FMT_YUYV = v4l2_fourcc('Y', 'U', 'Y', 'V')\nV4L2_PIX_FMT_YYUV = v4l2_fourcc('Y', 'Y', 'U', 'V')\nV4L2_PIX_FMT_YVYU = v4l2_fourcc('Y', 'V', 'Y', 'U')\nV4L2_PIX_FMT_UYVY = v4l2_fourcc('U', 'Y', 'V', 'Y')\nV4L2_PIX_FMT_VYUY = v4l2_fourcc('V', 'Y', 'U', 'Y')\nV4L2_PIX_FMT_YUV422P = v4l2_fourcc('4', '2', '2', 'P')\nV4L2_PIX_FMT_YUV411P = v4l2_fourcc('4', '1', '1', 'P')\nV4L2_PIX_FMT_Y41P = v4l2_fourcc('Y', '4', '1', 'P')\nV4L2_PIX_FMT_YUV444 = v4l2_fourcc('Y', '4', '4', '4')\nV4L2_PIX_FMT_YUV555 = v4l2_fourcc('Y', 'U', 'V', 'O')\nV4L2_PIX_FMT_YUV565 = v4l2_fourcc('Y', 'U', 'V', 'P')\nV4L2_PIX_FMT_YUV32 = v4l2_fourcc('Y', 'U', 'V', '4')\nV4L2_PIX_FMT_YUV410 = v4l2_fourcc('Y', 'U', 'V', '9')\nV4L2_PIX_FMT_YUV420 = v4l2_fourcc('Y', 'U', '1', '2')\nV4L2_PIX_FMT_HI240 = v4l2_fourcc('H', 'I', '2', '4')\nV4L2_PIX_FMT_HM12 = v4l2_fourcc('H', 'M', '1', '2')\n\n# two planes -- one Y, one Cr + Cb interleaved\nV4L2_PIX_FMT_NV12 = v4l2_fourcc('N', 'V', '1', '2')\nV4L2_PIX_FMT_NV21 = v4l2_fourcc('N', 'V', '2', '1')\nV4L2_PIX_FMT_NV16 = v4l2_fourcc('N', 'V', '1', '6')\nV4L2_PIX_FMT_NV61 = v4l2_fourcc('N', 'V', '6', '1')\n\n# Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm\nV4L2_PIX_FMT_SBGGR8 = v4l2_fourcc('B', 'A', '8', '1')\nV4L2_PIX_FMT_SGBRG8 = v4l2_fourcc('G', 'B', 'R', 'G')\nV4L2_PIX_FMT_SGRBG8 = v4l2_fourcc('G', 'R', 'B', 'G')\nV4L2_PIX_FMT_SRGGB8 = v4l2_fourcc('R', 'G', 'G', 'B')\nV4L2_PIX_FMT_SBGGR10 = v4l2_fourcc('B', 'G', '1', '0')\nV4L2_PIX_FMT_SGBRG10 = v4l2_fourcc('G', 'B', '1', '0')\nV4L2_PIX_FMT_SGRBG10 = v4l2_fourcc('B', 'A', '1', '0')\nV4L2_PIX_FMT_SRGGB10 = v4l2_fourcc('R', 'G', '1', '0')\nV4L2_PIX_FMT_SGRBG10DPCM8 = v4l2_fourcc('B', 'D', '1', '0')\nV4L2_PIX_FMT_SBGGR16 = v4l2_fourcc('B', 'Y', 'R', '2')\n\n# compressed formats\nV4L2_PIX_FMT_MJPEG = v4l2_fourcc('M', 'J', 'P', 'G')\nV4L2_PIX_FMT_JPEG = v4l2_fourcc('J', 'P', 'E', 'G')\nV4L2_PIX_FMT_DV = v4l2_fourcc('d', 'v', 's', 'd')\nV4L2_PIX_FMT_MPEG = v4l2_fourcc('M', 'P', 'E', 'G')\nV4L2_PIX_FMT_H264 = v4l2_fourcc('H', '2', '6', '4')\n\n# Vendor-specific formats\nV4L2_PIX_FMT_CPIA1 = v4l2_fourcc('C', 'P', 'I', 'A')\nV4L2_PIX_FMT_WNVA = v4l2_fourcc('W', 'N', 'V', 'A')\nV4L2_PIX_FMT_SN9C10X = v4l2_fourcc('S', '9', '1', '0')\nV4L2_PIX_FMT_SN9C20X_I420 = v4l2_fourcc('S', '9', '2', '0')\nV4L2_PIX_FMT_PWC1 = v4l2_fourcc('P', 'W', 'C', '1')\nV4L2_PIX_FMT_PWC2 = v4l2_fourcc('P', 'W', 'C', '2')\nV4L2_PIX_FMT_ET61X251 = v4l2_fourcc('E', '6', '2', '5')\nV4L2_PIX_FMT_SPCA501 = v4l2_fourcc('S', '5', '0', '1')\nV4L2_PIX_FMT_SPCA505 = v4l2_fourcc('S', '5', '0', '5')\nV4L2_PIX_FMT_SPCA508 = v4l2_fourcc('S', '5', '0', '8')\nV4L2_PIX_FMT_SPCA561 = v4l2_fourcc('S', '5', '6', '1')\nV4L2_PIX_FMT_PAC207 = v4l2_fourcc('P', '2', '0', '7')\nV4L2_PIX_FMT_MR97310A = v4l2_fourcc('M', '3', '1', '0')\nV4L2_PIX_FMT_SN9C2028 = v4l2_fourcc('S', 'O', 'N', 'X')\nV4L2_PIX_FMT_SQ905C = v4l2_fourcc('9', '0', '5', 'C')\nV4L2_PIX_FMT_PJPG = v4l2_fourcc('P', 'J', 'P', 'G')\nV4L2_PIX_FMT_OV511 = v4l2_fourcc('O', '5', '1', '1')\nV4L2_PIX_FMT_OV518 = v4l2_fourcc('O', '5', '1', '8')\nV4L2_PIX_FMT_STV0680 = v4l2_fourcc('S', '6', '8', '0')\n\n\n#\n# Format enumeration\n#\n\nclass v4l2_fmtdesc(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('type', ctypes.c_int),\n        ('flags', ctypes.c_uint32),\n        ('description', ctypes.c_char * 32),\n        ('pixelformat', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\nV4L2_FMT_FLAG_COMPRESSED = 0x0001\nV4L2_FMT_FLAG_EMULATED = 0x0002\n\n\n#\n# Experimental frame size and frame rate enumeration\n#\n\nv4l2_frmsizetypes = enum\n(\n    V4L2_FRMSIZE_TYPE_DISCRETE,\n    V4L2_FRMSIZE_TYPE_CONTINUOUS,\n    V4L2_FRMSIZE_TYPE_STEPWISE,\n) = range(1, 4)\n\n\nclass v4l2_frmsize_discrete(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n    ]\n\n\nclass v4l2_frmsize_stepwise(ctypes.Structure):\n    _fields_ = [\n        ('min_width', ctypes.c_uint32),\n        ('min_height', ctypes.c_uint32),\n        ('step_width', ctypes.c_uint32),\n        ('min_height', ctypes.c_uint32),\n        ('max_height', ctypes.c_uint32),\n        ('step_height', ctypes.c_uint32),\n    ]\n\n\nclass v4l2_frmsizeenum(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('discrete', v4l2_frmsize_discrete),\n            ('stepwise', v4l2_frmsize_stepwise),\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('pixel_format', ctypes.c_uint32),\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n        ('reserved', ctypes.c_uint32 * 2)\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Frame rate enumeration\n#\n\nv4l2_frmivaltypes = enum\n(\n    V4L2_FRMIVAL_TYPE_DISCRETE,\n    V4L2_FRMIVAL_TYPE_CONTINUOUS,\n    V4L2_FRMIVAL_TYPE_STEPWISE,\n) = range(1, 4)\n\n\nclass v4l2_frmival_stepwise(ctypes.Structure):\n    _fields_ = [\n        ('min', v4l2_fract),\n        ('max', v4l2_fract),\n        ('step', v4l2_fract),\n    ]\n\n\nclass v4l2_frmivalenum(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('discrete', v4l2_fract),\n            ('stepwise', v4l2_frmival_stepwise),\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('pixel_format', ctypes.c_uint32),\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Timecode\n#\n\nclass v4l2_timecode(ctypes.Structure):\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('frames', ctypes.c_uint8),\n        ('seconds', ctypes.c_uint8),\n        ('minutes', ctypes.c_uint8),\n        ('hours', ctypes.c_uint8),\n        ('userbits', ctypes.c_uint8 * 4),\n    ]\n\n\nV4L2_TC_TYPE_24FPS = 1\nV4L2_TC_TYPE_25FPS = 2\nV4L2_TC_TYPE_30FPS = 3\nV4L2_TC_TYPE_50FPS = 4\nV4L2_TC_TYPE_60FPS = 5\n\nV4L2_TC_FLAG_DROPFRAME = 0x0001\nV4L2_TC_FLAG_COLORFRAME = 0x0002\nV4L2_TC_USERBITS_field = 0x000C\nV4L2_TC_USERBITS_USERDEFINED = 0x0000\nV4L2_TC_USERBITS_8BITCHARS = 0x0008\n\n\nclass v4l2_jpegcompression(ctypes.Structure):\n    _fields_ = [\n        ('quality', ctypes.c_int),\n        ('APPn', ctypes.c_int),\n        ('APP_len', ctypes.c_int),\n        ('APP_data', ctypes.c_char * 60),\n        ('COM_len', ctypes.c_int),\n        ('COM_data', ctypes.c_char * 60),\n        ('jpeg_markers', ctypes.c_uint32),\n    ]\n\n\nV4L2_JPEG_MARKER_DHT = 1 << 3\nV4L2_JPEG_MARKER_DQT = 1 << 4\nV4L2_JPEG_MARKER_DRI = 1 << 5\nV4L2_JPEG_MARKER_COM = 1 << 6\nV4L2_JPEG_MARKER_APP = 1 << 7\n\n\n#\n# Memory-mapping buffers\n#\n\n# https://www.kernel.org/doc/html/v5.10/userspace-api/media/v4l/buffer.html#struct-v4l2-plane\nclass v4l2_plane(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [(\"mem_offset\", ctypes.c_uint32),\n                    (\"userptr\", ctypes.c_ulong),\n                    (\"fd\", ctypes.c_int32)]\n    _fields_ = [\n                ('bytesused', ctypes.c_uint32),\n                ('length', ctypes.c_uint32),\n                ('m', _u),\n                ('data_offset',  ctypes.c_uint32),\n                ('reserved', ctypes.c_uint32 * 11)\n            ]\n\nclass v4l2_requestbuffers(ctypes.Structure):\n    _fields_ = [\n        ('count', ctypes.c_uint32),\n        ('type', v4l2_buf_type),\n        ('memory', v4l2_memory),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nclass v4l2_buffer(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('offset', ctypes.c_uint32),\n            ('userptr', ctypes.c_ulong),\n            ('planes', ctypes.POINTER(v4l2_plane)),\n            ('fd', ctypes.c_int32)\n        ]\n\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('type', v4l2_buf_type),\n        ('bytesused', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('field', v4l2_field),\n        ('timestamp', timeval),\n        ('timecode', v4l2_timecode),\n        ('sequence', ctypes.c_uint32),\n        ('memory', v4l2_memory),\n        ('m', _u),\n        ('length', ctypes.c_uint32),\n        ('input', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32),\n    ]\n\n\nV4L2_BUF_FLAG_MAPPED = 0x0001\nV4L2_BUF_FLAG_QUEUED = 0x0002\nV4L2_BUF_FLAG_DONE = 0x0004\nV4L2_BUF_FLAG_KEYFRAME = 0x0008\nV4L2_BUF_FLAG_PFRAME = 0x0010\nV4L2_BUF_FLAG_BFRAME = 0x0020\nV4L2_BUF_FLAG_TIMECODE = 0x0100\nV4L2_BUF_FLAG_INPUT = 0x0200\n\n\n#\n# Overlay preview\n#\n\nclass v4l2_framebuffer(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('base', ctypes.c_void_p),\n        ('fmt', v4l2_pix_format),\n    ]\n\nV4L2_FBUF_CAP_EXTERNOVERLAY = 0x0001\nV4L2_FBUF_CAP_CHROMAKEY\t= 0x0002\nV4L2_FBUF_CAP_LIST_CLIPPING = 0x0004\nV4L2_FBUF_CAP_BITMAP_CLIPPING = 0x0008\nV4L2_FBUF_CAP_LOCAL_ALPHA = 0x0010\nV4L2_FBUF_CAP_GLOBAL_ALPHA = 0x0020\nV4L2_FBUF_CAP_LOCAL_INV_ALPHA = 0x0040\nV4L2_FBUF_CAP_SRC_CHROMAKEY = 0x0080\n\nV4L2_FBUF_FLAG_PRIMARY = 0x0001\nV4L2_FBUF_FLAG_OVERLAY = 0x0002\nV4L2_FBUF_FLAG_CHROMAKEY = 0x0004\nV4L2_FBUF_FLAG_LOCAL_ALPHA = 0x0008\nV4L2_FBUF_FLAG_GLOBAL_ALPHA = 0x0010\nV4L2_FBUF_FLAG_LOCAL_INV_ALPHA = 0x0020\nV4L2_FBUF_FLAG_SRC_CHROMAKEY = 0x0040\n\n\nclass v4l2_clip(ctypes.Structure):\n    pass\nv4l2_clip._fields_ = [\n    ('c', v4l2_rect),\n    ('next', ctypes.POINTER(v4l2_clip)),\n]\n\n\nclass v4l2_window(ctypes.Structure):\n    _fields_ = [\n        ('w', v4l2_rect),\n        ('field', v4l2_field),\n        ('chromakey', ctypes.c_uint32),\n        ('clips', ctypes.POINTER(v4l2_clip)),\n        ('clipcount', ctypes.c_uint32),\n        ('bitmap', ctypes.c_void_p),\n        ('global_alpha', ctypes.c_uint8),\n    ]\n\n\n#\n# Capture parameters\n#\n\nclass v4l2_captureparm(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('capturemode', ctypes.c_uint32),\n        ('timeperframe', v4l2_fract),\n        ('extendedmode', ctypes.c_uint32),\n        ('readbuffers', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_MODE_HIGHQUALITY = 0x0001\nV4L2_CAP_TIMEPERFRAME = 0x1000\n\n\nclass v4l2_outputparm(ctypes.Structure):\n    _fields_ = [\n        ('capability', ctypes.c_uint32),\n        ('outputmode', ctypes.c_uint32),\n        ('timeperframe', v4l2_fract),\n        ('extendedmode', ctypes.c_uint32),\n        ('writebuffers', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Input image cropping\n#\n\nclass v4l2_cropcap(ctypes.Structure):\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('bounds', v4l2_rect),\n        ('defrect', v4l2_rect),\n        ('pixelaspect', v4l2_fract),\n    ]\n\n\nclass v4l2_crop(ctypes.Structure):\n    _fields_ = [\n        ('type', ctypes.c_int),\n        ('c', v4l2_rect),\n    ]\n\n\n#\n# Analog video standard\n#\n\nv4l2_std_id = ctypes.c_uint64\n\n\nV4L2_STD_PAL_B = 0x00000001\nV4L2_STD_PAL_B1 = 0x00000002\nV4L2_STD_PAL_G = 0x00000004\nV4L2_STD_PAL_H = 0x00000008\nV4L2_STD_PAL_I = 0x00000010\nV4L2_STD_PAL_D = 0x00000020\nV4L2_STD_PAL_D1 = 0x00000040\nV4L2_STD_PAL_K = 0x00000080\n\nV4L2_STD_PAL_M = 0x00000100\nV4L2_STD_PAL_N = 0x00000200\nV4L2_STD_PAL_Nc = 0x00000400\nV4L2_STD_PAL_60 = 0x00000800\n\nV4L2_STD_NTSC_M = 0x00001000\nV4L2_STD_NTSC_M_JP = 0x00002000\nV4L2_STD_NTSC_443 = 0x00004000\nV4L2_STD_NTSC_M_KR = 0x00008000\n\nV4L2_STD_SECAM_B = 0x00010000\nV4L2_STD_SECAM_D = 0x00020000\nV4L2_STD_SECAM_G = 0x00040000\nV4L2_STD_SECAM_H = 0x00080000\nV4L2_STD_SECAM_K = 0x00100000\nV4L2_STD_SECAM_K1 = 0x00200000\nV4L2_STD_SECAM_L = 0x00400000\nV4L2_STD_SECAM_LC = 0x00800000\n\nV4L2_STD_ATSC_8_VSB = 0x01000000\nV4L2_STD_ATSC_16_VSB = 0x02000000\n\n\n# some common needed stuff\nV4L2_STD_PAL_BG = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_PAL_G)\nV4L2_STD_PAL_DK = (V4L2_STD_PAL_D | V4L2_STD_PAL_D1 | V4L2_STD_PAL_K)\nV4L2_STD_PAL = (V4L2_STD_PAL_BG | V4L2_STD_PAL_DK | V4L2_STD_PAL_H | V4L2_STD_PAL_I)\nV4L2_STD_NTSC = (V4L2_STD_NTSC_M | V4L2_STD_NTSC_M_JP | V4L2_STD_NTSC_M_KR)\nV4L2_STD_SECAM_DK = (V4L2_STD_SECAM_D | V4L2_STD_SECAM_K | V4L2_STD_SECAM_K1)\nV4L2_STD_SECAM = (V4L2_STD_SECAM_B | V4L2_STD_SECAM_G | V4L2_STD_SECAM_H | V4L2_STD_SECAM_DK | V4L2_STD_SECAM_L | V4L2_STD_SECAM_LC)\n\nV4L2_STD_525_60 = (V4L2_STD_PAL_M | V4L2_STD_PAL_60 | V4L2_STD_NTSC | V4L2_STD_NTSC_443)\nV4L2_STD_625_50 = (V4L2_STD_PAL | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_SECAM)\nV4L2_STD_ATSC = (V4L2_STD_ATSC_8_VSB | V4L2_STD_ATSC_16_VSB)\n\nV4L2_STD_UNKNOWN = 0\nV4L2_STD_ALL = (V4L2_STD_525_60 | V4L2_STD_625_50)\n\n# some merged standards\nV4L2_STD_MN = (V4L2_STD_PAL_M | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_NTSC)\nV4L2_STD_B = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_SECAM_B)\nV4L2_STD_GH = (V4L2_STD_PAL_G | V4L2_STD_PAL_H|V4L2_STD_SECAM_G | V4L2_STD_SECAM_H)\nV4L2_STD_DK = (V4L2_STD_PAL_DK | V4L2_STD_SECAM_DK)\n\n\nclass v4l2_standard(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('id', v4l2_std_id),\n        ('name', ctypes.c_char * 24),\n        ('frameperiod', v4l2_fract),\n        ('framelines', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\n#\n# Video timings dv preset\n#\n\nclass v4l2_dv_preset(ctypes.Structure):\n    _fields_ = [\n        ('preset', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4)\n    ]\n\n\n#\n# DV preset enumeration\n#\n\nclass v4l2_dv_enum_preset(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('preset', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n#\n# DV preset values\n#\n\nV4L2_DV_INVALID = 0\nV4L2_DV_480P59_94 = 1\nV4L2_DV_576P50 = 2\nV4L2_DV_720P24 = 3\nV4L2_DV_720P25 = 4\nV4L2_DV_720P30 = 5\nV4L2_DV_720P50 = 6\nV4L2_DV_720P59_94 = 7\nV4L2_DV_720P60 = 8\nV4L2_DV_1080I29_97 = 9\nV4L2_DV_1080I30\t= 10\nV4L2_DV_1080I25\t= 11\nV4L2_DV_1080I50\t= 12\nV4L2_DV_1080I60\t= 13\nV4L2_DV_1080P24\t= 14\nV4L2_DV_1080P25\t= 15\nV4L2_DV_1080P30\t= 16\nV4L2_DV_1080P50\t= 17\nV4L2_DV_1080P60\t= 18\n\n\n#\n# DV BT timings\n#\n\nclass v4l2_bt_timings(ctypes.Structure):\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('interlaced', ctypes.c_uint32),\n        ('polarities', ctypes.c_uint32),\n        ('pixelclock', ctypes.c_uint64),\n        ('hfrontporch', ctypes.c_uint32),\n        ('hsync', ctypes.c_uint32),\n        ('hbackporch', ctypes.c_uint32),\n        ('vfrontporch', ctypes.c_uint32),\n        ('vsync', ctypes.c_uint32),\n        ('vbackporch', ctypes.c_uint32),\n        ('il_vfrontporch', ctypes.c_uint32),\n        ('il_vsync', ctypes.c_uint32),\n        ('il_vbackporch', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 16),\n    ]\n\n    _pack_ = True\n\n# Interlaced or progressive format\nV4L2_DV_PROGRESSIVE = 0\nV4L2_DV_INTERLACED = 1\n\n# Polarities. If bit is not set, it is assumed to be negative polarity\nV4L2_DV_VSYNC_POS_POL = 0x00000001\nV4L2_DV_HSYNC_POS_POL = 0x00000002\n\n\nclass v4l2_dv_timings(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('bt', v4l2_bt_timings),\n            ('reserved', ctypes.c_uint32 * 32),\n        ]\n\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\n# Values for the type field\nV4L2_DV_BT_656_1120 = 0\n\n\n#\n# Video inputs\n#\n\nclass v4l2_input(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', ctypes.c_uint32),\n        ('audioset', ctypes.c_uint32),\n        ('tuner', ctypes.c_uint32),\n        ('std', v4l2_std_id),\n        ('status', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_INPUT_TYPE_TUNER = 1\nV4L2_INPUT_TYPE_CAMERA = 2\n\nV4L2_IN_ST_NO_POWER = 0x00000001\nV4L2_IN_ST_NO_SIGNAL = 0x00000002\nV4L2_IN_ST_NO_COLOR = 0x00000004\n\nV4L2_IN_ST_HFLIP = 0x00000010\nV4L2_IN_ST_VFLIP = 0x00000020\n\nV4L2_IN_ST_NO_H_LOCK = 0x00000100\nV4L2_IN_ST_COLOR_KILL = 0x00000200\n\nV4L2_IN_ST_NO_SYNC = 0x00010000\nV4L2_IN_ST_NO_EQU = 0x00020000\nV4L2_IN_ST_NO_CARRIER = 0x00040000\n\nV4L2_IN_ST_MACROVISION = 0x01000000\nV4L2_IN_ST_NO_ACCESS = 0x02000000\nV4L2_IN_ST_VTR = 0x04000000\n\nV4L2_IN_CAP_PRESETS = 0x00000001\nV4L2_IN_CAP_CUSTOM_TIMINGS = 0x00000002\nV4L2_IN_CAP_STD = 0x00000004\n\n#\n# Video outputs\n#\n\nclass v4l2_output(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', ctypes.c_uint32),\n        ('audioset', ctypes.c_uint32),\n        ('modulator', ctypes.c_uint32),\n        ('std', v4l2_std_id),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_OUTPUT_TYPE_MODULATOR = 1\nV4L2_OUTPUT_TYPE_ANALOG\t= 2\nV4L2_OUTPUT_TYPE_ANALOGVGAOVERLAY = 3\n\nV4L2_OUT_CAP_PRESETS = 0x00000001\nV4L2_OUT_CAP_CUSTOM_TIMINGS = 0x00000002\nV4L2_OUT_CAP_STD = 0x00000004\n\n#\n# Controls\n#\n\nclass v4l2_control(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('value', ctypes.c_int32),\n    ]\n\n\nclass v4l2_ext_control(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('value', ctypes.c_int32),\n            ('value64', ctypes.c_int64),\n            ('reserved', ctypes.c_void_p),\n        ]\n\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('reserved2', ctypes.c_uint32 * 2),\n        ('_u', _u)\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\nclass v4l2_ext_controls(ctypes.Structure):\n    _fields_ = [\n        ('ctrl_class', ctypes.c_uint32),\n        ('count', ctypes.c_uint32),\n        ('error_idx', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n        ('controls', ctypes.POINTER(v4l2_ext_control)),\n    ]\n\n\nV4L2_CTRL_CLASS_USER = 0x00980000\nV4L2_CTRL_CLASS_MPEG = 0x00990000\nV4L2_CTRL_CLASS_CAMERA = 0x009a0000\nV4L2_CTRL_CLASS_FM_TX = 0x009b0000\n\n\ndef V4L2_CTRL_ID_MASK():\n    return 0x0fffffff\n\n\ndef V4L2_CTRL_ID2CLASS(id_):\n    return id_ & 0x0fff0000 # unsigned long\n\n\ndef V4L2_CTRL_DRIVER_PRIV(id_):\n    return (id_ & 0xffff) >= 0x1000\n\n\nclass v4l2_queryctrl(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('type', v4l2_ctrl_type),\n        ('name', ctypes.c_char * 32),\n        ('minimum', ctypes.c_int32),\n        ('maximum', ctypes.c_int32),\n        ('step', ctypes.c_int32),\n        ('default_value', ctypes.c_int32),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nclass v4l2_querymenu(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('reserved', ctypes.c_uint32),\n    ]\n\n\nNONE = 0x0000\nV4L2_CTRL_FLAG_DISABLED = 0x0001\nV4L2_CTRL_FLAG_GRABBED = 0x0002\nV4L2_CTRL_FLAG_READ_ONLY = 0x0004\nV4L2_CTRL_FLAG_UPDATE = 0x0008\nV4L2_CTRL_FLAG_INACTIVE = 0x0010\nV4L2_CTRL_FLAG_SLIDER = 0x0020\nV4L2_CTRL_FLAG_WRITE_ONLY = 0x0040\nV4L2_CTRL_FLAG_VOLATILE = 0x0080\nV4L2_CTRL_FLAG_HAS_PAYLOAD = 0x0100\nV4L2_CTRL_FLAG_EXECUTE_ON_WRITE = 0x0200\nV4L2_CTRL_FLAG_MODIFY_LAYOUT = 0x0400\nV4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000\nV4L2_CTRL_FLAG_NEXT_COMPOUND = 0x40000000\n\nV4L2_CID_BASE = V4L2_CTRL_CLASS_USER | 0x900\nV4L2_CID_USER_BASE = V4L2_CID_BASE\nV4L2_CID_PRIVATE_BASE = 0x08000000\n\nV4L2_CID_USER_CLASS = V4L2_CTRL_CLASS_USER | 1\nV4L2_CID_BRIGHTNESS = V4L2_CID_BASE + 0\nV4L2_CID_CONTRAST = V4L2_CID_BASE + 1\nV4L2_CID_SATURATION = V4L2_CID_BASE + 2\nV4L2_CID_HUE = V4L2_CID_BASE + 3\nV4L2_CID_AUDIO_VOLUME = V4L2_CID_BASE + 5\nV4L2_CID_AUDIO_BALANCE = V4L2_CID_BASE + 6\nV4L2_CID_AUDIO_BASS = V4L2_CID_BASE + 7\nV4L2_CID_AUDIO_TREBLE = V4L2_CID_BASE + 8\nV4L2_CID_AUDIO_MUTE = V4L2_CID_BASE + 9\nV4L2_CID_AUDIO_LOUDNESS = V4L2_CID_BASE + 10\nV4L2_CID_BLACK_LEVEL = V4L2_CID_BASE + 11 # Deprecated\nV4L2_CID_AUTO_WHITE_BALANCE = V4L2_CID_BASE + 12\nV4L2_CID_DO_WHITE_BALANCE = V4L2_CID_BASE + 13\nV4L2_CID_RED_BALANCE = V4L2_CID_BASE + 14\nV4L2_CID_BLUE_BALANCE = V4L2_CID_BASE + 15\nV4L2_CID_GAMMA = V4L2_CID_BASE + 16\nV4L2_CID_WHITENESS = V4L2_CID_GAMMA # Deprecated\nV4L2_CID_EXPOSURE = V4L2_CID_BASE + 17\nV4L2_CID_AUTOGAIN = V4L2_CID_BASE + 18\nV4L2_CID_GAIN = V4L2_CID_BASE + 19\nV4L2_CID_HFLIP = V4L2_CID_BASE + 20\nV4L2_CID_VFLIP = V4L2_CID_BASE + 21\n\n# Deprecated; use V4L2_CID_PAN_RESET and V4L2_CID_TILT_RESET\nV4L2_CID_HCENTER = V4L2_CID_BASE + 22\nV4L2_CID_VCENTER = V4L2_CID_BASE + 23\n\nV4L2_CID_POWER_LINE_FREQUENCY = V4L2_CID_BASE + 24\n\nv4l2_power_line_frequency = enum\n(\n    V4L2_CID_POWER_LINE_FREQUENCY_DISABLED,\n    V4L2_CID_POWER_LINE_FREQUENCY_50HZ,\n    V4L2_CID_POWER_LINE_FREQUENCY_60HZ,\n) = range(3)\n\nV4L2_CID_HUE_AUTO = V4L2_CID_BASE + 25\nV4L2_CID_WHITE_BALANCE_TEMPERATURE = V4L2_CID_BASE + 26\nV4L2_CID_SHARPNESS = V4L2_CID_BASE + 27\nV4L2_CID_BACKLIGHT_COMPENSATION = V4L2_CID_BASE + 28\nV4L2_CID_CHROMA_AGC = V4L2_CID_BASE + 29\nV4L2_CID_COLOR_KILLER = V4L2_CID_BASE + 30\nV4L2_CID_COLORFX = V4L2_CID_BASE + 31\n\nv4l2_colorfx = enum\n(\n    V4L2_COLORFX_NONE,\n    V4L2_COLORFX_BW,\n    V4L2_COLORFX_SEPIA,\n) = range(3)\n\nV4L2_CID_AUTOBRIGHTNESS = V4L2_CID_BASE + 32\nV4L2_CID_BAND_STOP_FILTER = V4L2_CID_BASE + 33\n\nV4L2_CID_ROTATE = V4L2_CID_BASE + 34\nV4L2_CID_BG_COLOR = V4L2_CID_BASE + 35\nV4L2_CID_LASTP1 = V4L2_CID_BASE + 36\n\nV4L2_CID_MPEG_BASE = V4L2_CTRL_CLASS_MPEG | 0x900\nV4L2_CID_MPEG_CLASS = V4L2_CTRL_CLASS_MPEG | 1\n\n# MPEG streams\nV4L2_CID_MPEG_STREAM_TYPE = V4L2_CID_MPEG_BASE + 0\n\nv4l2_mpeg_stream_type = enum\n(\n    V4L2_MPEG_STREAM_TYPE_MPEG2_PS,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_TS,\n    V4L2_MPEG_STREAM_TYPE_MPEG1_SS,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_DVD,\n    V4L2_MPEG_STREAM_TYPE_MPEG1_VCD,\n    V4L2_MPEG_STREAM_TYPE_MPEG2_SVCD,\n) = range(6)\n\nV4L2_CID_MPEG_STREAM_PID_PMT = V4L2_CID_MPEG_BASE + 1\nV4L2_CID_MPEG_STREAM_PID_AUDIO = V4L2_CID_MPEG_BASE + 2\nV4L2_CID_MPEG_STREAM_PID_VIDEO = V4L2_CID_MPEG_BASE + 3\nV4L2_CID_MPEG_STREAM_PID_PCR = V4L2_CID_MPEG_BASE + 4\nV4L2_CID_MPEG_STREAM_PES_ID_AUDIO = V4L2_CID_MPEG_BASE + 5\nV4L2_CID_MPEG_STREAM_PES_ID_VIDEO = V4L2_CID_MPEG_BASE + 6\nV4L2_CID_MPEG_STREAM_VBI_FMT = V4L2_CID_MPEG_BASE + 7\n\nv4l2_mpeg_stream_vbi_fmt = enum\n(\n    V4L2_MPEG_STREAM_VBI_FMT_NONE,\n    V4L2_MPEG_STREAM_VBI_FMT_IVTV,\n) = range(2)\n\nV4L2_CID_MPEG_AUDIO_SAMPLING_FREQ = V4L2_CID_MPEG_BASE + 100\n\nv4l2_mpeg_audio_sampling_freq = enum\n(\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_44100,\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_48000,\n    V4L2_MPEG_AUDIO_SAMPLING_FREQ_32000,\n) = range(3)\n\nV4L2_CID_MPEG_AUDIO_ENCODING = V4L2_CID_MPEG_BASE + 101\n\nv4l2_mpeg_audio_encoding = enum\n(\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_1,\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_2,\n    V4L2_MPEG_AUDIO_ENCODING_LAYER_3,\n    V4L2_MPEG_AUDIO_ENCODING_AAC,\n    V4L2_MPEG_AUDIO_ENCODING_AC3,\n) = range(5)\n\nV4L2_CID_MPEG_AUDIO_L1_BITRATE = V4L2_CID_MPEG_BASE + 102\n\nv4l2_mpeg_audio_l1_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L1_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_288K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_320K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_352K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_384K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_416K,\n    V4L2_MPEG_AUDIO_L1_BITRATE_448K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_L2_BITRATE = V4L2_CID_MPEG_BASE + 103\n\nv4l2_mpeg_audio_l2_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L2_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_48K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_56K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_80K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_112K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_320K,\n    V4L2_MPEG_AUDIO_L2_BITRATE_384K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_L3_BITRATE = V4L2_CID_MPEG_BASE + 104\n\nv4l2_mpeg_audio_l3_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_L3_BITRATE_32K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_40K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_48K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_56K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_64K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_80K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_96K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_112K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_128K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_160K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_192K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_224K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_256K,\n    V4L2_MPEG_AUDIO_L3_BITRATE_320K,\n) = range(14)\n\nV4L2_CID_MPEG_AUDIO_MODE = V4L2_CID_MPEG_BASE + 105\n\nv4l2_mpeg_audio_mode = enum\n(\n    V4L2_MPEG_AUDIO_MODE_STEREO,\n    V4L2_MPEG_AUDIO_MODE_JOINT_STEREO,\n    V4L2_MPEG_AUDIO_MODE_DUAL,\n    V4L2_MPEG_AUDIO_MODE_MONO,\n) = range(4)\n\nV4L2_CID_MPEG_AUDIO_MODE_EXTENSION = V4L2_CID_MPEG_BASE + 106\n\nv4l2_mpeg_audio_mode_extension = enum\n(\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_4,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_8,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_12,\n    V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_16,\n) = range(4)\n\nV4L2_CID_MPEG_AUDIO_EMPHASIS = V4L2_CID_MPEG_BASE + 107\n\nv4l2_mpeg_audio_emphasis = enum\n(\n    V4L2_MPEG_AUDIO_EMPHASIS_NONE,\n    V4L2_MPEG_AUDIO_EMPHASIS_50_DIV_15_uS,\n    V4L2_MPEG_AUDIO_EMPHASIS_CCITT_J17,\n) = range(3)\n\nV4L2_CID_MPEG_AUDIO_CRC = V4L2_CID_MPEG_BASE + 108\n\nv4l2_mpeg_audio_crc = enum\n(\n    V4L2_MPEG_AUDIO_CRC_NONE,\n    V4L2_MPEG_AUDIO_CRC_CRC16,\n) = range(2)\n\nV4L2_CID_MPEG_AUDIO_MUTE = V4L2_CID_MPEG_BASE + 109\nV4L2_CID_MPEG_AUDIO_AAC_BITRATE = V4L2_CID_MPEG_BASE + 110\nV4L2_CID_MPEG_AUDIO_AC3_BITRATE\t= V4L2_CID_MPEG_BASE + 111\n\nv4l2_mpeg_audio_ac3_bitrate = enum\n(\n    V4L2_MPEG_AUDIO_AC3_BITRATE_32K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_40K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_48K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_56K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_64K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_80K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_96K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_112K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_128K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_160K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_192K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_224K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_256K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_320K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_384K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_448K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_512K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_576K,\n    V4L2_MPEG_AUDIO_AC3_BITRATE_640K,\n) = range(19)\n\nV4L2_CID_MPEG_VIDEO_ENCODING = V4L2_CID_MPEG_BASE + 200\n\nv4l2_mpeg_video_encoding = enum\n(\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_1,\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_2,\n    V4L2_MPEG_VIDEO_ENCODING_MPEG_4_AVC,\n) = range(3)\n\nV4L2_CID_MPEG_VIDEO_ASPECT = V4L2_CID_MPEG_BASE + 201\n\nv4l2_mpeg_video_aspect = enum\n(\n    V4L2_MPEG_VIDEO_ASPECT_1x1,\n    V4L2_MPEG_VIDEO_ASPECT_4x3,\n    V4L2_MPEG_VIDEO_ASPECT_16x9,\n    V4L2_MPEG_VIDEO_ASPECT_221x100,\n) = range(4)\n\nV4L2_CID_MPEG_VIDEO_B_FRAMES = V4L2_CID_MPEG_BASE + 202\nV4L2_CID_MPEG_VIDEO_GOP_SIZE = V4L2_CID_MPEG_BASE + 203\nV4L2_CID_MPEG_VIDEO_GOP_CLOSURE = V4L2_CID_MPEG_BASE + 204\nV4L2_CID_MPEG_VIDEO_PULLDOWN = V4L2_CID_MPEG_BASE + 205\nV4L2_CID_MPEG_VIDEO_BITRATE_MODE = V4L2_CID_MPEG_BASE + 206\n\nv4l2_mpeg_video_bitrate_mode = enum\n(\n    V4L2_MPEG_VIDEO_BITRATE_MODE_VBR,\n    V4L2_MPEG_VIDEO_BITRATE_MODE_CBR,\n) = range(2)\n\nV4L2_CID_MPEG_VIDEO_BITRATE = V4L2_CID_MPEG_BASE + 207\nV4L2_CID_MPEG_VIDEO_BITRATE_PEAK = V4L2_CID_MPEG_BASE + 208\nV4L2_CID_MPEG_VIDEO_TEMPORAL_DECIMATION = V4L2_CID_MPEG_BASE + 209\nV4L2_CID_MPEG_VIDEO_MUTE = V4L2_CID_MPEG_BASE + 210\nV4L2_CID_MPEG_VIDEO_MUTE_YUV = V4L2_CID_MPEG_BASE + 211\n\nV4L2_CID_MPEG_VIDEO_VBV_SIZE = V4L2_CID_MPEG_BASE + 222\nV4L2_CID_MPEG_VIDEO_DEC_PTS\t= V4L2_CID_MPEG_BASE + 223\nV4L2_CID_MPEG_VIDEO_DEC_FRAME = V4L2_CID_MPEG_BASE + 224\nV4L2_CID_MPEG_VIDEO_VBV_DELAY = V4L2_CID_MPEG_BASE + 225\nV4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER = V4L2_CID_MPEG_BASE + 226\nV4L2_CID_MPEG_VIDEO_MV_H_SEARCH_RANGE = V4L2_CID_MPEG_BASE + 227\nV4L2_CID_MPEG_VIDEO_MV_V_SEARCH_RANGE = V4L2_CID_MPEG_BASE + 228\nV4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME = V4L2_CID_MPEG_BASE + 229\n\nV4L2_CID_MPEG_VIDEO_H264_I_PERIOD = V4L2_CID_MPEG_BASE + 358\nV4L2_CID_MPEG_VIDEO_H264_LEVEL = V4L2_CID_MPEG_BASE + 359\n\nV4L2_CID_MPEG_CX2341X_BASE = V4L2_CTRL_CLASS_MPEG | 0x1000\nV4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 0\n\nv4l2_mpeg_cx2341x_video_spatial_filter_mode = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_MANUAL,\n    V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_AUTO,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 1\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 2\n\nv4l2_mpeg_cx2341x_video_luma_spatial_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_OFF,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_HOR,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_HV_SEPARABLE,\n    V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_SYM_NON_SEPARABLE,\n) = range(5)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 3\n\nv4l2_mpeg_cx2341x_video_chroma_spatial_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_OFF,\n    V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_1D_HOR,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 4\n\nv4l2_mpeg_cx2341x_video_temporal_filter_mode = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_MANUAL,\n    V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_AUTO,\n) = range(2)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 5\nV4L2_CID_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 6\n\nv4l2_mpeg_cx2341x_video_median_filter_type = enum\n(\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_OFF,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR_VERT,\n    V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_DIAG,\n) = range(5)\n\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 7\nV4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 8\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 9\nV4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 10\nV4L2_CID_MPEG_CX2341X_STREAM_INSERT_NAV_PACKETS = V4L2_CID_MPEG_CX2341X_BASE + 11\n\nV4L2_CID_CAMERA_CLASS_BASE = V4L2_CTRL_CLASS_CAMERA | 0x900\nV4L2_CID_CAMERA_CLASS = V4L2_CTRL_CLASS_CAMERA | 1\n\nV4L2_CID_EXPOSURE_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 1\n\nv4l2_exposure_auto_type = enum\n(\n    V4L2_EXPOSURE_AUTO,\n    V4L2_EXPOSURE_MANUAL,\n    V4L2_EXPOSURE_SHUTTER_PRIORITY,\n    V4L2_EXPOSURE_APERTURE_PRIORITY,\n) = range(4)\n\nV4L2_CID_EXPOSURE_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 2\nV4L2_CID_EXPOSURE_AUTO_PRIORITY = V4L2_CID_CAMERA_CLASS_BASE + 3\n\nV4L2_CID_PAN_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 4\nV4L2_CID_TILT_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 5\nV4L2_CID_PAN_RESET = V4L2_CID_CAMERA_CLASS_BASE + 6\nV4L2_CID_TILT_RESET = V4L2_CID_CAMERA_CLASS_BASE + 7\n\nV4L2_CID_PAN_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 8\nV4L2_CID_TILT_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 9\n\nV4L2_CID_FOCUS_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 10\nV4L2_CID_FOCUS_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 11\nV4L2_CID_FOCUS_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 12\n\nV4L2_CID_ZOOM_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 13\nV4L2_CID_ZOOM_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 14\nV4L2_CID_ZOOM_CONTINUOUS = V4L2_CID_CAMERA_CLASS_BASE + 15\n\nV4L2_CID_PRIVACY = V4L2_CID_CAMERA_CLASS_BASE + 16\n\nV4L2_CID_FM_TX_CLASS_BASE = V4L2_CTRL_CLASS_FM_TX | 0x900\nV4L2_CID_FM_TX_CLASS = V4L2_CTRL_CLASS_FM_TX | 1\n\nV4L2_CID_RDS_TX_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 1\nV4L2_CID_RDS_TX_PI = V4L2_CID_FM_TX_CLASS_BASE + 2\nV4L2_CID_RDS_TX_PTY = V4L2_CID_FM_TX_CLASS_BASE + 3\nV4L2_CID_RDS_TX_PS_NAME = V4L2_CID_FM_TX_CLASS_BASE + 5\nV4L2_CID_RDS_TX_RADIO_TEXT = V4L2_CID_FM_TX_CLASS_BASE + 6\n\nV4L2_CID_AUDIO_LIMITER_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 64\nV4L2_CID_AUDIO_LIMITER_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 65\nV4L2_CID_AUDIO_LIMITER_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 66\n\nV4L2_CID_AUDIO_COMPRESSION_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 80\nV4L2_CID_AUDIO_COMPRESSION_GAIN = V4L2_CID_FM_TX_CLASS_BASE + 81\nV4L2_CID_AUDIO_COMPRESSION_THRESHOLD = V4L2_CID_FM_TX_CLASS_BASE + 82\nV4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME = V4L2_CID_FM_TX_CLASS_BASE + 83\nV4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 84\n\nV4L2_CID_PILOT_TONE_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 96\nV4L2_CID_PILOT_TONE_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 97\nV4L2_CID_PILOT_TONE_FREQUENCY = V4L2_CID_FM_TX_CLASS_BASE + 98\n\nV4L2_CID_TUNE_PREEMPHASIS = V4L2_CID_FM_TX_CLASS_BASE + 112\n\nv4l2_preemphasis = enum\n(\n    V4L2_PREEMPHASIS_DISABLED,\n    V4L2_PREEMPHASIS_50_uS,\n    V4L2_PREEMPHASIS_75_uS,\n) = range(3)\n\nV4L2_CID_TUNE_POWER_LEVEL = V4L2_CID_FM_TX_CLASS_BASE + 113\nV4L2_CID_TUNE_ANTENNA_CAPACITOR = V4L2_CID_FM_TX_CLASS_BASE + 114\n\n\n#\n# Tuning\n#\n\nclass v4l2_tuner(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('type', v4l2_tuner_type),\n        ('capability', ctypes.c_uint32),\n        ('rangelow', ctypes.c_uint32),\n        ('rangehigh', ctypes.c_uint32),\n        ('rxsubchans', ctypes.c_uint32),\n        ('audmode', ctypes.c_uint32),\n        ('signal', ctypes.c_int32),\n        ('afc', ctypes.c_int32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nclass v4l2_modulator(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('rangelow', ctypes.c_uint32),\n        ('rangehigh', ctypes.c_uint32),\n        ('txsubchans', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n    ]\n\n\nV4L2_TUNER_CAP_LOW = 0x0001\nV4L2_TUNER_CAP_NORM = 0x0002\nV4L2_TUNER_CAP_STEREO = 0x0010\nV4L2_TUNER_CAP_LANG2 = 0x0020\nV4L2_TUNER_CAP_SAP = 0x0020\nV4L2_TUNER_CAP_LANG1 = 0x0040\nV4L2_TUNER_CAP_RDS = 0x0080\n\nV4L2_TUNER_SUB_MONO = 0x0001\nV4L2_TUNER_SUB_STEREO = 0x0002\nV4L2_TUNER_SUB_LANG2 = 0x0004\nV4L2_TUNER_SUB_SAP = 0x0004\nV4L2_TUNER_SUB_LANG1 = 0x0008\nV4L2_TUNER_SUB_RDS = 0x0010\n\nV4L2_TUNER_MODE_MONO = 0x0000\nV4L2_TUNER_MODE_STEREO = 0x0001\nV4L2_TUNER_MODE_LANG2 = 0x0002\nV4L2_TUNER_MODE_SAP = 0x0002\nV4L2_TUNER_MODE_LANG1 = 0x0003\nV4L2_TUNER_MODE_LANG1_LANG2 = 0x0004\n\n\nclass v4l2_frequency(ctypes.Structure):\n    _fields_ = [\n        ('tuner', ctypes.c_uint32),\n        ('type', v4l2_tuner_type),\n        ('frequency', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 8),\n    ]\n\n\nclass v4l2_hw_freq_seek(ctypes.Structure):\n    _fields_ = [\n        ('tuner', ctypes.c_uint32),\n        ('type', v4l2_tuner_type),\n        ('seek_upward', ctypes.c_uint32),\n        ('wrap_around', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 8),\n    ]\n\n\n#\n# RDS\n#\n\nclass v4l2_rds_data(ctypes.Structure):\n    _fields_ = [\n        ('lsb', ctypes.c_char),\n        ('msb', ctypes.c_char),\n        ('block', ctypes.c_char),\n    ]\n\n    _pack_ = True\n\n\nV4L2_RDS_BLOCK_MSK =  0x7\nV4L2_RDS_BLOCK_A = 0\nV4L2_RDS_BLOCK_B = 1\nV4L2_RDS_BLOCK_C = 2\nV4L2_RDS_BLOCK_D = 3\nV4L2_RDS_BLOCK_C_ALT = 4\nV4L2_RDS_BLOCK_INVALID = 7\n\nV4L2_RDS_BLOCK_CORRECTED = 0x40\nV4L2_RDS_BLOCK_ERROR = 0x80\n\n\n#\n# Audio\n#\n\nclass v4l2_audio(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('mode', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_AUDCAP_STEREO = 0x00001\nV4L2_AUDCAP_AVL = 0x00002\n\nV4L2_AUDMODE_AVL = 0x00001\n\n\nclass v4l2_audioout(ctypes.Structure):\n    _fields_ = [\n        ('index', ctypes.c_uint32),\n        ('name', ctypes.c_char * 32),\n        ('capability', ctypes.c_uint32),\n        ('mode', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\n#\n# Mpeg services (experimental)\n#\n\nV4L2_ENC_IDX_FRAME_I = 0\nV4L2_ENC_IDX_FRAME_P = 1\nV4L2_ENC_IDX_FRAME_B = 2\nV4L2_ENC_IDX_FRAME_MASK = 0xf\n\n\nclass v4l2_enc_idx_entry(ctypes.Structure):\n    _fields_ = [\n        ('offset', ctypes.c_uint64),\n        ('pts', ctypes.c_uint64),\n        ('length', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_ENC_IDX_ENTRIES = 64\n\n\nclass v4l2_enc_idx(ctypes.Structure):\n    _fields_ = [\n        ('entries', ctypes.c_uint32),\n        ('entries_cap', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 4),\n        ('entry', v4l2_enc_idx_entry * V4L2_ENC_IDX_ENTRIES),\n    ]\n\n\nV4L2_ENC_CMD_START = 0\nV4L2_ENC_CMD_STOP = 1\nV4L2_ENC_CMD_PAUSE = 2\nV4L2_ENC_CMD_RESUME = 3\n\nV4L2_ENC_CMD_STOP_AT_GOP_END = 1 << 0\n\n\nclass v4l2_encoder_cmd(ctypes.Structure):\n    class _u(ctypes.Union):\n        class _s(ctypes.Structure):\n            _fields_ = [\n                ('data', ctypes.c_uint32 * 8),\n            ]\n\n        _fields_ = [\n            ('raw', _s),\n        ]\n\n    _fields_ = [\n        ('cmd', ctypes.c_uint32),\n        ('flags', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n\n\n#\n# Data services (VBI)\n#\n\nclass v4l2_vbi_format(ctypes.Structure):\n    _fields_ = [\n        ('sampling_rate', ctypes.c_uint32),\n        ('offset', ctypes.c_uint32),\n        ('samples_per_line', ctypes.c_uint32),\n        ('sample_format', ctypes.c_uint32),\n        ('start', ctypes.c_int32 * 2),\n        ('count', ctypes.c_uint32 * 2),\n        ('flags', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_VBI_UNSYNC = 1 << 0\nV4L2_VBI_INTERLACED = 1 << 1\n\n\nclass v4l2_sliced_vbi_format(ctypes.Structure):\n    _fields_ = [\n        ('service_set', ctypes.c_uint16),\n        ('service_lines', ctypes.c_uint16 * 2 * 24),\n        ('io_size', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32 * 2),\n    ]\n\n\nV4L2_SLICED_TELETEXT_B = 0x0001\nV4L2_SLICED_VPS = 0x0400\nV4L2_SLICED_CAPTION_525 = 0x1000\nV4L2_SLICED_WSS_625 = 0x4000\nV4L2_SLICED_VBI_525 = V4L2_SLICED_CAPTION_525\nV4L2_SLICED_VBI_625 = (\n    V4L2_SLICED_TELETEXT_B | V4L2_SLICED_VPS | V4L2_SLICED_WSS_625)\n\n\nclass v4l2_sliced_vbi_cap(ctypes.Structure):\n    _fields_ = [\n        ('service_set', ctypes.c_uint16),\n        ('service_lines', ctypes.c_uint16 * 2 * 24),\n        ('type', v4l2_buf_type),\n        ('reserved', ctypes.c_uint32 * 3),\n    ]\n\n\nclass v4l2_sliced_vbi_data(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_uint32),\n        ('field', ctypes.c_uint32),\n        ('line', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint32),\n        ('data', ctypes.c_char * 48),\n    ]\n\n\n#\n# Sliced VBI data inserted into MPEG Streams\n#\n\n\nV4L2_MPEG_VBI_IVTV_TELETEXT_B = 1\nV4L2_MPEG_VBI_IVTV_CAPTION_525 = 4\nV4L2_MPEG_VBI_IVTV_WSS_625 = 5\nV4L2_MPEG_VBI_IVTV_VPS = 7\n\n\nclass v4l2_mpeg_vbi_itv0_line(ctypes.Structure):\n    _fields_ = [\n        ('id', ctypes.c_char),\n        ('data', ctypes.c_char * 42),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_mpeg_vbi_itv0(ctypes.Structure):\n    _fields_ = [\n        ('linemask', ctypes.c_uint32 * 2), # how to define __le32 in ctypes?\n        ('line', v4l2_mpeg_vbi_itv0_line * 35),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_mpeg_vbi_ITV0(ctypes.Structure):\n    _fields_ = [\n        ('line', v4l2_mpeg_vbi_itv0_line * 36),\n    ]\n\n    _pack_ = True\n\n\nV4L2_MPEG_VBI_IVTV_MAGIC0 = \"itv0\"\nV4L2_MPEG_VBI_IVTV_MAGIC1 = \"ITV0\"\n\n\nclass v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('itv0', v4l2_mpeg_vbi_itv0),\n            ('ITV0', v4l2_mpeg_vbi_ITV0),\n        ]\n\n    _fields_ = [\n        ('magic', ctypes.c_char * 4),\n        ('_u', _u)\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\n#\n# Aggregate structures\n#\n\nclass v4l2_plane_pix_format(ctypes.Structure):\n    _fields_ = [\n        ('sizeimage', ctypes.c_uint32),\n        ('bytesperline', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint16 * 6)\n    ]\n\nclass v4l2_sdr_format(ctypes.Structure):\n    _fields_ = [\n        ('pixelformat', ctypes.c_uint32),\n        ('buffersize', ctypes.c_uint32),\n        ('reserved', ctypes.c_uint8 * 24)\n    ]\n\nclass v4l2_meta_format(ctypes.Structure):\n    _fields_ = [\n        ('dataformat', ctypes.c_uint32),\n        ('buffersize', ctypes.c_uint32)\n    ]\n\nclass v4l2_pix_format_mplane(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('ycbcr_enc', ctypes.c_uint8),\n            ('hsv_enc', ctypes.c_uint8)\n        ]\n\n    _fields_ = [\n        ('width', ctypes.c_uint32),\n        ('height', ctypes.c_uint32),\n        ('pixelformat', ctypes.c_uint32),\n        ('field', ctypes.c_uint32),\n        ('colorspace', ctypes.c_uint32),\n        ('plane_fmt', v4l2_plane_pix_format * VIDEO_MAX_PLANES),\n        ('num_planes', ctypes.c_uint8),\n        ('flags', ctypes.c_uint8),\n        ('_u', _u),\n        ('quantization', ctypes.c_uint8),\n        ('xfer_func', ctypes.c_uint8),\n        ('reserved', ctypes.c_uint8 * 7)\n    ]\n\nclass v4l2_format(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('pix', v4l2_pix_format),\n            ('pix_mp', v4l2_pix_format_mplane),\n            ('win', v4l2_window),\n            ('vbi', v4l2_vbi_format),\n            ('sliced', v4l2_sliced_vbi_format),\n            ('sdr', v4l2_sdr_format),\n            ('meta', v4l2_meta_format),\n            ('raw_data', ctypes.c_char * 200)\n        ]\n\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('fmt', _u)\n    ]\n\n\nclass v4l2_streamparm(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('capture', v4l2_captureparm),\n            ('output', v4l2_outputparm),\n            ('raw_data', ctypes.c_char * 200),\n        ]\n\n    _fields_ = [\n        ('type', v4l2_buf_type),\n        ('parm', _u)\n    ]\n\n\n#\n# Advanced debugging\n#\n\nV4L2_CHIP_MATCH_HOST = 0\nV4L2_CHIP_MATCH_I2C_DRIVER = 1\nV4L2_CHIP_MATCH_I2C_ADDR = 2\nV4L2_CHIP_MATCH_AC97 = 3\n\n\nclass v4l2_dbg_match(ctypes.Structure):\n    class _u(ctypes.Union):\n        _fields_ = [\n            ('addr', ctypes.c_uint32),\n            ('name', ctypes.c_char * 32),\n        ]\n\n    _fields_ = [\n        ('type', ctypes.c_uint32),\n        ('_u', _u),\n    ]\n\n    _anonymous_ = ('_u',)\n    _pack_ = True\n\n\nclass v4l2_dbg_register(ctypes.Structure):\n    _fields_ = [\n        ('match', v4l2_dbg_match),\n        ('size', ctypes.c_uint32),\n        ('reg', ctypes.c_uint64),\n        ('val', ctypes.c_uint64),\n    ]\n\n    _pack_ = True\n\n\nclass v4l2_dbg_chip_ident(ctypes.Structure):\n    _fields_ = [\n        ('match', v4l2_dbg_match),\n        ('ident', ctypes.c_uint32),\n        ('revision', ctypes.c_uint32),\n    ]\n\n    _pack_ = True\n\n\n#\n# ioctl codes for video devices\n#\n\nVIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability)\nVIDIOC_RESERVED = _IO('V', 1)\nVIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc)\nVIDIOC_G_FMT = _IOWR('V', 4, v4l2_format)\nVIDIOC_S_FMT = _IOWR('V', 5, v4l2_format)\nVIDIOC_REQBUFS = _IOWR('V', 8, v4l2_requestbuffers)\nVIDIOC_QUERYBUF\t= _IOWR('V', 9, v4l2_buffer)\nVIDIOC_G_FBUF = _IOR('V', 10, v4l2_framebuffer)\nVIDIOC_S_FBUF = _IOW('V', 11, v4l2_framebuffer)\nVIDIOC_OVERLAY = _IOW('V', 14, ctypes.c_int)\nVIDIOC_QBUF = _IOWR('V', 15, v4l2_buffer)\nVIDIOC_DQBUF = _IOWR('V', 17, v4l2_buffer)\nVIDIOC_STREAMON = _IOW('V', 18, ctypes.c_int)\nVIDIOC_STREAMOFF = _IOW('V', 19, ctypes.c_int)\nVIDIOC_G_PARM = _IOWR('V', 21, v4l2_streamparm)\nVIDIOC_S_PARM = _IOWR('V', 22, v4l2_streamparm)\nVIDIOC_G_STD = _IOR('V', 23, v4l2_std_id)\nVIDIOC_S_STD = _IOW('V', 24, v4l2_std_id)\nVIDIOC_ENUMSTD = _IOWR('V', 25, v4l2_standard)\nVIDIOC_ENUMINPUT = _IOWR('V', 26, v4l2_input)\nVIDIOC_G_CTRL = _IOWR('V', 27, v4l2_control)\nVIDIOC_S_CTRL = _IOWR('V', 28, v4l2_control)\nVIDIOC_G_TUNER = _IOWR('V', 29, v4l2_tuner)\nVIDIOC_S_TUNER = _IOW('V', 30, v4l2_tuner)\nVIDIOC_G_AUDIO = _IOR('V', 33, v4l2_audio)\nVIDIOC_S_AUDIO = _IOW('V', 34, v4l2_audio)\nVIDIOC_QUERYCTRL = _IOWR('V', 36, v4l2_queryctrl)\nVIDIOC_QUERYMENU = _IOWR('V', 37, v4l2_querymenu)\nVIDIOC_G_INPUT = _IOR('V', 38, ctypes.c_int)\nVIDIOC_S_INPUT = _IOWR('V', 39, ctypes.c_int)\nVIDIOC_G_OUTPUT = _IOR('V', 46, ctypes.c_int)\nVIDIOC_S_OUTPUT = _IOWR('V', 47, ctypes.c_int)\nVIDIOC_ENUMOUTPUT = _IOWR('V', 48, v4l2_output)\nVIDIOC_G_AUDOUT = _IOR('V', 49, v4l2_audioout)\nVIDIOC_S_AUDOUT\t= _IOW('V', 50, v4l2_audioout)\nVIDIOC_G_MODULATOR = _IOWR('V', 54, v4l2_modulator)\nVIDIOC_S_MODULATOR = _IOW('V', 55, v4l2_modulator)\nVIDIOC_G_FREQUENCY = _IOWR('V', 56, v4l2_frequency)\nVIDIOC_S_FREQUENCY = _IOW('V', 57, v4l2_frequency)\nVIDIOC_CROPCAP = _IOWR('V', 58, v4l2_cropcap)\nVIDIOC_G_CROP = _IOWR('V', 59, v4l2_crop)\nVIDIOC_S_CROP = _IOW('V', 60, v4l2_crop)\nVIDIOC_G_JPEGCOMP = _IOR('V', 61, v4l2_jpegcompression)\nVIDIOC_S_JPEGCOMP = _IOW('V', 62, v4l2_jpegcompression)\nVIDIOC_QUERYSTD = _IOR('V', 63, v4l2_std_id)\nVIDIOC_TRY_FMT = _IOWR('V', 64, v4l2_format)\nVIDIOC_ENUMAUDIO = _IOWR('V', 65, v4l2_audio)\nVIDIOC_ENUMAUDOUT = _IOWR('V', 66, v4l2_audioout)\nVIDIOC_G_PRIORITY = _IOR('V', 67, v4l2_priority)\nVIDIOC_S_PRIORITY = _IOW('V', 68, v4l2_priority)\nVIDIOC_G_SLICED_VBI_CAP = _IOWR('V', 69, v4l2_sliced_vbi_cap)\nVIDIOC_LOG_STATUS = _IO('V', 70)\nVIDIOC_G_EXT_CTRLS = _IOWR('V', 71, v4l2_ext_controls)\nVIDIOC_S_EXT_CTRLS = _IOWR('V', 72, v4l2_ext_controls)\nVIDIOC_TRY_EXT_CTRLS = _IOWR('V', 73, v4l2_ext_controls)\n\nVIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum)\nVIDIOC_ENUM_FRAMEINTERVALS = _IOWR('V', 75, v4l2_frmivalenum)\nVIDIOC_G_ENC_INDEX = _IOR('V', 76, v4l2_enc_idx)\nVIDIOC_ENCODER_CMD = _IOWR('V', 77, v4l2_encoder_cmd)\nVIDIOC_TRY_ENCODER_CMD = _IOWR('V', 78, v4l2_encoder_cmd)\n\nVIDIOC_DBG_S_REGISTER = _IOW('V', 79, v4l2_dbg_register)\nVIDIOC_DBG_G_REGISTER = _IOWR('V', 80, v4l2_dbg_register)\n\nVIDIOC_DBG_G_CHIP_IDENT = _IOWR('V', 81, v4l2_dbg_chip_ident)\n\nVIDIOC_S_HW_FREQ_SEEK = _IOW('V', 82, v4l2_hw_freq_seek)\nVIDIOC_ENUM_DV_PRESETS = _IOWR('V', 83, v4l2_dv_enum_preset)\nVIDIOC_S_DV_PRESET = _IOWR('V', 84, v4l2_dv_preset)\nVIDIOC_G_DV_PRESET = _IOWR('V', 85, v4l2_dv_preset)\nVIDIOC_QUERY_DV_PRESET = _IOR('V', 86, v4l2_dv_preset)\nVIDIOC_S_DV_TIMINGS = _IOWR('V', 87, v4l2_dv_timings)\nVIDIOC_G_DV_TIMINGS = _IOWR('V', 88, v4l2_dv_timings)\n\nVIDIOC_OVERLAY_OLD = _IOWR('V', 14, ctypes.c_int)\nVIDIOC_S_PARM_OLD = _IOW('V', 22, v4l2_streamparm)\nVIDIOC_S_CTRL_OLD = _IOW('V', 28, v4l2_control)\nVIDIOC_G_AUDIO_OLD = _IOWR('V', 33, v4l2_audio)\nVIDIOC_G_AUDOUT_OLD = _IOWR('V', 49, v4l2_audioout)\nVIDIOC_CROPCAP_OLD = _IOR('V', 58, v4l2_cropcap)\n\nBASE_VIDIOC_PRIVATE = 192\n\n\n\n\nv4l2_colorspace_dict = {0:'DEFAULT',\n                        1:'SMPTE170M',\n                        2:'SMPTE240M',\n                        3:'REC709',\n                        4:'BT878',\n                        5:'470_SYSTEM_M',\n                        6:'470_SYSTEM_BG',\n                        7:'JPEG',\n                        8:'SRGB',\n                        9:'ADOBERGB',\n                        10:'BT2020',\n                        11:'RAW',\n                        12:'DCI_P3'}\n\n\n\nv4l2_field_dict = {0:'ANY',\n                   1:'NONE',\n                   2:'TOP',\n                   3:'BOTTOM',\n                   4:'INTERLACED',\n                   5:'SEQ_TB',\n                   6:'SEQ_BT',\n                   7:'ALTERNATE',\n                   8:'INTERLACED_TB',\n                   9:'INTERLACED_BT'}\n\n\n\nv4l2_CID_dict = {V4L2_CID_BRIGHTNESS:'V4L2_CID_BRIGHTNESS',\n                       V4L2_CID_CONTRAST:'V4L2_CID_CONTRAST',\n                       V4L2_CID_SATURATION:'V4L2_CID_SATURATION',\n                       V4L2_CID_HUE:'V4L2_CID_HUE',\n                       V4L2_CID_AUTO_WHITE_BALANCE:'V4L2_CID_AUTO_WHITE_BALANCE',\n                       V4L2_CID_GAMMA:'V4L2_CID_GAMMA',\n                       V4L2_CID_GAIN:'V4L2_CID_GAIN',\n                       V4L2_CID_POWER_LINE_FREQUENCY:'V4L2_CID_POWER_LINE_FREQUENCY',\n                       V4L2_CID_WHITE_BALANCE_TEMPERATURE:'V4L2_CID_WHITE_BALANCE_TEMPERATURE',\n                       V4L2_CID_SHARPNESS:'V4L2_CID_SHARPNESS',\n                       V4L2_CID_BACKLIGHT_COMPENSATION:'V4L2_CID_BACKLIGHT_COMPENSATION',\n                       V4L2_CID_EXPOSURE_AUTO:'V4L2_CID_EXPOSURE_AUTO',\n                       V4L2_CID_EXPOSURE_ABSOLUTE:'V4L2_CID_EXPOSURE_ABSOLUTE',\n                       V4L2_CID_EXPOSURE_AUTO_PRIORITY:'V4L2_CID_EXPOSURE_AUTO_PRIORITY'}\n\n\n\nv4l2_CTRL_FLAG_dict = {NONE:'NONE',\n                       V4L2_CTRL_FLAG_DISABLED:'V4L2_CTRL_FLAG_DISABLED',\n                       V4L2_CTRL_FLAG_GRABBED:'V4L2_CTRL_FLAG_GRABBED',\n                       V4L2_CTRL_FLAG_READ_ONLY:'V4L2_CTRL_FLAG_READ_ONLY',\n                       V4L2_CTRL_FLAG_UPDATE:'V4L2_CTRL_FLAG_UPDATE',\n                       V4L2_CTRL_FLAG_INACTIVE:'V4L2_CTRL_FLAG_INACTIVE',\n                       V4L2_CTRL_FLAG_SLIDER:'V4L2_CTRL_FLAG_SLIDER',\n                       V4L2_CTRL_FLAG_WRITE_ONLY:'V4L2_CTRL_FLAG_WRITE_ONLY',\n                       V4L2_CTRL_FLAG_VOLATILE:'V4L2_CTRL_FLAG_VOLATILE',\n                       V4L2_CTRL_FLAG_HAS_PAYLOAD:'V4L2_CTRL_FLAG_HAS_PAYLOAD',\n                       V4L2_CTRL_FLAG_EXECUTE_ON_WRITE:'V4L2_CTRL_FLAG_EXECUTE_ON_WRITE',\n                       V4L2_CTRL_FLAG_MODIFY_LAYOUT:'V4L2_CTRL_FLAG_MODIFY_LAYOUT',\n                       V4L2_CTRL_FLAG_NEXT_CTRL:'V4L2_CTRL_FLAG_NEXT_CTRL',\n                       V4L2_CTRL_FLAG_NEXT_COMPOUND:'V4L2_CTRL_FLAG_NEXT_COMPOUND'}\n\n\n\nv4l2_ctrl_type_dict = {V4L2_CTRL_TYPE_INTEGER:'V4L2_CTRL_TYPE_INTEGER',\n                       V4L2_CTRL_TYPE_BOOLEAN:'V4L2_CTRL_TYPE_BOOLEAN',\n                       V4L2_CTRL_TYPE_MENU:'V4L2_CTRL_TYPE_MENU',\n                       V4L2_CTRL_TYPE_BUTTON:'V4L2_CTRL_TYPE_BUTTON',\n                       V4L2_CTRL_TYPE_INTEGER64:'V4L2_CTRL_TYPE_INTEGER64',\n                       V4L2_CTRL_TYPE_CTRL_CLASS:'V4L2_CTRL_TYPE_CTRL_CLASS',\n                       V4L2_CTRL_TYPE_STRING:'V4L2_CTRL_TYPE_STRING',\n                       V4L2_CTRL_TYPE_BITMASK:'V4L2_CTRL_TYPE_BITMASK',\n                       V4L2_CTRL_TYPE_INTEGER_MENU:'V4L2_CTRL_TYPE_INTEGER_MENU',\n                       V4L2_CTRL_COMPOUND_TYPES:'V4L2_CTRL_COMPOUND_TYPES',\n                       V4L2_CTRL_TYPE_U8:'V4L2_CTRL_TYPE_U8',\n                       V4L2_CTRL_TYPE_U16:'V4L2_CTRL_TYPE_U16',\n                       V4L2_CTRL_TYPE_U32:'V4L2_CTRL_TYPE_U32'}\n\n\n\nv4l2_capabilities_dict = {V4L2_CAP_VIDEO_CAPTURE:'V4L2_CAP_VIDEO_CAPTURE',\n                          V4L2_CAP_VIDEO_OUTPUT:'V4L2_CAP_VIDEO_OUTPUT',\n                          V4L2_CAP_VIDEO_OVERLAY:'V4L2_CAP_VIDEO_OVERLAY',\n                          V4L2_CAP_VBI_CAPTURE:'V4L2_CAP_VBI_CAPTURE',\n                          V4L2_CAP_VBI_OUTPUT:'V4L2_CAP_VBI_OUTPUT',\n                          V4L2_CAP_SLICED_VBI_CAPTURE:'V4L2_CAP_SLICED_VBI_CAPTURE',\n                          V4L2_CAP_SLICED_VBI_OUTPUT:'V4L2_CAP_SLICED_VBI_OUTPUT',\n                          V4L2_CAP_RDS_CAPTURE:'V4L2_CAP_RDS_CAPTURE',\n                          V4L2_CAP_VIDEO_OUTPUT_OVERLAY:'V4L2_CAP_VIDEO_OUTPUT_OVERLAY',\n                          V4L2_CAP_HW_FREQ_SEEK:'V4L2_CAP_HW_FREQ_SEEK',\n                          V4L2_CAP_RDS_OUTPUT:'V4L2_CAP_RDS_OUTPUT',\n                          V4L2_CAP_VIDEO_CAPTURE_MPLANE:'V4L2_CAP_VIDEO_CAPTURE_MPLANE',\n                          V4L2_CAP_VIDEO_OUTPUT_MPLANE:'V4L2_CAP_VIDEO_OUTPUT_MPLANE',\n                          V4L2_CAP_VIDEO_M2M_MPLANE:'V4L2_CAP_VIDEO_M2M_MPLANE',\n                          V4L2_CAP_VIDEO_M2M:'V4L2_CAP_VIDEO_M2M',\n                          V4L2_CAP_TUNER:'V4L2_CAP_TUNER',\n                          V4L2_CAP_AUDIO:'V4L2_CAP_AUDIO',\n                          V4L2_CAP_RADIO:'V4L2_CAP_RADIO',\n                          V4L2_CAP_MODULATOR:'V4L2_CAP_MODULATOR',\n                          V4L2_CAP_SDR_CAPTURE:'V4L2_CAP_SDR_CAPTURE',\n                          V4L2_CAP_EXT_PIX_FORMAT:'V4L2_CAP_EXT_PIX_FORMAT',\n                          V4L2_CAP_SDR_OUTPUT:'V4L2_CAP_SDR_OUTPUT',\n                          V4L2_CAP_META_CAPTURE:'V4L2_CAP_META_CAPTURE',\n                          V4L2_CAP_READWRITE:'V4L2_CAP_READWRITE',\n                          V4L2_CAP_ASYNCIO:'V4L2_CAP_ASYNCIO',\n                          V4L2_CAP_STREAMING:'V4L2_CAP_STREAMING',\n                          V4L2_CAP_TOUCH:'V4L2_CAP_TOUCH',\n                          V4L2_CAP_DEVICE_CAPS:'V4L2_CAP_DEVICE_CAPS'}\n\n\nv4l2_BUF_TYPE_dict = {V4L2_BUF_TYPE_VIDEO_CAPTURE:'V4L2_BUF_TYPE_VIDEO_CAPTURE',\n                      V4L2_BUF_TYPE_VIDEO_OUTPUT:'V4L2_BUF_TYPE_VIDEO_OUTPUT',\n                      V4L2_BUF_TYPE_VIDEO_OVERLAY:'V4L2_BUF_TYPE_VIDEO_OVERLAY',\n                      V4L2_BUF_TYPE_VBI_CAPTURE:'V4L2_BUF_TYPE_VBI_CAPTURE',\n                      V4L2_BUF_TYPE_VBI_OUTPUT:'V4L2_BUF_TYPE_VBI_OUTPUT',\n                      V4L2_BUF_TYPE_SLICED_VBI_CAPTURE:'V4L2_BUF_TYPE_SLICED_VBI_CAPTURE',\n                      V4L2_BUF_TYPE_SLICED_VBI_OUTPUT:'V4L2_BUF_TYPE_SLICED_VBI_OUTPUT',\n                      V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY:'V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY',\n                      V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE:'V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE',\n                      V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE:'V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE',\n                      V4L2_BUF_TYPE_SDR_CAPTURE:'V4L2_BUF_TYPE_SDR_CAPTURE',\n                      V4L2_BUF_TYPE_SDR_OUTPUT:'V4L2_BUF_TYPE_SDR_OUTPUT',\n                      V4L2_BUF_TYPE_META_CAPTURE:'V4L2_BUF_TYPE_META_CAPTURE',\n                      V4L2_BUF_TYPE_PRIVATE:'V4L2_BUF_TYPE_PRIVATE'}\n\n\nv4l2_MEMORY_dict = {V4L2_MEMORY_MMAP:'V4L2_MEMORY_MMAP',\n                    V4L2_MEMORY_USERPTR:'V4L2_MEMORY_USERPTR',\n                    V4L2_MEMORY_OVERLAY:'V4L2_MEMORY_OVERLAY',\n                    V4L2_MEMORY_DMABUF:'V4L2_MEMORY_DMABUF'}\n"
  },
  {
    "path": "prusa/link/cameras/v4l2_driver.py",
    "content": "\"\"\"Contains implementation of a camera driver utilizing the V4L2 API\"\"\"\nimport ctypes\nimport errno\nimport fcntl\nimport fractions\nimport logging\nimport os\nimport pathlib\nimport re\nimport select\nfrom glob import glob\nfrom types import MappingProxyType\nfrom typing import Any, NamedTuple\n\nfrom prusa.connect.printer.camera import Resolution\nfrom prusa.connect.printer.camera_driver import CameraDriver\nfrom prusa.connect.printer.const import (\n    CAMERA_WAIT_TIMEOUT,\n    CapabilityType,\n    NotSupported,\n)\n\nfrom ..util import is_potato_cpu, prctl_name\nfrom . import v4l2\nfrom .encoders import BufferDetails, MJPEGEncoder, get_appropriate_encoder\nfrom .v4l2 import (\n    V4L2_CID_FOCUS_ABSOLUTE,\n    V4L2_CID_FOCUS_AUTO,\n    VIDIOC_QUERYCTRL,\n    VIDIOC_S_CTRL,\n    v4l2_control,\n    v4l2_queryctrl,\n)\n\nlog = logging.getLogger(__name__)\n\n\n# --- code taken from v4l2py, unused features cut\n\nclass Info(NamedTuple):\n    \"\"\"Contains information about the device\"\"\"\n    driver: Any\n    card: Any\n    bus_info: Any\n    version: Any\n    physical_capabilities: Any\n    capabilities: Any\n    formats: Any\n    frame_sizes: Any\n    focus_info: Any\n\n\nclass ImageFormat(NamedTuple):\n    \"\"\"Contains information about a specific image format\"\"\"\n    type: Any\n    description: Any\n    flags: Any\n    pixel_format: Any\n\n\nclass FrameType(NamedTuple):\n    \"\"\"Contains information about a specific frame type\"\"\"\n    pixel_format: Any\n    width: Any\n    height: Any\n\n\nclass FocusInfo(NamedTuple):\n    \"\"\"Contains information about the focus capabilities of the device\"\"\"\n    available: Any\n    min: Any\n    max: Any\n    step: Any\n\n\nSTREAM_TYPE = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE\nIGNORED_BUS_INFO_REGEX = re.compile(\n    r\"(platform:[0-9a-fA-F]+\\.csi)|(platform:bcm2835-isp)\")\n\n\ndef frame_sizes(file_descriptor, pixel_formats):\n    \"\"\"Gets a list of frame sizes for a specified pixel format\"\"\"\n    size = v4l2.v4l2_frmsizeenum()\n    sizes = []\n    for pixel_format in pixel_formats:\n        size.pixel_format = pixel_format\n        size.index = 0\n        while True:\n            try:\n                fcntl.ioctl(\n                    file_descriptor, v4l2.VIDIOC_ENUM_FRAMESIZES, size)\n            except OSError:\n                break\n            if size.type == v4l2.V4L2_FRMSIZE_TYPE_DISCRETE:\n                sizes.append(FrameType(\n                    pixel_format=pixel_format,\n                    width=size.discrete.width,\n                    height=size.discrete.height,\n                ))\n            size.index += 1\n    return sizes\n\n\ndef read_capabilities(file_descriptor):\n    \"\"\"Reads device capabilities in the raw flag format\"\"\"\n    caps = v4l2.v4l2_capability()\n    fcntl.ioctl(file_descriptor, v4l2.VIDIOC_QUERYCAP, caps)\n    return caps\n\n\ndef read_info(filename):\n    \"\"\"Reads device specific info needed for device initialization\"\"\"\n    with fopen(filename) as file_descriptor:\n        caps = read_capabilities(file_descriptor)\n        version_tuple = (\n            (caps.version & 0xFF0000) >> 16,\n            (caps.version & 0x00FF00) >> 8,\n            (caps.version & 0x0000FF),\n        )\n        version_str = \".\".join(map(str, version_tuple))\n        device_capabilities = caps.capabilities\n\n        formats = []\n        pixel_formats = set()\n\n        fmt = v4l2.v4l2_fmtdesc()\n        fmt.type = STREAM_TYPE\n        for index in range(128):\n            fmt.index = index\n            try:\n                fcntl.ioctl(file_descriptor, v4l2.VIDIOC_ENUM_FMT, fmt)\n            except OSError as error:\n                if error.errno == errno.EINVAL:\n                    break\n                raise\n            try:\n                pixel_format = fmt.pixelformat\n            except ValueError:\n                continue\n            formats.append(\n                ImageFormat(\n                    type=STREAM_TYPE,\n                    flags=fmt.flags,\n                    description=fmt.description.decode(),\n                    pixel_format=pixel_format,\n                ),\n            )\n            pixel_formats.add(pixel_format)\n\n        focus_info = None\n\n        focus_auto = v4l2_queryctrl()\n        focus_auto.id = V4L2_CID_FOCUS_AUTO\n\n        focus_absolute = v4l2_queryctrl()\n        focus_absolute.id = V4L2_CID_FOCUS_ABSOLUTE\n\n        try:\n            if fcntl.ioctl(file_descriptor, VIDIOC_QUERYCTRL, focus_auto) != 0:\n                raise RuntimeError(\"Unable to get focus auto\")\n            if fcntl.ioctl(\n                    file_descriptor, VIDIOC_QUERYCTRL, focus_absolute) != 0:\n                raise RuntimeError(\"Unable to get focus absolute\")\n        except (OSError, RuntimeError):\n            focus_info = FocusInfo(\n                available=False,\n                min=None,\n                max=None,\n                step=None,\n            )\n        else:\n            focus_info = FocusInfo(\n                available=True,\n                min=focus_absolute.minimum,\n                max=focus_absolute.maximum,\n                step=focus_absolute.step,\n            )\n\n        return Info(\n            driver=caps.driver.decode(),\n            card=caps.card.decode(),\n            bus_info=caps.bus_info.decode(),\n            version=version_str,\n            physical_capabilities=caps.capabilities,\n            capabilities=device_capabilities,\n            formats=formats,\n            frame_sizes=frame_sizes(file_descriptor, pixel_formats),\n            focus_info=focus_info,\n        )\n\n\ndef fopen(path, write=False):\n    \"\"\"Opens a specified video device file\"\"\"\n    return open(path, \"rb+\" if write else \"rb\", buffering=0, opener=opener)\n\n\ndef opener(path, flags):\n    \"\"\"Adds flags for the open function\"\"\"\n    return os.open(path, flags | os.O_NONBLOCK)\n\n\ndef iter_video_files(path=\"/dev\"):\n    \"\"\"Iterates over the linux detected video files under /dev\"\"\"\n    path = pathlib.Path(path)\n    return path.glob(\"video*\")\n\n\ndef iter_devices(path=\"/dev\"):\n    \"\"\"Returns a tuple of all detected video devices as an objects\"\"\"\n    return (V4L2Camera(name) for name in iter_video_files(path=path))\n\n\ndef iter_video_capture_devices(path=\"/dev\"):\n    \"\"\"Returns all video devices that report the ability to capture video\"\"\"\n    def filt(filename):\n        with fopen(filename) as fobj:\n            caps = read_capabilities(fobj.fileno())\n            return v4l2.V4L2_CAP_VIDEO_CAPTURE & caps.capabilities\n\n    return (V4L2Camera(name) for name in filter(filt, iter_video_files(path)))\n\n\n# --- Video device\n\nclass MediaDeviceInfo(ctypes.Structure):\n    \"\"\"A data structure for getting media device info\"\"\"\n    _fields_ = (\n        (\"driver\", ctypes.c_char * 16),\n        (\"model\", ctypes.c_char * 32),\n        (\"serial\", ctypes.c_char * 40),\n        (\"bus_info\", ctypes.c_char * 32),\n        (\"media_version\", ctypes.c_uint32),\n        (\"hw_revision\", ctypes.c_uint32),\n        (\"driver_version\", ctypes.c_uint32),\n        (\"reserved\", ctypes.c_uint32 * 31),\n    )\n\n\nSUPPORTED_PIXEL_FORMATS = {v4l2.V4L2_PIX_FMT_MJPEG, v4l2.V4L2_PIX_FMT_YUYV}\nBYTES_PER_PIXEL = {v4l2.V4L2_PIX_FMT_YUYV: 2}\n\n\n# pylint: disable=protected-access\nMEDIA_IOC_DEVICE_INFO = v4l2._IOWR('|', 0x00, MediaDeviceInfo)\n\n\ndef read_media_device_info(path):\n    \"\"\"Given a media device path, reads its associated info\n    :raises PermissionError\"\"\"\n    info = MediaDeviceInfo()\n    # pylint: disable=unspecified-encoding\n    with open(path, \"r\") as file:\n        file_descriptor = file.fileno()\n        if fcntl.ioctl(file_descriptor, MEDIA_IOC_DEVICE_INFO, info):\n            raise RuntimeError(\"Failed getting media device info \"\n                               f\"for device {path}\")\n    return info\n\n\nclass V4L2Camera:\n    \"\"\"An object allowing us to easily control a camera\"\"\"\n\n    buffer_type = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE\n    # To support more, more coding is needed\n    buffer_size = 1\n\n    def __init__(self, path):\n        self.path = pathlib.Path(path)\n\n        self.width = None\n        self.height = None\n        self.pixel_format = v4l2.V4L2_PIX_FMT_MJPEG\n        self.fps = None\n\n        self.info = read_info(self.path)\n        self.buffer_details = None\n        self._file_object = None\n\n        if not v4l2.V4L2_CAP_VIDEO_CAPTURE & self.info.capabilities:\n            raise RuntimeError(\"This device cannot capture video\")\n\n    def _ioctl(self, request, arg: Any = 0):\n        \"\"\"A helper method to call a linux kernel function\"\"\"\n        return fcntl.ioctl(self._file_object, request, arg)\n\n    @property\n    def is_stopped(self):\n        \"\"\"Is the driver currently operating or not\"\"\"\n        return self._file_object is None\n\n    def _set_format(self):\n        \"\"\"Uses the V4L2 api to set the stream format\"\"\"\n        f = v4l2.v4l2_format()\n        f.type = self.buffer_type\n        if self.width is None or self.height is None:\n            self._ioctl(v4l2.VIDIOC_G_FMT, f)\n            self.width = f.fmt.pix.width\n            self.height = f.fmt.pix.height\n            self.pixel_format = f.fmt.pix.pixelformat\n        else:\n            f.fmt.pix.pixelformat = self.pixel_format\n            f.fmt.pix.field = v4l2.V4L2_FIELD_ANY\n            f.fmt.pix.width = self.width\n            f.fmt.pix.height = self.height\n            f.fmt.pix.bytesperline = 0\n        return self._ioctl(v4l2.VIDIOC_S_FMT, f)\n\n    def _set_fps(self):\n        \"\"\"Uses the V4L2 API to set the fps, leaves the default\n         if None is provided\"\"\"\n        stream_params = v4l2.v4l2_streamparm()\n        stream_params.type = self.buffer_type\n        if self.fps is None:\n            self._ioctl(v4l2.VIDIOC_G_PARM, stream_params)\n            self.fps = (stream_params.parm.capture.timeperframe.numerator /\n                        stream_params.parm.capture.timeperframe.denominator)\n        else:\n            fps = fractions.Fraction(self.fps)\n            stream_params.parm.capture.timeperframe.numerator = fps.denominator\n            stream_params.parm.capture.timeperframe.denominator = fps.numerator\n        return self._ioctl(v4l2.VIDIOC_S_PARM, stream_params)\n\n    def _buffer_request(self, count=1):\n        \"\"\"Requests either zero or one buffer to be prepared\n\n        the zero is to de-allocate existing ones\"\"\"\n        if count > 1:\n            raise RuntimeError(\"We don't support more buffers\")\n        buffer_request = v4l2.v4l2_requestbuffers()\n        buffer_request.count = self.buffer_size  # only one is supported\n        buffer_request.type = self.buffer_type\n        buffer_request.memory = v4l2.V4L2_MEMORY_MMAP\n        self._ioctl(v4l2.VIDIOC_REQBUFS, buffer_request)\n\n        if not buffer_request.count:\n            raise IOError(\"Not enough buffer memory\")\n\n    def _v4l2_buffer(self):\n        \"\"\"Pre-fills a new buffer structure with the correct buffer type\"\"\"\n        buff = v4l2.v4l2_buffer()\n        buff.index = 0\n        buff.type = self.buffer_type\n        buff.memory = v4l2.V4L2_MEMORY_MMAP\n        return buff\n\n    def start(self):\n        \"\"\"Sets up and starts the V4L2 capture, so we can request frames\"\"\"\n        if not self.is_stopped:\n            raise RuntimeError(\"Already running\")\n        self._file_object = fopen(self.path, write=True)\n\n        # Set up the device parameters\n        self._set_format()\n        self._set_fps()\n\n        # Ask for one buffer from the device (can't do more)\n        self._buffer_request(count=1)\n\n        # Query what the buffer looks like and map the memory, so we can look\n        # at its data\n        buffer = self._v4l2_buffer()\n        self._ioctl(v4l2.VIDIOC_QUERYBUF, buffer)\n        self.buffer_details = BufferDetails(self._file_object.fileno(),\n                                            length=buffer.length,\n                                            offset=buffer.m.offset)\n\n        # Turn on the stream\n        btype = v4l2.v4l2_buf_type(self.buffer_type)\n        try:\n            self._ioctl(v4l2.VIDIOC_STREAMON, btype)\n        except OSError as exception:\n            if exception.args[0] == 28:\n                log.error(\n                    \"You have probably plugged too many cameras into a \"\n                    \"Single-TT USB3 (or higher) or a USB2 (or lower) USB hub. \"\n                    \"This guy explains it quite well https://www.amazon.com/\"\n                    \"review/R12F7RYUKPCQX7/?ie=UTF8 \")\n            raise\n\n        if self.info.focus_info.available:\n            # Set the focus to absolute\n            self._ioctl(v4l2.VIDIOC_S_CTRL,\n                        v4l2.v4l2_control(id=V4L2_CID_FOCUS_AUTO,\n                                          value=0),\n                        )\n\n    def stop(self):\n        \"\"\"Stops all V4L2 capturing activity and frees everything\"\"\"\n        if self.is_stopped:\n            raise RuntimeError(\"Already stopped\")\n\n        btype = v4l2.v4l2_buf_type(self.buffer_type)\n        self._ioctl(v4l2.VIDIOC_STREAMOFF, btype)\n\n        # Request there be 0 buffers ready - deallocate them\n        self._buffer_request(count=0)\n        if self.buffer_details is not None:\n            self.buffer_details.mmap.close()\n        if self._file_object is not None:\n            self._file_object.close()\n            self._file_object = None\n\n    def next_frame(self):\n        \"\"\"Asks for the next frame, leaves the buffer memory accessible\n        from the outside, returns the buffer details\"\"\"\n        buffer = self._v4l2_buffer()\n        self._ioctl(v4l2.VIDIOC_QBUF, buffer)\n\n        # The same piece of code in picamera driver broke,\n        # this one seems to work fine\n        events, *_ = select.select((self._file_object,),\n                                   (), (), CAMERA_WAIT_TIMEOUT)\n        if not events:\n            raise TimeoutError(\"Getting the next frame timed out\")\n        self._ioctl(v4l2.VIDIOC_DQBUF, buffer)\n        return buffer\n\n    def set_focus(self, value):\n        \"\"\"Sets absolute focus - source value from 0 to 1\"\"\"\n        value_range = self.info.focus_info.max - self.info.focus_info.min\n        scaled_value = value * value_range\n        value_in_step = (scaled_value\n                         - (scaled_value % self.info.focus_info.step))\n        final_value = int(value_in_step + self.info.focus_info.min)\n\n        # Create a v4l2_control structure with the control ID and value\n        control = v4l2_control()\n        control.id = V4L2_CID_FOCUS_ABSOLUTE\n        control.value = final_value\n\n        # Use the ioctl call to set the control value\n        if self._ioctl(VIDIOC_S_CTRL, control) != 0:\n            raise RuntimeError(\"Unable to set control value\")\n\n\ndef get_media_device_path(device: V4L2Camera):\n    \"\"\"Gets the media device path for a video device\n\n    Pairs /dev/video* to /dev/media*\"\"\"\n    bus_info = device.info.bus_info\n    paths = glob(\"/dev/media*\")\n    for path in paths:\n        try:\n            info = read_media_device_info(path)\n        except PermissionError:\n            log.exception(\"Failed getting a media device for %s. \"\n                          \"This is commonly caused by the linux user \"\n                          \"not being a member of the 'video' group\",\n                          device.path)\n        else:\n            if bus_info == info.bus_info.decode(\"UTF-8\"):\n                return path\n    return None\n\n\ndef param_change(func):\n    \"\"\"Wraps any settings change with a stop and start of the video\n    stream, so the camera driver does not return it's busy\"\"\"\n\n    def inner(self, new_param):\n        # pylint: disable=protected-access\n        self.device.stop()\n        self.encoder.stop()\n        func(self, new_param)\n        self.device.start()\n        self.encoder.source_details = self.device.buffer_details\n        self.encoder.start()\n\n    return inner\n\n\nclass V4L2Driver(CameraDriver):\n    \"\"\"Linux V4L2 USB webcam driver\"\"\"\n\n    name = \"V4L2\"\n    REQUIRES_SETTINGS = MappingProxyType({\n        \"path\": \"Path to the V4L2 device like '/dev/video1'\",\n    })\n\n    @staticmethod\n    def _scan():\n        \"\"\"Implements the mandated scan method, returns available USB\n        cameras\"\"\"\n        available = {}\n        devices = iter_video_capture_devices()\n        for device in devices:\n            # Ignore picameras as they are handled by their own driver\n            if IGNORED_BUS_INFO_REGEX.match(device.info.bus_info) is not None:\n                continue\n\n            if not device.info.formats:\n                continue\n\n            media_device_path = get_media_device_path(device)\n            if media_device_path is None:\n                continue\n\n            path = str(device.path)\n            name = device.info.card\n            try:\n                info = read_media_device_info(media_device_path)\n                serial = info.serial.decode(\"ascii\")\n            except (OSError, PermissionError):\n                log.exception(\"Getting camera sn failed for camera %s at %s\",\n                              name, path)\n                continue\n            else:\n                camera_id = \" \".join((name, serial))\n                log.debug(\"Camera id is %s\", camera_id)\n                available[camera_id] = {\n                    \"path\": path,\n                    \"name\": name,\n                }\n        return available\n\n    def __init__(self, camera_id, config, unavailable_cb):\n        # pylint: disable=duplicate-code\n        super().__init__(camera_id, config, unavailable_cb)\n\n        self._resolution_to_format = {}\n        self.device = None\n        self.stream = None\n        self.encoder = None\n\n    def _connect(self):\n        \"\"\"Connects to the V4L2 camera\"\"\"\n        path = self.config[\"path\"]\n\n        self._capabilities = ({\n            CapabilityType.TRIGGER_SCHEME,\n            CapabilityType.IMAGING,\n            CapabilityType.RESOLUTION,\n        })\n\n        extra_unsupported_formats = set()\n        self.device = V4L2Camera(path)\n        if self.device.info.focus_info.available:\n            self._capabilities.add(CapabilityType.FOCUS)\n            self._config[\"focus\"] = self._config.get(\"focus\", str(0.0))\n\n        self._available_resolutions = set()\n        for frame_type in self.device.info.frame_sizes:\n            resolution = Resolution(width=frame_type.width,\n                                    height=frame_type.height)\n\n            # Prefer MJPEG to others\n            if resolution in self._resolution_to_format:\n                pixel_format = self._resolution_to_format[resolution]\n                if pixel_format == v4l2.V4L2_PIX_FMT_MJPEG:\n                    continue\n\n            pixel_format = frame_type.pixel_format\n            if pixel_format not in SUPPORTED_PIXEL_FORMATS:\n                if pixel_format not in extra_unsupported_formats:\n                    log.debug(\"Pixel format %s not supported\",\n                              pixel_format)\n                extra_unsupported_formats.add(pixel_format)\n                continue\n\n            max_resolution = max(resolution.width, resolution.height)\n            if (pixel_format != v4l2.V4L2_PIX_FMT_MJPEG\n                    and is_potato_cpu()\n                    and max_resolution > MJPEGEncoder.WIDTH_LIMIT):\n                # The format needs to be encoded, but we cannot encode this\n                # using the HW encoder, and our CPU is not good either\n                continue\n\n            self._available_resolutions.add(resolution)\n            self._resolution_to_format[resolution] = pixel_format\n\n        if not self.available_resolutions:\n            raise NotSupported(\n                \"Sorry, PrusaLink supports only YUYV 4:2:2 and MJPEG. \"\n                f\"Camera {self.camera_id} supports only these formats: \"\n                f\"{extra_unsupported_formats}\")\n\n        initial_resolution = self._get_initial_resolution(\n            self._available_resolutions, self._config)\n        self._set_resolution(initial_resolution)\n        self._config[\"resolution\"] = str(initial_resolution)\n\n        self.device.start()\n        self.encoder.start()\n        if CapabilityType.FOCUS in self.capabilities:\n            self.set_focus(float(self._config[\"focus\"]))\n\n    @param_change\n    def set_resolution(self, resolution):\n        \"\"\"Sets the camera resolution\"\"\"\n        self._set_resolution(resolution)\n\n    def _set_resolution(self, resolution):\n        \"\"\"Sets the camera resolution\"\"\"\n        pixel_format = self._resolution_to_format[resolution]\n\n        self.device.width = resolution.width\n        self.device.height = resolution.height\n        self.device.pixel_format = pixel_format\n\n        self.encoder = get_appropriate_encoder(\n            resolution, pixel_format, use_mmap=True)\n        self.encoder.width = resolution.width\n        self.encoder.height = resolution.height\n        self.encoder.stride = (resolution.width\n                               * BYTES_PER_PIXEL.get(pixel_format, 0))\n\n    def set_focus(self, focus):\n        \"\"\"Sets the camera focus\"\"\"\n        self.device.set_focus(focus)\n\n    def take_a_photo(self):\n        \"\"\"Takes a photo, blocking while doing it\"\"\"\n        prctl_name()\n        v4l2_source_buffer = self.device.next_frame()\n        return self.encoder.encode(v4l2_source_buffer.bytesused)\n\n    def _disconnect(self):\n        \"\"\"Disconnects from the camera\"\"\"\n        if self.device is None:\n            return\n        try:\n            self.device.stop()\n        except OSError:\n            log.exception(\"Camera %s could not be closed\",\n                          self.camera_id)\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Camera %s could not be closed - unknown error\",\n                          self.camera_id)\n\n        try:\n            self.encoder.stop()\n        except OSError:\n            log.exception(\"Encoder for %s could not be closed\",\n                          self.camera_id)\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Encoder for %s could not be closed - unknown error\",\n                          self.camera_id)\n"
  },
  {
    "path": "prusa/link/conditions.py",
    "content": "\"\"\"PrusaLink error states.html\n\nFor more information see prusalink_states.txt.\n\"\"\"\n\nfrom typing import Optional\n\nfrom poorwsgi import state\nfrom poorwsgi.response import JSONResponse, TextResponse\nfrom prusa.connect.printer.conditions import (\n    COND_TRACKER,\n    HTTP,\n    INTERNET,\n    TOKEN,\n    Condition,\n    ConditionTracker,\n)\n\nfrom .config import Settings\n\nassert HTTP is not None\nassert TOKEN is not None\n\nOK_MSG = {\"ok\": True, \"message\": \"OK\"}\n\nROOT_COND = Condition(\"Root\", \"The root of everything, it's almost always OK\")\n\nDEVICE = Condition(\"Device\",\n                   \"Eth|WLAN device does not exist\",\n                   short_msg=\"No WLAN device\",\n                   parent=ROOT_COND,\n                   priority=1020)\nPHY = Condition(\"Phy\",\n                \"Eth|WLAN device is not connected\",\n                parent=DEVICE,\n                short_msg=\"No WLAN conn\",\n                priority=1010)\nLAN = Condition(\"Lan\",\n                \"Eth|WLAN has no IP address\",\n                parent=PHY,\n                short_msg=\"No WLAN IP addr\",\n                priority=1000)\n\nINTERNET.set_parent(LAN)\n\nSERIAL = Condition(\"Port\",\n                   \"Serial device does not exist\",\n                   parent=ROOT_COND,\n                   priority=570)\nRPI_ENABLED = Condition(\"RPIenabled\",\n                        \"RPi port is not enabled\",\n                        parent=SERIAL,\n                        priority=560)\nID = Condition(\"ID\",\n               \"Device is not supported\",\n               parent=RPI_ENABLED,\n               priority=550)\nUPGRADED = Condition(\"Upgraded\",\n                     \"Printer upgraded, re-register it\",\n                     parent=ID,\n                     priority=500)\nFW = Condition(\"Firmware\",\n               \"Firmware is not up-to-date\",\n               parent=RPI_ENABLED,\n               priority=540)\nSN = Condition(\"SN\",\n               \"Serial number cannot be obtained\",\n               parent=RPI_ENABLED,\n               priority=530)\nJOB_ID = Condition(\"JobID\",\n                   \"Job ID cannot be obtained\",\n                   parent=RPI_ENABLED,\n                   priority=520)\nHW = Condition(\"HW\",\n               \"Firmware detected a hardware issue\",\n               parent=RPI_ENABLED,\n               priority=510)\n\nCOND_TRACKER.add_tracked_condition_tree(ROOT_COND)\n\nNET_TRACKER = ConditionTracker()\nNET_TRACKER.add_tracked_condition_tree(DEVICE)\n\nPRINTER_TRACKER = ConditionTracker()\nPRINTER_TRACKER.add_tracked_condition_tree(SERIAL)\n\n\ndef use_connect_errors(use_connect):\n    \"\"\"Set whether to use Connect related errors or not\"\"\"\n    if use_connect:\n        COND_TRACKER.add_tracked_condition_tree(INTERNET)\n        NET_TRACKER.add_tracked_condition_tree(INTERNET)\n    else:\n        COND_TRACKER.remove_tracked_condition_tree(INTERNET)\n        NET_TRACKER.remove_tracked_condition_tree(INTERNET)\n\n\ndef status():\n    \"\"\"Return a dict with representation of all current conditions\"\"\"\n    result = {}\n    for condition in reversed(list(ROOT_COND)):\n        result[condition.name] = (condition.state.name, condition.long_msg)\n    return result\n\n\ndef printer_status():\n    \"\"\"Returns a representation of the currently broken printer condition\"\"\"\n    worst = PRINTER_TRACKER.get_worst()\n    if worst is None:\n        return OK_MSG\n    return {\"ok\": False, \"message\": worst.long_msg}\n\n\ndef connect_status():\n    \"\"\"Returns a representation of the currently broken Connect condition\"\"\"\n    worst = NET_TRACKER.get_worst()\n    if worst is None:\n        if not Settings.instance.use_connect():\n            return {\"ok\": True, \"message\": \"Connect isn't configured\"}\n        return OK_MSG\n    return {\"ok\": False, \"message\": worst.long_msg}\n\n\nclass LinkError(RuntimeError):\n    \"\"\"Link error structure.\"\"\"\n    title: str\n    text: str\n    id: Optional[str] = None\n    status_code: int\n    path: Optional[str] = None\n    details: Optional[str] = None\n    url: str = ''\n    use_basic_template = True\n\n    def __init__(self, details: str = \"\"):\n        if details:\n            self.details = details\n        if self.id:\n            self.path = '/error/' + self.id\n        # pylint: disable=consider-using-f-string\n        if self.use_basic_template:\n            self.template = \"error.html\"\n        else:\n            self.template = 'error-%s.html' % self.id\n        super().__init__(self.text)\n\n    def set_url(self, req):\n        \"\"\"Set url from request and self.path.\"\"\"\n        self.url = req.construct_url(self.path) if self.path else ''\n\n    def gen_headers(self):\n        \"\"\"Return headers with Content-Location if id was set.\"\"\"\n        return {'Content-Location': self.url} if self.url else {}\n\n    def json_response(self):\n        \"\"\"Return JSONResponse for error.\"\"\"\n        kwargs = {\n            \"title\": self.title,\n            \"message\": self.text,\n        }\n        if self.url:\n            kwargs['url'] = self.url\n        return JSONResponse(status_code=self.status_code,\n                            headers=self.gen_headers(),\n                            **kwargs)\n\n    def text_response(self):\n        \"\"\"Return TextResponse for error.\"\"\"\n        url = \"\\n\\nSee: \" + self.url if self.url else ''\n        # pylint: disable=consider-using-f-string\n        text_response = \"%s\\n%s\\n%s%s\" % \\\n                        (self.title, self.text,\n                         self.details if self.details else \"\", url)\n        return TextResponse(text_response,\n                            status_code=self.status_code,\n                            headers=self.gen_headers())\n\n\nclass BadRequestError(LinkError):\n    \"\"\"400 Bad Request error\"\"\"\n    status_code = state.HTTP_BAD_REQUEST\n\n\nclass TemperatureTooLow(BadRequestError):\n    \"\"\"400 Temperature is too low\"\"\"\n    title = \"Temperature too low\"\n    text = \"Desired temperature is too low\"\n    id = \"temperature-too-low\"\n\n\nclass TemperatureTooHigh(BadRequestError):\n    \"\"\"400 Temperature is too high\"\"\"\n    title = \"Temperature too high\"\n    text = \"Desired temperature is too high\"\n    id = \"temperature-too-high\"\n\n\nclass ValueTooLow(BadRequestError):\n    \"\"\"400 Generic value is too low\"\"\"\n    title = \"Value too low\"\n    text = \"Desired value is too low\"\n    id = \"value-too-low\"\n\n\nclass ValueTooHigh(BadRequestError):\n    \"\"\"400 Generic value is too high\"\"\"\n    title = \"Value too high\"\n    text = \"Desired value is too high\"\n    id = \"value-too-high\"\n\n\nclass CantMoveAxis(BadRequestError):\n    \"\"\"400 Can't Move Axis\"\"\"\n    title = \"Can't move axis\"\n    text = \"Can't move axis in current state\"\n    id = \"cant-move-axis\"\n\n\nclass CantMoveAxisZ(BadRequestError):\n    \"\"\"400 Can't move axis in current state\"\"\"\n    title = \"Can't Move Axis Z in current state\"\n    text = \"Axis Z can't be moved in current state\"\n    id = \"cant-move-axis-z\"\n\n\nclass DestinationSameAsSource(BadRequestError):\n    \"\"\"400 Destination is same as source\"\"\"\n    title = \"Destination same as source\"\n    text = \"Destination to move file is same as the source of the file\"\n    id = \"destination-same-as-source\"\n\n\nclass NoFileInRequest(BadRequestError):\n    \"\"\"400 File not found in request payload.\"\"\"\n    title = \"Missing file in payload.\"\n    text = \"File is not send in request payload or it hasn't right name.\"\n    id = \"no-file-in-request\"\n\n\nclass FileSizeMismatch(BadRequestError):\n    \"\"\"400 File size mismatch.\"\"\"\n    title = \"File Size Mismatch\"\n    text = \"You sent more or less data than is in Content-Length header.\"\n    id = \"file-size-mismatch\"\n\n\nclass InvalidIniFileFormat(BadRequestError):\n    \"\"\"400 Invalid ini file format.\"\"\"\n    title = \"Invalid ini File Format\"\n    text = \"Format or the structure of your ini file is invalid.\"\n    id = \"invalid-ini-file-format\"\n\n\nclass InvalidBooleanHeader(BadRequestError):\n    \"\"\"400 Invalid Boolean Header\"\"\"\n    title = \"Invalid Boolean Header\"\n    text = \"Invalid Boolean Header according to RFC8941 / 3.3.6\"\n    id = \"invalid-boolean-header\"\n\n\nclass ForbiddenCharacters(BadRequestError):\n    \"\"\"400 Forbidden Characters.\"\"\"\n    title = \"Forbidden Characters\"\n    text = \"Forbidden characters in file or folder name.\"\n    id = \"forbidden-characters\"\n\n\nclass FilenameTooLong(BadRequestError):\n    \"\"\"400 Filename Too Long\"\"\"\n    title = \"Filename Too Long\"\n    text = \"File name length is too long\"\n    id = \"filename-too-long\"\n\n\nclass FoldernameTooLong(BadRequestError):\n    \"\"\"400 Foldername Too Long\"\"\"\n    title = \"Foldername Too Long\"\n    text = \"Folder name length is too long\"\n    id = \"foldername-too-long\"\n\n\nclass FileUploadFailed(BadRequestError):\n    \"\"\"400 File Upload Failed\"\"\"\n    title = \"File Upload Failed\"\n    text = \"File upload has failed\"\n    id = \"file-upload-failed\"\n\n\nclass CantConnect(BadRequestError):\n    \"\"\"400 Can't connect to PrusaConnect\"\"\"\n    title = \"Can't Connect\"\n    text = \"Can't connect to PrusaConnect\"\n    id = \"cant-connect\"\n\n\nclass CantResolveHostname(BadRequestError):\n    \"\"\"400 Can't resolve PrusaConnect hostname\"\"\"\n    title = \"Can't resolve hostname\"\n    text = \"Can't resolve PrusaConnect hostname\"\n    id = \"cant-resolve-hostname\"\n\n\nclass NotSupportedFileType(LinkError):\n    \"\"\"415 Not supported file\"\"\"\n    title = \"Not Supported File Type\"\n    text = \"Uploaded file type is not supported.\"\n    id = \"not-supported-file-type\"\n    status_code = state.HTTP_UNSUPPORTED_MEDIA_TYPE\n\n\nclass ForbiddenError(LinkError):\n    \"\"\"403 Forbidden\"\"\"\n    title = \"Forbidden\"\n    text = \"You don not have permission to access this.\"\n    status_code = state.HTTP_FORBIDDEN\n    id = \"forbidden\"\n\n\nclass NotFoundError(LinkError):\n    \"\"\"404 Not Found error\"\"\"\n    title = \"Not Found\"\n    text = \"Resource you want not found.\"\n    status_code = state.HTTP_NOT_FOUND\n    id = \"not-found\"\n\n\nclass NotCurrentJob(NotFoundError):\n    \"\"\"404 Not current job\"\"\"\n    title = \"Not Current Job\"\n    text = \"Given job id does not belong to current job\"\n\n\nclass GoneError(LinkError):\n    \"\"\"410 Gone\"\"\"\n    title = \"Target Resource Unavailable\"\n    text = \"Target resource is unavailable.\"\n    status_code = state.HTTP_GONE\n    id = \"file-gone\"\n\n\nclass ThumbnailUnavailable(GoneError):\n    \"\"\"410 Thumbnail Unavailable\"\"\"\n    title = \"Thumbnail Unavailable\"\n    text = \"Thumbnail is unavailable.\"\n\n\nclass FileNotFound(NotFoundError):\n    \"\"\"404 File Not Found\"\"\"\n    title = \"File Not Found\"\n    text = \"File you want was not found.\"\n\n\nclass FolderNotFound(NotFoundError):\n    \"\"\"404 Folder Not Found\"\"\"\n    title = \"Folder Not Found\"\n    text = \"Folder you want was not found.\"\n\n\nclass LocationNotFound(NotFoundError):\n    \"\"\"404 Location from url not found.\"\"\"\n    title = \"Location Not Found\"\n    text = \"Location not found, use local.\"\n    id = \"location-not-found\"\n    status_code = state.HTTP_NOT_FOUND\n\n\nclass ConflictError(LinkError):\n    \"\"\"409 Conflict error.\"\"\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass DirectoryNotEmpty(ConflictError):\n    \"\"\"409 Directory is not empty\"\"\"\n    title = \"Directory is not empty\"\n    text = \"Directory can't be deleted, because it's not empty.\"\n    id = \"directory-not-empty\"\n\n\nclass CurrentlyPrinting(ConflictError):\n    \"\"\"409 Printer is currently printing\"\"\"\n    title = \"Printer is currently printing\"\n    text = \"Printer is currently printing.\"\n\n\nclass NotStateToPrint(ConflictError):\n    \"\"\"409 Printer is not in state to print\"\"\"\n    title = \"Not in state to print\"\n    text = \"Printer is not in state to print.\"\n    id = \"not-state-to-print\"\n\n\nclass NotPrinting(ConflictError):\n    \"\"\"409 Printer is not printing\"\"\"\n    title = \"Printer Is Not Printing\"\n    text = \"Operation you want can only be done when printer is printing.\"\n\n\nclass NotPaused(ConflictError):\n    \"\"\"409 Printer is not paused\"\"\"\n    title = \"Printer Is Not Paused\"\n    text = \"Operation you want can only be done when printer is paused.\"\n\n\nclass FileCurrentlyPrinted(ConflictError):\n    \"\"\"409 File is currently printed\"\"\"\n    title = \"File is currently printed\"\n    text = \\\n        \"You try to do an operation with the file, which is currently printed.\"\n    id = \"file-currently-printed\"\n\n\nclass TransferConflict(ConflictError):\n    \"\"\"409 Already in transfer process.\"\"\"\n    title = \"Already in transfer process\"\n    text = \"Only one file at time can be transferred.\"\n    id = \"transfer-conflict\"\n\n\n# TODO: html variant\nclass TransferStopped(ConflictError):\n    \"\"\"409 Transfer process was stopped by user.\"\"\"\n    title = \"Transfer stopped\"\n    text = \"Transfer process was stopped by user.\"\n    id = \"transfer-stopped\"\n\n\nclass UnavailableUpdate(ConflictError):\n    \"\"\"409 Update is unavailable to install\"\"\"\n    title = \"Unavailable update\"\n    text = \"Update is unavailable to install\"\n    id = \"unavailable-update\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass UnableToUpdate(ConflictError):\n    \"\"\"409 Unable to install update\"\"\"\n    title = \"Unable to update\"\n    text = \"Unable to install update\"\n    id = \"unable-to-update\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass LengthRequired(LinkError):\n    \"\"\"411 Length Required.\"\"\"\n    title = \"Length Required\"\n    text = \"Missing Content-Length header or no content in request.\"\n    id = \"length-required\"\n    status_code = state.HTTP_LENGTH_REQUIRED\n\n\nclass EntityTooLarge(LinkError):\n    \"\"\"413 Payload Too Large\"\"\"\n    title = \"Request Entity Too Large\"\n    text = \"Not enough space in storage.\"\n    id = \"entity-too-large\"\n    status_code = state.HTTP_REQUEST_ENTITY_TOO_LARGE\n\n\nclass FileAlreadyExists(LinkError):\n    \"\"\"409 File Already Exists\"\"\"\n    title = \"File Already Exists\"\n    text = \"File already exists.\"\n    id = \"file-already-exists\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass FolderAlreadyExists(LinkError):\n    \"\"\"409 Folder Already Exists\"\"\"\n    title = \"Folder Already Exists\"\n    text = \"Folder already exists.\"\n    id = \"folder-already-exists\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass StorageNotExist(LinkError):\n    \"\"\"409 Storage Does Not Exist\"\"\"\n    title = \"Storage Does Not Exist\"\n    text = \"Storage doest not exist.\"\n    id = \"storage-not-exist\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass SDCardReadOnly(LinkError):\n    \"\"\"409 SD Card Read Only\"\"\"\n    title = \"SD Card Read Only\"\n    text = \"SD Card storage is read only.\"\n    id = \"entity-too-large\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass SDCardNotSupported(LinkError):\n    \"\"\"409 Some operations are not possible on SDCard.\"\"\"\n    title = \"SDCard is not Suppported\"\n    text = \"Location `sdcard` is not supported, use local.\"\n    id = \"sdcard-not-supported\"\n    status_code = state.HTTP_CONFLICT\n\n\nclass UnsupportedMediaError(LinkError):\n    \"\"\"415 Unsupported Media Type\"\"\"\n    title = \"Unsupported Media Type\"\n    text = \"Only G-Code for FDM printer can be uploaded.\"\n    id = \"unsupported-media-type\"\n    status_code = state.HTTP_UNSUPPORTED_MEDIA_TYPE\n\n\nclass InternalServerError(LinkError):\n    \"\"\"500 Internal Server Error.\"\"\"\n    title = \"Internal Server Error\"\n    text = (\"We're sorry, but there is a error in service. \"\n            \"Please try again later.\")\n    id = \"internal-server-error\"\n    status_code = state.HTTP_INTERNAL_SERVER_ERROR\n\n\nclass ResponseTimeout(InternalServerError):\n    \"\"\"500 Response Timeout\"\"\"\n    title = \"Response Timeout\"\n    text = \"There is some problem on PrusaLink.\"\n    id = \"response-timeout\"\n\n\nclass PrinterUnavailable(LinkError):\n    \"\"\"503 Printer Unavailable.\"\"\"\n    title = \"Printer Unavailable.\"\n    text = \"PrusaLink not finished initializing or Printer not connected.\"\n    id = \"printer-unavailable\"\n    status_code = state.HTTP_SERVICE_UNAVAILABLE\n\n\nclass RequestTimeout(LinkError):\n    \"\"\"408 Request timeout.\"\"\"\n    title = \"Request timeout.\"\n    text = \"PrusaLink got tired of waiting for your request. \" \\\n           \"cancelled upload?\"\n    id = \"request-timeout\"\n    status_code = state.HTTP_REQUEST_TIME_OUT\n"
  },
  {
    "path": "prusa/link/config.py",
    "content": "\"\"\"Config class definition.\"\"\"\nimport logging\nfrom logging import Formatter, StreamHandler\nfrom logging.handlers import SysLogHandler\nfrom os import getuid\nfrom os.path import abspath, join\nfrom pathlib import Path\nfrom pwd import getpwnam, getpwuid\nfrom typing import Iterable\n\nfrom extendparser.get import Get\n\nfrom .const import PRINTER_CONF_TYPES\n\nCONNECT = 'connect.prusa3d.com'\n\nLOG_FORMAT_FOREGROUND = \\\n    \"%(asctime)s %(levelname)s {%(module)s.%(funcName)s():%(lineno)d} \"\\\n    \"[%(threadName)s]: %(message)s \"\nLOG_FORMAT_SYSLOG = \\\n    \"%(name)s[%(process)d]: \"\\\n    \"%(levelname)s: %(message)s {%(funcName)s():%(lineno)d}\"\n\n# pylint: disable=too-many-ancestors\n\n\ndef get_log_level_dict(log_levels: Iterable[str]):\n    \"\"\"Parse log level from command line arguments.\"\"\"\n    log_level_dict = {}\n    for log_config in log_levels:\n        parts = log_config.split(\"=\")\n        if len(parts) != 2:\n            raise ValueError(\"Log level settings needs to contain exactly one \"\n                             \"\\\"=\\\"\")\n        name, loglevel = parts\n        log_level_dict[name] = loglevel\n    return log_level_dict\n\n\ndef check_log_level(value):\n    \"\"\"Check valid log level.\"\"\"\n    if value not in (\"CRITICAL\", \"ERROR\", \"WARNING\", \"INFO\", \"DEBUG\"):\n        raise ValueError(f\"Invalid value {value}\")\n\n\ndef check_server_type(value):\n    \"\"\"Check valid server class\"\"\"\n    if value not in (\"single\", \"threading\", \"forking\"):\n        raise ValueError(f\"Invalid value {value}\")\n\n\nclass Model(dict):\n    \"\"\"Config model based on dictionary.\n\n    It simply implements set and get attr methods.\n    \"\"\"\n    def __getattr__(self, key):\n        try:\n            return self[key]\n        except KeyError as err:\n            raise AttributeError(err) from err\n\n    def __setattr__(self, key, val):\n        self[key] = val\n\n    @staticmethod\n    def get(cfg, name, options):\n        return Model(cfg.get_section(name, options))\n\n\nclass FakeArgs:\n    \"\"\"Fake arguments for the config.py component\"\"\"\n\n    def __init__(self, path):\n        self.config = path\n        self.debug = False\n        self.foreground = True\n        self.pidfile = None\n        self.module_log_level = None\n        self.address = None\n        self.tcp_port = None\n        self.link_info = None\n        self.serial_port = None\n        self.debug = False\n        self.info = False\n        self.printer_number = None\n\n\nclass Config(Get):\n    \"\"\"This class handles prusalink.ini configuration file.\"\"\"\n    # pylint: disable=too-many-branches\n\n    def __init__(self, args):\n        super().__init__()\n\n        self.read(args.config)\n        self.debug = args.debug\n\n        # [daemon]\n        self.daemon = Model(\n            self.get_section(\n                \"daemon\",\n                (\n                    (\"data_dir\", str, ''),  # user home by default\n                    (\"pid_file\", str, \"./prusalink.pid\"),\n                    (\"power_panic_file\", str, \"./power_panic\"),\n                    (\"threshold_file\", str, \"./threshold.data\"),\n                    (\"user\", str, \"pi\"),\n                    (\"group\", str, \"pi\"),\n                    (\"printer_number\", int, None),\n                )))\n        if args.foreground or getuid() != 0:\n            pwd = getpwuid(getuid())\n            self.daemon.user = pwd.pw_name\n            self.daemon.home = pwd.pw_dir\n        else:\n            self.daemon.home = getpwnam(self.daemon.user).pw_dir\n\n        if not self.daemon.data_dir:\n            self.daemon.data_dir = self.daemon.home\n\n        if args.pidfile:\n            self.daemon.pid_file = abspath(args.pidfile)\n        if args.printer_number is not None:\n            self.daemon.printer_number = args.printer_number\n\n        for file_ in ('pid_file', 'power_panic_file', 'threshold_file'):\n            setattr(\n                self.daemon, file_,\n                abspath(join(self.daemon.data_dir, getattr(self.daemon,\n                                                           file_))))\n\n        # [logging]\n        self.set_global_log_level(args)\n\n        # Let's combine the config log setting and cmd args\n        # with cmd args overriding config values\n        self.log_settings = {}\n        if \"log\" in self:\n            for module_name, log_level in self[\"log\"].items():\n                check_log_level(log_level)\n                self.log_settings[module_name] = log_level\n\n        if args.module_log_level is not None:\n            override_log_settings = get_log_level_dict(args.module_log_level)\n            self.log_settings.update(override_log_settings)\n\n        # Let's save the handler we've configured for later use\n        self.configured_handler = self.get_log_handler(args)\n\n        # [http]\n        self.http = Model(\n            self.get_section(\"http\", (\n                (\"address\", str, \"0.0.0.0\"),\n                (\"port\", int, 8080),\n                (\"link_info\", bool, False),\n            )))\n\n        if args.address:\n            self.http.address = args.address\n        if args.tcp_port:\n            self.http.port = args.tcp_port\n        if args.link_info:\n            self.http.link_info = args.link_info\n\n        # [printer]\n        self.printer = Model(\n            self.get_section(\n                \"printer\",\n                (\n                    (\"port\", str, \"auto\"),\n                    (\"baudrate\", int, 115200),\n\n                    # Dangerous, it writes to the EEPROM on the little 32u2/8u2\n                    # This wears it out. Enabling this, you get PowerPanic\n                    # for the SD prints with RPi over USB, but you get\n                    # Around 40000 guaranteed working SD prints. After that\n                    # Your 32u2 EEPROM might wear out and the enable/disable\n                    # would get stuck in one or the other state\n                    (\"reset_disabling\", bool, False),\n                    (\"settings\", str, \"./prusa_printer_settings.ini\"),\n                    # Support for monitoring mountpoints temporarily off\n                    # (\"storage\", tuple, [], ':'),\n                    # relative to HOME\n                    (\"directory\", str, \"./PrusaLink gcodes\"),\n                )))\n        if args.serial_port:\n            self.printer.port = args.serial_port\n\n        self.printer.settings = abspath(\n            join(self.daemon.data_dir, self.printer.settings))\n        self.printer.directory = abspath(join(self.daemon.data_dir,\n                                              self.printer.directory))\n        self.printer.directory_name = Path(self.printer.directory).name\n\n        # [cameras]\n        self.cameras = Model(\n            self.get_section(\n                \"cameras\",\n                (\n                    (\"auto_detect\", bool, True),\n                )))\n\n    def set_section(self, name, model):\n        \"\"\"Set section from model\"\"\"\n        if name not in self:\n            self.add_section(name)\n        for key, val in model.items():\n            # FIXME: HACKS! We are at the limits of extendparser\n            if name == \"printer\" and key == \"storage\":\n                value = \":\".join(val)\n                self.set(name, key, value)\n            elif name == \"daemon\" and key == \"home\":\n                continue\n            elif name == \"printer\" and key == \"directory_name\":\n                continue\n            else:\n                self.set(name, key, str(val))\n\n    def update_sections(self):\n        \"\"\"Update config from attributes.\"\"\"\n        self.set_section('daemon', self.daemon)\n        self.set_section('log', self.log_settings)\n        self.set_section('http', self.http)\n        self.set_section('printer', self.printer)\n        self.set_section('cameras', self.cameras)\n\n    def set_global_log_level(self, args):\n        \"\"\"Set default global log level from command line.\"\"\"\n        if args.debug:\n            log_level = \"DEBUG\"\n        elif args.info:\n            log_level = \"INFO\"\n        else:\n            log_level = logging.root.level\n\n        logging.root.setLevel(log_level)\n        logging.getLogger(\"urllib3\").setLevel(log_level)\n        logging.getLogger(\"connect-printer\").setLevel(log_level)  # FIXME\n\n    def get_log_handler(self, args):\n        \"\"\"Logger setting are more complex.\"\"\"\n\n        if args.foreground:\n            log_format = LOG_FORMAT_FOREGROUND\n            configured_handler = StreamHandler()\n        else:\n            log_format = LOG_FORMAT_SYSLOG\n            log_syslog = self.get(\"logging\", \"syslog\", fallback=\"/dev/log\")\n            configured_handler = SysLogHandler(log_syslog,\n                                               SysLogHandler.LOG_DAEMON)\n\n        log_format = self.get(\"logging\", \"format\", fallback=log_format)\n\n        for handler in logging.root.handlers:  # reset root logger handlers\n            logging.root.removeHandler(handler)\n        logging.root.addHandler(configured_handler)\n        formatter = Formatter(log_format)\n        configured_handler.setFormatter(formatter)\n        return configured_handler\n\n\nclass Settings(Get):\n    \"\"\"This class handles prusa_printer_settings.ini configuration file.\n\n    File prusa_printer_settings.ini is official Prusa settings file, which has\n    shared format between all printers, and PrusaConnect can generate it.\n    \"\"\"\n    instance = None\n\n    def __init__(self, settings_file):\n        if Settings.instance is not None:\n            raise RuntimeError('Config is singleton')\n\n        super().__init__(interpolation=None)\n\n        self.read(settings_file)\n\n        # [printer]\n        self.printer = Model(\n            self.get_section('printer', (('type', str, ''), ('name', str, ''),\n                                         ('location', str, ''),\n                                         ('farm_mode', bool, False),\n                                         (\"network_error_chime\", bool, False),\n                                         )))\n\n        self.printer['name'] = self.printer['name'].strip()\n        self.printer['location'] = self.printer['location'].strip()\n\n        if self.printer.type and self.printer.type not in PRINTER_CONF_TYPES:\n            raise ValueError(\"Settings file for an unsupported printer\")\n\n        # [network]\n        self.network = Model(\n            self.get_section('network', (('hostname', str, ''), )))\n\n        # [service::connect]\n        self.service_connect = Model(\n            self.get_section(\n                'service::connect',\n                (\n                    ('hostname', str, CONNECT),\n                    ('tls', bool, True),\n                    ('port', int,\n                     0),  # 0 means 443 with tls, or 80 without tls\n                    ('token', str, ''))))\n\n        # [service::local]\n        self.service_local = Model(\n            self.get_section('service::local',\n                             (('enable', int, 1), ('username', str, ''),\n                              ('digest', str, ''), ('api_key', str, ''))))\n\n        Settings.instance = self\n\n        # Reflect possible changes back to prusa_printer_settings.ini file\n        self.update_sections()\n        with open(settings_file, 'w', encoding='utf-8') as ini:\n            Settings.instance.write(ini)\n\n    def set_section(self, name, model):\n        \"\"\"Set section from model\"\"\"\n        if name not in self:\n            self.add_section(name)\n        for key, val in model.items():\n            self.set(name, key, str(val))\n\n    def update_sections(self, connect_skip=False):\n        \"\"\"Update config from attributes.\"\"\"\n        self.set_section('printer', self.printer)\n        self.set_section('network', self.network)\n        if not connect_skip:\n            self.set_section('service::connect', self.service_connect)\n        self.set_section('service::local', self.service_local)\n\n    def is_wizard_needed(self):\n        \"\"\"\n        Is there a reason for the wizard to be shown?\n        \"\"\"\n        interested_in = [\n            self.printer[\"type\"], self.service_local[\"username\"],\n            self.service_local[\"digest\"],\n        ]\n        return not all(interested_in)\n\n    def use_connect(self):\n        \"\"\"\n        Gets the user's wish to use or not tu use connect\n        Needs its own value, now substituted by token\n        \"\"\"\n        return bool(self.service_connect[\"token\"])\n"
  },
  {
    "path": "prusa/link/const.py",
    "content": "\"\"\"\nContains almost every constant for the printer communication part of\nPrusaLink\n\"\"\"\n\nimport uuid\nfrom importlib.resources import files  # type: ignore\nfrom os import path\nfrom typing import List\n\nfrom bidict import bidict\nfrom packaging.version import Version\nfrom prusa.connect.printer.const import PrinterType, State\n\nfrom .printer_adapter.structures.model_classes import PrintMode, PrintState\n\ninstance_id = uuid.uuid4()\n\n# e.g. Mon, 07 Nov 2022 13:52:49 GMT\nHEADER_DATETIME_FORMAT = \"%a, %d %b %Y %X GMT\"\n\nPRINTER_TYPES = {\n    250: PrinterType.I3MK25,\n    252: PrinterType.I3MK25S,\n    20250: PrinterType.I3MK25,\n    20252: PrinterType.I3MK25S,\n    300: PrinterType.I3MK3,\n    20300: PrinterType.I3MK3,\n    302: PrinterType.I3MK3S,\n    20302: PrinterType.I3MK3S,\n    30302: PrinterType.I3MK3S,\n}\n\nMMU3_TYPE_CODE = 30302\n\nPRINTER_CONF_TYPES = bidict({\n    \"MK2.5\": PrinterType.I3MK25,\n    \"MK2.5S\": PrinterType.I3MK25S,\n    \"MK3\": PrinterType.I3MK3,\n    \"MK3S\": PrinterType.I3MK3S,\n})\n\nDATA_PATH = path.abspath(path.join(str(files('prusa.link')), 'data'))\n\nBASE_STATES = {State.IDLE, State.BUSY, State.READY}\nPRINTING_STATES = {State.PRINTING, State.PAUSED, State.FINISHED, State.STOPPED}\n\nMK25_PRINTERS = {PrinterType.I3MK25.value, PrinterType.I3MK25S.value}\n\nJOB_STARTING_STATES = {State.PRINTING, State.PAUSED}\nJOB_ENDING_STATES = {\n    State.FINISHED,\n    State.STOPPED,\n}\nJOB_DESTROYING_STATES = {\n     State.ERROR,\n     State.IDLE,  # These are needed for the job to end through ATTENTION\n     State.BUSY,\n     }\n\nJITTER_THRESHOLD = 0.5\nPRUSA_VENDOR_ID = \"2c99\"\n\n# --- Intervals ---\n# Values are in seconds\n\nTELEMETRY_IDLE_INTERVAL = 0.25\nTELEMETRY_PRINTING_INTERVAL = 1\nTELEMETRY_SLEEPING_INTERVAL = 4  # can be sleeping in any state\nTELEMETRY_SLEEP_AFTER = 3 * 60\nTELEMETRY_REFRESH_INTERVAL = 5 * 60  # full telemetry re-send\n\nFAST_POLL_INTERVAL = 1\nSLOW_POLL_INTERVAL = 10  # for values, that aren't that important\nVERY_SLOW_POLL_INTERVAL = 30\nIP_UPDATE_INTERVAL = 5\nQUIT_INTERVAL = 0.2\nSD_INTERVAL = 0.2\nSD_FILESCAN_INTERVAL = 60\nDIR_RESCAN_INTERVAL = 1\nPRINTER_BOOT_WAIT = 8\nSEND_INFO_RETRY = 5\nSERIAL_REOPEN_TIMEOUT = 2\nREPORTING_TIMEOUT = 60\nFW_MESSAGE_TIMEOUT = 10\nSTATE_CHANGE_TIMEOUT = 15\nIP_WRITE_TIMEOUT = 5\nSN_OBTAIN_INTERVAL = 5\nEXIT_TIMEOUT = 15\nERROR_REASON_TIMEOUT = 2\nPATH_WAIT_TIMEOUT = 10\nSLEEP_SCREEN_TIMEOUT = 20\nSELF_PING_TIMEOUT = 5\nSELF_PING_RETRY_INTERVAL = 10\nATTENTION_CLEAR_INTERVAL = 5\nCAMERA_INIT_DELAY = 2\nCAMERA_SCAN_INTERVAL = 30\nCAMERA_REGISTER_TIMEOUT = 5\nTIME_FOR_SNAPSHOT = 1\nPRINT_END_TIMEOUT = 11\nKEEPALIVE_INTERVAL = 12\n\nPP_MOVES_DELAY = 20\n\n# --- Lcd queue ---\nLCD_QUEUE_SIZE = 30\n\n# --- Serial queue ---\nRX_SIZE = 128  # Not used much, limits the max serial message size\nSERIAL_QUEUE_TIMEOUT = 25\nSERIAL_QUEUE_MONITOR_INTERVAL = 1\nHISTORY_LENGTH = 100  # How many messages to remember for Resends\n\n# --- Is planner fed ---\nQUEUE_SIZE = 10000  # From how many messages to compute the percentile\nHEAP_RATIO = 0.95  # What percentile to compute\nIGNORE_ABOVE = 1.0  # Ignore instructions, that take longer than x sec\nDEFAULT_THRESHOLD = 0.13  # Percentile for uninitialised component\nUSE_DYNAMIC_THRESHOLD = True  # Compute the percentile or use a fixed value?\n\n# --- File printer ---\nSTATS_EVERY = 100\nTAIL_COMMANDS = 10  # how many commands after the last progress report\nPRINT_QUEUE_SIZE = 4\n\n# --- Storage ---\nMAX_FILENAME_LENGTH = 52\nSD_STORAGE_NAME = \"SD Card\"\nBLACKLISTED_TYPES: List[str] = []\nBLACKLISTED_PATHS = [\n    \"/dev\",\n    \"/sys\",\n    \"/proc\",\n    \"/tmp\",\n]\nBLACKLISTED_NAMES = [SD_STORAGE_NAME]\nSFN_TO_LFN_EXTENSIONS = {\"GCO\": \"gcode\", \"G\": \"g\", \"GC\": \"gc\"}\n\nRESET_PIN = 22  # RPi gpio pin for resetting printer\nSUPPORTED_FIRMWARE = \"3.14.0\"\nMINIMAL_FIRMWARE = Version(SUPPORTED_FIRMWARE)\nMAX_INT = (2**31) - 1\nSTATE_HISTORY_SIZE = 10\n\n# --- Interesting_Logger ---\nLOG_BUFFER_SIZE = 200\nAFTERMATH_LOG_SIZE = 100\n\n# --- Selected log files---\nGZ_SUFFIX = \".gz\"\nLOGS_PATH = \"/var/log\"\nLOGS_FILES = (\"auth.log\", \"daemon.log\", \"kern.log\", \"messages\", \"syslog\",\n              \"user.log\")\n\n\n# --- Hardware limits for commands ---\nclass LimitsFDM:\n    \"\"\"Generic FDM Limits object\"\"\"\n\n    # --- Printer Object info ---\n    id: str\n    name: str\n    type: int\n    version: int\n    subversion: int\n\n    # --- Hardware limits ---\n    extrusion_min = -10\n    extrusion_max = 100\n    feedrate_e_min = 0\n    feedrate_e_max = 100\n    feedrate_x_min = 0\n    feedrate_x_max = 2700\n    feedrate_y_min = 0\n    feedrate_y_max = 2700\n    feedrate_z_min = 0\n    feedrate_z_max = 1000\n    min_temp_nozzle_e = 170\n    position_x_min = 0\n    position_x_max = 255\n    position_y_min = -4\n    position_y_max = 212.5\n    position_z_min = 0.15\n    position_z_max = 210\n    print_flow_min = 10\n    print_flow_max = 999\n    print_speed_min = 10\n    print_speed_max = 999\n    temp_bed_min = 0\n    temp_bed_max = 125\n    temp_nozzle_min = 0\n    temp_nozzle_max = 305\n\n\nclass LimitsMK25(LimitsFDM):\n    \"\"\"Printer MK2.5 Limits object\"\"\"\n    id = '1.2.5'\n    name = 'Original Prusa i3 MK2.5'\n    type = 1\n    version = 2\n    subversion = 5\n\n\nclass LimitsMK25S(LimitsFDM):\n    \"\"\"Printer MK2.5S Limits object\"\"\"\n    id = '1.2.6'\n    name = 'Original Prusa i3 MK2.5S'\n    type = 1\n    version = 2\n    subversion = 6\n\n\nclass LimitsMK3(LimitsFDM):\n    \"\"\"Printer MK3 Limits object\"\"\"\n    id = '1.3.0'\n    name = 'Original Prusa i3 MK3'\n    type = 1\n    version = 3\n    subversion = 0\n\n\nclass LimitsMK3S(LimitsFDM):\n    \"\"\"Printer MK3S Limits object\"\"\"\n    id = '1.3.1'\n    name = 'Original Prusa i3 MK3S'\n    type = 1\n    version = 3\n    subversion = 1\n\n\nPRINT_STATE_PAIRING = {\n    \"sdn_lfn\": PrintState.SD_PRINTING,\n    \"sd_paused\": PrintState.SD_PAUSED,\n    \"serial_paused\": PrintState.SERIAL_PAUSED,\n    \"no_print\": PrintState.NOT_SD_PRINTING,\n}\n\nPRINT_MODE_PAIRING = {\"SILENT\": PrintMode.SILENT, \"NORMAL\": PrintMode.NORMAL}\n\nPRINT_MODE_ID_PAIRING = {\n    0: PrintMode.NORMAL,\n    1: PrintMode.SILENT,\n    2: PrintMode.AUTO,\n}\n\n# keys are the manufacturer ids, values are supported models\nSUPPORTED_PRINTERS = {\n    \"2c99\": {\"0001\", \"0002\"},\n}\n\nMMU_SLOTS = 5\n\nMMU_PROGRESS_MAP = {\n    \"OK\": 0,\n\n    \"Engaging idler\": 1,\n    \"Disengaging idler\": 2,\n    \"Unloading to FINDA\": 3,\n    \"Unloading to pulley\": 4,\n    \"Feeding to FINDA\": 5,\n    \"Feeding to extruder\": 6,\n    \"Feeding to nozzle\": 7,\n    \"Avoiding grind\": 8,\n    \"ERR Disengaging idler\": 10,\n    \"ERR Engaging idler\": 11,\n    \"ERR Wait for User\": 12,\n    \"ERR Internal\": 13,\n    \"ERR Help filament\": 14,\n    \"ERR TMC failed\": 15,\n    \"Selecting fil. slot\": 18,\n    \"Preparing blade\": 19,\n    \"Pushing filament\": 20,\n    \"Performing cut\": 21,\n    \"Returning selector\": 22,\n    \"Ejecting filament\": 24,\n    \"Parking selector\": 23,\n    \"Retract from FINDA\": 25,\n    \"Homing\": 26,\n    \"Moving selector\": 27,\n    \"Feeding to FSensor\": 28,\n}\n\nMMU_ERROR_MAP = {\n    0x8001: 101,  # FINDA didn't switch on -> FINDA didn't trigger\n    0x8002: 102,  # FINDA didn't switch off -> FINDA filament stuck\n    0x8003: 103,  # Filament sensor didn't switch on -> FSENSOR didn't trigger\n    0x8004: 104,  # Filament sensor didn't switch off -> FSENSOR filament stuck\n    0x800b: 105,  # MOVE_PULLEY_FAILED -> MECHANICAL pulley cannot move\n    0x8009: 106,  # FSensor triggered too early -> MECHANICAL FSENSOR too early\n    0x800a: 107,  # FINDA flickers -> MECHANICAL inspect FINDA\n    0x802a: 108,  # LOAD_TO_EXTRUDER_FAILED -> Loading to extruder failed.\n    #               Inspect the filament tip shape. Refine the sensor\n    #               calibration, if needed\n\n    0x8007 | 0x0080: 115,  # Selector homing failed\n    0x800b | 0x0080: 116,  # Selector move failed\n    0x8007 | 0x0100: 125,  # Idler homing failed\n    0x800b | 0x0100: 126,  # Idler move failed\n\n    0xA000: 201,  # TMC_OVER_TEMPERATURE_WARN -> Temperature warning TMC pulley\n    #               too hot\n    0xC000: 202,  # TMC_OVER_TEMPERATURE_ERROR -> Temperature TMC pulley\n    #               overheat error\n\n    # Temperature errors for the selector driver\n    0xA000 | 0x0080: 211,  # Temperature warning TMC selector too hot\n    0xC000 | 0x0080: 212,  # Temperature TMC selector overheat error\n\n    # Temperature errors for the idler driver\n    0xA000 | 0x0100: 221,  # Temperature warning TMC idler too hot\n    0xC000 | 0x0100: 222,  # Temperature TMC idler overheat error\n\n    # Electrical errors\n    0x8200: 301,  # TMC_IOIN_MISMATCH -> Electrical TMC pulley driver error\n    0x8400: 302,  # TMC_RESET -> Electrical TMC pulley driver reset\n    0x8800: 303,  # TMC_UNDERVOLTAGE_ON_CHARGE_PUMP -> Electrical TMC\n    #               pulley undervoltage error\n    0x9000: 304,  # TMC_SHORT_TO_GROUND -> Electrical TMC pulley driver shorted\n    0xC200: 305,  # ERR_ELECTRICAL_MMU_PULLEY_SELFTEST_FAILED -> Electrical\n    #               TMC pulley selftest failed\n\n    # Electrical errors for the selector driver\n    0x8200 | 0x0080: 311,  # Electrical TMC selector driver error\n    0x8400 | 0x0080: 312,  # Electrical TMC selector driver reset\n    0x8800 | 0x0080: 313,  # Electrical TMC selector undervoltage error\n    0x9000 | 0x0080: 314,  # Electrical TMC selector driver shorted\n    0xC200 | 0x0080: 315,  # Electrical TMC selector selftest failed\n\n    # Electrical errors for the idler driver\n    0x8200 | 0x0100: 321,  # Electrical TMC idler driver error\n    0x8400 | 0x0100: 322,  # Electrical TMC idler driver reset\n    0x8800 | 0x0100: 323,  # Electrical TMC idler undervoltage error\n    0x9000 | 0x0100: 324,  # Electrical TMC idler driver shorted\n    0xC200 | 0x0100: 325,  # Electrical TMC idler selftest failed\n\n    0x0800d: 306,  # MMU MCU detected a 5V undervoltage. There might be an\n    #                issue with the electronics. Check the wiring and\n    #                connectors\n\n    # Connectivity errors\n    0x802e: 401,  # MMU not responding -> CONNECT MMU not responding\n    0x802d: 402,  # MMU not responding correctly. Check the wiring and\n                  # connectors\n\n    # System errors\n    0x8005: 501,  # Filament already loaded -> SYSTEM filament already loaded\n    0x8006: 502,  # Invalid tool -> SYSTEM invalid tool\n    0x802b: 503,  # QUEUE_FULL -> MMU Firmware internal error, please reset\n    #               the MMU\n    0x802c: 504,  # VERSION_MISMATCH -> The MMU firmware version is\n    #               incompatible with the printer's FW. Update to compatible\n    #               version\n    0x802f: 505,  # PROTOCOL_ERROR -> Internal runtime error. Try resetting\n    #               the MMU or updating the firmware\n    0x8008: 506,  # FINDA_VS_EEPROM_DISCREPANCY -> Unload manually\n    0x800c: 507,  # Filament was ejected -> SYSTEM filament ejected\n    0x8029: 508,  # FILAMENT_CHANGE -> SYSTEM filament change\n\n}\n"
  },
  {
    "path": "prusa/link/daemon.py",
    "content": "\"\"\"Daemon class implementation.\"\"\"\nimport logging\nimport sys\nfrom subprocess import Popen\nfrom typing import List\n\nimport prctl  # type: ignore\n\nfrom .config import Settings\nfrom .printer_adapter import prusa_link\nfrom .printer_adapter.prusa_link import PrusaLink\nfrom .web import WebServer, init_web_app\nfrom .web.lib.core import app\n\nlog = logging.getLogger(__name__)\n\n\nclass Daemon:\n    \"\"\"HTTP Daemon based on wsgiref.\"\"\"\n    instance = None\n\n    # pylint: disable=too-few-public-methods\n    def __init__(self, config, argv: List):\n        if Daemon.instance:\n            raise RuntimeError(\"Daemon can be only one.\")\n\n        self.cfg = config\n        self.argv = argv\n        self.settings = None\n\n        self.http = None\n        self.prusa_link = None\n        Daemon.instance = self\n\n    def run(self, daemon=True):\n        \"\"\"Run daemon.\"\"\"\n\n        prctl.set_name(\"pl#main\")\n        self.settings = Settings(self.cfg.printer.settings)\n\n        init_web_app(self)\n        self.http = WebServer(app, self.cfg.http.address, self.cfg.http.port,\n                              exit_on_error=not daemon)\n\n        if self.settings.service_local.enable:\n            self.http.start()\n\n        # Log daemon stuff as printer_adapter\n        adapter_logger = logging.getLogger(prusa_link.__name__)\n        try:\n            self.prusa_link = PrusaLink(self.cfg, self.settings)\n        except Exception:  # pylint: disable=broad-except\n            adapter_logger.exception(\"Adapter was not start\")\n            self.http.stop()\n            return 1\n\n        try:\n            self.prusa_link.stopped_event.wait()\n            return 0\n        except KeyboardInterrupt:\n            adapter_logger.info('Keyboard interrupt')\n            adapter_logger.info(\"Shutdown adapter\")\n            self.prusa_link.stop()\n            self.http.stop()\n            return 0\n        except Exception:  # pylint: disable=broad-except\n            adapter_logger.exception(\"Unknown Exception\")\n            self.http.stop()\n            return 1\n\n    @staticmethod\n    def restart(argv: List):\n        \"\"\"Restart prusa link by command line tool.\"\"\"\n        # pylint: disable=consider-using-with\n        Popen([sys.executable, '-m', 'prusa.link', 'restart'] + argv,\n              start_new_session=True,\n              stdin=sys.stdin,\n              stdout=sys.stdout,\n              stderr=sys.stderr,\n              close_fds=True)\n\n    def sigterm(self, *_):\n        \"\"\"Raise KeyboardInterrupt exceptions in threads.\"\"\"\n        log.info(\"SIGTERM received, shutting down PrusaLink\")\n\n        self.http.stop()\n        if self.prusa_link:\n            self.prusa_link.stop()\n        log.warning(\"Shutdown complete\")\n"
  },
  {
    "path": "prusa/link/data/image_builder/boot-message.service",
    "content": "[Unit]\nDescription=Boot message\n\n[Service]\nType=simple\nExecStart=/bin/sh -c 'stty -F /dev/ttyAMA0 115200; printf \\'M117 RPi booting...\\n\\' > /dev/ttyAMA0'\n\n[Install]\nWantedBy=basic.target\n"
  },
  {
    "path": "prusa/link/data/image_builder/first-boot.sh",
    "content": "set_up_port () {\n   # Sets the baudrate and cancels the hangup at the end of a connection\n   stty -F \"$1\" 115200 -hupcl || true\n}\n\nmessage() {\n   printf \"M117 $2\\n\" > \"$1\" || true\n}\n\nset_up_port \"/dev/ttyAMA0\"\nmessage \"/dev/ttyAMA0\" \"Please wait < 10min\";\n\nfor i in {0..5}; do\n set_up_port \"/dev/ttyACM$i\"\ndone\n\nsleep 8\n\nfor i in {0..5}; do\n message \"/dev/ttyACM$i\" \"Please wait < 10min\"\ndone\n\n# This generates the host keys for the ssh server to work\nssh-keygen -A\n"
  },
  {
    "path": "prusa/link/data/image_builder/manager-start-script.sh",
    "content": "# Forward the port 80 to 8080 even on the loopback, so we can ping ourselves\niptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -j REDIRECT --to-port 8080\niptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080\niptables -t nat -I OUTPUT -p tcp -o lo -d localhost --dport 80 -j REDIRECT --to-ports 8080\n\nset_up_port () {\n   # Sets the baudrate and cancels the hangup at the end of a connection\n   stty -F \"$1\" 115200 -hupcl || true\n}\n\nmessage() {\n   printf \"M117 $2\\n\" > \"$1\" || true\n}\n\nwifi_nic_name=$(find /sys/class/net -follow -maxdepth 2 -name wireless 2> /dev/null | cut -d / -f 5)\nif [ $? -eq 0 ] && [ -n \"$wifi_nic_name\" ]; then\n    /sbin/iwconfig \"$wifi_nic_name\" power off\n    if [ $? -eq 0 ]; then\n        printf \"Turned off power management for $wifi_nic_name\\n\" > \"$1\"\n    fi\nfi\n\nusername=$(id -nu 1000)\nuser_site=$(su $username -c \"python -m site --user-site\")\n\nset_up_port \"/dev/ttyAMA0\"\nmessage \"/dev/ttyAMA0\" \"Starting PrusaLink\";\n\n/home/$username/.local/bin/prusalink-boot\nrm -f /run/prusalink/manager.pid\nexport PYTHONOPTIMIZE=2\nPYTHONPATH=$user_site /home/$username/.local/bin/prusalink-manager -p \"PYTHONPATH=$user_site /home/$username/.local/bin/\" start\n"
  },
  {
    "path": "prusa/link/data/image_builder/prusalink-start-script.sh",
    "content": "# Forward the port 80 to 8080 even on the loopback, so we can ping ourselves\niptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -j REDIRECT --to-port 8080\niptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080\niptables -t nat -I OUTPUT -p tcp -o lo -d localhost --dport 80 -j REDIRECT --to-ports 8080\n\nset_up_port () {\n   # Sets the baudrate and cancels the hangup at the end of a connection\n   stty -F \"$1\" 115200 -hupcl || true\n}\n\nmessage() {\n   printf \"M117 $2\\n\" > \"$1\" || true\n}\n\nwifi_nic_name=$(find /sys/class/net -follow -maxdepth 2 -name wireless 2> /dev/null | cut -d / -f 5)\nif [ $? -eq 0 ] && [ -n \"$wifi_nic_name\" ]; then\n    /sbin/iwconfig \"$wifi_nic_name\" power off\n    if [ $? -eq 0 ]; then\n        printf \"Turned off power management for $wifi_nic_name\\n\"\n    fi\nfi\n\nusername=$(id -nu 1000)\n\nset_up_port \"/dev/ttyAMA0\"\nmessage \"/dev/ttyAMA0\" \"Starting PrusaLink\";\n\n/home/$username/.local/bin/prusalink-boot\nrm -f /home/$username/prusalink.pid\nexport PYTHONOPTIMIZE=2\nsu $username -c \"/home/$username/.local/bin/prusalink -i start\"\n"
  },
  {
    "path": "prusa/link/data/prusalink.ini",
    "content": "[daemon]\n; data_dir is used as default directory for other files, like\n; prusa_printer_settings.ini or threshold_file\n; default is user home\n; data_dir =\n\n; pid_file = ./prusalink.pid\n\n; power_panic backup file - not supported yet\n; power_panic_file = ./power_panic_file\n\n; threshold_file = ./threshold.data\n\n; user and group, when PrusaLink was start by root account\n; user = pi\n; group = pi\n\n[http]\n; address = 0.0.0.0\n; port = 8080\n;\n; Special /link-info debug page.\n; link_info = False\n\n[printer]\n; port = /dev/ttyAMA0\n; baudrate = 115200\n; settings = ./prusa_printer_settings.ini\n; directory = ./PrusaLink gcodes\n\n; Dangerous, it writes to the EEPROM on the little 32u2/8u2 each time an\n; SD print starts or ends\n; This wears it out. Enabling this, you get PowerPanic\n; for the SD prints with RPi over USB, but you get\n; around 50 000 guaranteed working SD prints. After that\n; Your 32u2 EEPROM might wear out and the enable/disable\n; would get stuck in one or the other state\n ; reset_disabling = False\n\n[cameras]\n; auto_detect = True\n"
  },
  {
    "path": "prusa/link/interesting_logger.py",
    "content": "\"\"\"Implements the InterestingLogRotator and InterestingLogger classes\"\"\"\nimport logging\nimport sys\nimport threading\nimport traceback\nfrom collections import deque\nfrom copy import copy\nfrom logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARNING, Logger\nfrom multiprocessing import RLock\n\nfrom .const import AFTERMATH_LOG_SIZE, LOG_BUFFER_SIZE\nfrom .printer_adapter.structures.mc_singleton import MCSingleton\n\nlog = logging.getLogger(\"interesting_logger\")\n\n\nclass DecoySrcfile:\n    \"\"\"\n    Hi, you have found a hack, please make yourself a coffee ;)\n\n    If we want to make our own Logger which will function as a\n    normal vanilla Logger, we need it to skip more stack frames.\n    From Python 3.8 this is possible as you can give the _log() method\n    a number of frames to skip, originally, this has been done differently\n    Each stack frame knows from which file it originated, so they compare\n    those against their own filename and skip those, that match.\n    As this is a different file, we need to skip it too, otherwise the log\n    messages would just list the function name and line number from here.\n    So let's trick the logging component by sneaking in a decoy \"_srcfile\"\n    that will equal the original, plus our own file path. That way both frames\n    from logging and here will get skipped and the real function name and line\n    number will be shown.\n    \"\"\"\n\n    def __init__(self):\n        self.original_logging_srcfile = copy(logging._srcfile)\n\n    def __eq__(self, other):\n        return other in {__file__, self.original_logging_srcfile}\n\n    def __hash__(self):\n        return hash(self.original_logging_srcfile)\n\n\n# pylint: disable=protected-access\nlogging._srcfile = DecoySrcfile()  # type: ignore\n\n\nclass InterestingLogRotator(metaclass=MCSingleton):\n    \"\"\"\n    Stores all logs in a rotating queue, on trigger logs the current queue\n    plus AFTERMATH_LOG_SIZE messages forward\n    \"\"\"\n\n    def __init__(self):\n        self.log_buffer = deque(maxlen=LOG_BUFFER_SIZE)\n        self.additional_messages_to_print = 0\n        self.log_lock = RLock()\n        self.skipped_loggers = set()\n\n    def skip_logger(self, logger_to_skip):\n        \"\"\"\n        Add a skipped logger to the set of skipped ones\n        Reset cached skip values\n        \"\"\"\n        with self.log_lock:\n            name = logger_to_skip.name\n            self.skipped_loggers.add(name)\n\n            # Reset the skip caches of all the loggers\n            for logger in logging.getLogger().manager.loggerDict.values():\n                if isinstance(logger, InterestingLogger):\n                    logger._skipped = None\n\n    def is_skipped(self, logger_name):\n        \"\"\"Is the logger name in the skipped set?\"\"\"\n        return logger_name in self.skipped_loggers\n\n    def process_log_entry(self, got_printed, level, msg, *args, **kwargs):\n        \"\"\"\n        If the log entry should be written out and was not, lets do it\n        if there is nothing interesting going on, adds the log entry\n        ino the rotating queue\n        \"\"\"\n        with self.log_lock:\n            if self.additional_messages_to_print > 0:\n                self.additional_messages_to_print -= 1\n                if not got_printed:\n                    self._log(level, msg, *args, **kwargs)\n            else:\n                self.log_buffer.appendleft((level, msg, args, kwargs))\n\n    @staticmethod\n    def _log(level, msg, *args, **kwargs):\n        \"\"\"\n        Writes the message to the log, bumps its priority\n        to warning and reports the original one in the text\n        \"\"\"\n        msg = f\"Was[{logging.getLevelName(level)}]: \" + str(msg)\n        log.warning(msg, *args, **kwargs)\n\n    @staticmethod\n    def trigger(by_what: str):\n        \"\"\"\n        Static proxy for the instance_trigger method\n\n        :param by_what: Interesting log triggered by ______\n        \"\"\"\n        InterestingLogRotator.get_instance().instance_trigger(by_what)\n\n    def instance_trigger(self, by_what: str):\n        \"\"\"\n        Triggers the mechanism to start dumping log messages\n\n        :param by_what: Interesting log triggered by ______\n        \"\"\"\n        with self.log_lock:\n            self.additional_messages_to_print = AFTERMATH_LOG_SIZE\n            log.warning(\"Interesting log triggered by %s\", by_what)\n            while self.log_buffer:\n                level, msg, args, kwargs = self.log_buffer.pop()\n                self._log(level, msg, *args, **kwargs)\n\n            log.warning(\"Repeat - triggered by %s\", by_what)\n            log.warning(\"Listing all threads with stack traces for debugging\")\n\n            frames = sys._current_frames()\n            # Print where all the threads are\n            for thread in threading.enumerate():\n                if thread.ident is None:\n                    continue\n                try:\n                    current_frame = frames[thread.ident]\n                    stack = traceback.extract_stack(current_frame)\n                    stacktrace_strings = stack.format()\n                    log.warning(\"Thread %s stack trace:\", thread.name)\n                    for stack_trace_frame in stacktrace_strings:\n                        for line in stack_trace_frame.split(\"\\n\"):\n                            if line:\n                                log.warning(line)\n                except KeyError:\n                    log.warning(\"Couldn't get a stacktrace for thread %s\",\n                                thread.name)\n                log.warning(\"\")  # An empty line for better orientation\n\n\nclass InterestingLogger(Logger):\n    \"\"\"The logger that will mirror log entries to the log rotator\"\"\"\n\n    def __init__(self, name, level=NOTSET):\n        super().__init__(name, level)\n\n        self.log_rotator = InterestingLogRotator.get_instance()\n        self._skipped = None\n\n    def is_skipped(self):\n        \"\"\"\n        Recursively figure out if we are supposed to skip appending\n        to log_rotator. Cache the result\n        \"\"\"\n        if self._skipped is not None:\n            return self._skipped\n\n        # Lock our log modification lock - a bit hacky\n        with self.log_rotator.log_lock:\n            if self.log_rotator.is_skipped(self.name):\n                self._skipped = True\n            elif isinstance(self.parent, logging.RootLogger):\n                self._skipped = False\n            else:\n                if isinstance(self.parent, InterestingLogger):\n                    self._skipped = self.parent.is_skipped()\n                else:\n                    # Should not get triggered ever\n                    log.warning(\"Unsupported logger found: %s\",\n                                self.parent.name)\n                    return False\n            return self._skipped\n\n    def debug(self, msg, *args, **kwargs):\n        \"\"\"\n        As a normal debug, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(DEBUG), DEBUG,\n                                               msg, *args, **kwargs)\n        super().debug(msg, *args, **kwargs)\n\n    def info(self, msg, *args, **kwargs):\n        \"\"\"\n        As a normal info, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(INFO), INFO,\n                                               msg, *args, **kwargs)\n        super().info(msg, *args, **kwargs)\n\n    def warning(self, msg, *args, **kwargs):\n        \"\"\"\n        As a normal warning, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(WARNING),\n                                               WARNING, msg, *args, **kwargs)\n        super().warning(msg, *args, **kwargs)\n\n    def error(self, msg, *args, **kwargs):\n        \"\"\"\n        As a normal error, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(ERROR), ERROR,\n                                               msg, *args, **kwargs)\n        super().error(msg, *args, **kwargs)\n\n    def critical(self, msg, *args, **kwargs):\n        \"\"\"\n        As a normal critical, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(CRITICAL),\n                                               CRITICAL, msg, *args, **kwargs)\n        super().critical(msg, *args, **kwargs)\n\n    def log(self, level, msg, *args, **kwargs):\n        \"\"\"\n        As a normal log, with the added functionality of this class\n        documented in the Class docstring\n        \"\"\"\n        if not self.is_skipped():\n            self.log_rotator.process_log_entry(self.isEnabledFor(level), level,\n                                               msg, *args, **kwargs)\n        super().log(level, msg, *args, **kwargs)\n"
  },
  {
    "path": "prusa/link/multi_instance/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/multi_instance/__main__.py",
    "content": "\"\"\"The module for starting PrusaLink Instance Manager components\"\"\"\nimport argparse\nimport logging\nimport os\nimport pwd\nimport signal\nimport sys\nimport threading\nfrom concurrent.futures import ThreadPoolExecutor\nfrom logging.handlers import SysLogHandler\nfrom pathlib import Path\n\nfrom daemon import DaemonContext  # type: ignore\nfrom lockfile.pidlockfile import PIDLockFile  # type: ignore\n\nfrom ..__main__ import check_process\nfrom ..__main__ import stop as stop_process\nfrom ..config import LOG_FORMAT_SYSLOG, Config, FakeArgs\nfrom ..util import ensure_directory\nfrom .config_component import MultiInstanceConfig\nfrom .const import (\n    DEFAULT_UID,\n    MANAGER_PID_PATH,\n    MULTI_INSTANCE_CONFIG_PATH,\n    RUN_DIRECTORY,\n    SERVER_PID_PATH,\n    UDEV_REFRESH_QUEUE_NAME,\n)\nfrom .controller import Controller\nfrom .ipc_queue_adapter import IPCSender\nfrom .web import get_web_server\n\nlog = logging.getLogger(__name__)\n\n\ndef main_thread_exception(exc_type, exc_value, exc_traceback):\n    \"\"\"Log unhandled exceptions\"\"\"\n    if issubclass(exc_type, KeyboardInterrupt):\n        sys.__excepthook__(exc_type, exc_value, exc_traceback)\n        return\n\n    log.exception(\"Unhandled exception reached top level\",\n                  exc_info=(exc_type, exc_value, exc_traceback))\n\n\ndef thread_exception(_):\n    \"\"\"Re-raise unhandled exceptions in threads to call sys.excepthook\"\"\"\n    # ruff: noqa: PLE0704\n    raise  # pylint: disable=misplaced-bare-raise\n\n\nthreading.excepthook = thread_exception\nsys.excepthook = main_thread_exception\n\n\ndef get_logger_file_descriptors():\n    \"\"\"Get the file descriptors for all loggers\"\"\"\n    file_descriptors = []\n    for handler in logging.root.handlers:\n        if hasattr(handler, \"socket\"):\n            file_descriptors.append(handler.socket.fileno())\n        if hasattr(handler, \"stream\"):\n            file_descriptors.append(handler.stream.fileno())\n    return file_descriptors\n\n\nclass Manager:\n    \"\"\"This class represents the process that runs the controller\"\"\"\n\n    pid_file = PIDLockFile(MANAGER_PID_PATH)\n\n    def __init__(self, user_info, prepend_executables_with):\n        self.user_info = user_info\n\n        if self.pid_file.is_locked():\n            if check_process(self.pid_file.read_pid()):\n                print(\"Manager already running\")\n                log.error(\"Manager already running\")\n                sys.exit(1)\n\n            self.pid_file.break_lock()\n\n        context = DaemonContext(\n            pidfile=self.pid_file,\n            files_preserve=get_logger_file_descriptors(),\n            signal_map={signal.SIGTERM: self._sigterm_handler},\n            detach_process=True,\n        )\n\n        with context:\n            self.controller = Controller(\n                user_info=self.user_info,\n                prepend_executables_with=prepend_executables_with)\n            self.controller.run()\n\n    def _sigterm_handler(self, *_):\n        \"\"\"Stops the controller. Has to return as fast as possible\"\"\"\n        log.info(\"Received SIGTERM. Stopping Multi Instance Manager\")\n        self.controller.stop()\n\n\nclass Server:\n    \"\"\"This class represents the process that runs the web server\"\"\"\n\n    pid_file = PIDLockFile(SERVER_PID_PATH)\n\n    def __init__(self, user_info):\n        self.user_info = user_info\n\n        self.web_server = None\n\n        if self.pid_file.is_locked():\n            if check_process(self.pid_file.read_pid()):\n                stop_process(self.pid_file.read_pid())\n\n            self.pid_file.break_lock()\n\n        context = DaemonContext(\n            uid=self.user_info.pw_uid,\n            gid=self.user_info.pw_gid,\n            files_preserve=get_logger_file_descriptors(),\n            pidfile=self.pid_file,\n            signal_map={signal.SIGTERM: self._sigterm_handler},\n            detach_process=True,\n        )\n\n        with context:\n            config = MultiInstanceConfig()\n            self.web_server = get_web_server(config.web.port_range_start)\n            self.web_server.start()\n            self.web_server.thread.join()\n\n    def _sigterm_handler(self, *_):\n        \"\"\"Stop the web server. Has to return as fast as possible\"\"\"\n\n        log.info(\"Received SIGTERM. Stopping Multi Instance Web Server\")\n        self.web_server.stop()\n\n\ndef get_username(username=None):\n    \"\"\"Return a valid username, if possible\"\"\"\n    if username is not None:\n        try:\n            return pwd.getpwnam(username).pw_name\n        except KeyError:\n            log.error(\"Could not find configured user %s. Exiting..\",\n                      username)\n            raise\n    else:\n        try:\n            return pwd.getpwuid(DEFAULT_UID).pw_name\n        except KeyError:\n            log.error(\"Could not get user for uid %s. Exiting...\",\n                      DEFAULT_UID)\n            raise\n\n\ndef start(user_info, prepend_executables_with):\n    \"\"\"Starts the instance manager processes\"\"\"\n    if os.fork() == 0:\n        Manager(user_info, prepend_executables_with)\n        sys.exit(0)\n    if os.fork() == 0:\n        Server(user_info)\n        sys.exit(0)\n\n\ndef handle_process_stop(pid_file, name=\"Process\", quiet=False):\n    \"\"\"Stops a process handling pid file edge cases\"\"\"\n    pid = pid_file.read_pid()\n    if pid is not None:\n        name = f\"{name} PID {pid}\"\n\n    if pid_file.is_locked() and check_process(pid):\n        stop_process(pid)\n    else:\n        if not quiet:\n            print(f\"{name} not running\")\n        log.warning(\"%s not running\", name)\n\n\ndef stop(quiet=False):\n    \"\"\"Stops the instance manager and all PrusaLink instances\"\"\"\n    multi_instance_config = MultiInstanceConfig()\n\n    stop_thread_count = len(multi_instance_config.printers) + 2\n\n    with ThreadPoolExecutor(max_workers=stop_thread_count) as executor:\n        executor.submit(handle_process_stop,\n                        Manager.pid_file,\n                        \"Instance Manager\",\n                        quiet)\n        executor.submit(handle_process_stop,\n                        Server.pid_file,\n                        \"Multi Instance Server\",\n                        quiet)\n        for printer in multi_instance_config.printers:\n            config = Config(FakeArgs(path=printer.config_path))\n            pid_file = PIDLockFile(Path(config.daemon.data_dir,\n                                        config.daemon.pid_file))\n            executor.submit(handle_process_stop,\n                            pid_file,\n                            \"PrusaLink instance\",\n                            quiet)\n\n\ndef clean(user_info, prepend_executables_with):\n    \"\"\"Stops the MultiInstance Manager and removes all printers\"\"\"\n    stop(quiet=True)\n    controller = Controller(user_info, prepend_executables_with)\n    controller.remove_all_printers()\n\n\ndef rescan():\n    \"\"\"Notify the manager that a connection has been established\n    by writing \"connected\" to the communication pipe.\"\"\"\n    try:\n        IPCSender.send_and_close(UDEV_REFRESH_QUEUE_NAME, \"rescan\")\n    except FileNotFoundError:\n        log.error(\"Cannot communicate to manager. Missing queue\")\n\n\ndef main():\n    \"\"\"The main function for the PrusaLink instance manager.\n    Parses command-line arguments and runs the instance controller\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Multi-instance suite for PrusaLink\")\n\n    parser.add_argument(\"-i\",\n                        \"--info\",\n                        action=\"store_true\",\n                        help=\"include log messages up to the INFO level\")\n    parser.add_argument(\"-d\",\n                        \"--debug\",\n                        action=\"store_true\",\n                        help=\"include log messages up to the INFO level\")\n\n    parser.add_argument(\n        \"-u\", \"--username\", required=False,\n        help=\"Which users to use for running and storing everything\")\n    parser.add_argument(\n        \"-p\", \"--prepend-executables-with\", required=False,\n        help=\"Environment variables and path to the executables directory\")\n\n    subparsers = parser.add_subparsers(dest=\"command\",\n                                       help=\"Available commands\")\n\n    # Create a subparser for the start_daemon command\n    subparsers.add_parser(\n        \"start\",\n        help=\"Start the instance managing daemon (needs root privileges)\")\n\n    subparsers.add_parser(\n        \"stop\",\n        help=\"Stop any manager daemon running (needs root privileges)\")\n\n    subparsers.add_parser(\n        \"clean\",\n        help=\"Danger! cleans all PrusaLink multi-instance configuration\")\n\n    # Create a subparser for the printer_connected command\n    subparsers.add_parser(\n        \"rescan\",\n        help=\"Notify the daemon a printer has been connected\")\n\n    args = parser.parse_args()\n\n    log_level = logging.WARNING\n    if args.info:\n        log_level = logging.INFO\n    if args.debug:\n        log_level = logging.DEBUG\n\n    logging.basicConfig(\n        level=log_level,\n        format=LOG_FORMAT_SYSLOG,\n        handlers=[SysLogHandler(address='/dev/log')],\n    )\n\n    safe_username = get_username(args.username)\n    user_info = pwd.getpwnam(safe_username)\n    prepend_executables_with = args.prepend_executables_with or \"\"\n\n    ensure_directory(RUN_DIRECTORY, chown_username=safe_username)\n    ensure_directory(Path(MULTI_INSTANCE_CONFIG_PATH).parent)\n\n    if args.command == \"start\":\n        start(user_info, prepend_executables_with)\n    elif args.command == \"stop\":\n        stop()\n    elif args.command == \"clean\":\n        clean(user_info, prepend_executables_with)\n    elif args.command == \"rescan\":\n        rescan()\n    else:\n        parser.print_help()\n"
  },
  {
    "path": "prusa/link/multi_instance/config_component.py",
    "content": "\"\"\"A module for managing the configuration files of multiple\nPrusaLink instances\"\"\"\nimport grp\nimport logging\nimport os\nimport shutil\nimport stat\nimport subprocess\nfrom pathlib import Path\nfrom time import monotonic, sleep\nfrom typing import List\n\nfrom blinker import Signal\nfrom extendparser import Get\n\nfrom ..config import Config, FakeArgs, Model\nfrom ..const import SUPPORTED_PRINTERS\nfrom ..util import PrinterDevice, ensure_directory, get_usb_printers\nfrom .const import (\n    CONFIG_PATH_PATTERN,\n    CONNECTED_RULE_PATH,\n    CONNECTED_RULE_PATTERN,\n    DEV_PATH,\n    MULTI_INSTANCE_CONFIG_PATH,\n    PORT_RANGE_START,\n    PRINTER_FOLDER_NAME_PATTERN,\n    PRINTER_NAME_PATTERN,\n    PRINTER_SYMLINK_PATTERN,\n    RULE_PATH_PATTERN,\n    RULE_PATTERN,\n    UDEV_SYMLINK_TIMEOUT,\n)\n\nlog = logging.getLogger(__name__)\n\n\nclass MultiInstanceConfig(Get):\n    \"\"\"This class handles the multi instance config file\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.read(MULTI_INSTANCE_CONFIG_PATH)\n        self.printers = []\n        self.web = None\n\n        self.web = Model(\n            self.get_section(\n                \"web\",\n                (\n                    (\"port_range_start\", int, PORT_RANGE_START),\n                ),\n            ),\n        )\n\n        for section in self.sections():\n            if section == \"web\":\n                continue\n\n            try:\n                self.add_from_section(section)\n            except (FileNotFoundError, AttributeError):\n                continue\n\n    def add(self, printer_number, serial_number, config_path):\n        \"\"\"Adds a new printer config using specified parameters\"\"\"\n        printer_name = PRINTER_NAME_PATTERN.format(\n            printer_number=printer_number)\n        printer = Model(\n            self.get_section(\n                printer_name,\n                (\n                    (\"number\", int, printer_number),\n                    (\"serial_number\", str, serial_number),\n                    (\"config_path\", str, config_path),\n                ),\n            ),\n        )\n        printer.name = printer_name\n        self.printers.append(printer)\n\n    def add_from_section(self, section_name: str):\n        \"\"\"Adds a new printer config using a section read from config\"\"\"\n        printer = Model(\n            self.get_section(\n                section_name,\n                (\n                    (\"number\", int, None),\n                    (\"serial_number\", str, None),\n                    (\"config_path\", str, None),\n                ),\n            ),\n        )\n        printer.name = section_name\n        for value in printer.values():\n            if value is None:\n                raise ValueError(f\"Invalid config for printer {section_name}\")\n        if not os.path.isfile(printer.config_path):\n            raise FileNotFoundError(\"The configured printer config \"\n                                    \"file is missing\")\n        self.printers.append(printer)\n\n    def save(self):\n        \"\"\"Writes everything from RAM to the config file\"\"\"\n        known_printers = set()\n        for printer in self.printers:\n            known_printers.add(printer.name)\n            if printer.name not in self:\n                self.add_section(printer.name)\n            for key, val in printer.items():\n                if key == \"name\":\n                    continue\n                self.set(printer.name, key, str(val))\n\n        if \"web\" not in self:\n            self.add_section(\"web\")\n        for key, val in self.web.items():\n            self.set(\"web\", key, str(val))\n\n        for section in self.sections():\n            if section in known_printers:\n                continue\n            if section == \"web\":\n                continue\n            self.remove_section(section)\n\n        with open(MULTI_INSTANCE_CONFIG_PATH, \"w\", encoding=\"UTF-8\") as file:\n            self.write(file)\n\n\nclass ConfigComponent:\n    \"\"\"Manages the configuration files and directories\"\"\"\n\n    def __init__(self, multi_instance_config, user_info,\n                 prepend_executables_with):\n        # -- create multi instance config --\n        self.multi_instance_config = multi_instance_config\n        self.user_info = user_info\n        self.prepend_executables_with = prepend_executables_with\n\n        self.highest_printer_number = self._get_highest_printer_number()\n\n        self.config_changed_signal = Signal()\n\n    def configure_instance(self, printer: PrinterDevice, printer_number):\n        \"\"\"Oversees the creation of an instance configuration for\n        a detected prnter device\"\"\"\n        try:\n            symlink_path = self._create_udev_rule(printer, printer_number)\n            config_path = CONFIG_PATH_PATTERN.format(number=printer_number)\n\n            # save multi_instance_config first\n            # we rely on it for deleting the config stuff if anything fails\n            self.multi_instance_config.add(\n                printer_number=printer_number,\n                serial_number=printer.serial_number,\n                config_path=config_path)\n            self.multi_instance_config.save()\n\n            # Create data folder\n            data_folder_name = PRINTER_FOLDER_NAME_PATTERN.format(\n                number=printer_number)\n            data_folder = os.path.join(\n                self.user_info.pw_dir, data_folder_name)\n            ensure_directory(data_folder, self.user_info.pw_name)\n\n            # Create printer config\n            self._create_printer_config(\n                printer_number=printer_number,\n                serial_port=symlink_path,\n                data_folder=data_folder,\n                config_path=config_path)\n\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Failed adding printer number %s\", printer_number)\n            self.remove_printers(numbers_to_remove=[printer_number])\n            raise\n\n    def remove_all_printers(self):\n        \"\"\"Clears the configuration of all printers\"\"\"\n        numbers_to_remove = [p.number for p in\n                             self.multi_instance_config.printers]\n        self.remove_printers(numbers_to_remove=numbers_to_remove)\n\n    def is_configured(self, serial_number):\n        \"\"\"Checks whether a printer with the specified serial number\n        is already configured or not\"\"\"\n        for printer in self.multi_instance_config.printers:\n            if printer.serial_number == serial_number:\n                return True\n        return False\n\n    def _get_highest_printer_number(self):\n        \"\"\"Gets the highest printer number among configured printers\"\"\"\n        highest = 0\n        for printer in self.multi_instance_config.printers:\n            highest = max(highest, printer.number)\n        return highest\n\n    def configure_new(self):\n        \"\"\"\n        Configure new printers found by scanning USB devices.\n\n        Returns:\n            list: A list of serial numbers of newly configured printers.\n        \"\"\"\n        configured = []\n        printer_number = self.highest_printer_number\n        for printer in get_usb_printers():\n            log.debug(\"Found printer: %s\", printer.serial_number)\n            if self.is_configured(printer.serial_number):\n                continue\n\n            printer_number += 1\n            log.debug(\"Configuring: %s\", printer.serial_number)\n            try:\n                self.configure_instance(printer, printer_number)\n            except Exception:  # pylint: disable=broad-except\n                printer_number -= 1\n                continue\n            configured.append(printer.serial_number)\n        self.highest_printer_number = printer_number\n\n        if configured:\n            self.config_changed_signal.send()\n\n        return configured\n\n    def setup_connected_trigger(self):\n        \"\"\"Sets up the udev rule that notifies us about the newly\n        connected printers\"\"\"\n        self.teardown_connected_trigger()\n\n        rule_lines = []\n        for vendor_id, model_ids in SUPPORTED_PRINTERS.items():\n            for model_id in model_ids:\n                log.info(\"Adding rule for %s:%s\", vendor_id, model_id)\n                rule_lines.append(CONNECTED_RULE_PATTERN.format(\n                    vendor_id=vendor_id,\n                    model_id=model_id,\n                    username=self.user_info.pw_name,\n                    prepend=self.prepend_executables_with,\n                ))\n        contents = \"\\n\".join(rule_lines)\n        log.info(\"Writing udev rule:\\n%s\", contents)\n        with open(CONNECTED_RULE_PATH, \"w\", encoding=\"UTF-8\") as file:\n            file.write(contents)\n        os.chmod(CONNECTED_RULE_PATH,\n                 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)\n        self.refresh_udev_rules()\n\n    def teardown_connected_trigger(self):\n        \"\"\"Removes the udev rule that notifies us about the newly\n        connected printers\"\"\"\n        if os.path.exists(CONNECTED_RULE_PATH):\n            os.remove(CONNECTED_RULE_PATH)\n        self.refresh_udev_rules()\n\n    def _create_udev_rule(self, printer: PrinterDevice, printer_number):\n        \"\"\"\n        Create a udev rule for the specified printer and printer number.\n\n        Args:\n            printer: PrinterDevice object representing a printer.\n            printer_number: An integer representing the printer number.\n\n        Returns:\n            str: The path of the created symlink.\n        \"\"\"\n        symlink_name = PRINTER_SYMLINK_PATTERN.format(number=printer_number)\n        symlink_path = os.path.join(DEV_PATH, symlink_name)\n        rule = RULE_PATTERN.format(\n            vendor_id=printer.vendor_id,\n            model_id=printer.model_id,\n            serial_number=printer.serial_number,\n            symlink_name=symlink_name,\n        )\n\n        log.debug(\"Udev rule: %s\", printer.serial_number)\n        rule_file_path = RULE_PATH_PATTERN.format(number=printer_number)\n        with open(rule_file_path, \"w\", encoding=\"UTF-8\") as file:\n            file.write(rule)\n\n        os.chmod(rule_file_path,\n                 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)\n\n        self.refresh_udev_rules()\n\n        self.wait_for_symlink(symlink_path)\n        return symlink_path\n\n    def _create_printer_config(self,\n                               printer_number,\n                               serial_port,\n                               data_folder,\n                               config_path):\n        \"\"\"\n        Create printer configuration file for the specified printer number,\n        serial port, and other parameters.\n\n        Args:\n            printer_number: An integer representing the printer number.\n            serial_port: A string representing the serial port for the printer.\n            data_folder: A string representing the path to the\n                         printer's data folder.\n\n        Returns:\n            str: The path of the created configuration file.\n        \"\"\"\n        port = self.multi_instance_config.web.port_range_start + printer_number\n        auto_detect_cameras = printer_number == 1\n\n        config = Config(FakeArgs(path=config_path))\n        config.daemon.data_dir = data_folder\n        config.daemon.pid_file = Path(data_folder, \"prusalink.pid\")\n        config.daemon.power_panic_file = Path(data_folder, \"power_panic\")\n        config.daemon.threshold_file = Path(data_folder, \"threshold.data\")\n        config.daemon.user = self.user_info.pw_name\n        config.daemon.group = grp.getgrgid(self.user_info.pw_gid).gr_name\n        config.daemon.printer_number = printer_number\n        config.printer.port = serial_port\n        config.printer.settings = Path(data_folder,\n                                       \"prusa_printer_settings.ini\")\n        directory = Path(data_folder, \"PrusaLink gcodes\").as_posix()\n        config.printer.directory = directory\n        config.http.port = port\n        # Only the first printer gets cameras, whichever that ends up being\n        config.cameras.auto_detect = auto_detect_cameras\n        config.update_sections()\n        with open(config_path, \"w\", encoding=\"UTF-8\") as file:\n            config.write(file)\n        log.debug(str(config_path))\n\n    def remove_printers(self, numbers_to_remove: List[int]):\n        \"\"\"Remove printer configuration files, udev rules,\n        and printer directories according to multi_instance_config.ini\n\n        numbers_to_remove: A list of printer numbers to remove\"\"\"\n\n        multi_instance_config = MultiInstanceConfig()\n\n        to_remove = []\n        valid_numbers = set()\n\n        for printer in multi_instance_config.printers:\n            if printer.number in numbers_to_remove:\n                to_remove.append(printer)\n                valid_numbers.add(printer.number)\n\n        # Check for non-existent printer numbers\n        invalid_numbers = set(numbers_to_remove) - valid_numbers\n        if invalid_numbers:\n            log.warning(\"Invalid printer numbers: %s. Not cleaning those\",\n                        invalid_numbers)\n\n        log.debug(\"Removing %s\", list(map(lambda i: i.name, to_remove)))\n        for printer in to_remove:\n            log.debug(\"removing printer %s\", printer.number)\n            # Delete the printer's data folder contents\n            if os.path.exists(printer.config_path):\n                config = Config(FakeArgs(path=printer.config_path))\n\n                data_dir = config.daemon.data_dir\n\n                # Delete PrusaLink files in the data directory\n                ConfigComponent.delete_file(\n                    config.daemon.pid_file)\n                ConfigComponent.delete_file(\n                    config.daemon.power_panic_file)\n                ConfigComponent.delete_file(\n                    config.daemon.threshold_file)\n\n                ConfigComponent.delete_folder(\n                    config.printer.directory)\n                ConfigComponent.delete_file(\n                    config.printer.settings)\n\n                # If the data directory is now empty, delete it\n                if not os.listdir(data_dir):\n                    log.debug(\"Folder %s empty, deleting it too!\", data_dir)\n                    os.rmdir(data_dir)\n\n            # Delete the printer's configuration file\n            ConfigComponent.delete_file(\n                CONFIG_PATH_PATTERN.format(number=printer.number))\n\n            # Delete the printer's udev rule\n            ConfigComponent.delete_file(\n                RULE_PATH_PATTERN.format(number=printer.number))\n\n            # Delete the printer's multi_instance_config.ini entry\n            multi_instance_config.printers.remove(printer)\n\n        multi_instance_config.save()\n\n        ConfigComponent.refresh_udev_rules()\n        self.config_changed_signal.send()\n\n    @staticmethod\n    def refresh_udev_rules():\n        \"\"\"Tells the udev system to load its rules again\"\"\"\n        subprocess.run(['udevadm', 'control', '--reload'], check=True)\n        subprocess.run(['udevadm', 'trigger', '-s', 'tty'], check=True)\n\n    @staticmethod\n    def delete_file(path):\n        \"\"\"Deletes a file, catching exceptions\"\"\"\n        try:\n            os.remove(path)\n            log.debug(\"Deleted %s\", path)\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Error deleting %s\", path)\n\n    @staticmethod\n    def delete_folder(path):\n        \"\"\"Deletes a folder, catching exceptions\"\"\"\n        try:\n            shutil.rmtree(path)\n            log.debug(\"Deleted %s\", path)\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Error deleting %s\", path)\n\n    @staticmethod\n    def wait_for_symlink(symlink_path):\n        \"\"\"Waits for a symlink to appear on the specified path\"\"\"\n        time_started = monotonic()\n        while not os.path.islink(symlink_path):\n            sleep(0.5)\n            log.debug(\"Waiting for symlink: %s\", symlink_path)\n            if monotonic() - time_started > UDEV_SYMLINK_TIMEOUT:\n                raise TimeoutError(\"The expected printer symlinks \"\n                                   \"didn't appear in tme\")\n"
  },
  {
    "path": "prusa/link/multi_instance/const.py",
    "content": "\"\"\"Contains constants used by the multi instance manager\"\"\"\nimport os\nimport re\n\nDEFAULT_UID = 1000  # Default user UID\n\nRUN_DIRECTORY = \"/run/prusalink\"\n\nMANAGER_PID_PATH = os.path.join(RUN_DIRECTORY, \"manager.pid\")\nSERVER_PID_PATH = os.path.join(RUN_DIRECTORY, \"server.pid\")\n# Named pipe for communication from not privileged to the privileged component\nUDEV_REFRESH_QUEUE_NAME = \"/prusalink_mi_udev_refresh\"\nWEB_REFRESH_QUEUE_NAME = \"/prusalink_mi_web_refresh\"\nWEB_COMMAND_QUEUE_NAME = \"/prusalink_mi_web_cmd\"\n\n# An udev rule to call a script that will tell us a printer has been connected\nCONNECTED_RULE_PATH = \"/etc/udev/rules.d/99-prusalink-manager-trigger.rules\"\nCONNECTED_RULE_PATTERN = \\\n    'SUBSYSTEM==\"tty\", ATTRS{{idVendor}}==\"{vendor_id}\", ' \\\n    'ATTRS{{idProduct}}==\"{model_id}\", ' \\\n    'RUN+=\"/bin/su {username} -c \\\\\"{prepend}prusalink-manager rescan\\\\\"\"'\n\nVALID_SN_REGEX = re.compile(r\"^(?P<sn>^CZPX\\d{4}X\\d{3}X.\\d{5})$\")\n\nMULTI_INSTANCE_CONFIG_PATH = \"/etc/prusalink/multi_instance.ini\"\n\nPRINTER_NAME_PATTERN = \"printer{printer_number}\"\nPRINTER_FOLDER_NAME_PATTERN = \"PrusaLink{number}\"\n\nCONFIG_PATH_PATTERN = \"/etc/prusalink/prusalink{number}.ini\"\n\nDEV_PATH = \"/dev/\"\nPRINTER_SYMLINK_PATTERN = \"ttyPRINTER{number}\"\n\nRULE_PATH_PATTERN = \"/etc/udev/rules.d/99-printer{number}.rules\"\nRULE_PATTERN = 'SUBSYSTEM==\"tty\", ' \\\n               'ATTRS{{idVendor}}==\"{vendor_id}\", ' \\\n               'ATTRS{{idProduct}}==\"{model_id}\", ' \\\n               'ATTRS{{serial}}==\"{serial_number}\", ' \\\n               'SYMLINK+=\"{symlink_name}\"'\n\nPRUSALINK_START_PATTERN = \\\n    'su {username} -c \"{prepend}prusalink -i -c {config_path} start\"'\n\n# How long to wait for the printer symlink to appear in devices\nUDEV_SYMLINK_TIMEOUT = 30  # seconds\n\n# The port of the main site\n# This plus one, so 8081 will be the port of the first PrusaLink instance\nPORT_RANGE_START = 8080\n"
  },
  {
    "path": "prusa/link/multi_instance/controller.py",
    "content": "\"\"\"A module implementing the controller of the PrusaLink Instance Manager\"\"\"\n\nimport logging\nimport os\n\nfrom .config_component import ConfigComponent, MultiInstanceConfig\nfrom .const import UDEV_REFRESH_QUEUE_NAME, WEB_REFRESH_QUEUE_NAME\nfrom .ipc_queue_adapter import IPCConsumer, IPCSender\nfrom .runner_component import RunnerComponent\n\nlog = logging.getLogger(__name__)\n\n\nclass Controller:\n    \"\"\"Glue between the multi instance components\"\"\"\n\n    def __init__(self, user_info, prepend_executables_with):\n        self.user_info = user_info\n\n        self.multi_instance_config = MultiInstanceConfig()\n\n        self.config_component = ConfigComponent(\n            self.multi_instance_config,\n            self.user_info,\n            prepend_executables_with)\n        self.runner_component = RunnerComponent(\n            self.multi_instance_config,\n            self.user_info,\n            prepend_executables_with)\n\n        self.ipc_consumer = IPCConsumer(UDEV_REFRESH_QUEUE_NAME,\n                                        chown_uid=self.user_info.pw_uid,\n                                        chown_gid=self.user_info.pw_gid)\n        self.ipc_consumer.add_handler(\"rescan\", self.rescan)\n\n        self.config_component.config_changed_signal.connect(\n            self.config_changed)\n\n    def run(self):\n        \"\"\"Starts the controller\"\"\"\n        self.runner_component.start_configured()\n        self.ipc_consumer.start()\n\n        self.config_component.setup_connected_trigger()\n\n        self.ipc_consumer.ipc_queue_thread.join()\n        log.info(\"Multi Instance Controller stopped\")\n\n    def rescan(self):\n        \"\"\"Handles the rescan notification by attempting to configure\n        all not configured printers and starting instances for them\"\"\"\n        log.debug(\"Rescanning printers\")\n        configured = self.config_component.configure_new()\n        for printer in self.multi_instance_config.printers:\n            if printer.serial_number not in configured:\n                continue\n            self.runner_component.load_instance(printer.config_path)\n\n    def stop(self):\n        \"\"\"Stops the controller\"\"\"\n        self.config_component.teardown_connected_trigger()\n        self.ipc_consumer.stop()\n\n    def remove_all_printers(self):\n        \"\"\"Removes all printers from the config\"\"\"\n        self.config_component.remove_all_printers()\n\n    def config_changed(self, *_):\n        \"\"\"A callback handler for when the config changes\"\"\"\n        # Notify the web server that the config has changed\n        IPCSender(WEB_REFRESH_QUEUE_NAME).send(\"refresh\")\n        # Try to prevent config corruption on unexpected shutdown\n        os.sync()\n"
  },
  {
    "path": "prusa/link/multi_instance/ipc_queue_adapter.py",
    "content": "\"\"\"A module implementing the IPC queue message consumer\"\"\"\nimport logging\nimport os\nimport queue\nfrom threading import Thread\nfrom typing import Callable\n\nfrom ipcqueue import posixmq  # type: ignore\n\nfrom ..const import QUIT_INTERVAL\nfrom ..util import prctl_name\n\nlog = logging.getLogger(__name__)\n\n\ndef get_queue_path(queue_name):\n    \"\"\"Returns the path to a message queue with the given name\"\"\"\n    # os path join needs the queue name without the leading slash\n    if queue_name.startswith(\"/\"):\n        queue_name = queue_name[1:]\n    return os.path.join(\"/dev/mqueue\", queue_name)\n\n\nclass IPCConsumer:\n    \"\"\"Class that sets up and consumes a message queue\"\"\"\n\n    def __init__(self,\n                 queue_name,\n                 chown_uid=None,\n                 chown_gid=None):\n        if not queue_name.startswith(\"/\"):\n            raise ValueError(\"Queue name must start with a slash\")\n\n        self.queue_name = queue_name\n        self.queue_path = get_queue_path(queue_name)\n        self.chown_uid = chown_uid if chown_uid is not None else os.getuid()\n        self.chown_gid = chown_gid if chown_gid is not None else os.getgid()\n\n        self.running = False\n        self.ipc_queue = None\n        self.command_handlers = {}\n\n        self.ipc_queue_thread = Thread(\n            target=self._read_commands, name=\"mi_cmd_reader\")\n\n    def add_handler(self, command: str, handler: Callable[[], None]):\n        \"\"\"Adds a handler for a text command\"\"\"\n        # TODO: add support for args and kwargs\n        self.command_handlers[command] = handler\n\n    def start(self):\n        \"\"\"Starts the message queue consumer\"\"\"\n        self.running = True\n        self._setup_queue()\n        self.ipc_queue_thread.start()\n\n    def stop(self):\n        \"\"\"Stops the consumer\"\"\"\n        self.running = False\n        self.ipc_queue_thread.join()\n        self.ipc_queue.unlink()\n\n    def _setup_queue(self):\n        \"\"\"Creates the pipe and sets the correct permissions\"\"\"\n        if os.path.exists(self.queue_path):\n            os.remove(self.queue_path)\n            # If this fails, we should exit, the queue\n            # could contain malicious messages\n\n        self.ipc_queue = posixmq.Queue(self.queue_name)\n\n        os.chown(self.queue_path,\n                 uid=self.chown_uid,\n                 gid=self.chown_gid)\n\n    def _read_commands(self):\n        \"\"\"Reads commands from the pipe and executes their handlers\"\"\"\n        # pylint: disable=deprecated-method\n        prctl_name()\n\n        while self.running:\n            try:\n                message = self.ipc_queue.get(block=True, timeout=QUIT_INTERVAL)\n            except queue.Empty:\n                continue\n            except posixmq.QueueError as exc:\n                if exc.errno == posixmq.QueueError.INTERRUPTED:\n                    continue\n                raise\n\n            command, args, kwargs = message\n\n            # pylint: disable=logging-too-many-args\n            log.debug(\"read: '%s' from ipc queue '%s'\",\n                      message, self.queue_name)\n            try:\n                if command in self.command_handlers:\n                    self.command_handlers[command](*args, **kwargs)\n                else:\n                    log.debug(\"Unknown command for multi instance '%s'\",\n                              command)\n            except Exception:  # pylint: disable=broad-except\n                log.exception(\"Exception occurred while handling an IPC\"\n                              \" command\")\n\n\nclass IPCSender:\n    \"\"\"A class that allows for easy sending of messages to message consumers\"\"\"\n\n    @staticmethod\n    def send_and_close(queue_name, command, *args, **kwargs):\n        \"\"\"Sends a message to the specified queue, if it exists,\n        then detaches from it\"\"\"\n        ipc_sender = IPCSender(queue_name)\n        ipc_sender.send(command, *args, **kwargs)\n        ipc_sender.close()\n\n    def __init__(self, queue_name):\n        self.queue_name = queue_name\n        self.queue_path = get_queue_path(queue_name)\n        if not os.path.exists(self.queue_path):\n            raise FileNotFoundError(f\"The ipc queue named {self.queue_path} \"\n                                    f\"does not exist\")\n\n        self.ipc_queue = posixmq.Queue(self.queue_name)\n\n    def send(self, command, *args, **kwargs):\n        \"\"\"Sends a message to the queue\"\"\"\n        message = (command, args, kwargs)\n        while True:\n            try:\n                self.ipc_queue.put(message)\n            except posixmq.QueueError as exc:\n                if exc.errno == posixmq.QueueError.INTERRUPTED:\n                    continue\n                raise\n\n            # pylint: disable=logging-too-many-args\n            log.debug(\"sent: '%s' to ipc queue '%s'\",\n                      message, self.queue_name)\n            break\n\n    def close(self):\n        \"\"\"Detaches from the queue\"\"\"\n        self.ipc_queue.close()\n\n    def __del__(self):\n        \"\"\"Make sure the queue got closed on destruct\"\"\"\n        try:\n            self.close()\n        except posixmq.QueueError:\n            pass\n"
  },
  {
    "path": "prusa/link/multi_instance/runner_component.py",
    "content": "\"\"\"The component that manages PrusaLink instances\nSadly stopping cannot be handled here for readability reasons\"\"\"\nimport logging\nimport os\nimport shlex\nimport subprocess\nfrom pathlib import Path\nfrom threading import Thread\n\nfrom ..config import Config, FakeArgs\nfrom .const import PRUSALINK_START_PATTERN\n\nlog = logging.getLogger(__name__)\n\n\nclass LoadedInstance:\n    \"\"\"Keeps info about already running instances\"\"\"\n\n    def __init__(self, config: Config, config_path: str):\n        self.config = config\n        self.config_path = config_path\n\n\nclass RunnerComponent:\n    \"\"\"The component that handles starting instance\"\"\"\n\n    def __init__(self, multi_instance_config, user_info,\n                 prepend_executables_with):\n        self.multi_instance_config = multi_instance_config\n        self.user_info = user_info\n        self.prepend_executables_with = prepend_executables_with\n        self.loaded = []\n\n    def start_configured(self):\n        \"\"\"Starts PrusaLink instances for configured printers\n        in multiple threads\"\"\"\n        threads = []\n        for printer in self.multi_instance_config.printers:\n            threads.append(\n                Thread(target=self.load_instance,\n                       name=printer.name,\n                       args=(printer.config_path,)),\n            )\n\n        for thread in threads:\n            thread.start()\n\n        for thread in threads:\n            thread.join()\n\n    def load_instance(self, config_path: str):\n        \"\"\"Starts an instance and gives it the specified config\n        in an argument\"\"\"\n        for loaded in self.loaded:\n            if config_path == loaded.config_path:\n                return\n\n        config = Config(FakeArgs(path=config_path))\n        pid_file = Path(config.daemon.data_dir, config.daemon.pid_file)\n        try:\n            os.remove(pid_file)\n        except FileNotFoundError:\n            pass\n        start_command = PRUSALINK_START_PATTERN.format(\n            prepend=self.prepend_executables_with,\n            username=self.user_info.pw_name,\n            config_path=config_path,\n        )\n        log.debug(shlex.split(start_command))\n        subprocess.run(shlex.split(start_command),\n                       check=True,\n                       timeout=10,\n                       stdin=subprocess.DEVNULL,  # DaemonContext needs\n                       stdout=subprocess.DEVNULL,  # these to not be None\n                       stderr=subprocess.DEVNULL)\n        self.loaded.append(LoadedInstance(config, config_path))\n"
  },
  {
    "path": "prusa/link/multi_instance/web.py",
    "content": "\"\"\"Init file for web application module.\"\"\"\nimport logging\nfrom hashlib import sha256\nfrom multiprocessing import Lock\nfrom time import monotonic\nfrom typing import Optional\n\nimport urllib3  # type: ignore\nfrom poorwsgi import Application\nfrom poorwsgi.response import GeneratorResponse, JSONResponse\nfrom poorwsgi.state import METHOD_ALL\n\nfrom ..config import Config, FakeArgs\nfrom ..web import WebServer\nfrom ..web.errors import not_found\nfrom ..web.lib.core import STATIC_DIR\nfrom ..web.lib.view import generate_page\nfrom .config_component import MultiInstanceConfig\nfrom .const import WEB_REFRESH_QUEUE_NAME\nfrom .ipc_queue_adapter import IPCConsumer\n\nlog = logging.getLogger(__name__)\n\nADDRESS = \"0.0.0.0\"\nCHUNK_SIZE = 32 * 1024  # 32 kiB\n\n\nclass InfoKeeper:\n    \"\"\"Keeps track of printers defined in the multi instance config file\"\"\"\n    class PrinterInfo:\n        \"\"\"Holds the info crucial for the landing page\"\"\"\n        def __init__(self, number, name, port):\n            self.number = number\n            self.name = name\n            self.port = port\n\n    def __init__(self):\n        self._lock = Lock()\n        self._refresh = True\n        self.ipc_consumer = IPCConsumer(WEB_REFRESH_QUEUE_NAME)\n        self.ipc_consumer.add_handler(\"refresh\", self.refresh)\n        self.ipc_consumer.start()\n        self._printer_info = {}\n\n    def refresh(self):\n        \"\"\"Causes the printer info to be refreshed on the next access\"\"\"\n        self._refresh = True\n\n    @property\n    def printer_info(self):\n        \"\"\"Gets the current printer info, updates it if anything changes on\n        disk\"\"\"\n        with self._lock:\n            if not self._refresh:\n                return self._printer_info\n\n            self._refresh = False\n            multi_instance_config = MultiInstanceConfig()\n            self._printer_info.clear()\n            for printer in multi_instance_config.printers:\n                config = Config(FakeArgs(path=printer.config_path))\n                self._printer_info[printer.number] = InfoKeeper.PrinterInfo(\n                    number=printer.number,\n                    name=printer.name,\n                    port=config.http.port,\n                )\n        return self._printer_info\n\n\nclass MultInstanceApp(Application):\n    \"\"\"WSGI application with info_keeper for the multi instance manager\"\"\"\n    info_keeper: Optional[InfoKeeper] = None\n\n\napp = MultInstanceApp(\"PrusaLink Multi Instance\")\napp.keep_blank_values = 1\napp.auto_form = False  # only POST /api/files/<target> endpoints get HTML form\napp.auto_json = False\napp.auto_data = False\napp.auto_cookies = False\napp.secret_key = sha256(str(monotonic()).encode()).hexdigest()\napp.document_root = STATIC_DIR\napp.debug = True\n\n\ndef single_instance_redirect(func):\n    \"\"\"Decorator that redirects to the single instance if there is only one\n    printer configured\"\"\"\n    def wrapper(req, *args, **kwargs):\n        \"\"\"Wrapper function\"\"\"\n        if len(req.app.info_keeper.printer_info) == 1:\n            first_printer = next(iter(\n                req.app.info_keeper.printer_info.values()))\n            return proxy(req,\n                         first_printer.number,\n                         req.path,\n                         use_proxy_headers=False)\n        return func(req, *args, **kwargs)\n\n    return wrapper\n\n\ndef get_web_server(port):\n    \"\"\"Returns an instance of the instance manager web server\"\"\"\n    app.info_keeper = InfoKeeper()\n    log.info('Starting server for http://%s:%d', ADDRESS, port)\n    web_server = WebServer(app, ADDRESS, port)\n    return web_server\n\n\n@app.route('/')\n@single_instance_redirect\ndef index(req):\n    \"\"\"The waypoint to point the user to a PrusaLink instance\"\"\"\n\n    return generate_page(req,\n                         \"multi-instance.html\",\n                         printer_info=req.app.info_keeper.printer_info)\n\n\n@app.route('/api/list')\ndef list_printers(req):\n    \"\"\"Get current S/N of the printer\"\"\"\n    # pylint: disable=unused-argument\n\n    response = []\n    for printer_number, printer in req.app.info_keeper.printer_info.items():\n        response.append(\n            {\n                \"number\": printer_number,\n                \"name\": printer.name,\n                \"port\": printer.port,\n            },\n        )\n    return JSONResponse(printer_list=response)\n\n\ndef get_content_length(headers):\n    \"\"\"Get content length from headers - 0 if not present\"\"\"\n    raw_content_length = headers.get('Content-Length')\n    if not raw_content_length:\n        return None\n    return int(raw_content_length)\n\n\ndef file_data_generator(file_like, length):\n    \"\"\"Pass an object with a read method and its length and get a generator\n    that yields chunks of the file's data.\"\"\"\n    transferred = 0\n    while True:\n        chunk_size = min(CHUNK_SIZE, length - transferred)\n        if chunk_size == 0:\n            break\n        data = file_like.read(chunk_size)\n        log.debug(\"Chunk-size: %s, Data: %s\", chunk_size, data)\n        yield data\n        transferred += chunk_size\n\n\n@app.route(r'/<printer_number:re:\\d+>/<path:re:.*>', method=METHOD_ALL)\n@app.route(r'/<printer_number:re:\\d+>', method=METHOD_ALL)\ndef proxy(req, printer_number, path=\"/\", use_proxy_headers=True):\n    \"\"\"A reverse proxy to pass requests to IP/number to IP:printer_port\n\n    @param use_proxy_headers: When re-directing to a single instance,\n    we re-use the whole uri path, no need for an extra prefix header\"\"\"\n    if path.startswith(\"/\"):\n        path = path[1:]\n    printer_info = req.app.info_keeper.printer_info\n    printer = printer_info.get(int(printer_number))\n    if printer is not None:\n        pool_manager = urllib3.PoolManager()\n\n        proxied_headers = dict(req.headers)\n        if use_proxy_headers:\n            proxied_headers[\"X-Forwarded-Prefix\"] = f\"/{printer_number}\"\n\n        log.debug(\"Passing request for path %s\", path)\n        request_to_pass = req\n        if (length := get_content_length(req.headers)) is not None:\n            request_to_pass = file_data_generator(req, length)\n\n        response = pool_manager.request(\n            method=req.method,\n            url=f\"http://localhost:{printer.port}/{path}?{req.query}\",\n            headers=proxied_headers,\n            preload_content=False,\n            body=request_to_pass,\n            redirect=False,\n        )\n\n        log.debug(\"Response for path %s: %s\", path, response.status)\n\n        response_to_pass = response\n        if (length := get_content_length(response.headers)) is not None:\n            response_to_pass = file_data_generator(response, length)\n\n        return GeneratorResponse(\n            generator=response_to_pass,\n            content_type=response.headers.get(\n                'Content-Type', \"text/html; charset=utf-8\"),\n            status_code=response.status,\n            headers=dict(response.headers))\n    return not_found(req)\n\n\n@app.default(METHOD_ALL)\n@single_instance_redirect\ndef fallback(req):\n    \"\"\"If there's more or less than one printer configured, this is the\n    404 page\"\"\"\n    return not_found(req)\n"
  },
  {
    "path": "prusa/link/printer_adapter/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/printer_adapter/auto_telemetry.py",
    "content": "\"\"\"Contains implementation of the ReportingEnsurer class\"\"\"\nfrom re import Match\nfrom time import time\n\nfrom ..const import REPORTING_TIMEOUT\nfrom ..serial.helpers import enqueue_instruction, wait_for_instruction\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom ..serial.serial_queue import SerialQueue\nfrom .model import Model\nfrom .structures.model_classes import Telemetry\nfrom .structures.regular_expressions import (\n    FAN_REGEX,\n    HEATING_HOTEND_REGEX,\n    HEATING_REGEX,\n    POSITION_REGEX,\n    TEMPERATURE_REGEX,\n)\nfrom .telemetry_passer import TelemetryPasser\nfrom .updatable import ThreadedUpdatable\n\n\nclass AutoTelemetry(ThreadedUpdatable):\n    \"\"\"\n    Monitors and parses autoreporting output, if any is missing, tries to turn\n    the autoreporting back on\n    \"\"\"\n    thread_name = \"temp_ensurer\"\n    update_interval = 10\n\n    def __init__(self, serial_parser: ThreadedSerialParser,\n                 serial_queue: SerialQueue,\n                 model: Model, telemetry_passer: TelemetryPasser):\n        super().__init__()\n        self.serial_parser = serial_parser\n        self.serial_queue = serial_queue\n        self.model: Model = model\n        self.telemetry_passer = telemetry_passer\n        self.serial_parser.add_decoupled_handler(\n                TEMPERATURE_REGEX, self.temps_recorded)\n        self.serial_parser.add_decoupled_handler(\n                HEATING_REGEX, self.temps_recorded)\n        self.serial_parser.add_decoupled_handler(\n                HEATING_HOTEND_REGEX, self.temps_recorded)\n        self.serial_parser.add_decoupled_handler(\n                POSITION_REGEX, self.positions_recorded)\n        self.serial_parser.add_decoupled_handler(FAN_REGEX, self.fans_recorded)\n\n        self.last_seen_positions = 0.\n        self.last_seen_fans = 0.\n        self.last_seen_temps = 0.\n\n    def temps_recorded(self, sender, match: Match):\n        \"\"\"\n        Reset the timeout for temperatures\n        and write them through to the model\n        \"\"\"\n        assert sender is not None\n        self.last_seen_temps = time()\n\n        values = match.groupdict()\n        telemetry = Telemetry(temp_nozzle=float(values[\"ntemp\"]))\n        if \"btemp\" in values:\n            telemetry.temp_bed = float(values[\"btemp\"])\n        if \"set_ntemp\" in values and \"set_btemp\" in values:\n            telemetry.target_nozzle = float(values[\"set_ntemp\"])\n            telemetry.target_bed = float(values[\"set_btemp\"])\n        self.telemetry_passer.set_telemetry(telemetry)\n\n    def positions_recorded(self, sender, match: Match):\n        \"\"\"\n        Reset the timeout for positions\n        and write them through to the model\n        \"\"\"\n        assert sender is not None\n        self.last_seen_positions = time()\n\n        values = match.groupdict()\n        self.telemetry_passer.set_telemetry(\n            Telemetry(axis_x=float(values[\"x\"]),\n                      axis_y=float(values[\"y\"]),\n                      axis_z=float(values[\"z\"])))\n\n    def fans_recorded(self, sender, match: Match):\n        \"\"\"\n        Reset the timeout for fans\n        and write their RPMs through to the model\n        \"\"\"\n        assert sender is not None\n        self.last_seen_fans = time()\n\n        values = match.groupdict()\n        self.telemetry_passer.set_telemetry(\n            Telemetry(fan_extruder=int(values[\"hotend_rpm\"]),\n                      fan_hotend=int(values[\"hotend_rpm\"]),\n                      fan_print=int(values[\"print_rpm\"]),\n                      target_fan_extruder=int(values[\"hotend_power\"]),\n                      target_fan_hotend=int(values[\"hotend_power\"]),\n                      target_fan_print=int(values[\"print_power\"])))\n\n    def update(self):\n        \"\"\"\n        If any one of the report intervals is larger than REPORTING_TIMEOUT\n        calls turn_reporting_on()\n        \"\"\"\n        refresh_times = (self.last_seen_temps, self.last_seen_positions,\n                         self.last_seen_fans)\n        biggest_interval = time() - min(refresh_times)\n\n        if biggest_interval > REPORTING_TIMEOUT:\n            self.turn_reporting_on()\n\n    def turn_reporting_on(self):\n        \"\"\"\n        Tries to turn reporting on using the M155\n        The C argument is the bitmask for type of autoreporting\n        The S argument is the frequency of autoreports\n        \"\"\"\n        instruction = enqueue_instruction(self.serial_queue, \"M155 S2 C7\")\n        wait_for_instruction(instruction, should_wait_evt=self.quit_evt)\n        self._reset_last_seen()\n\n    def proper_stop(self):\n        \"\"\"\n        Stops the autoreporting ensurer\n        and tries to turn the auto-reporting off\n        \"\"\"\n        timeout_at = time() + 5\n        instruction = enqueue_instruction(self.serial_queue, \"M155 S0 C0\")\n        wait_for_instruction(instruction, lambda: time() < timeout_at)\n        super().stop()\n\n    def _reset_last_seen(self):\n        \"\"\"Resets the last seen time of all tracked values\"\"\"\n        self.last_seen_positions = time()\n        self.last_seen_fans = time()\n        self.last_seen_temps = time()\n"
  },
  {
    "path": "prusa/link/printer_adapter/command.py",
    "content": "\"\"\"Contains implementation of the Command class\"\"\"\nimport abc\nimport logging\nimport re\nfrom threading import Event\nfrom typing import Any, Dict\n\nfrom prusa.connect.printer.const import Source\n\nfrom ..sdk_augmentation.printer import MyPrinter\nfrom ..serial.helpers import (\n    enqueue_instruction,\n    enqueue_matchable,\n    wait_for_instruction,\n)\nfrom ..serial.serial_adapter import SerialAdapter\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom ..serial.serial_queue import MonitoredSerialQueue\nfrom .file_printer import FilePrinter\nfrom .job import Job\nfrom .model import Model\nfrom .state_manager import StateManager\n\nlog = logging.getLogger(__name__)\n\n\nclass CommandFailed(Exception):\n    \"\"\"Exception class for signalling that a command has failed\"\"\"\n\n\nclass NotStateToPrint(CommandFailed):\n    \"\"\"Exception class for signalling that printer is not in state to print\"\"\"\n\n\nclass FileNotFound(CommandFailed):\n    \"\"\"A specific error for files that have not been found and the command\n    failing because of that\"\"\"\n\n\nclass Command:\n    \"\"\"Commands are like controllers, they do stuff and need a lot of info to\n    do it. This class provides most of the components a command could want to\n    access or use.\"\"\"\n    # pylint: disable=too-many-instance-attributes\n    command_name = \"command\"\n\n    def __init__(self, command_id=None, source=Source.CONNECT) -> None:\n        self.serial_queue: MonitoredSerialQueue = \\\n            MonitoredSerialQueue.get_instance()\n        self.serial_adapter: SerialAdapter = SerialAdapter.get_instance()\n        self.serial_parser: ThreadedSerialParser = \\\n            ThreadedSerialParser.get_instance()\n        self.model: Model = Model.get_instance()\n        self.printer: MyPrinter = MyPrinter.get_instance()\n        self.state_manager: StateManager = StateManager.get_instance()\n        self.file_printer: FilePrinter = FilePrinter.get_instance()\n        self.job: Job = Job.get_instance()\n\n        self.command_id = command_id\n        self.source = source\n\n        self.quit_evt = Event()\n\n    def wait_while_running(self, instruction):\n        \"\"\"Wait until the instruction is done, or we quit\"\"\"\n        wait_for_instruction(instruction, should_wait_evt=self.quit_evt)\n\n    def do_instruction(self, message):\n        \"\"\"Shorthand for enqueueing and waiting for an instruction\n        Enqueues everything to front as commands have a higher priority\"\"\"\n        instruction = enqueue_instruction(self.serial_queue,\n                                          message,\n                                          to_front=True)\n        self.wait_for_instruction(instruction)\n        return instruction\n\n    def do_matchable(self, message, regexp: re.Pattern):\n        \"\"\"Shorthand for enqueueing an waiting for a matchable instruction\n        Enqueues everything to front as commands have a higher priority\"\"\"\n        instruction = enqueue_matchable(self.serial_queue,\n                                        message,\n                                        regexp,\n                                        to_front=True)\n        self.wait_for_instruction(instruction)\n        return instruction\n\n    def wait_for_instruction(self, instruction):\n        \"\"\"Waits for instruction until it gets confirmed or we quit\"\"\"\n        self.wait_while_running(instruction)\n\n        if not instruction.is_confirmed():\n            raise CommandFailed(\"Command interrupted\")\n\n    def run_command(self) -> Dict[str, Any]:\n        \"\"\"Encapsulates the run command, provides default data for\n        returning\"\"\"\n        data = self._run_command()\n        default_data = {\"source\": self.source}\n        if data is not None:\n            default_data.update(data)\n        return default_data\n\n    @abc.abstractmethod\n    def _run_command(self):\n        \"\"\"Put implementation here\"\"\"\n\n    def stop(self):\n        \"\"\"Stops the command\"\"\"\n        self.quit_evt.set()\n"
  },
  {
    "path": "prusa/link/printer_adapter/command_handlers.py",
    "content": "\"\"\"\nImplements all command PrusaLink command handlers\nStart, pause, resume and stop print as well as one for executing arbitrary\ngcodes, resetting the printer and sending the job info\n\"\"\"\n\nimport abc\nimport json\nimport logging\nimport os\nfrom pathlib import Path\nfrom re import Match\nfrom subprocess import STDOUT, CalledProcessError, check_call, check_output\nfrom sys import executable\nfrom threading import Event\nfrom time import monotonic, time\nfrom typing import Dict, Optional, Set\n\nfrom prusa.connect.printer.const import Event as EventConst\nfrom prusa.connect.printer.const import Source, State\n\nfrom ..const import (\n    PRINTER_BOOT_WAIT,\n    QUIT_INTERVAL,\n    RESET_PIN,\n    SERIAL_QUEUE_TIMEOUT,\n    STATE_CHANGE_TIMEOUT,\n)\nfrom ..serial.helpers import enqueue_instruction, enqueue_list_from_str\nfrom ..util import (\n    _parse_little_endian_uint32,\n    file_is_on_sd,\n    get_d3_code,\n    round_to_five,\n)\nfrom .command import Command, CommandFailed, FileNotFound, NotStateToPrint\nfrom .model import Model\nfrom .state_manager import StateChange\nfrom .structures.model_classes import EEPROMParams, JobState, PPData\nfrom .structures.regular_expressions import (\n    D3_OUTPUT_REGEX,\n    OPEN_RESULT_REGEX,\n    PRINTER_BOOT_REGEX,\n    REJECTION_REGEX,\n    RESET_ACTIVATED_REGEX,\n    RESET_DEACTIVATED_REGEX,\n)\n\nlog = logging.getLogger(__name__)\n\n\ndef check_update_prusalink():\n    \"\"\"Run the bash script to check for PrusaLink updates and return output\"\"\"\n    return check_output(\n        [executable, '-m', 'pip', 'install', '--no-deps', '--dry-run',\n         '-U', 'prusalink'], stderr=STDOUT).decode()\n\n\ndef update_prusalink():\n    \"\"\"Run the bash script to update PrusaLink and return output\"\"\"\n    return check_output(\n        [executable, '-m', 'pip', 'install', '-U', '--upgrade-strategy',\n         'only-if-needed', '--break-system-packages', 'prusalink'],\n        stderr=STDOUT).decode()\n\n\ndef change_reset_mode(model, serial_adapter, serial_parser, quit_evt,\n                      timeout=1, enable=True):\n    \"\"\"Used for enabling or disabling the reset signal propagation of the\n    printer USB interface chip. DTR -> reset line\"\"\"\n    # pylint: disable=too-many-arguments\n    # The reset disabling is off - ignore the command\n    if not model.serial_adapter.reset_disabling:\n        return\n    # Already set to the target state, return early\n    if model.serial_adapter.resets_enabled == enable:\n        return\n\n    # Cannot disable resets from the gpio pins, give up early\n    using_port = model.serial_adapter.using_port\n    if using_port is None or using_port.is_rpi_port:\n        return\n\n    times_out_at = monotonic() + timeout\n    event = Event()\n\n    def waiter(sender, match):\n        \"\"\"Stops the wait for printer boot\"\"\"\n        assert sender is not None\n        assert match is not None\n        event.set()\n\n    confirm_regex = (RESET_ACTIVATED_REGEX if enable\n                     else RESET_DEACTIVATED_REGEX)\n    serial_parser.add_decoupled_handler(\n        confirm_regex, waiter)\n\n    if enable:\n        serial_adapter.enable_dtr_resets()\n    else:\n        serial_adapter.disable_dtr_resets()\n\n    while not quit_evt.is_set() and monotonic() < times_out_at:\n        if event.wait(QUIT_INTERVAL):\n            break\n\n    serial_parser.remove_handler(confirm_regex, waiter)\n\n    if monotonic() > times_out_at:\n        raise CommandFailed(\"Failed disabling USB DTR resets\")\n\n    model.serial_adapter.resets_enabled = enable\n\n\nclass TryUntilState(Command):\n    \"\"\"A base for commands stop, pause and resume print\"\"\"\n    command_name = \"pause/stop/resume print\"\n\n    def __init__(self, command_id=None, source=Source.CONNECT):\n        \"\"\"\n        Sends a gcode in hopes of getting into a specific state.\n        :param command_id: Which command asked for the state change\n        :param source: Who asked us to change state\n        \"\"\"\n        super().__init__(command_id=command_id, source=source)\n        self.right_state = Event()\n\n    def _try_until_state(self, gcode: str, desired_states: Set[State]):\n        \"\"\"\n        Sends a gcode in hopes of reaching a desired_state.\n        :param gcode: Which gcode to send. For example: \"M603\"\n        :param desired_states: Into which state do we hope to get\n        \"\"\"\n\n        def state_changed(sender, from_state, to_state, *args, **kwargs):\n            # --- pylint section ---\n            \"\"\"Reacts to every state change, if the desired state has been\n            reached, stops the wait by setting an event\"\"\"\n            assert sender is not None\n            assert from_state is not None\n            assert to_state is not None\n            assert args is not None\n            assert kwargs is not None\n\n            # --- actual code ---\n            if to_state in desired_states:\n                self.right_state.set()\n\n        if self.state_manager.get_state() not in desired_states:\n            to_states = dict.fromkeys(desired_states, self.source)\n            self.state_manager.expect_change(\n                StateChange(command_id=self.command_id, to_states=to_states))\n        state_list = list(map(lambda item: item.name, desired_states))\n        state_names = \", \".join(state_list)\n\n        log.debug(\"Trying to get to one of %s states.\", state_names)\n\n        self.state_manager.state_changed_signal.connect(state_changed)\n\n        self.do_instruction(gcode)\n\n        # Wait max n seconds for the desired state\n        wait_until = time() + STATE_CHANGE_TIMEOUT\n        succeeded = False\n\n        # Crush an edge case where we already are in the desired state\n        if self.model.state_manager.current_state in desired_states:\n            self.right_state.set()\n\n        while (not self.quit_evt.is_set()\n               and time() < wait_until\n               and not succeeded):\n            succeeded = self.right_state.wait(QUIT_INTERVAL)\n\n        self.state_manager.state_changed_signal.disconnect(state_changed)\n        self.state_manager.stop_expecting_change()\n\n        if not succeeded:\n            log.debug(\"Could not get from %s to one of these: %s\",\n                      self.state_manager.get_state(), desired_states)\n            raise CommandFailed(\n                f\"Couldn't get to any of {state_names} states.\")\n\n    @abc.abstractmethod\n    def _run_command(self):\n        ...\n\n\nclass StopPrint(TryUntilState):\n    \"\"\"Class for stopping a print\"\"\"\n    command_name = \"stop print\"\n\n    def _run_command(self):\n        \"\"\"\n        For serial prints, it first stops the flow of new commands using the\n        file printer component, then it uses its parent to go through the stop\n        sequence.\n        \"\"\"\n        if self.model.file_printer.printing:\n            self.file_printer.stop_print()\n\n        self._try_until_state(gcode=\"M603\",\n                              desired_states={\n                                  State.STOPPED, State.IDLE, State.READY,\n                                  State.FINISHED,\n                              })\n\n\nclass PausePrint(TryUntilState):\n    \"\"\"Class for pausing a running print\"\"\"\n    command_name = \"pause print\"\n\n    def _run_command(self):\n        \"\"\"If a print is in progress, pauses it.\n        When printing from serial, it pauses the file_printer,\n        before telling the printer to do the pause sequence.\n        \"\"\"\n        if self.state_manager.get_state() != State.PRINTING:\n            raise CommandFailed(\"Cannot pause when not printing.\")\n\n        if self.model.file_printer.printing:\n            self.file_printer.pause()\n\n        self._try_until_state(gcode=\"M601\", desired_states={State.PAUSED})\n\n\nclass ResumePrint(TryUntilState):\n    \"\"\"Class for resuming a paused print\"\"\"\n    command_name = \"resume print\"\n\n    def _run_command(self):\n        \"\"\"\n        If the print is paused, it gets resumed. The file_printer\n        component picks up on this by itself from the serial line,\n        so no communication here is required\n        \"\"\"\n        if self.state_manager.get_state() != State.PAUSED:\n            raise CommandFailed(\"Cannot resume when not paused.\")\n\n        self._try_until_state(gcode=\"M602\", desired_states={State.PRINTING})\n\n        # If we were file printing, the module itself will recognize\n        # it should resume from serial\n        # if self.file_printer.printing:\n        #     self.file_printer.resume()\n\n\nclass StartPrint(Command):\n    \"\"\"Class for starting a print from a given path\"\"\"\n    command_name = \"start print\"\n\n    def __init__(self, path: str, **kwargs):\n        super().__init__(**kwargs)\n        self.path_string = path\n\n    def _run_command(self):\n        \"\"\"\n        Starts a print using a file path. If the file resides on the SD,\n        it tells the printer to print it. If it's on the internal storage,\n        the file_printer component will be used.\n        :return:\n        \"\"\"\n\n        # No new print jobs while already printing\n        # or when there is an Error/Attention state\n        if self.model.state_manager.printing_state is not None:\n            raise NotStateToPrint(\"Already printing\")\n\n        if self.model.state_manager.override_state is not None:\n            raise NotStateToPrint(\n                f\"Cannot print in {self.state_manager.get_state()} state.\")\n\n        self.state_manager.expect_change(\n            StateChange(to_states={State.PRINTING: self.source},\n                        command_id=self.command_id))\n\n        path = Path(self.path_string)\n        parts = path.parts\n\n        if file_is_on_sd(parts):\n            # Cut the first \"/\" and \"SD Card\" off\n            sd_path = str(Path(\"/\", *parts[2:]))\n            try:\n                short_path = self.model.sd_card.lfn_to_sfn_paths[sd_path]\n            except KeyError:\n                # If this failed, try to use the supplied path as is\n                # in hopes it was the short path.\n                short_path = sd_path\n\n            self._load_file(short_path)\n            self._start_print()\n        else:\n            if self.printer.fs.get(self.path_string) is None:\n                raise FileNotFound(\n                    f\"The file at {self.path_string} does not exist.\")\n            self._start_file_print(self.path_string)\n\n        self.job.set_file_path(str(path),\n                               path_incomplete=False,\n                               prepend_sd_storage=False)\n        self.state_manager.printing()\n        self.state_manager.stop_expecting_change()\n\n    def _start_file_print(self, path):\n        \"\"\"\n        Converts connect path to os path\n        :param path:\n        \"\"\"\n        os_path = self.printer.fs.get_os_path(path)\n        self.file_printer.print(os_path)\n\n    def _load_file(self, raw_sd_path: str) -> None:\n        \"\"\"\n        Sends the gcod required to load the file from a given sd path\n        :param raw_sd_path: The absolute sd path (starts with a \"/\")\n        \"\"\"\n        sd_path = raw_sd_path.lower()  # FW requires lower case\n\n        instruction = self.do_matchable(f\"M23 {sd_path}\", OPEN_RESULT_REGEX)\n        match: Match = instruction.match()\n\n        if not match or match.group(\"ok\") is None:  # Opening failed\n            raise CommandFailed(\n                f\"Wrong file name, or bad file. File name: {sd_path}\")\n\n    def _start_print(self):\n        \"\"\"Sends a gcode to start the print of an already loaded file\"\"\"\n        self.do_instruction(\"M24\")\n\n\nclass ExecuteGcode(Command):\n    \"\"\"Class for executing an arbitrary gcode or gcode list\"\"\"\n    command_name = \"execute_gcode\"\n\n    def __init__(self, gcode, force=False, **kwargs):\n        \"\"\"\n        If all checks pass, runs the specified gcode.\n        :param gcode: \"\\n\" separated gcodes to send to the printer\"\"\n        :param force: Whether to skip state checks\n        \"\"\"\n        super().__init__(**kwargs)\n        self.gcode = gcode\n        self.force = force\n\n    def _run_command(self):\n        \"\"\"\n        Sends the commands set if __init__ if all checks pass.\n        Attributes the first state change to connect.\n        Doesn't renew the expected state change, so the other state changes\n        will fall back onto defaults\n        \"\"\"\n        if self.force:\n            log.debug(\"Force sending gcode: '%s'\", self.gcode)\n\n        state = self.model.state_manager.current_state\n        if not self.force:\n            if state in {State.PRINTING, State.ATTENTION, State.ERROR}:\n                raise CommandFailed(\n                    f\"Can't run '{self.gcode}' while in f{state.name} state.\")\n\n        self.state_manager.expect_change(\n            StateChange(command_id=self.command_id,\n                        default_source=self.source))\n\n        line_list = []\n        for line in self.gcode.split(\"\\n\"):\n            if line.strip():\n                line_list.append(line.replace(\"\\r\", \"\"))\n\n        # try running every line\n        # Do this manually as it's the only place where a list\n        # has to be enqueued\n        instruction_list = enqueue_list_from_str(self.serial_queue,\n                                                 line_list,\n                                                 REJECTION_REGEX,\n                                                 to_front=True)\n\n        for instruction in instruction_list:\n            self.wait_while_running(instruction)\n\n            if not instruction.is_confirmed():\n                raise CommandFailed(\"Command interrupted\")\n\n            match = instruction.match()\n            if match:\n                if match.group(\"unknown\") is not None:\n                    raise CommandFailed(f\"Unknown command '{self.gcode}')\")\n                if match.group(\"cold\") is not None:\n                    raise CommandFailed(\"Cold extrusion prevented\")\n\n        # If the gcode execution did not cause a state change\n        # stop expecting it\n        self.state_manager.stop_expecting_change()\n\n    @staticmethod\n    def _get_state_change(default_source):\n        return StateChange(default_source=default_source)\n\n\nclass FilamentCommand(Command):\n    \"\"\"The shared code for Loading and Unloading of filament\"\"\"\n\n    def __init__(self, parameters: Optional[Dict], **kwargs):\n        super().__init__(**kwargs)\n        self.parameters = parameters\n\n    def prepare_for_load_unload(self):\n        \"\"\"\n        Check if the state allows for this operation\n        Set temperatures for load/unload filament, wait only if it's colder\n\n        Does not block, the assumption being that the command\n        we're preheating for will wait for its completion\n        \"\"\"\n        state = self.model.state_manager.current_state\n        if state in {State.PRINTING, State.ATTENTION, State.ERROR}:\n            raise CommandFailed(\n                f\"Can't run {self.command_name} while in {state.name} state.\")\n\n        target_bed = self.parameters[\"bed_temperature\"]\n        target_print_temp = self.parameters[\"nozzle_temperature\"]\n        # Extrusion temperature = 90% of target nozzle temperature\n        target_extrude_temp = round_to_five(target_print_temp * 0.9)\n\n        # Heat up the bed\n        enqueue_instruction(self.serial_queue,\n                            f\"M140 S{target_bed}\",\n                            to_front=True)\n\n        # M109 is supposed to wait only for heating\n        # when the S argument is given. Since it's broken,\n        # let's check ourselves and skip waiting if we're hotter than required\n        temp_nozzle = self.model.latest_telemetry.temp_nozzle\n        if temp_nozzle is None or temp_nozzle < target_extrude_temp:\n            enqueue_instruction(self.serial_queue,\n                                f\"M109 S{target_extrude_temp}\",\n                                to_front=True)\n        enqueue_instruction(self.serial_queue,\n                            f\"M104 S{target_print_temp}\",\n                            to_front=True)\n\n    @abc.abstractmethod\n    def _run_command(self):\n        ...\n\n\nclass LoadFilament(FilamentCommand):\n    \"\"\"Class for load filament command\"\"\"\n\n    command_name = \"load_filament\"\n\n    def _run_command(self):\n        \"\"\"Load filament - see FilamentCommand\"\"\"\n        # The load and unload have the same preheat\n        self.prepare_for_load_unload()\n        # A little workaround for M701 not actually supporting our use case\n        enqueue_instruction(self.serial_queue, \"M300 P500 S1\", to_front=True)\n        enqueue_instruction(self.serial_queue,\n                            \"M0 Insert the filament\",\n                            to_front=True)\n        self.do_instruction(\"M701\")\n\n\nclass UnloadFilament(FilamentCommand):\n    \"\"\"Class for unload filament command\"\"\"\n\n    command_name = \"unload_filament\"\n\n    def _run_command(self):\n        \"\"\"Unload filament - see FilamentCommand\"\"\"\n        # The load and unload have the same preheat\n        self.prepare_for_load_unload()\n        self.do_instruction(\"M702\")\n\n\nclass ResetPrinter(Command):\n    \"\"\"Class for resetting the printer\"\"\"\n\n    command_name = \"reset_printer\"\n    timeout = 30\n    if timeout < PRINTER_BOOT_WAIT or timeout < SERIAL_QUEUE_TIMEOUT:\n        raise RuntimeError(\"Cannot have smaller timeout than what the printer \"\n                           \"needs to boot.\")\n\n    def _run_command(self):\n        \"\"\"\n        Checks whether we have pigpio available, if yes, uses the RESET_PIN,\n        if not, uses USB DTR to reset the printer. Thanks @leptun.\n\n        Waits until the printer boots and checks, if the printer wrote \"start\"\n        as it shoul do on every boot.\n        \"\"\"\n        if RESET_PIN == 23:\n            raise CommandFailed(\n                \"Pin BCM_23 is by default connected straight to \"\n                \"ground. This would destroy your pin.\")\n\n        times_out_at = time() + self.timeout\n        event = Event()\n\n        def waiter(sender, match):\n            \"\"\"Stops the wait for printer boot\"\"\"\n            assert sender is not None\n            assert match is not None\n            event.set()\n\n        self.serial_parser.add_decoupled_handler(PRINTER_BOOT_REGEX, waiter)\n\n        self.state_manager.expect_change(\n            StateChange(default_source=self.source,\n                        command_id=self.command_id))\n\n        # Make sure the USB DTR resets are on\n        try:\n            change_reset_mode(self.model, self.serial_adapter,\n                              self.serial_parser, self.quit_evt,\n                              timeout=self.timeout, enable=True)\n        except CommandFailed:\n            # If we fail for whatever reason, try and reset the printer anyways\n            pass\n\n        self.serial_adapter.reset_client()\n\n        while not self.quit_evt.is_set() and time() < times_out_at:\n            if event.wait(QUIT_INTERVAL):\n                break\n\n        self.serial_parser.remove_handler(PRINTER_BOOT_REGEX, waiter)\n\n        if time() > times_out_at:\n            raise CommandFailed(\n                \"Your printer has ignored the reset signal, your RPi \"\n                \"is broken or you have configured a wrong pin,\"\n                \"or our serial reading component broke..\")\n\n\nclass UpgradeLink(Command):\n    \"\"\"Class for upgrading PrusaLink\"\"\"\n    command_name = \"upgrade_link\"\n\n    def _run_command(self):\n        try:\n            output = update_prusalink()\n\n            # No update available\n            if \"Installing collected packages\" not in output:\n                raise CommandFailed(\"No update available\")\n\n            # New version was installed correctly - restart PrusaLink\n            check_call([executable, '-m', 'prusalink', 'restart'])\n            log.info(\"PrusaLink upgraded successfully\")\n\n        # There's a problem with package installation, or it does not exist\n        except CalledProcessError as exception:\n            raise CommandFailed(\"There's a problem with package installation, \"\n                                \"or it does not exist\") from exception\n\n\nclass JobInfo(Command):\n    \"\"\"Class for sending/getting the job info\"\"\"\n    command_name = \"job_info\"\n\n    def _run_command(self):\n        \"\"\"Returns job_info from the job component\"\"\"\n        if self.model.job.job_state == JobState.IDLE:\n            raise CommandFailed(\n                \"Cannot get job info, when there is no job in progress.\")\n\n        if self.model.job.job_id is None:\n            raise CommandFailed(\n                \"Cannot get job info, don't know the job id yet.\")\n\n        # Happens when launching into a paused print\n        if self.model.job.selected_file_path is None:\n            raise CommandFailed(\n                \"Cannot get job info, don't know the file details yet.\")\n\n        data = self.job.get_job_info_data(\n            for_connect=self.command_id is not None)\n\n        response = {\n            \"job_id\": self.model.job.get_job_id_for_api(),\n            \"state\": self.model.state_manager.current_state,\n            \"event\": EventConst.JOB_INFO,\n            \"source\": Source.CONNECT,\n            \"time_printing\": self.model.latest_telemetry.time_printing,\n            \"time_remaining\": self.model.latest_telemetry.time_remaining,\n            \"progress\": self.model.latest_telemetry.progress,\n            **data}\n\n        log.debug(\"Job Info retrieved: %s\", response)\n        return response\n\n\nclass SetReady(Command):\n    \"\"\"Class for setting the printer into READY\"\"\"\n    command_name = \"set_ready\"\n\n    def _run_command(self):\n        \"\"\"Sets the printer into ready, if it's IDLE\"\"\"\n        if self.state_manager.get_state() not in {State.IDLE, State.READY}:\n            raise CommandFailed(\n                \"Cannot get into READY from anywhere other than IDLE\")\n        self.state_manager.expect_change(\n            StateChange(command_id=self.command_id,\n                        default_source=self.source))\n        self.state_manager.ready()\n        self.state_manager.stop_expecting_change()\n        self.do_instruction(\"M72 S1\")\n\n\nclass CancelReady(Command):\n    \"\"\"Class for setting the printer into READY\"\"\"\n    command_name = \"cancel_ready\"\n\n    def _run_command(self):\n        \"\"\"Cancels the READY state\"\"\"\n        # Sets the LCD menu to reflect reality even if our state is not READY\n        self.do_instruction(\"M72 S0\")\n\n        if self.model.state_manager.base_state != State.READY:\n            raise CommandFailed(\"Cannot cancel READY when not actually ready.\")\n        self.state_manager.expect_change(\n            StateChange(command_id=self.command_id,\n                        default_source=self.source))\n        self.state_manager.idle()\n        self.state_manager.stop_expecting_change()\n\n\nclass RePrint(StartPrint):\n    \"\"\"Class for starting the last job again\"\"\"\n    command_name = \"re-print\"\n\n    def __init__(self, **kwargs):\n        # Need to get the model sooner than it's available in self\n        model = Model.get_instance()\n        path = model.job.last_job_path\n        if path is None:\n            path = \"\"\n        super().__init__(path=path, **kwargs)\n\n    def _run_command(self):\n        \"\"\"Re-prints the last job, makes a noise and sends an LCD message\n        if that fails\"\"\"\n        try:\n            super()._run_command()\n        except CommandFailed as exception:\n            # Not an ideal way to do this, but less time-consuming\n            enqueue_instruction(self.serial_queue, \"M300 P200 S600\")\n            enqueue_instruction(self.serial_queue, \"M117 \\x7ECannot re-print\")\n            raise exception\n\n\nclass DisableResets(Command):\n    \"\"\"Class for disabling printer USB DTR resets\"\"\"\n    command_name = \"disable_resets\"\n    timeout = 1\n\n    def _run_command(self):\n        \"\"\"Disables resets\"\"\"\n        change_reset_mode(self.model, self.serial_adapter, self.serial_parser,\n                          self.quit_evt, timeout=self.timeout, enable=False)\n\n\nclass EnableResets(Command):\n    \"\"\"Class for enabling printer USB DTR resets\"\"\"\n    command_name = \"enable_resets\"\n    timeout = 1\n\n    def _run_command(self):\n        \"\"\"Enables resets\"\"\"\n        change_reset_mode(self.model, self.serial_adapter, self.serial_parser,\n                          self.quit_evt, timeout=self.timeout, enable=True)\n\n\nclass PPRecovery(Command):\n    \"\"\"Class for recovering from the host power panic\"\"\"\n    command_name = \"pp_recovery\"\n\n    def _run_command(self):\n        \"\"\"Recovers from host power panic\"\"\"\n        if self.model.file_printer.recovering:\n            return\n        try:\n            if not self.file_printer.pp_exists:\n                raise CommandFailed(\"No PP file exists, cannot recover.\")\n\n            d_code = get_d3_code(*EEPROMParams.EEPROM_FILE_POSITION.value)\n            match = self.do_matchable(d_code, D3_OUTPUT_REGEX).match()\n            if match is None:\n                raise CommandFailed(\"Failed to get file position\")\n            line_number = _parse_little_endian_uint32(match)\n            self.serial_queue.set_message_number(line_number)\n            if not self.file_printer.pp_exists:\n                log.warning(\"Cannot recover from power panic, \"\n                            \"no pp state found\")\n                raise RuntimeError(\"Cannot recover from power panic, \"\n                                   \"no pp state found\")\n\n            with open(self.model.file_printer.pp_file_path, \"r\",\n                      encoding=\"UTF-8\") as pp_file:\n                pp_data = PPData(**json.load(pp_file))\n\n                gcode_number = (pp_data.gcode_number\n                                + (line_number - pp_data.message_number))\n                path = pp_data.file_path\n                connect_path = pp_data.connect_path\n\n            if not os.path.isfile(path):\n                raise CommandFailed(\n                    \"The file we were previously printing from has \"\n                    \"disappeared.\")\n\n        except CommandFailed as exception:\n            enqueue_instruction(\n                self.serial_queue, \"M117 \\x7ERecovery failed\", to_front=True)\n            enqueue_instruction(\n                self.serial_queue, \"M603\", to_front=True)\n            raise exception\n\n        self.file_printer.print(path, gcode_number - 1)\n        self.job.set_file_path(str(connect_path),\n                               path_incomplete=False,\n                               prepend_sd_storage=False)\n"
  },
  {
    "path": "prusa/link/printer_adapter/command_queue.py",
    "content": "\"\"\"\nImplements the CommandQueue with CommandAdapter class, the objects of\nwithch are the queue members\n\"\"\"\n\nimport logging\nfrom queue import Empty, Queue\nfrom threading import Event, RLock\nfrom typing import Any, Dict, Optional\n\nfrom ..const import QUIT_INTERVAL\nfrom ..util import prctl_name\nfrom .command import Command, CommandFailed\nfrom .telemetry_passer import TelemetryPasser\nfrom .updatable import Thread\n\nlog = logging.getLogger(__name__)\n\n\nCommandResult = Dict[str, Any]\n\n\nclass CommandAdapter:\n    \"\"\"Adapts the command class for processing in a queue\"\"\"\n\n    # pylint: disable=too-few-public-methods\n    def __init__(self, command) -> None:\n        self.processed = Event()\n        self.data: CommandResult = {}\n        self.exception: Optional[Exception] = None\n        self.command: Command = command\n\n\nclass CommandQueue:\n    \"\"\"\n    Executes commands from queue in its own thread\n    Prevents command racing\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.running = False\n        self.command_queue: Queue[CommandAdapter] = Queue()\n        self.current_command_adapter: Optional[CommandAdapter] = None\n        self.runner_thread = Thread(target=self.process_queue,\n                                    name=\"command_queue\",\n                                    daemon=True)\n        self.enqueue_lock = RLock()\n\n    def start(self) -> None:\n        \"\"\"Start the command processing\"\"\"\n        self.running = True\n        self.runner_thread.start()\n\n    def stop(self) -> None:\n        \"\"\"Stop the command processing\"\"\"\n        self.running = False\n        self._stop_current()\n\n    def enqueue_command(self, command: Command) -> CommandAdapter:\n        \"\"\"\n        Ask for a command to be processed\n        :param command: The command to be processed\n        \"\"\"\n        with self.enqueue_lock:\n            adapter = CommandAdapter(command)\n            self.command_queue.put(adapter)\n            return adapter\n\n    def do_command(self, command: Command):\n        \"\"\"\n        Block until the command gets processed, pass what it returns\n        :param command: The command to be processed\n        \"\"\"\n        TelemetryPasser.get_instance().activity_observed()\n\n        if not self.running:\n            log.warning(\"Don't wait for commands enqueued in a non-\"\n                        \"running command queue\")\n\n        adapter = self.enqueue_command(command)\n        while self.running:\n            if adapter.processed.wait(QUIT_INTERVAL):\n                break\n        if adapter.exception is not None:\n            raise adapter.exception  # pylint: disable=raising-bad-type\n        if not adapter.processed.is_set():\n            log.warning(\"Unprocessed command %s!\", adapter.command)\n            raise CommandFailed(\"Command has not been processed because \"\n                                \"PrusaLink is stopping or in an error state\")\n        return adapter.data\n\n    def force_command(self, command: Command):\n        \"\"\"Drops everything and does the supplied command\"\"\"\n        with self.enqueue_lock:\n            self.clear_queue()\n            return self.do_command(command)\n\n    def process_queue(self) -> None:\n        \"\"\"\n        Runs until stopped, processes commands in queue, writes outputs\n        into a dict\n        \"\"\"\n        prctl_name()\n        while self.running:\n            try:\n                adapter: CommandAdapter = self.command_queue.get(\n                    timeout=QUIT_INTERVAL)\n            except Empty:\n                continue\n\n            try:\n                self.current_command_adapter = adapter\n                adapter.data = adapter.command.run_command()\n            except Exception as exception:  # pylint: disable=broad-except\n                # Don't forget to pass exceptions as well as values\n                adapter.exception = exception\n            adapter.processed.set()\n\n    def _stop_current(self):\n        \"\"\"Stops current command, if there is any\"\"\"\n        if self.current_command_adapter is not None:\n            self.current_command_adapter.command.stop()\n\n    def clear_queue(self):\n        \"\"\"Clears the whole command queue\"\"\"\n        with self.enqueue_lock:\n            self._stop_current()\n            while not self.command_queue.empty():\n                adapter = self.command_queue.get()\n                adapter.command.stop()\n"
  },
  {
    "path": "prusa/link/printer_adapter/file_printer.py",
    "content": "\"\"\"Contains implementation of the FilePrinter class\"\"\"\nimport json\nimport logging\nimport os\nfrom collections import deque\nfrom threading import RLock\nfrom time import sleep\nfrom typing import Optional\n\nfrom blinker import Signal  # type: ignore\n\nfrom ..config import Config\nfrom ..const import (\n    HISTORY_LENGTH,\n    PRINT_QUEUE_SIZE,\n    QUIT_INTERVAL,\n    STATS_EVERY,\n    TAIL_COMMANDS,\n)\nfrom ..serial.helpers import enqueue_instruction, wait_for_instruction\nfrom ..serial.instruction import Instruction\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom ..serial.serial_queue import SerialQueue\nfrom ..util import get_clean_path, get_gcode, get_print_stats_gcode, prctl_name\nfrom .model import Model\nfrom .print_stats import PrintStats\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.model_classes import PPData\nfrom .structures.module_data_classes import FilePrinterData\nfrom .structures.regular_expressions import (\n    CANCEL_REGEX,\n    RESUMED_REGEX,\n)\nfrom .updatable import Thread\n\nlog = logging.getLogger(__name__)\n\n\nclass FilePrinter(metaclass=MCSingleton):\n    \"\"\"\n    Facilitates serial printing, its pausing, resuming and stopping as well,\n    controls print_stats, which provide info about progress and time left\n    for gcodes without said info\n    \"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self, serial_queue: SerialQueue,\n                 serial_parser: ThreadedSerialParser, model: Model,\n                 cfg: Config) -> None:\n        self.print_stats = PrintStats(model)\n        self.serial_queue = serial_queue\n        self.serial_parser = serial_parser\n        self.model = model\n\n        self.new_print_started_signal = Signal()\n        self.print_stopped_signal = Signal()\n        self.print_finished_signal = Signal()\n        self.time_printing_signal = Signal()\n        self.byte_position_signal = Signal()  # kwargs: current: int\n        #                                               total: int\n        self.layer_trigger_signal = Signal()\n        self.recovery_done_signal = Signal()\n\n        self.lock = RLock()\n\n        self.model.file_printer = FilePrinterData(\n            printing=False,\n            paused=False,\n            recovering=False,\n            was_stopped=False,\n            power_panic=False,\n            recovery_ready=False,\n            file_path=\"\",\n            pp_file_path=get_clean_path(cfg.daemon.power_panic_file),\n            enqueued=deque(),\n            gcode_number=0)\n        self.data = self.model.file_printer\n\n        self.serial_parser.add_decoupled_handler(\n            CANCEL_REGEX, lambda sender, match: self.stop_print())\n        self.serial_parser.add_decoupled_handler(\n            RESUMED_REGEX, lambda sender, match: self.resume())\n\n        self.thread: Optional[Thread] = None\n\n    def start(self) -> None:\n        \"\"\"Power panic is not yet implemented, sso this does nothing\"\"\"\n        # self.check_failed_print()\n\n    def stop(self) -> None:\n        \"\"\"Indicate to the printing thread to stop\"\"\"\n        if self.data.printing:\n            self.stop_print()\n\n    def wait_stopped(self) -> None:\n        \"\"\"Wait for the printing thread to stop\"\"\"\n        if self.thread is not None and self.thread.is_alive():\n            self.thread.join()\n\n    @property\n    def pp_exists(self) -> bool:\n        \"\"\"Checks whether a file created on power panic exists\"\"\"\n        return os.path.exists(self.data.pp_file_path)\n\n    def print(self, os_path: str, from_gcode_number=None) -> None:\n        \"\"\"Starts a file print for the supplied path\"\"\"\n        if self.data.printing:\n            raise RuntimeError(\"Cannot print two things at once\")\n\n        if from_gcode_number is None and self.pp_exists:\n            os.remove(self.data.pp_file_path)\n\n        self.data.file_path = os_path\n        self.thread = Thread(target=self._print,\n                             name=\"file_print\",\n                             args=(from_gcode_number,),\n                             daemon=True)\n        self.data.printing = True\n        self.data.recovering = from_gcode_number is not None\n        self.data.was_stopped = False\n        self.data.power_panic = False\n        self.data.paused = False\n        self.data.enqueued.clear()\n        self.print_stats.start_time_segment()\n        self.new_print_started_signal.send(self)\n        self.print_stats.track_new_print(self.data.file_path,\n                                         from_gcode_number)\n        self.thread.start()\n\n    def power_panic(self) -> None:\n        \"\"\"Handle the printer sending us a power panic  signal\n        This means halt the serial print, do not send any more instructions\n        Do not delete the power panic file\"\"\"\n        self.data.power_panic = True\n        self.data.printing = False\n        log.warning(\"Power panic!\")\n\n    def _print(self, from_gcode_number=None):\n        \"\"\"\n        Parses and sends the gcode commands from the file to serial.\n        Supports pausing, resuming and stopping.\n\n        param from_gcode_number:\n            the gcode number to start from. Implies power panic recovery -\n            goes into pause when the correct gcode number is reached\n        \"\"\"\n        history_accumulator = []\n\n        prctl_name()\n        total_size = os.path.getsize(self.data.file_path)\n        with open(self.data.file_path, \"r\", encoding='utf-8') as file:\n            self.data.gcode_number = 0\n            self.data.enqueued.clear()\n\n            if not self.data.recovering:\n                # Reset the line counter, printing a new file\n                self.serial_queue.reset_message_number()\n                self.do_instruction(\"M75\")  # start printer's print timer\n\n            while True:\n                line = file.readline()\n\n                # Recognise the end of the file\n                if line == \"\" or not self.data.printing:\n                    break\n\n                gcode = get_gcode(line)\n                # Skip to the part we need to recover from\n                if (self.data.recovering\n                        and from_gcode_number > self.data.gcode_number):\n                    if gcode:\n                        history_from = from_gcode_number - HISTORY_LENGTH\n                        if self.data.gcode_number >= history_from:\n                            history_accumulator.append(gcode)\n                        self.data.gcode_number += 1\n                    continue\n\n                # Skip finished, pause here, remove the recovering flag\n                if self.data.recovering:\n                    history_accumulator.append(gcode)\n                    self.serial_queue.replenish_history(history_accumulator)\n                    self.pause()\n\n                # This will make it PRINT_QUEUE_SIZE lines in front of what\n                # is being sent to the printer, which is another as much as\n                # 16 gcode commands in front of what's actually being printed.\n                current_byte = file.tell()\n                self.byte_position_signal.send(self,\n                                               current=current_byte,\n                                               total=total_size)\n\n                if self.data.paused:\n                    self._print_pause()\n                    if not self.data.printing:\n                        break\n\n                # Trigger cameras on layer change\n                if \";LAYER_CHANGE\" in line:\n                    self.layer_trigger_signal.send()\n\n                if gcode:\n                    self.print_gcode(gcode)\n                    self.wait_for_queue()\n                    self.react_to_gcode(gcode)\n\n            # Print ended\n            self._print_end()\n\n    def _print_pause(self):\n        \"\"\"Handles the specific of a paused flie print\"\"\"\n        log.debug(\"Pausing USB print\")\n        if self.data.recovering:\n            self.data.recovery_ready = True\n        else:\n            # pause printer's print timer\n            self.do_instruction(\"M76\")\n        while self.data.paused:\n            sleep(QUIT_INTERVAL)\n\n        if self.data.recovering:\n            self.data.recovering = False\n            self.data.recovery_ready = False\n            self.recovery_done_signal.send()\n\n        # If we ended the pause by a print stop, do not unpause the timer\n        if self.data.printing:\n            log.debug(\"Resuming USB print\")\n            self.do_instruction(\"M75\")  # resume printer's print timer\n\n    def _print_end(self):\n        \"\"\"Handles the end of a file print\"\"\"\n        self.data.enqueued.clear()\n        self.print_stats.reset_stats()\n        log.debug(\"Print ended\")\n\n        if self.data.power_panic:\n            return\n\n        os.remove(self.data.pp_file_path)\n        self.do_instruction(\"M77\")  # stop printer's print timer\n\n        self.data.printing = False\n\n        if self.data.was_stopped:\n            self.serial_queue.flush_print_queue()\n            # Prevents the print head from stopping in the print\n            enqueue_instruction(self.serial_queue, \"M603\", to_front=True)\n            self.print_stopped_signal.send(self)\n        else:\n            self.print_finished_signal.send(self)\n\n    def do_instruction(self, message):\n        \"\"\"Shorthand for enqueueing and waiting for an instruction\n        Enqueues everything to front as commands have a higher priority\"\"\"\n        instruction = enqueue_instruction(self.serial_queue,\n                                          message,\n                                          to_front=True)\n        wait_for_instruction(instruction, lambda: self.data.printing)\n        return instruction\n\n    def print_gcode(self, gcode):\n        \"\"\"Sends a gcode to print, keeps a small buffer of gcodes\n         and inlines print stats for files without them\n        (estimated time left and progress)\"\"\"\n        with self.lock:\n            self.data.gcode_number += 1\n\n            divisible = self.data.gcode_number % STATS_EVERY == 0\n            if divisible:\n                time_printing = int(self.print_stats.get_time_printing())\n                self.time_printing_signal.send(\n                    self, time_printing=time_printing)\n\n            if self.to_print_stats(self.data.gcode_number):\n                self.send_print_stats()\n\n            log.debug(\"USB enqueuing gcode: %s\", gcode)\n            instruction = enqueue_instruction(self.serial_queue,\n                                              gcode,\n                                              to_front=True,\n                                              to_checksum=True)\n            self.data.enqueued.append(instruction)\n\n    def wait_for_queue(self) -> None:\n        \"\"\"Gets rid of already confirmed messages and waits for any\n        unconfirmed surplus\"\"\"\n        # Pop all already confirmed instructions from the queue\n        while self.data.enqueued:  # ensure there is at least one item\n            instruction = self.data.enqueued.popleft()\n            if not instruction.is_confirmed():\n                self.data.enqueued.appendleft(instruction)\n                break\n            log.debug(\"Throwing out trash %s\", instruction.message)\n        # If there are more than allowed and yet unconfirmed messages\n        # Wait for the surplus ones\n        while len(self.data.enqueued) >= PRINT_QUEUE_SIZE:\n            wait_for: Instruction = self.data.enqueued.popleft()\n            wait_for_instruction(wait_for, lambda: self.data.printing)\n\n            log.debug(\"%s confirmed\", wait_for.message)\n\n    def react_to_gcode(self, gcode):\n        \"\"\"\n        Some gcodes need to be reacted to right after they get enqueued\n         in order to compensate for the file_printer gcode buffer\n\n        For example M601 - Pause needs to pause the file read process\n        as soon as it's sent\n        :param gcode: gcode to react to\n        \"\"\"\n        if gcode.startswith(\"M601\") or gcode.startswith(\"M25\"):\n            self.pause()\n\n    def send_print_stats(self):\n        \"\"\"Sends a gcode to the printer, which tells it the progress\n        percentage and estimated time left, the printer is expected to send\n        back its standard print stats output for parsing in telemetry\"\"\"\n        percent_done, time_remaining = self.print_stats.get_stats(\n            self.data.gcode_number)\n\n        # Idk what to do here, idk what would have happened if we used\n        # the other mode, so let's report both modes the same\n        stat_command = get_print_stats_gcode(\n            normal_percent=percent_done,\n            normal_left=time_remaining,\n            quiet_percent=percent_done,\n            quiet_left=time_remaining)\n        instruction = enqueue_instruction(self.serial_queue,\n                                          stat_command,\n                                          to_front=True)\n        self.data.enqueued.append(instruction)\n\n    def to_print_stats(self, gcode_number):\n        \"\"\"\n        Decides whether to calculate and send print stats based on the\n        file being printed having stats or not,v the gcode number\n        divisibility, or just before the end of a file print\n        \"\"\"\n        divisible = gcode_number % STATS_EVERY == 0\n        do_stats = not self.model.print_stats.has_inbuilt_stats\n        print_ending = (\n            gcode_number == self.model.print_stats.total_gcode_count -\n            TAIL_COMMANDS)\n        return do_stats and (divisible or print_ending)\n\n    def pause(self):\n        \"\"\"Pauses the print by flipping a flag, pauses print timer\"\"\"\n        if self.data.paused:\n            return\n        self.data.paused = True\n        self.print_stats.end_time_segment()\n\n    def resume(self):\n        \"\"\"\n        If paused, resumes the print by flipping a flag,\n        resumes print timer\n        \"\"\"\n        # TODO: wrong, needs to be in line with the rest of commands\n        if not self.data.printing:\n            return\n        if not self.data.paused:\n            return\n        self.data.paused = False\n        self.print_stats.start_time_segment()\n\n    def stop_print(self):\n        \"\"\"If printing, stops the print and indicates by a flag, that the\n        print has been stopped and did not finish on its own\"\"\"\n        # TODO: wrong, needs to be in line with the rest of commands\n        if self.data.printing:\n            self.data.was_stopped = True\n            self.data.printing = False\n            self.data.paused = False\n\n    def write_file_stats(self, file_path, message_number, gcode_number):\n        \"\"\"Writes the data needed for power panic recovery\"\"\"\n        data = PPData(\n            file_path=file_path,\n            connect_path=self.model.job.selected_file_path,\n            message_number=message_number,\n            gcode_number=gcode_number,\n            using_rip_port=self.model.serial_adapter.using_port.is_rpi_port,\n        )\n        with open(self.data.pp_file_path, \"w\", encoding=\"UTF-8\") as pp_file:\n            pp_file.write(json.dumps(data.dict()))\n            os.fsync(pp_file)  # make sure this gets written to storage\n\n    def serial_message_number_changed(self, message_number):\n        \"\"\"Updates the pairing of the FW message number to gcode line number\n\n        If all the instructions in the buffer are sent\n        The message number belongs to the next instruction\n        that will be sent\n\n        Here's an illustration of the situation\n        _________________________________________\n        |enqueued   |gcode_number|message_number|\n        |           | current=25 | current=100  |\n        |___________|____________|______________|\n        |next instr.|     26     |     102      |\n        | I0        |    *25*    |     101      |\n        | I1        |     24     |    *100*     |\n        | I2 (sent) |     23     |     99       |\n        | I3 (sent) |     22     |     98       |\n        |___________|____________|______________|\n        \"\"\"\n\n        with self.lock:\n            instruction_gcode_number = self.data.gcode_number + 1\n            for instruction in self.data.enqueued:\n                if instruction.is_sent():\n                    break\n                instruction_gcode_number -= 1\n            self.write_file_stats(self.data.file_path, message_number,\n                                  instruction_gcode_number)\n"
  },
  {
    "path": "prusa/link/printer_adapter/filesystem/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/printer_adapter/filesystem/sd_card.py",
    "content": "\"\"\"Contains implementation of the class for keeping track of the sd status\nand its files\"\"\"\n\nimport calendar\nimport logging\nimport re\nfrom itertools import islice\nfrom pathlib import Path\nfrom threading import Lock\nfrom time import time\nfrom typing import Optional\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.const import State\n\nfrom ...const import (\n    MAX_FILENAME_LENGTH,\n    SD_INTERVAL,\n    SD_STORAGE_NAME,\n    SFN_TO_LFN_EXTENSIONS,\n)\nfrom ...sdk_augmentation.file import SDFile\nfrom ...serial.helpers import (\n    enqueue_list_from_str,\n    enqueue_matchable,\n    wait_for_instruction,\n)\nfrom ...serial.serial_parser import ThreadedSerialParser\nfrom ...serial.serial_queue import SerialQueue\nfrom ...util import fat_datetime_to_tuple\nfrom ..model import Model\nfrom ..structures.model_classes import SDState\nfrom ..structures.module_data_classes import SDCardData\nfrom ..structures.regular_expressions import (\n    CONFIRMATION_REGEX,\n    LFN_CAPTURE,\n    SD_EJECTED_REGEX,\n    SD_PRESENT_REGEX,\n)\nfrom ..updatable import ThreadedUpdatable\n\nlog = logging.getLogger(__name__)\n\n\ndef alternative_filename(long_filename: str,\n                         short_filename: str,\n                         long_extension: Optional[str] = None):\n    \"\"\"\n    Ensures uniqueness of a file name by prepending it with its\n    guaranteed to be unique short name\n    \"\"\"\n    new_filename = f\"{short_filename} - ({long_filename})\"\n    if long_extension is not None:\n        new_filename += f\".{long_extension}\"\n    log.warning(\"Filename %s too long, using an alternative: %s\",\n                long_filename, new_filename)\n    return new_filename\n\n\ndef get_root():\n    \"\"\"Gets the root node for sd card files\"\"\"\n    return SDFile(name=SD_STORAGE_NAME, is_dir=True, read_only=True)\n\n\nclass FileTreeParser:\n    \"\"\"\n    Parses the file tree from a printer supplied format\n    \"\"\"\n\n    def __init__(self, matches):\n        self.matches = matches\n        self.tree = get_root()\n        self.current_dir = Path(\"/\")\n        self.lfn_to_sfn_paths = {}\n        self.sfn_to_lfn_paths = {}\n        self.mixed_to_lfn_paths = {}\n\n        if not matches:\n            return\n\n        first_line_group = matches[0].group(\"begin\")\n        last_line_group = matches[-1].group(\"end\")\n        if first_line_group is None or last_line_group is None:\n            log.warning(\"Captured unexpected output.\")\n            return\n\n        # Captured can be three distinct lines.\n        # Dir entry, dir exit, or a file listing.\n        for match in islice(matches, 1, len(matches) - 1):\n            groups = match.groupdict()\n            if groups[\"dir_enter\"] is not None:  # Dir entry\n                self.parse_dir(groups)\n            elif groups[\"file\"] is not None:  # The list item\n                self.parse_file(groups)\n            elif groups[\"dir_exit\"] is not None:  # Dir exit\n                self.current_dir = self.current_dir.parent\n\n    def check_uniqueness(self, path: Path):\n        \"\"\"Checks, whether the supplied path is not present in the tree\"\"\"\n        # Ignores the first \"/\"\n        if self.tree.get(path.parts[1:]) is not None:\n            log.error(\"Despite our efforts, there is a name conflict for %s\",\n                      path)\n\n    def parse_file(self, groups):\n        \"\"\"Parses the file listing using the _captured groups\"\"\"\n        # pylint: disable=too-many-locals\n        short_path_string = groups[\"sfn\"].lower()\n        if short_path_string[0] != \"/\":\n            short_path_string = \"/\" + short_path_string\n        short_filename = Path(short_path_string).name\n        short_dir_path = Path(short_path_string).parent\n        short_extension = groups[\"extension\"]\n        long_extension = SFN_TO_LFN_EXTENSIONS[short_extension]\n        raw_long_filename = groups[\"lfn\"]\n\n        if raw_long_filename is None:\n            return\n\n        # --- Parse the long file name ---\n\n        too_long = len(raw_long_filename) >= MAX_FILENAME_LENGTH\n\n        if too_long:\n            long_file_name = alternative_filename(raw_long_filename,\n                                                  short_filename,\n                                                  long_extension)\n        else:\n            long_file_name = raw_long_filename\n\n        long_path = self.current_dir.joinpath(long_file_name)\n        self.check_uniqueness(long_path)\n        long_path_string = str(long_path)\n\n        mixed_path = short_dir_path.joinpath(raw_long_filename)\n        mixed_path_string = str(mixed_path).lower()\n\n        # Add translation between the two\n        log.debug(\"Adding translation between %s and %s\", long_path_string,\n                  short_path_string)\n        log.debug(\"Adding translation from %s to %s\", mixed_path,\n                  long_path_string)\n        self.lfn_to_sfn_paths[long_path_string] = short_path_string\n        self.sfn_to_lfn_paths[short_path_string] = long_path_string\n        self.mixed_to_lfn_paths[mixed_path_string] = long_path_string\n\n        # --- parse additional properties ---\n\n        additional_properties = {}\n\n        str_size = groups[\"size\"]\n        if str_size is not None:\n            additional_properties[\"size\"] = int(str_size)\n\n        str_m_time = groups[\"m_time\"]\n        if str_m_time is not None:\n            m_time = fat_datetime_to_tuple(int(str_m_time, 16))\n            m_timestamp = calendar.timegm(m_time)\n            additional_properties[\"m_timestamp\"] = m_timestamp\n\n        # Add the file to the tree\n        try:\n            self.tree.add_file(self.current_dir,\n                               long_file_name,\n                               short_filename,\n                               filename_too_long=too_long,\n                               **additional_properties)\n        except FileNotFoundError as exception:\n            log.exception(exception)\n\n    def parse_dir(self, groups):\n        \"\"\"Parses the dir info using the _captured groups\"\"\"\n        long_dir_name = groups[\"ldn\"]\n        short_dir_name = Path(groups[\"sdn\"]).name\n\n        # Sanitize the dir name\n        too_long = len(long_dir_name) >= MAX_FILENAME_LENGTH\n        if too_long:\n            new_name = alternative_filename(long_dir_name, short_dir_name)\n            self.current_dir = self.current_dir.joinpath(new_name)\n        else:\n            self.current_dir = self.current_dir.joinpath(long_dir_name)\n\n        self.check_uniqueness(self.current_dir)\n        # Add the dir to the tree\n        try:\n            self.tree.add_directory(self.current_dir.parent,\n                                    self.current_dir.name,\n                                    short_dir_name,\n                                    filename_too_long=too_long)\n        except FileNotFoundError as exception:\n            log.exception(exception)\n\n\nclass SDCard(ThreadedUpdatable):\n    \"\"\"\n    Keeps track of the SD Card presence and content\n\n    The SD state can start only in the UNSURE state, we know nothing\n\n    From there, we will ask the printer about the files present.\n    If there are files, the SD card is present.\n    If not, we still know nothing and need to ask the printer to re-init the\n    card that provides the information about SD card presence\n\n    Now that there's the SD ejection message, no more fortune-telling wizardry\n    needs to be happening\n\n    Unlikely now, was very likely before:\n    The card removal could've gone unnoticed and the printer is telling\n    us about an SD insertion. Let's tell connect the card got removed and go\n    to the INITIALISING state\n    \"\"\"\n    thread_name = \"sd_updater\"\n\n    # Cycle fast, but re-scan only on events or in big intervals\n    update_interval = SD_INTERVAL\n\n    def __init__(self, serial_queue: SerialQueue,\n                 serial_parser: ThreadedSerialParser, model: Model):\n\n        self.tree_updated_signal = Signal()  # kwargs: tree: FileTree\n        self.state_changed_signal = Signal()  # kwargs: sd_state: SDState\n        self.sd_attached_signal = Signal()  # kwargs: files: SDFile\n        self.sd_detached_signal = Signal()\n        self.menu_found_signal = Signal()  # kwargs: menu_sfn: str\n\n        self.serial_parser = serial_parser\n        self.serial_parser.add_decoupled_handler(SD_PRESENT_REGEX,\n                                                 self.sd_inserted)\n        self.serial_parser.add_decoupled_handler(SD_EJECTED_REGEX,\n                                                 self.sd_ejected)\n        self.serial_queue: SerialQueue = serial_queue\n        self.model = model\n\n        self.model.sd_card = SDCardData(expecting_insertion=False,\n                                        invalidated=True,\n                                        last_updated=time(),\n                                        last_checked_flash_air=time(),\n                                        sd_state=SDState.UNSURE,\n                                        files=None,\n                                        lfn_to_sfn_paths={},\n                                        sfn_to_lfn_paths={},\n                                        mixed_to_lfn_paths={},\n                                        is_flash_air=False)\n        self.data = self.model.sd_card\n        self.lock = Lock()\n\n        super().__init__()\n\n    def handle_special_menu(self, file_tree_parser):\n        \"\"\"If the SD contains a special menu folder, add the menu items\n        and inform others that the menu exists.\"\"\"\n        if \"PrusaLink menu\" not in file_tree_parser.tree.children:\n            return\n        node = file_tree_parser.tree.children[\"PrusaLink menu\"]\n        if not node.is_dir:\n            return\n        menu_sfn = node.attrs[\"sfn\"].lower()\n        if \"SETREADY.G\" not in node.children:\n            enqueue_list_from_str(\n                self.serial_queue,\n                [f\"M28 {menu_sfn}/setready.g\", \"M84\", \"M29\"],\n                CONFIRMATION_REGEX,\n                to_front=True)\n        del file_tree_parser.tree.children[\"PrusaLink menu\"]\n        self.menu_found_signal.send(menu_sfn=menu_sfn)\n\n    def update(self):\n        \"\"\"\n        Updates the file list on the SD Card.\n        Except:\n        - when the printer state is not IDLE\n        - when we already have a file listing and no FlashAir is connected\n        - When FlashAir is connected and configured, but it hasn't been long\n          enough from the previous update\n        \"\"\"\n        # Update only if IDLE\n        if self.model.state_manager.current_state != State.IDLE:\n            return\n\n        # since_last_update = time() - self.data.last_updated\n        # due_for_update = since_last_update > SD_FILESCAN_INTERVAL\n\n        # Do not update, if the tree wasn't invalidated.\n        # Also, if there is no flash air, or if there is, but it wasn't long\n        # enough from the last update\n        if not self.data.invalidated:\n            # or due_for_update and self.data.is_flash_air:\n            return\n\n        self.data.last_updated = time()\n        self.data.invalidated = False\n\n        if self.data.sd_state == SDState.ABSENT:\n            return\n\n        file_tree_parser = self._construct_file_tree()\n\n        self.handle_special_menu(file_tree_parser)\n\n        to_decide_presence = False\n\n        with self.lock:\n            self._set_files(file_tree_parser)\n\n            if self.data.sd_state == SDState.UNSURE:\n                # The files are of type SDFile - the root is always present\n                # Check if it has any children -> files were found on the SD\n                if self.data.files.children:\n                    self._sd_state_changed(SDState.PRESENT)\n                else:\n                    # If we do not know the sd state and no files were found,\n                    # check the SD presence\n                    to_decide_presence = True\n\n            if self.data.sd_state == SDState.INITIALISING:\n                self._sd_state_changed(SDState.PRESENT)\n\n        if to_decide_presence:\n            self.decide_presence()\n\n        self.tree_updated_signal.send(self, tree=self.data.files)\n\n    def _set_files(self, file_tree_parser: FileTreeParser):\n        \"\"\"Sets the file variables according to the supplied parsing context\"\"\"\n        assert self.lock.locked()\n        self.data.files = file_tree_parser.tree\n        # Try to be as atomic as possible\n        self.data.lfn_to_sfn_paths = file_tree_parser.lfn_to_sfn_paths\n        self.data.sfn_to_lfn_paths = file_tree_parser.sfn_to_lfn_paths\n        # 8.3/8.3/LFN format to LFN/LFN/LFN\n        self.data.mixed_to_lfn_paths = file_tree_parser.mixed_to_lfn_paths\n\n    def set_flash_air(self, is_flash_air):\n        \"\"\"\n        Sets the value determining if flash air functionality should be on\n        (temporary)\n        \"\"\"\n        self.data.is_flash_air = is_flash_air\n\n    def _construct_file_tree(self) -> FileTreeParser:\n        \"\"\"\n        Uses M20 LT to get the list of paths.\n\n        Some shorthand terms need explaining here:\n        SFN - short file name\n        LFN - long file name\n        SDN - short directory name\n        LDN - long directory name\n\n        The readout is a little complicated as SDN paths are provided inline,\n        but SDN -> LDN pairings are provided only when entering a directory\n\n        The long file names over the size limit of 52 chars have a chance of\n        not being unique, so this also ensures their uniqueness and\n        fills in missing extensions\n\n        :return: The constructed file tree. Also the translation data for\n        converting between all used path formats get saved at the end\n        \"\"\"\n\n        instruction = enqueue_matchable(self.serial_queue,\n                                        message=\"M20 LT\",\n                                        regexp=LFN_CAPTURE)\n        wait_for_instruction(instruction, should_wait_evt=self.quit_evt)\n        matches = instruction.get_matches()\n        file_tree_parser = FileTreeParser(matches)\n        return file_tree_parser\n\n    def sd_inserted(self, sender, match: re.Match):\n        \"\"\"\n        If received while expecting it, stop expecting another one\n        If received unexpectedly, this signalises someone physically\n        inserting a card\n        \"\"\"\n        assert sender is not None\n        # Using a multi-purpose regex, only interested in the first group\n        if match.group(\"ok\"):\n            with self.lock:\n                if self.data.expecting_insertion:\n                    self.data.expecting_insertion = False\n                else:\n                    self.data.invalidated = True\n                    self._sd_state_changed(SDState.INITIALISING)\n\n    def sd_ejected(self, sender, match: re.Match):\n        \"\"\"\n        Handler for sd ejected serial messages.\n        Sets the card state to absent and notifies others\n        \"\"\"\n        assert sender is not None\n        assert match is not None\n        with self.lock:\n            self.data.invalidated = True\n            self._sd_state_changed(SDState.ABSENT)\n\n    def _sd_state_changed(self, new_state):\n        \"\"\"\n        Transforms the internal state changes to Signals about sd card\n        attaching/detaching. Also sets the internal state to the supplied one\n        :param new_state: the state to switch to\n        \"\"\"\n        assert self.lock.locked()\n        log.debug(\"SD state changed from %s to %s\", self.data.sd_state,\n                  new_state)\n\n        if self.data.sd_state in {SDState.INITIALISING, SDState.UNSURE} and \\\n                new_state == SDState.PRESENT:\n            log.debug(\"SD Card inserted\")\n            self.sd_attached_signal.send(self, files=self.data.files)\n\n        elif self.data.sd_state == SDState.PRESENT and \\\n                new_state in {SDState.ABSENT, SDState.INITIALISING}:\n            log.debug(\"SD Card removed\")\n            self.sd_detached_signal.send(self)\n            self._set_files(FileTreeParser(matches=[]))\n\n        self.data.sd_state = new_state\n        self.state_changed_signal.send(self, sd_state=self.data.sd_state)\n\n    def decide_presence(self):\n        \"\"\"\n        Calling this can be disruptive to the user experience,\n        the card will reload. If there is nothing on the SD card or\n        if we suspect there is no SD card, calling this should be fine\n\n        Asks the firmware to re-init the SD card, uses the output,\n        to determine SD presence\n        \"\"\"\n        self.data.expecting_insertion = True\n        instruction = enqueue_matchable(self.serial_queue, \"M21\",\n                                        SD_PRESENT_REGEX)\n        wait_for_instruction(instruction, should_wait_evt=self.quit_evt)\n        self.data.expecting_insertion = False\n\n        match = instruction.match()\n        if match is not None:\n            with self.lock:\n                if match.group(\"ok\") is not None:\n                    if self.data.sd_state == SDState.UNSURE:\n                        self._sd_state_changed(SDState.PRESENT)\n                else:\n                    self._sd_state_changed(SDState.ABSENT)\n        else:\n            log.debug(\"Failed determining the SD presence.\")\n"
  },
  {
    "path": "prusa/link/printer_adapter/filesystem/storage.py",
    "content": "\"\"\"\nContains the implementation of Storage, FSStorage and FolderStorage for keeping\ntrack of Linux and folder storage.\n\"\"\"\nimport abc\nimport logging\nimport os\nimport select\nfrom typing import ClassVar, List, Set\n\nfrom blinker import Signal  # type: ignore\n\nfrom ...config import Config\nfrom ...const import (\n    BLACKLISTED_NAMES,\n    BLACKLISTED_PATHS,\n    BLACKLISTED_TYPES,\n    DIR_RESCAN_INTERVAL,\n    QUIT_INTERVAL,\n)\nfrom ...util import ensure_directory, get_clean_path\nfrom ..model import Model\nfrom ..structures.module_data_classes import StorageData\nfrom ..updatable import ThreadedUpdatable\n\nlog = logging.getLogger(__name__)\n\n\nclass Storage(ThreadedUpdatable):\n    \"\"\"\n    This module is the base for modules tracking attaching and detaching\n    of storage\n    \"\"\"\n\n    paths_to_storage: ClassVar[List[str]] = []\n\n    def __init__(self, model: Model):\n        super().__init__()\n        self.model: Model = model\n\n        self.attached_signal = Signal()  # kwargs = path: str\n        self.detached_signal = Signal()  # kwargs = path: str\n\n        self.data: StorageData = self.get_data_object()\n\n        self.data.blacklisted_paths = self._get_clean_paths(BLACKLISTED_PATHS)\n        self.data.blacklisted_names = BLACKLISTED_NAMES\n\n        candidate_storage = self._get_clean_paths(self.paths_to_storage)\n\n        # Cannot start with blacklisted paths\n        finalist_storage = set(\n            self.filter_blacklisted_paths(candidate_storage,\n                                          self.data.blacklisted_paths))\n\n        # Cannot have a blacklisted name\n        self.data.configured_storage = set(\n            self.filter_blacklisted_names(finalist_storage,\n                                          self.data.blacklisted_names))\n\n        log.debug(\"Configured mounpoints: %s\", self.data.configured_storage)\n\n        self.data.attached_set = set()\n\n    def update(self):\n        \"\"\"\n        Synchronizes our data model with the OS, produces signals for\n        storage we're interested in\n        \"\"\"\n\n        new_storage_set = self.get_storage()\n\n        added, removed = self.get_differences(new_storage_set)\n\n        for path in added:\n            log.info(\"Newly attached %s\", path)\n            self.attached_signal.send(self, path=path)\n\n        for path in removed:\n            log.info(\"Detached %s\", path)\n            self.detached_signal.send(self, path=path)\n\n        self.data.attached_set = new_storage_set\n\n    @staticmethod\n    def filter_blacklisted_paths(candidate_list, black_list):\n        \"\"\"Filter out anything that is inside of the blacklisted dirs\"\"\"\n        filtered = []\n\n        for candidate in candidate_list:\n            if not Storage.is_path_blacklisted(candidate, black_list):\n                filtered.append(candidate)\n        return filtered\n\n    @staticmethod\n    def filter_blacklisted_names(candidate_list, black_list):\n        \"\"\"Filter out anything that is inside of the blacklisted dirs\"\"\"\n        filtered = []\n\n        for candidate in candidate_list:\n            if not Storage.is_path_blacklisted(candidate, black_list):\n                filtered.append(candidate)\n        return filtered\n\n    @staticmethod\n    def is_path_blacklisted(candidate, black_list):\n        \"\"\"Returns the blacklist item that caused tha candidate to be flagged\n        \"\"\"\n        for blacklisted in black_list:\n            if candidate.startswith(blacklisted):\n                log.warning(\"Ignoring %s because it's blacklisted by %s\",\n                            candidate, blacklisted)\n                return True\n        return False\n\n    @staticmethod\n    def is_name_blacklisted(candidate, black_list):\n        \"\"\"Returns the blacklist item that caused tha candidate to be flagged\n        \"\"\"\n        clean_candidate = candidate.strip(\"/\").split(\"/\")[-1]\n        for blacklisted in black_list:\n            if clean_candidate == blacklisted:\n                log.warning(\"Ignoring %s because it's blacklisted by %s\",\n                            clean_candidate, blacklisted)\n                return True\n        return False\n\n    @staticmethod\n    def _get_clean_paths(dirty_paths):\n        \"\"\"\n        Cleans a list of paths by converting them to Path objects and back\n        \"\"\"\n        return [get_clean_path(path) for path in dirty_paths]\n\n    def get_differences(self, new_storage_set: Set[str]):\n        \"\"\"Retur the added and removed items from a given set\"\"\"\n        removed = self.data.attached_set.difference(new_storage_set)\n        added = new_storage_set.difference(self.data.attached_set)\n        return added, removed\n\n    @abc.abstractmethod\n    def get_storage(self) -> Set[str]:\n        \"\"\"\n        The implementation is expected to return a set of valid\n        storage based on its configuration\n        \"\"\"\n\n    @abc.abstractmethod\n    def get_data_object(self) -> StorageData:\n        \"\"\"\n        There need to be two different object for the two different storage\n        types. This method takes care of that\n        \"\"\"\n\n\nclass FilesystemStorage(Storage):\n    \"\"\"\n    Responsible for reporting which valid linux storage was attached\n    \"\"\"\n\n    thread_name = \"filesystem_storage_thread\"\n    update_interval = 0  # The waiting is done in epoll timeout instead of here\n\n    def __init__(self, model: Model, cfg: Config):\n        FilesystemStorage.paths_to_storage = \\\n            list(cfg.printer.storage)\n\n        model.filesystem_storage = StorageData(blacklisted_paths=[],\n                                               blacklisted_names=[],\n                                               configured_storage=set(),\n                                               attached_set=set())\n        # Call this after initializing the data\n        super().__init__(model)\n\n        # Force the update, even if no events are caught, we need to see\n        # which things are attached, before beginning to only observe changes\n        self.force_update = True\n\n        # pylint: disable=consider-using-with\n        self.mtab = open(\"/etc/mtab\", \"r\", encoding='utf-8')\n        self.epoll_obj = select.epoll(1)\n        self.epoll_obj.register(self.mtab.fileno(), select.EPOLLOUT)\n\n    def get_data_object(self) -> StorageData:\n        return self.model.filesystem_storage\n\n    def get_storage(self) -> Set[str]:\n        \"\"\"\n        Checks epoll for storage changes. if there are changes, gets\n        a new storage list from mtab.\n        If not, returns the current storage\n        \"\"\"\n        # Non-empty epoll result means something regarding storage has changed\n        epoll_result = self.epoll_obj.poll(QUIT_INTERVAL)\n        if epoll_result or self.force_update:\n            self.force_update = False\n\n            self.mtab.seek(0)\n            new_storage_set: Set[str] = set()\n\n            line_list = self.mtab.readlines()\n            for line in line_list:\n                _name, string_path, fs_type, *_ = line.split(\" \")\n                clean_path = get_clean_path(string_path)\n\n                if self.storage_belongs(clean_path, fs_type):\n                    new_storage_set.add(clean_path)\n            # If something changed, return the newly constructed dict\n            return new_storage_set\n        # Otherwise, return the same dict\n        return self.data.attached_set\n\n    def storage_belongs(self, path, fs_type):\n        \"\"\"Checks if we are interested in tracking a given storage\"\"\"\n        is_wanted = str(path) in self.data.configured_storage\n        type_valid = is_wanted and fs_type not in BLACKLISTED_TYPES\n        return is_wanted and type_valid\n\n    def stop(self):\n        \"\"\"Stops this component\"\"\"\n        super().stop()\n        self.mtab.close()\n\n\nclass FolderStorage(Storage):\n    \"\"\"\n    Configured directories are reported as storage too,\n    having the fs_type of \"directory\".\n    \"\"\"\n\n    def __init__(self, model: Model, cfg: Config):\n        FolderStorage.paths_to_storage = [cfg.printer.directory]\n\n        model.folder_storage = StorageData(blacklisted_paths=[],\n                                           blacklisted_names=[],\n                                           configured_storage=set(),\n                                           attached_set=set())\n\n        # Call this after initializing the data\n        super().__init__(model)\n\n        for directory in self.data.configured_storage:\n            ensure_directory(directory)\n\n    thread_name = \"folder_storage_thread\"\n    update_interval = DIR_RESCAN_INTERVAL\n\n    def get_data_object(self) -> StorageData:\n        \"\"\"\n        There need to be two different object for the two different storage\n        types. This method takes care of that\n        \"\"\"\n        return self.model.folder_storage\n\n    def get_storage(self) -> Set[str]:\n        new_directory_set: Set[str] = set()\n        for directory in self.data.configured_storage:\n\n            # try to create non-existing ones\n            try:\n                ensure_directory(directory)\n            except OSError:\n                log.exception(\"Cannot create a directory at %s\", directory)\n\n            if self.dir_belongs(directory):\n                new_directory_set.add(directory)\n            else:\n                log.warning(\"Directory %s does not exist or isn't readable.\",\n                            directory)\n        return new_directory_set\n\n    @staticmethod\n    def dir_belongs(directory: str):\n        \"\"\"\n        Checks if we are interested in tracking a given directory storage\n        \"\"\"\n        exists = os.path.exists(directory)\n        readable = exists and os.access(directory, os.R_OK)\n        return exists and readable\n"
  },
  {
    "path": "prusa/link/printer_adapter/filesystem/storage_controller.py",
    "content": "\"\"\"\nContains implementation of the  controller for interfacing with the storage\n\"subsystem\", which included the linux filesystem and sd card file management,\nnow only sd card and storage tracking remain\n\"\"\"\n\nimport logging\nfrom typing import Optional\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.files import File\n\nfrom ...printer_adapter.model import Model\nfrom ...sdk_augmentation.file import SDFile\nfrom ...serial.serial_parser import ThreadedSerialParser\nfrom ...serial.serial_queue import SerialQueue\nfrom .sd_card import SDCard\nfrom .storage import FolderStorage  # FilesystemStorage\n\nlog = logging.getLogger(__name__)\n\n\nclass StorageController:\n    \"\"\"\n    Sort of an interface layer between the (once larger) storage system\n    and the rest of the app\n    \"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self, cfg, serial_queue: SerialQueue,\n                 serial_parser: ThreadedSerialParser,\n                 model: Model):\n        self.folder_attached_signal = Signal()\n        self.folder_detached_signal = Signal()\n        self.sd_attached_signal = Signal()\n        self.sd_detached_signal = Signal()\n        self.menu_found_signal = Signal()\n\n        self.serial_parser = serial_parser\n        self.serial_queue: SerialQueue = serial_queue\n        self.model = model\n\n        self.sd_card = SDCard(self.serial_queue, self.serial_parser,\n                              self.model)\n        self.sd_card.sd_attached_signal.connect(self.sd_attached)\n        self.sd_card.sd_detached_signal.connect(self.sd_detached)\n        self.sd_card.menu_found_signal.connect(self.menu_found)\n\n        # self.filesystem_storage = FilesystemStorage(self.model, cfg)\n        self.folder_storage = FolderStorage(self.model, cfg)\n        # self.filesystem_storage.attached_signal.connect(self.folder_attached)\n        # self.filesystem_storage.detached_signal.connect(self.folder_detached)\n        self.folder_storage.attached_signal.connect(self.folder_attached)\n        self.folder_storage.detached_signal.connect(self.folder_detached)\n\n        self.sd_tree: Optional[SDFile] = None\n\n    def folder_attached(self, sender, path: str):\n        \"\"\"Signal pass-through\"\"\"\n        assert sender is not None\n        self.folder_attached_signal.send(self, path=path)\n\n    def folder_detached(self, sender, path: str):\n        \"\"\"Signal pass-through\"\"\"\n        assert sender is not None\n        self.folder_detached_signal.send(self, path=path)\n\n    def sd_attached(self, sender, files: File):\n        \"\"\"Signal pass-through\"\"\"\n        assert sender is not None\n        self.sd_attached_signal.send(self, files=files)\n\n    def sd_detached(self, sender):\n        \"\"\"Signal pass-through\"\"\"\n        assert sender is not None\n        self.sd_detached_signal.send(self)\n\n    def menu_found(self, _, menu_sfn):\n        \"\"\"Secret menu has been found signal passthrough\"\"\"\n        self.menu_found_signal.send(menu_sfn=menu_sfn)\n\n    def update(self):\n        \"\"\"Passes the call to update() to all its submodules\"\"\"\n        self.sd_card.update()\n        # self.filesystem_storage.update()\n        self.folder_storage.update()\n\n    def start(self):\n        \"\"\"Starts submodules\"\"\"\n        self.sd_card.start()\n        # self.filesystem_storage.start()\n        self.folder_storage.start()\n\n    def stop(self):\n        \"\"\"Stops submodules\"\"\"\n        self.sd_card.stop()\n        # self.filesystem_storage.stop()\n        self.folder_storage.stop()\n\n    def wait_stopped(self):\n        \"\"\"SWait for storage submodules to quit\"\"\"\n        self.sd_card.wait_stopped()\n        # self.filesystem_storage.wait_stopped()\n        self.folder_storage.wait_stopped()\n"
  },
  {
    "path": "prusa/link/printer_adapter/ip_updater.py",
    "content": "\"\"\"Contains implementation of the IPUpdater class\"\"\"\n\nimport logging\nimport socket\nfrom time import time\n\nimport pyric  # type: ignore\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.conditions import CondState\nfrom pyric import pyw  # type: ignore\nfrom pyric.pyw import Card  # type: ignore\n\nfrom ..conditions import LAN\nfrom ..const import IP_UPDATE_INTERVAL, IP_WRITE_TIMEOUT\nfrom ..serial.helpers import enqueue_instruction, wait_for_instruction\nfrom ..serial.serial_queue import SerialQueue\nfrom ..util import get_local_ip, get_local_ip6\nfrom .model import Model\nfrom .structures.module_data_classes import IPUpdaterData\nfrom .updatable import ThreadedUpdatable\n\nlog = logging.getLogger(__name__)\n\n\nclass IPUpdater(ThreadedUpdatable):\n    \"\"\"\n    Keeps track of what ip does the machine currently use when accessing the\n    internet\n    \"\"\"\n    thread_name = \"ip_updater\"\n    update_interval = IP_UPDATE_INTERVAL\n\n    def __init__(self, model: Model, serial_queue: SerialQueue):\n        self.serial_queue = serial_queue\n\n        self.updated_signal = Signal()\n\n        model.ip_updater = IPUpdaterData(local_ip=None,\n                                         local_ip6=None,\n                                         is_wireless=False,\n                                         update_ip_on=time(),\n                                         ssid=None,\n                                         mac=None,\n                                         hostname=None,\n                                         username=None,\n                                         digest=None)\n\n        self.data = model.ip_updater\n        self.first_update = True\n        super().__init__()\n\n    @staticmethod\n    def get_mac(card):\n        \"\"\"\n        Pyric returns an error, but in that case, there probably is no mac\n        to be gotten, so None is the most fitting value to send\n        \"\"\"\n        try:\n            return pyw.macget(card)\n        except pyric.error:\n            return None\n\n    def update_additional_info(self, ip):\n        \"\"\"Updates the mac address and info about the network being wireless\n        \"\"\"\n        if ip is None:\n            return\n        nics = pyw.interfaces()\n\n        is_wireless = False\n        mac = None\n        ssid = None\n        for nic in nics:\n            try:\n                # A hack to work around a block for non-wireless cards\n                card = Card(None, nic, None)\n                ips = pyw.ifaddrget(card)\n            except pyric.error:\n                continue\n            if ip not in ips:\n                continue\n            mac = self.get_mac(card)\n            is_wireless = pyw.iswireless(nic)\n            if not is_wireless:\n                continue\n            card = pyw.getcard(nic)\n            try:\n                card_info = pyw.link(card)\n            except pyric.error:\n                continue\n            else:\n                if card_info is None:\n                    continue\n            ssid_bytes = card_info[\"ssid\"]\n            ssid = ssid_bytes.decode()\n\n        self.data.ssid = ssid\n        self.data.is_wireless = is_wireless\n        self.data.mac = mac\n        self.data.hostname = socket.gethostname()\n\n    def update(self):\n        \"\"\"\n        Gets the current local ip. Calls update_ip(), if it changed,\n        or if it was over X seconds since the last update\n        \"\"\"\n        old_ip = self.data.local_ip\n        old_ip6 = self.data.local_ip6\n        self.update_ip()\n        self.update_ip6()\n        LAN.state = CondState(self.data.local_ip is not None)\n\n        if old_ip != self.data.local_ip or old_ip6 != self.data.local_ip6:\n            self.update_additional_info(self.data.local_ip)\n            self.updated_signal.send(self)\n        self.first_update = False\n\n    def update_ip(self):\n        \"\"\"\n        Only updates the IPv4\n\n        On ip change, sends the new one to the printer, so it can be displayed\n        in the printer support menu.\n\n        Generates a signal on ip change\n        \"\"\"\n        try:\n            new_ip = get_local_ip()\n        except socket.error:\n            log.warning(\n                \"Failed getting the local IP, are we connected to LAN?\")\n\n            self.data.mac = None\n            new_ip = None\n\n        if self.data.local_ip != new_ip:\n            log.debug(\n                \"Our IP has changed, or we reconnected. \"\n                \"The new one is %s\", new_ip)\n            self.data.local_ip = new_ip\n            self.send_ip_to_printer(new_ip)\n\n    def update_ip6(self):\n        \"\"\"\n        Looks on what IPv6 we have and updates it if necessary\n        \"\"\"\n        try:\n            new_ip6 = get_local_ip6()\n        except socket.error:\n            if self.data.local_ip6 is not None or self.first_update:\n                log.debug(\"Failed getting the local IPv6\")\n            new_ip6 = None\n        if new_ip6 is not None and new_ip6.startswith(\"fe80\"):\n            new_ip6 = None\n        if self.data.local_ip6 != new_ip6:\n            log.debug(\n                \"Our IPv6 has changed, or we reconnected. \"\n                \"The new one is %s\", new_ip6)\n            self.data.local_ip6 = new_ip6\n\n    def send_ip_to_printer(self,\n                           ip_address=None,\n                           reset=False,\n                           timeout: float = IP_WRITE_TIMEOUT):\n        \"\"\"\n        Uses the M552 gcode, to set the ip for displaying in the printer\n        support menu\n        :param ip_address: the ip to send to the printer, if unfilled, use the\n        current one\n        :param reset: whether to reset the IP to blank even if other is known\n        :param timeout: if supplied changes the timeout from the default\n                        IP_WRITE_TIMEOUT\n        \"\"\"\n        if ip_address is None:\n            ip_address = self.data.local_ip\n\n        if ip_address is None or reset:\n            instruction = enqueue_instruction(self.serial_queue,\n                                              \"M552 P0.0.0.0\")\n        else:\n            instruction = enqueue_instruction(self.serial_queue,\n                                              f\"M552 P{ip_address}\")\n\n        if timeout > 0:\n            timeout_at = time() + timeout\n            wait_for_instruction(\n                instruction,\n                lambda: not self.quit_evt.is_set() and time() < timeout_at)\n\n    def proper_stop(self):\n        \"\"\"\n        Stops the ip updater and resets the IP shown in the support menu\n        \"\"\"\n        self.send_ip_to_printer(None, reset=True)\n        super().stop()\n"
  },
  {
    "path": "prusa/link/printer_adapter/job.py",
    "content": "\"\"\"Contains implementation of the Job class\"\"\"\n\nimport logging\nimport os\nimport re\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer import Printer\n\nfrom ..const import (\n    JOB_DESTROYING_STATES,\n    JOB_ENDING_STATES,\n    JOB_STARTING_STATES,\n    SD_STORAGE_NAME,\n)\nfrom ..serial.helpers import enqueue_instruction\nfrom ..serial.serial_queue import SerialQueue\nfrom .model import Model\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.model_classes import JobState\nfrom .structures.module_data_classes import JobData\n\nlog = logging.getLogger(__name__)\n\n\nclass Job(metaclass=MCSingleton):\n    \"\"\"Keeps track of print jobs and their properties\"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self,\n                 serial_queue: SerialQueue,\n                 model: Model, printer: Printer):\n        # Sent every time the job id should disappear, appear or update\n        self.printer = printer\n        self.serial_queue = serial_queue\n\n        # Unused\n        self.job_id_updated_signal = Signal()  # kwargs: job_id: int\n        self.job_info_updated_signal = Signal()\n\n        self.model: Model = model\n        self.model.job = JobData(already_sent=False,\n                                 job_start_cmd_id=None,\n                                 path_incomplete=True,\n                                 from_sd=None,\n                                 inbuilt_reporting=None,\n                                 selected_file_path=None,\n                                 selected_file_m_timestamp=None,\n                                 selected_file_size=None,\n                                 printing_file_byte=None,\n                                 job_state=JobState.IDLE,\n                                 job_id=None,\n                                 job_id_offset=0,\n                                 last_job_path=None)\n        self.data = self.model.job\n\n        self.job_id_updated_signal.send(self,\n                                        job_id=self.data.get_job_id_for_api())\n\n    def file_opened(self, _, match: re.Match):\n        \"\"\"Handles the M23 output by extracting the mixed path and sends it\n        for parsing\"\"\"\n        if match is not None and match.group(\"sdn_lfn\") != \"\":\n            mixed_path = match.group(\"sdn_lfn\")\n            self.process_mixed_path(mixed_path)\n\n    def process_mixed_path(self, mixed_path):\n        \"\"\"Takes the mixed path and tries translating it into the long format\n        Sends the result to set_file_path\n        :param mixed_path: the path in SDR_LFN format\n        (short dir name, long file name)\"\"\"\n        log.debug(\"Processing %s\", mixed_path)\n        if mixed_path.lower() in self.model.sd_card.mixed_to_lfn_paths:\n            log.debug(\"It has been found in the SD card file tree\")\n            self.set_file_path(\n                self.model.sd_card.mixed_to_lfn_paths[mixed_path.lower()],\n                path_incomplete=False,\n                prepend_sd_storage=True)\n        else:\n            log.debug(\"It has not been found in the SD card file tree.\")\n            self.set_file_path(mixed_path,\n                               path_incomplete=True,\n                               prepend_sd_storage=True)\n\n    def job_started(self, command_id=None):\n        \"\"\"Reacts to a new job happening, increments job_id and fills out\n        as much info as possible about the print job\n\n        Also writes the new job_id to a file, so there aren't two jobs with\n        the same id\"\"\"\n        self.data.already_sent = False\n        # Try to not increment the job id on PP recovery\n        if not self.model.file_printer.recovering:\n            if self.data.job_id is None:\n                self.data.job_id_offset += 1\n            else:\n                self.data.job_id += 1\n        self.data.job_start_cmd_id = command_id\n        # If we don't print from sd, we know this immediately\n        # If not, let's leave it None, it will get filled later\n        if not self.data.from_sd:\n            self.data.inbuilt_reporting = \\\n                self.model.print_stats.has_inbuilt_stats\n        self.change_state(JobState.IN_PROGRESS)\n        self.write()\n        self.update_last_job_path()\n        log.debug(\"New job started, id = %s\", self.data.job_id)\n        self.job_id_updated_signal.send(self,\n                                        job_id=self.data.get_job_id_for_api())\n\n    def job_ended(self):\n        \"\"\"Resets the job info \"\"\"\n        self.data.already_sent = False\n        self.data.job_start_cmd_id = None\n        self.data.path_incomplete = True\n        self.data.inbuilt_reporting = None\n        self.change_state(JobState.IDLE)\n        log.info(\"Job ended\")\n        self.job_id_updated_signal.send(self,\n                                        job_id=self.data.get_job_id_for_api())\n\n    def state_changed(self, command_id=None):\n        \"\"\"Called before anything regarding state is sent\"\"\"\n        to_state = self.model.state_manager.current_state\n        if to_state in JOB_STARTING_STATES and \\\n                self.data.job_state == JobState.IDLE:\n            self.job_started(command_id)\n        if to_state in JOB_ENDING_STATES and \\\n                self.data.job_state is JobState.IN_PROGRESS:\n            self.change_state(JobState.ENDING)\n        if to_state in JOB_DESTROYING_STATES and \\\n                self.data.job_state is JobState.IN_PROGRESS:\n            self.job_ended()\n\n    def tick(self):\n        \"\"\"Called after sending, if the job was ending, it ends now\"\"\"\n        if self.data.job_state == JobState.ENDING:\n            self.job_ended()\n\n    def change_state(self, state: JobState):\n        \"\"\"\n        Previously wrote the state into a file, now only logs the state change\n        \"\"\"\n        log.debug(\"Job changed state to %s\", state)\n        self.data.job_state = state\n\n    def write(self):\n        \"\"\"Writes_the job_id into the printer EEPROM\"\"\"\n        # TODO: prime candidate for refactoring, it's awful\n        # Cannot block\n        if self.data.job_id is None:\n            return\n\n        enqueue_instruction(self.serial_queue,\n                            f\"D3 Ax0D05 X{self.data.job_id:08x}\",\n                            to_front=True)\n\n    def set_file_path(self, path, path_incomplete, prepend_sd_storage):\n        \"\"\"Decides if the supplied file path is better, than what we had\n        previously, and updates the job info file parameters accordingly\n        :param path: the path/file name to assign to the job\n        :param path_incomplete: flag for distinguishing between paths which\n        could not be linked to an SD file and those which could\n        :param prepend_sd_storage: Whether to prepend the SD Card\n        storage name\"\"\"\n        # If asked to, prepend the SD storage name\n        if prepend_sd_storage:\n            # Path joins don't work on paths with leading slashes\n            if path.startswith(\"/\"):\n                path = path[1:]\n            log.debug(\"prepending %s, result = %s\", SD_STORAGE_NAME,\n                      os.path.join(f\"/{SD_STORAGE_NAME}\", path))\n            path = os.path.join(f\"/{SD_STORAGE_NAME}\", path)\n\n        log.debug(\n            \"Processing a file path: %s, incomplete path=%s, \"\n            \"already known path is incomplete=%s, job state=%s, \"\n            \"known path=%s\", path, path_incomplete, self.data.path_incomplete,\n            self.data.job_state, self.data.selected_file_path)\n\n        # If we have a full path, don't overwrite it with an incomplete one\n        if path_incomplete and not self.data.path_incomplete:\n            return\n\n        log.debug(\"Overwriting file path with %s\", path)\n        self.data.selected_file_path = path\n        self.data.path_incomplete = path_incomplete\n\n        if not path_incomplete:\n            file_obj = self.printer.fs.get(self.data.selected_file_path)\n            if file_obj:\n                if \"m_timestamp\" in file_obj.attrs:\n                    self.data.selected_file_m_timestamp = file_obj.attrs[\n                        \"m_timestamp\"]\n                if 'size' in file_obj.attrs:\n                    self.data.selected_file_size = file_obj.attrs[\"size\"]\n        self.model.job.from_sd = path.startswith(\n            os.path.join(\"/\", SD_STORAGE_NAME))\n        self.update_last_job_path()\n        self.job_info_updated()\n\n    def update_last_job_path(self):\n        \"\"\"Updates the last job path to be used for the re-print menu item\"\"\"\n        if self.data.job_state != JobState.IN_PROGRESS:\n            return\n        self.data.last_job_path = self.data.selected_file_path\n\n    def get_job_info_data(self, for_connect=False):\n        \"\"\"Compiles the job info data into a dict\"\"\"\n        if for_connect:\n            self.data.already_sent = True\n\n        data = {}\n\n        if self.data.path_incomplete:\n            data[\"path_incomplete\"] = self.data.path_incomplete\n        if self.data.job_start_cmd_id is not None:\n            data[\"start_cmd_id\"] = self.data.job_start_cmd_id\n        if self.data.selected_file_path is not None:\n            data[\"path\"] = self.data.selected_file_path\n        if self.data.selected_file_m_timestamp is not None:\n            data[\"m_timestamp\"] = self.data.selected_file_m_timestamp\n        if self.data.selected_file_size is not None:\n            data[\"size\"] = self.data.selected_file_size\n        if self.data.from_sd is not None:\n            data[\"from_sd\"] = self.data.from_sd\n        if self.printer.mbl is not None:\n            data[\"mbl\"] = self.printer.mbl\n\n        return data\n\n    def progress_broken(self, progress_broken):\n        \"\"\"Uses the info about whether the progress percentage reported by\n        the printer is broken, to deduce, whether the gcode has inbuilt\n        percentage reporting for sd prints.\"\"\"\n        if self.data.from_sd:\n            old_inbuilt_reporting = self.data.inbuilt_reporting\n            if self.data.inbuilt_reporting is None and progress_broken:\n                self.data.inbuilt_reporting = False\n            elif not progress_broken:\n                self.data.inbuilt_reporting = True\n\n            if old_inbuilt_reporting != self.data.inbuilt_reporting:\n                self.job_info_updated()\n\n    def file_position(self, current, total):\n        \"\"\"Call to report a position in a file that's being printed\n        :param current: The byte number being printed\n        :param total: The file size\"\"\"\n        self.data.printing_file_byte = current\n        if self.data.selected_file_size is not None and \\\n                self.data.selected_file_size != total:\n            log.warning(\"Reported file sizes differ %s vs %s\",\n                        self.data.selected_file_size, total)\n        if self.data.selected_file_size is None:\n            # In the future, this should be pointless, now it may get used\n            self.data.selected_file_size = total\n            self.job_info_updated()\n\n    def job_info_updated(self):\n        \"\"\"If a job is in progress, a signal about an update will be sent\"\"\"\n        # The same check as in the job info command, se we aren't trying\n        # to send the job info, when it'll just fail instantly\n        if self.data.job_state == JobState.IN_PROGRESS \\\n                and self.data.selected_file_path is not None \\\n                and self.data.already_sent \\\n                and self.data.job_id is not None:\n            self.job_info_updated_signal.send(self)\n\n    def select_file(self, path):\n        \"\"\"For Octoprint API to select a file to print\n        supply only existing file paths\n\n        :param path: The connect path to a file, including the storage name\"\"\"\n        if self.printer.fs.get(path) is None:\n            raise RuntimeError(f\"Cannot select a non existing file {path}\")\n        self.set_file_path(path,\n                           path_incomplete=False,\n                           prepend_sd_storage=False)\n\n    def deselect_file(self):\n        \"\"\"For Octoprint API to deselect a file\n        Only works when IDLE\"\"\"\n        if self.data.job_state != JobState.IDLE:\n            raise RuntimeError(\"Cannot deselect a file while printing it\")\n        self.data.selected_file_path = None\n        self.model.job.from_sd = None\n\n    def job_id_from_eeprom(self, job_id):\n        \"\"\"Sets the job id read from the printer EEPROM\"\"\"\n        if self.data.job_id is not None:\n            return\n\n        self.data.job_id = job_id\n        if self.data.job_id_offset > 0:\n            self.data.job_id += self.data.job_id_offset\n            self.data.job_id_offset = 0\n            self.write()\n            self.job_info_updated()\n"
  },
  {
    "path": "prusa/link/printer_adapter/keepalive.py",
    "content": "\"\"\"Contains the keepalive implementation\"\"\"\nfrom enum import Enum\nfrom threading import Event, Thread\nfrom time import monotonic\n\nfrom ..const import KEEPALIVE_INTERVAL\nfrom ..serial.helpers import enqueue_instruction, wait_for_instruction\nfrom ..serial.serial_queue import SerialQueue\nfrom .structures.mc_singleton import MCSingleton\n\n\nclass KeepaliveMode(Enum):\n    \"\"\"The modes the keepalive can be in\"\"\"\n    PL = \"PL\"  # PrusaLink\n    PC = \"PC\"  # PrusaConnect\n\n\nclass Keepalive(metaclass=MCSingleton):\n    \"\"\"Its job is to keep the PrusaLink printer mode on\"\"\"\n\n    def __init__(self, serial_queue: SerialQueue):\n        self.serial_queue: SerialQueue = serial_queue\n\n        self.mode = KeepaliveMode.PL\n\n        self.quit_evt = Event()\n        self.wait_evt = Event()\n        self.keepalive_thread: Thread = Thread(target=self._keepalive,\n                                               name=\"Keepalive\")\n\n        self.last_keepalive = monotonic() - KEEPALIVE_INTERVAL\n\n    def start(self):\n        \"\"\"Starts the module\"\"\"\n        self.keepalive_thread.start()\n\n    def set_use_connect(self, use_connect: bool):\n        \"\"\"Changes the mode of the keepalive\"\"\"\n        self.mode = KeepaliveMode.PC if use_connect else KeepaliveMode.PL\n        self.last_keepalive = monotonic() - KEEPALIVE_INTERVAL\n        self.wait_evt.set()\n\n    def _keepalive(self):\n        \"\"\"Keep sending out a signal, that PrusaLink is connected\"\"\"\n        while not self.quit_evt.is_set():\n            instruction = enqueue_instruction(\n                self.serial_queue, f\"M79 S\\\"{self.mode.value}\\\"\",\n                to_front=True)\n            self.last_keepalive = monotonic()\n            wait_for_instruction(instruction, should_wait_evt=self.quit_evt)\n            to_wait = self.last_keepalive + KEEPALIVE_INTERVAL - monotonic()\n            if to_wait >= 0:\n                self.wait_evt.wait(to_wait)\n            if self.wait_evt.is_set():\n                self.wait_evt.clear()\n\n    def stop(self):\n        \"\"\"Stops the keepalive sender\"\"\"\n        self.quit_evt.set()\n        self.wait_evt.set()\n\n    def wait_stopped(self):\n        \"\"\"Waits for Keepalive thread to stop\"\"\"\n        self.keepalive_thread.join()\n"
  },
  {
    "path": "prusa/link/printer_adapter/lcd_printer.py",
    "content": "\"\"\"\nShould inform the user about everything important in PrusaLink while\nnod obstructing anything else the printer wrote.\n\"\"\"\nimport logging\nimport math\nfrom functools import partial\nfrom pathlib import Path\nfrom queue import Queue\nfrom threading import Event\nfrom time import time\nfrom typing import Callable\n\nimport unidecode\nfrom prusa.connect.printer import Printer\nfrom prusa.connect.printer.conditions import (\n    API,\n    COND_TRACKER,\n    HTTP,\n    INTERNET,\n    TOKEN,\n)\nfrom prusa.connect.printer.const import State, TransferType\n\nfrom ..conditions import (\n    DEVICE,\n    FW,\n    ID,\n    JOB_ID,\n    LAN,\n    NET_TRACKER,\n    PHY,\n    SN,\n    UPGRADED,\n)\nfrom ..config import Settings\nfrom ..const import (\n    FW_MESSAGE_TIMEOUT,\n    PRINTING_STATES,\n    QUIT_INTERVAL,\n    SLEEP_SCREEN_TIMEOUT,\n)\nfrom ..serial.helpers import enqueue_instruction, wait_for_instruction\nfrom ..serial.serial_queue import SerialQueue\nfrom ..util import prctl_name\nfrom .model import Model\nfrom .structures.carousel import Carousel, LCDLine, Screen\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.model_classes import JobState\nfrom .updatable import Thread\n\nlog = logging.getLogger(__name__)\n\nWELCOME_CHIME = [\n    \"M300 P100 S3200\", \"M300 P25 S0\", \"M300 P25 S4800\", \"M300 P75 S0\",\n    \"M300 P25 S4800\",\n]\n\nERROR_CHIME = [\"M300 P600 S5\"]\nUPLOAD_CHIME = [\"M300 P14 S50\"]\n\nERROR_MESSAGES = {\n    ID: \"Unsupported printer\",\n    FW: \"Err unsupported FW\",\n    SN: \"Err obtaining S/N\",\n    UPGRADED: \"Upgraded - re-reg.\",\n    JOB_ID: \"Err reading job id\",\n    HTTP: \"HTTP error 5xx\",\n    TOKEN: \"Error bad token\",\n    # This needs updating, but currently there's nothing better to say\n    API: \"HTTP error 4xx\",\n    INTERNET: \"No Internet access\",\n    LAN: \"No LAN access\",\n    PHY: \"No usable NIC\",\n    DEVICE: \"No network hardware\",\n}\n\nFROM_TRANSFER_TYPES = {\n    TransferType.FROM_PRINTER, TransferType.FROM_WEB,\n    TransferType.FROM_CONNECT, TransferType.FROM_CLIENT,\n    TransferType.FROM_SLICER,\n}\n\nTO_TRANSFER_TYPES = {TransferType.TO_CONNECT, TransferType.TO_CLIENT}\n\nERROR_GRACE = 15\n\nRECOVERY_PRIORITY = 60\nPRINT_PRIORITY = 50\nWIZARD_PRIORITY = 40\nERROR_PRIORITY = 30\nERROR_WAIT_PRIORITY = 31\nUPLOAD_PRIORITY = 20\nREADY_PRIORITY = 11\nIDLE_PRIORITY = 10\n\nNETWORK_ERROR_GRACE = 20\n\n\ndef through_queue(func):\n    \"\"\"A decorator to mke functions use the LCDPrinter event queue when called\n    Prevents thread racing and notifies the CLDPrinter to check\n    what to print next\"\"\"\n\n    def wrapper(self, *args, **kwargs):\n        func_with_args = partial(func, self, *args, **kwargs)\n        self.add_event(func_with_args)\n\n    return wrapper\n\n\nclass LCDPrinter(metaclass=MCSingleton):\n    \"\"\"Reports PrusaLink status on the printer LCD whenever possible\"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self,\n                 serial_queue: SerialQueue,\n                 model: Model,\n                 settings: Settings,\n                 printer: Printer,\n                 printer_number):\n        self.serial_queue: SerialQueue = serial_queue\n        self.model: Model = model\n        self.settings: Settings = settings\n        self.printer: Printer = printer\n        self.printer_number = printer_number\n\n        self.event_queue: Queue[Callable[[], None]] = Queue()\n\n        self.quit_evt = Event()\n        self.display_thread: Thread = Thread(target=self._lcd_printer,\n                                             name=\"LCDPrinter\")\n\n        self.notiff_event = Event()\n\n        self.error_screen = Screen(chime_gcode=ERROR_CHIME)\n\n        self.upload_screen = Screen(chime_gcode=UPLOAD_CHIME)\n        self.wizard_screen = Screen(chime_gcode=WELCOME_CHIME)\n        self.print_screen = Screen(order=1)\n        self.wait_screen = Screen(resets_idle=False)\n        self.ready_screen = Screen(resets_idle=False)\n        self.idle_screen = Screen(resets_idle=False)\n        self.recovery_screen = Screen(resets_idle=False)\n\n        self.carousel = Carousel([\n            self.print_screen, self.wizard_screen, self.wait_screen,\n            self.error_screen, self.upload_screen, self.ready_screen,\n            self.idle_screen, self.recovery_screen,\n        ])\n\n        self.carousel.set_priority(self.print_screen, PRINT_PRIORITY)\n        self.carousel.set_priority(self.wizard_screen, WIZARD_PRIORITY)\n        self.carousel.set_priority(self.error_screen, ERROR_PRIORITY)\n        self.carousel.set_priority(self.upload_screen, UPLOAD_PRIORITY)\n        self.carousel.set_priority(self.ready_screen, READY_PRIORITY)\n        self.carousel.set_priority(self.idle_screen, IDLE_PRIORITY)\n        self.carousel.set_priority(self.recovery_screen, RECOVERY_PRIORITY)\n\n        wait_zip = zip([\"Please wait\"] * 7, [\".\" * i for i in range(1, 8)])\n        wait_text = \"\".join((\"\".join(i).ljust(19) for i in wait_zip))\n        self.carousel.set_text(self.wait_screen,\n                               wait_text,\n                               scroll_delay=1.5,\n                               scroll_amount=19,\n                               first_line_extra=0,\n                               last_line_extra=0)\n\n        self.carousel.set_text(self.ready_screen,\n                               \"Ready to print\",\n                               scroll_delay=5,\n                               first_line_extra=0,\n                               last_line_extra=0)\n\n        self.carousel.set_text(self.recovery_screen,\n                               \"Ready to recover\",\n                               scroll_delay=5,\n                               first_line_extra=0,\n                               last_line_extra=0)\n\n        # Need to implement this in state manager. Only problem is, it's driven\n        # Cannot update itself. For now, this is the workaround\n        self.ignore_errors_to = 0\n        self.reset_error_grace()\n\n        # The error reporting for connection problems is too eager\n        # and cannot be turned off. Let's put a rug over the intermittent\n        # issues here.\n        # pylint: disable=fixme\n        # THIS HAS TO GO! FIXME!!!!\n        self.network_error_at = None\n\n        self.fw_msg_end_at = time()\n        self.idle_from = time()\n        # Used for ignoring LCD status updated that we generate\n        self.ignore = 0\n\n        self.current_line = None\n\n    def start(self):\n        \"\"\"Starts the module\"\"\"\n        self.display_thread.start()\n\n    def lcd_updated(self, sender, match):\n        \"\"\"\n        Gets called each time the firmware prints out \"LCD status changed\n        The ignore parameter counts how many messages have we sent, so\n        we don't misrecognize our messages as FW printing something by\n        itself\n        \"\"\"\n        assert sender is not None\n        assert match is not None\n\n        if self.ignore > 0:\n            self.ignore -= 1\n        else:\n            self._reset_idle()\n            self.fw_msg_end_at = time() + FW_MESSAGE_TIMEOUT\n            self.add_event(self.carousel.set_rewind)\n\n    def _message_and_disable(self, screen: Screen, message):\n        \"\"\"If the screen is enabled, disable it, and print out a message\"\"\"\n        if not self.carousel.is_enabled(screen):\n            return\n        self.carousel.add_message(LCDLine(message))\n        self.carousel.disable(screen)\n\n    def whats_going_on(self):\n        \"\"\"Get a grip on the situation and set up the screens and carousel\n        accordingly\"\"\"\n        self._check_printing()\n        self._check_errors()\n        self._check_wizard()\n        self._check_upload()\n        self._check_ready()\n        self._check_idle()\n        self._check_recovery()\n\n    def _check_printing(self):\n        \"\"\"Should a printing display be activated? And what should it say?\"\"\"\n        if self.model.job.job_state == JobState.IN_PROGRESS and \\\n                self.model.job.selected_file_path is not None:\n            # We're printing! Display the file name\n            self.carousel.enable(self.print_screen)\n\n            filename = Path(self.model.job.selected_file_path).name\n            # MK3 cannot print semicolons, replace them with an approximation\n            safe_filename = filename.replace(\";\", \",:\")\n            rewinding = self.model.file_printer.recovering\n            conditions = {\"filename\": safe_filename, \"rewinding\": rewinding}\n            if self.print_screen.conditions != conditions:\n                self.print_screen.conditions = conditions\n                if rewinding:\n                    self.carousel.set_text(\n                        self.print_screen, \"Preparing recovery\")\n                else:\n                    self.carousel.set_text(self.print_screen, safe_filename)\n        else:\n            self.carousel.disable(self.print_screen)\n\n    def _filter_http(self, error):\n        \"\"\"Filter any network errors for the first X seconds\"\"\"\n        if error is None:\n            self.network_error_at = None\n\n        if NET_TRACKER.is_tracked(error):  # Silence the error until timeout\n            if self.network_error_at is None:\n                self.network_error_at = time()\n                return None\n\n            time_since_error = time() - self.network_error_at\n            if time_since_error < NETWORK_ERROR_GRACE:\n                return None\n\n        return error\n\n    def _check_errors(self):\n        \"\"\"Should an error display be activated? And what should it say?\"\"\"\n        unfiltered_error = COND_TRACKER.get_worst()\n        error_grace_ended = time() - self.ignore_errors_to > 0\n\n        error = self._filter_http(unfiltered_error)\n\n        if error is not None and not error_grace_ended:\n            self.carousel.enable(self.wait_screen)\n        else:\n            self._message_and_disable(self.wait_screen, \"PrusaLink OK\")\n\n        if error is None:\n            self._message_and_disable(self.error_screen, \"Errors resolved\")\n        elif error not in ERROR_MESSAGES:\n            self.carousel.disable(self.error_screen)\n        elif error is not None and error_grace_ended:\n            # An error has been discovered, tell the user what it is\n            current_state = self.model.state_manager.current_state\n\n            silence_because_network = (\n                NET_TRACKER.is_tracked(error)\n                and not self.settings.printer.network_error_chime\n            )\n            silence_because_printing = current_state == State.PRINTING\n\n            self.carousel.enable(\n                screen=self.error_screen,\n                silent=silence_because_network or silence_because_printing,\n            )\n\n            conditions = {\n                \"lan\": LAN.state,\n                \"error\": error,\n            }\n            if self.error_screen.conditions != conditions:\n                self.error_screen.conditions = conditions\n\n                # No scrolling errors, just a screen worth of explanations\n                # and another one for the IP address\n                text = ERROR_MESSAGES[error][:19].ljust(19)\n                log.warning(\"Displaying an error message %s\", text)\n\n                ip = self.model.ip_updater.local_ip\n                if ip is not None:\n                    text += f\"see {ip}\".ljust(19)\n                self.carousel.set_text(self.error_screen,\n                                       text,\n                                       scroll_amount=19,\n                                       last_line_extra=8)\n\n            if self.model.job.job_state == JobState.IN_PROGRESS:\n                self.carousel.set_priority(self.error_screen, 50)\n            else:\n                self.carousel.set_priority(self.error_screen, ERROR_PRIORITY)\n\n    def _check_wizard(self):\n        \"\"\"Should a welcome display be shown? What should it say?\"\"\"\n        wizard_needed = self.settings.is_wizard_needed()\n        if wizard_needed and LAN:\n            self.carousel.enable(self.wizard_screen)\n            ip = self.model.ip_updater.local_ip\n            conditions = {\n                \"lan\": LAN.state,\n                \"wizard_needed\": wizard_needed,\n                \"ip\": ip,\n            }\n            if self.wizard_screen.conditions != conditions:\n                self.wizard_screen.conditions = conditions\n                local_ip = self.model.ip_updater.local_ip\n                if self.printer_number is not None:\n                    text = f\"{local_ip}/{self.printer_number}\"\n                else:\n                    # Can't have a capital G because old FW doesn't understand\n                    # What's a print command and what's not. It differentiated\n                    # between them using `\"G\" in command` condition\n                    text = f\"Go: {local_ip}\"\n                self.carousel.set_text(\n                    self.wizard_screen, text, last_line_extra=10)\n        else:\n            self._message_and_disable(self.wizard_screen, \"Setup completed\")\n\n    def _get_progress_graphic(self, progress, sync_type: TransferType):\n        bar_length = 12\n        # Have 12 characters for the load bar,\n        # increased to 14 by the arrow visibility\n        # [Sync|->:     0%     ]\n        # [Sync|->:>    5%     ]\n        # [Sync|->:=====95%===>]\n        # [Sync|->:====100%====]\n\n        # index of 0 and 13 means a hidden arrow\n        rough_index = progress / (100 / (bar_length + 2))\n        index = min(math.floor(rough_index), bar_length + 1)\n        display_arrow = 0 < index < 13\n\n        progress_background = \"=\" * max(0, (index - 1))\n        if display_arrow:\n            progress_background += \">\"\n        progress_background = progress_background.ljust(bar_length)\n\n        # Put percentage over the background\n        int_progress = round(progress)\n        string_progress = f\"{int_progress}%\"\n        centered_progress = string_progress.center(bar_length)\n        centering_index = centered_progress.index(string_progress)\n\n        progress_graphic = \"Sync  :\"\n        if sync_type in FROM_TRANSFER_TYPES:\n            progress_graphic = \"Sync\\x7E|:\"\n        if sync_type in TO_TRANSFER_TYPES:\n            progress_graphic = \"Sync|\\x7E:\"\n        progress_graphic += progress_background[:centering_index]\n        progress_graphic += string_progress\n        progress_graphic += progress_background[centering_index +\n                                                len(string_progress):]\n        return progress_graphic\n\n    def _check_upload(self):\n        \"\"\"Should an upload display be visible? And what should it say?\"\"\"\n        state = self.model.state_manager.current_state\n        if state in PRINTING_STATES and state != State.PRINTING:\n            self.carousel.set_priority(self.upload_screen, PRINT_PRIORITY + 10)\n        else:\n            self.carousel.set_priority(self.upload_screen, PRINT_PRIORITY)\n        if self.printer.transfer.in_progress:\n            self.carousel.enable(self.upload_screen)\n            progress_graphic = self._get_progress_graphic(\n                progress=self.printer.transfer.progress,\n                sync_type=self.printer.transfer.type)\n            self.carousel.set_text(self.upload_screen,\n                                   progress_graphic,\n                                   scroll_delay=0.5,\n                                   last_line_extra=0,\n                                   first_line_extra=0)\n        elif self.carousel.is_enabled(self.upload_screen):\n            transfer = self.printer.transfer\n            finished = transfer.transferred == transfer.size\n            if finished:\n                self._message_and_disable(self.upload_screen,\n                                          \"Transfer finished\")\n            else:\n                self._message_and_disable(self.upload_screen,\n                                          \"Transfer stopped\")\n        else:\n            self.carousel.disable(self.upload_screen)\n\n    def _check_ready(self):\n        \"\"\"Should the ready screen be shown?\"\"\"\n        if self.model.state_manager.current_state == State.READY and LAN:\n            self.carousel.enable(self.ready_screen)\n            ip = self.model.ip_updater.local_ip\n            conditions = {\"ip\": ip}\n            if self.ready_screen.conditions != conditions:\n                self.ready_screen.conditions = conditions\n                self.carousel.set_text(self.ready_screen,\n                                       \"Ready to print\".ljust(19) +\n                                       f\"{ip}\".ljust(19),\n                                       scroll_amount=19,\n                                       scroll_delay=4,\n                                       last_line_extra=5)\n        else:\n            self.carousel.disable(self.ready_screen)\n\n    def _check_recovery(self):\n        \"\"\"Should the ready screen be shown?\"\"\"\n        if self.model.file_printer.recovery_ready:\n            self.carousel.enable(self.recovery_screen)\n        else:\n            self.carousel.disable(self.recovery_screen)\n\n    def _check_idle(self):\n        \"\"\"Should the idle screen be shown? And what should it say?\"\"\"\n        if time() - self.idle_from > SLEEP_SCREEN_TIMEOUT and LAN:\n            self.carousel.enable(self.idle_screen)\n            local_ip = self.model.ip_updater.local_ip\n            if self.printer_number is not None:\n                ip_text = f\"{local_ip}/{self.printer_number}\"\n            else:\n                ip_text = f\"{local_ip}\"\n            speed = self.model.latest_telemetry.speed\n            conditions = {\"ip\": local_ip, \"speed\": speed}\n            if self.idle_screen.conditions != conditions:\n                self.idle_screen.conditions = conditions\n                if speed != 42:\n                    self.carousel.set_text(self.idle_screen,\n                                           \"PrusaLink OK.\".ljust(19) +\n                                           f\"{ip_text}\".ljust(19),\n                                           scroll_amount=19,\n                                           last_line_extra=12)\n                else:\n                    self.carousel.set_text(\n                        self.idle_screen,\n                        \"The Answer to the Great Question... Of Life, the \"\n                        \"Universe and Everything... Is... Forty-Two.\",\n                        scroll_delay=0.3,\n                        scroll_amount=1,\n                        first_line_extra=2,\n                        last_line_extra=5)\n        else:\n            self.carousel.disable(self.idle_screen)\n\n    def get_wait_interval(self):\n        \"\"\"How long to wait until the next line might want to be shown\"\"\"\n        current_time = time()\n\n        wait_for = QUIT_INTERVAL\n        wait_for = max(wait_for, self.fw_msg_end_at - current_time)\n        if self.current_line is not None:\n            wait_for = max(wait_for, self.current_line.ends_at - current_time)\n        return wait_for\n\n    def should_advance_carousel(self):\n        \"\"\"Should we get a new line from the carousel?\"\"\"\n        to_advance_carousel = True\n        line = self.current_line\n        if time() < self.fw_msg_end_at:\n            to_advance_carousel = False\n        elif line is not None and time() < line.ends_at:\n            if not self.carousel.to_rewind:\n                to_advance_carousel = False\n        return to_advance_carousel\n\n    def _lcd_printer(self):\n        \"\"\"This is the thread controlling what gets displayed\"\"\"\n        prctl_name()\n        self._print(LCDLine(\"PrusaLink started\"))\n        while not self.quit_evt.is_set():\n            self.notiff_event.wait(self.get_wait_interval())\n            self.notiff_event.clear()\n\n            if not self.event_queue.empty():\n                handler = self.event_queue.get()\n                handler()\n\n            # Lets update our state\n            self.whats_going_on()\n\n            if self.should_advance_carousel():\n                self.current_line = self.carousel.get_next()\n                # Get the line and send it to the printer\n                if self.current_line is not None:\n                    self._print(self.current_line)\n\n    def _print(self, line: LCDLine, to_wait=None):\n        \"\"\"\n        Sends the given message using M117 gcode and waits for its\n        confirmation\n\n        :param line: Text to be shown in the status portion of the printer LCD\n        \"\"\"\n        if line.resets_idle:\n            self._reset_idle()\n        ascii_text = unidecode.unidecode(line.text)\n        self.ignore += 1\n        instruction = enqueue_instruction(self.serial_queue,\n                                          f\"M117 \\x7E{ascii_text}\",\n                                          to_front=True)\n\n        # Play a sound accompanying the newly shown thing\n        if line.chime_gcode:\n            for command in line.chime_gcode:\n                enqueue_instruction(self.serial_queue, command)\n\n        if to_wait is None:\n            success = wait_for_instruction(instruction,\n                                           should_wait_evt=self.quit_evt)\n        else:\n            success = wait_for_instruction(instruction, to_wait)\n        if success:\n            log.debug(\"Printed: '%s' on the LCD.\", line.text)\n        line.reset_end()\n\n    def _reset_idle(self):\n        \"\"\"Reset the idle time form to the current time\"\"\"\n        self.idle_from = time()\n\n    def stop(self, fast=False):\n        \"\"\"\n        Stops the module, if not required to go fast, prints a goodbye message\n        \"\"\"\n        self.quit_evt.set()\n        if not fast:\n            time_out_at = time() + 5\n            self.wait_stopped()\n            self._print(LCDLine(\"PrusaLink stopped\"),\n                        lambda: time() < time_out_at)\n\n    def wait_stopped(self):\n        \"\"\"Waits for LCD Printer to quit\"\"\"\n        self.display_thread.join()\n\n    def add_event(self, handler):\n        \"\"\"Adds a handler to the LCDPrinter event queue\"\"\"\n        self.event_queue.put(handler)\n        self.notify()\n\n    def notify(self):\n        \"\"\"Wakes up the LCD printer, so it checks its state\"\"\"\n        self.notiff_event.set()\n\n    def reset_error_grace(self):\n        \"\"\"Resets the grace period for errors to clear\"\"\"\n        self.ignore_errors_to = time() + ERROR_GRACE\n\n    @through_queue\n    def print_message(self, line: LCDLine):\n        \"\"\"Print a message at most 19 chars long\"\"\"\n        self.carousel.add_message(line)\n"
  },
  {
    "path": "prusa/link/printer_adapter/mmu_observer.py",
    "content": "\"\"\"Contains the mmu output observing code, that compiles the readouts into\n telemetry values\"\"\"\nfrom re import Match\n\nfrom blinker import Signal\nfrom prusa.connect.printer.const import Event, Source\n\nfrom ..const import MMU_ERROR_MAP, MMU_PROGRESS_MAP\nfrom ..sdk_augmentation.printer import MyPrinter\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom .model import Model\nfrom .structures.model_classes import Slot, Telemetry\nfrom .structures.module_data_classes import MMUObserverData\nfrom .structures.regular_expressions import (\n    MMU_PROGRESS_REGEX,\n    MMU_Q0_REGEX,\n    MMU_Q0_RESPONSE_REGEX,\n    MMU_SLOT_REGEX,\n)\nfrom .telemetry_passer import TelemetryPasser\n\n\nclass MMUObserver:\n    \"\"\"The class that observes the MMU output and sends passes the info\n    from it as telemetry\"\"\"\n\n    def __init__(self,\n                 serial_parser: ThreadedSerialParser,\n                 model: Model,\n                 printer: MyPrinter,\n                 telemetry_passer: TelemetryPasser):\n        self.serial_parser = serial_parser\n        self.model = model\n        self.model.mmu_observer = MMUObserverData(current_error_code=None)\n        self.data = self.model.mmu_observer\n        self.printer = printer\n        self.telemetry_passer = telemetry_passer\n\n        self.capture_q0 = False\n\n        self.serial_parser.add_decoupled_handler(\n            MMU_PROGRESS_REGEX, self._handle_mmu_progress)\n        self.serial_parser.add_decoupled_handler(\n            MMU_SLOT_REGEX, self._handle_active_slot)\n        self.serial_parser.add_decoupled_handler(\n            MMU_Q0_RESPONSE_REGEX, self._handle_q0_response)\n        self.serial_parser.add_decoupled_handler(\n            MMU_Q0_REGEX, self._prime_q0)\n\n        self.error_changed_signal = Signal()\n\n        self.telemetry_passer.set_telemetry(\n            Telemetry(\n                slot=Slot(\n                    active=0,\n                ),\n            ),\n        )\n\n    def _prime_q0(self, _, match: Match) -> None:\n        \"\"\"Starts listening for the Q0 response\"\"\"\n        assert match is not None\n        self.capture_q0 = True\n\n    def _handle_mmu_progress(self, _, match: Match):\n        message = match.group(\"message\")\n        code = MMU_PROGRESS_MAP.get(message)\n        self.telemetry_passer.set_telemetry(\n            Telemetry(\n                slot=Slot(\n                    state=code,\n                ),\n            ),\n        )\n\n    def _handle_active_slot(self, _, match: Match):\n        raw_active_slot = int(match.group(\"slot\"))\n        if raw_active_slot == 99:\n            active_slot = 0\n        else:\n            active_slot = raw_active_slot + 1\n        self.telemetry_passer.set_telemetry(\n            Telemetry(\n                slot=Slot(\n                    active=active_slot,\n                ),\n            ),\n        )\n\n    def _handle_mmu_error(self, error_code):\n        \"\"\"Report an mmu error\"\"\"\n        prusa_error_code = \"04\" + str(MMU_ERROR_MAP.get(error_code))\n        if self.data.current_error_code == prusa_error_code:\n            return\n        self.data.current_error_code = prusa_error_code\n        self.printer.event_cb(\n            Event.SLOT_EVENT,\n            source=Source.SLOT,\n            code=prusa_error_code,\n        )\n        self.error_changed_signal.send()\n\n    def _handle_mmu_no_error(self):\n        \"\"\"Clear the mmu error\"\"\"\n        self.data.current_error_code = None\n        self.error_changed_signal.send()\n\n    def _handle_q0_response(self, _, match: Match):\n        \"\"\"Parse the mmu Q0 status response\"\"\"\n        if not self.capture_q0:\n            return\n        self.capture_q0 = False\n\n        command_code = match.group(\"command\")\n        progress_code = match.group(\"progress\")\n\n        # Is there a command in progress? If yes, send it\n        if progress_code[0] in \"PE\":\n            self.telemetry_passer.set_telemetry(\n                Telemetry(\n                    slot=Slot(\n                        command=command_code,\n                    ),\n                ),\n            )\n\n        # Figure out if there's an error being reported\n        if progress_code.startswith(\"E\"):\n            error_code = int(progress_code[1:], 16)\n            self._handle_mmu_error(error_code)\n        else:\n            self._handle_mmu_no_error()\n"
  },
  {
    "path": "prusa/link/printer_adapter/model.py",
    "content": "\"\"\"Contains implementation of the Model class\"\"\"\n\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.model_classes import Telemetry\nfrom .structures.module_data_classes import (\n    FilePrinterData,\n    IPUpdaterData,\n    JobData,\n    MMUObserverData,\n    PrintStatsData,\n    SDCardData,\n    SerialAdapterData,\n    StateManagerData,\n    StorageData,\n)\n\n\nclass Model(metaclass=MCSingleton):\n    \"\"\"\n    This class should collect every bit of info from all the informer classes\n    Some values are reset upon reading, other, more state oriented should stay\n    \"\"\"\n    latest_telemetry: Telemetry = Telemetry()\n\n    # Let's try and share inner module states for cooperation\n    # The idea is, every module will get the model.\n    # Every component HAS TO write its OWN INFO ONLY but can read\n    # everything\n    serial_adapter: SerialAdapterData\n    file_printer: FilePrinterData\n    print_stats: PrintStatsData\n    state_manager: StateManagerData\n    job: JobData\n    ip_updater: IPUpdaterData\n    sd_card: SDCardData\n    folder_storage: StorageData\n    filesystem_storage: StorageData\n    mmu_observer: MMUObserverData\n\n    def __init__(self) -> None:\n        self.latest_telemetry: Telemetry = Telemetry()\n"
  },
  {
    "path": "prusa/link/printer_adapter/print_stat_doubler.py",
    "content": "\"\"\"Implements the print stat line doubling\"\"\"\nimport re\nfrom typing import List\n\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom .printer_polling import PrinterPolling\nfrom .structures.regular_expressions import (\n    CONFIRMATION_REGEX,\n    PRINT_INFO_REGEX,\n)\n\n\nclass PrintStatDoubler:\n    \"\"\"\n    The print stats are coming automatically, as we read a line at a time, we\n    lose the info of which one is valid and so cannot decide\n    on which one to use.\n    With this, we can handle both lines at the same time without heavily\n    modifying the underlying serial communication layers\n    \"\"\"\n\n    def __init__(self, serial_parser: ThreadedSerialParser,\n                 printer_polling: PrinterPolling):\n        self.printer_polling = printer_polling\n        self.serial_parser = serial_parser\n\n        self.matches: List[re.Match] = []\n\n        self.serial_parser.add_decoupled_handler(\n                PRINT_INFO_REGEX, self.matched)\n        self.serial_parser.add_decoupled_handler(\n                CONFIRMATION_REGEX, self.reset)\n\n    def reset(self, sender, match):\n        \"\"\"Resets the accumulated stat lines from the list\"\"\"\n        assert sender is not None\n        assert match is not None\n        self.matches.clear()\n\n    def matched(self, sender, match):\n        \"\"\"A print stat line was matched, add it to the list. If we have both,\n        send them along to the handler\"\"\"\n        assert sender is not None\n        self.matches.append(match)\n\n        if len(self.matches) >= 2:\n            self.printer_polling.print_info_handler(self, self.matches)\n            self.matches.clear()\n"
  },
  {
    "path": "prusa/link/printer_adapter/print_stats.py",
    "content": "\"\"\"Contains implementation of the PrintStats class\"\"\"\nimport logging\nfrom time import time\n\nfrom ..const import TAIL_COMMANDS\nfrom ..util import get_gcode\nfrom .model import Model\nfrom .structures.module_data_classes import PrintStatsData\n\nlog = logging.getLogger(__name__)\n\n\nclass PrintStats:\n    \"\"\"\n    For serial prints without inbuilt progress and estimated time left\n    reporting, this component tries to estimate those values\n    \"\"\"\n\n    def __init__(self, model: Model):\n        self.model = model\n\n        self.model.print_stats = PrintStatsData(\n            print_time=0,\n            segment_start=time(),\n            has_inbuilt_stats=False,\n            total_gcode_count=0,\n        )\n        self.data = self.model.print_stats\n\n    def track_new_print(self, file_path, from_gcode_number=None):\n        \"\"\"\n        Analyzes the file, to determine whether it contains progress and time\n        reporting\n        :param file_path: path of the file to analyze\n        :param from_gcode_number: the number of gcode already printed\n                                  to account for pp recoveries\n        \"\"\"\n        self.reset_stats()\n        self.data.start_gcode_number = from_gcode_number or 0\n        with open(file_path, encoding='utf-8') as gcode_file:\n            for line in gcode_file:\n                gcode = get_gcode(line)\n                if gcode:\n                    self.data.total_gcode_count += 1\n                if \"M73\" in gcode:\n                    self.data.has_inbuilt_stats = True\n                    break\n\n        log.info(\n            \"New file analyzed. It %s inbuilt percent and time reporting.\",\n            'has' if self.data.has_inbuilt_stats else 'does not have')\n\n    def reset_stats(self):\n        \"\"\"resets the tracked print stats\"\"\"\n        self.data.total_gcode_count = 0\n        self.data.print_time = 0\n        self.data.has_inbuilt_stats = False\n\n    def end_time_segment(self):\n        \"\"\"\n        Ends the current time segment and adds its length to the print time\n        \"\"\"\n        if self.data.segment_start is None:\n            return\n        self.data.print_time += time() - self.data.segment_start\n        self.data.segment_start = None\n\n    def start_time_segment(self):\n        \"\"\"\n        Starts a new time segment for the print time measuring\n        \"\"\"\n        self.data.segment_start = time()\n\n    def get_stats(self, gcode_number):\n        \"\"\"\n        Based on which gcode are we now processing and how long is the print\n        running, estimates the progress and time left\n\n        :param gcode_number: the gcode number being printed\n        :return tuple containing the percentage and the estimated minutes\n        remaining\n        \"\"\"\n        self.end_time_segment()\n        self.start_time_segment()\n\n        gcode_number_after_pp = gcode_number - self.data.start_gcode_number\n        time_per_command = self.data.print_time / gcode_number_after_pp\n        total_gcodes_after_pp = (self.data.total_gcode_count\n                                 - self.data.start_gcode_number)\n        total_time = time_per_command * total_gcodes_after_pp\n        sec_remaining = total_time - self.data.print_time\n        min_remaining = round(sec_remaining / 60)\n        log.debug(\"sec: %s, min: %s}, print_time: %s\", sec_remaining,\n                  min_remaining, self.data.print_time)\n        fraction_done = gcode_number / self.data.total_gcode_count\n        percent_done = round(fraction_done * 100)\n\n        log.debug(\"Print stats: %s%% done,  %s\", percent_done, min_remaining)\n\n        if gcode_number == self.data.total_gcode_count - TAIL_COMMANDS:\n            return 100, min_remaining\n        return percent_done, min_remaining\n\n    def get_time_printing(self):\n        \"\"\"Returns for how long was the print running\"\"\"\n        return self.data.print_time + (time() - self.data.segment_start)\n"
  },
  {
    "path": "prusa/link/printer_adapter/printer_polling.py",
    "content": "\"\"\"\nUses info updater to keep up with the printer info.\nHope I can get most of printer polling to use this mechanism.\n\"\"\"\nimport itertools\nimport logging\nimport re\nimport struct\nfrom datetime import timedelta\nfrom typing import List\n\nfrom packaging.version import Version\nfrom prusa.connect.printer import Printer\nfrom prusa.connect.printer.conditions import CondState\n\nfrom ..conditions import FW, ID, JOB_ID, SN\nfrom ..const import (\n    FAST_POLL_INTERVAL,\n    MINIMAL_FIRMWARE,\n    MK25_PRINTERS,\n    MMU3_TYPE_CODE,\n    PRINT_MODE_ID_PAIRING,\n    PRINT_STATE_PAIRING,\n    PRINTER_TYPES,\n    QUIT_INTERVAL,\n    SLOW_POLL_INTERVAL,\n    VERY_SLOW_POLL_INTERVAL,\n)\nfrom ..serial.helpers import enqueue_matchable, wait_for_instruction\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom ..serial.serial_queue import SerialQueue\nfrom ..util import _parse_little_endian_uint32, get_d3_code, make_fingerprint\nfrom .filesystem.sd_card import SDCard\nfrom .job import Job\nfrom .model import Model\nfrom .structures.item_updater import (\n    ItemUpdater,\n    SideEffectOnly,\n    WatchedGroup,\n    WatchedItem,\n)\nfrom .structures.model_classes import (\n    EEPROMParams,\n    NetworkInfo,\n    PrintMode,\n    Telemetry,\n)\nfrom .structures.module_data_classes import Sheet\nfrom .structures.regular_expressions import (\n    D3_OUTPUT_REGEX,\n    FW_REGEX,\n    M27_OUTPUT_REGEX,\n    MBL_REGEX,\n    MMU_BUILD_REGEX,\n    MMU_MAJOR_REGEX,\n    MMU_MINOR_REGEX,\n    MMU_REVISION_REGEX,\n    NOZZLE_REGEX,\n    PERCENT_REGEX,\n    PRINT_INFO_REGEX,\n    PRINTER_TYPE_REGEX,\n    SN_REGEX,\n    VALID_SN_REGEX,\n)\nfrom .telemetry_passer import TelemetryPasser\n\nlog = logging.getLogger(__name__)\n\n# pylint: disable=too-many-lines\n\n\nclass InfoGroup(WatchedGroup):\n    \"\"\"A WatchedGroup with a flag for sending\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self.to_send = False\n        super().__init__(*args, **kwargs)\n\n    def mark_for_send(self):\n        \"\"\"Marks printer info for sending\"\"\"\n        self.to_send = True\n\n\n# TODO: Don't like how parsing and result signal handling are mixed\n# instead, i would put the signal handling elsewhere\n# Also, having the external validators and whatnot seems unnecessarily complex\n# subclass WatchedItems and move them inside\nclass PrinterPolling:\n    \"\"\"Sets up the tracked values for info_updater\"\"\"\n\n    quit_interval = QUIT_INTERVAL\n\n    # pylint: disable=too-many-statements, too-many-arguments\n    def __init__(self, serial_queue: SerialQueue,\n                 serial_parser: ThreadedSerialParser,\n                 printer: Printer, model: Model,\n                 telemetry_passer: TelemetryPasser,\n                 job: Job, sd_card: SDCard) -> None:\n        super().__init__()\n        self.item_updater = ItemUpdater()\n        self.serial_queue = serial_queue\n        self.serial_parser = serial_parser\n        self.printer = printer\n        self.model = model\n        self.telemetry_passer = telemetry_passer\n        self.job = job\n        self.sd_card = sd_card\n\n        # Printer info (for init and SEND_INFO)\n        self.network_info = WatchedItem(\"network_info\",\n                                        gather_function=self._get_network_info,\n                                        write_function=self._set_network_info)\n\n        self.printer_type = WatchedItem(\n            \"printer_type\",\n            gather_function=self._get_printer_type,\n            write_function=self._set_printer_type,\n            validation_function=self._validate_printer_type,\n            interval=VERY_SLOW_POLL_INTERVAL,\n            on_fail_interval=SLOW_POLL_INTERVAL)\n        self.printer_type.became_valid_signal.connect(\n            self._printer_type_became_valid)\n        self.printer_type.val_err_timeout_signal.connect(\n            lambda _: self._set_id_condition(CondState.NOK), weak=False)\n\n        self.firmware_version = WatchedItem(\n            \"firmware_version\",\n            gather_function=self._get_firmware_version,\n            write_function=self._set_firmware_version,\n            validation_function=self._validate_fw_version)\n        self.firmware_version.became_valid_signal.connect(\n            self._firmware_version_became_valid)\n        self.firmware_version.val_err_timeout_signal.connect(\n            lambda _: self._set_fw_condition(CondState.NOK), weak=False)\n\n        self.nozzle_diameter = WatchedItem(\n            \"nozzle_diameter\",\n            gather_function=self._get_nozzle_diameter,\n            write_function=self._set_nozzle_diameter)\n        self.nozzle_diameter.interval = 10\n\n        self.serial_number = WatchedItem(\n            \"serial_number\",\n            gather_function=self._get_serial_number,\n            write_function=self._set_serial_number,\n            validation_function=self._validate_serial_number)\n        self.serial_number.timeout = 25\n        self.serial_number.became_valid_signal.connect(\n            lambda _: self._set_sn_condition(CondState.OK), weak=False)\n        self.serial_number.val_err_timeout_signal.connect(\n            lambda _: self._set_sn_condition(CondState.NOK), weak=False)\n\n        self.sheet_settings = WatchedItem(\n            \"sheet_settings\",\n            gather_function=self._get_sheet_settings,\n        )\n\n        self.active_sheet = WatchedItem(\n            \"active_sheet\",\n            gather_function=self.get_active_sheet,\n        )\n\n        self.mmu_connected = WatchedItem(\n            \"mmu_connected\",\n        )\n        self.mmu_connected.became_valid_signal.connect(\n            self._mmu_connected_became_valid)\n\n        self.mmu_version = WatchedItem(\n            \"mmu_version\",\n            gather_function=self._get_mmu_version,\n        )\n        self.mmu_version.became_valid_signal.connect(\n            self._printer_info_became_valid)\n\n        self.printer_info = InfoGroup([\n            self.network_info, self.printer_type, self.firmware_version,\n            self.nozzle_diameter, self.serial_number, self.sheet_settings,\n            self.active_sheet, self.mmu_connected,\n        ])\n\n        for item in self.printer_info:\n            self.item_updater.add_item(item, start_tracking=False)\n\n        self.item_updater.add_item(self.mmu_version, start_tracking=False)\n\n        # TODO: Put this outside\n        for item in self.printer_info:\n            if item.name in {\"active_sheet\", \"sheet_settings\"}:\n                continue\n\n            item.value_changed_signal.connect(\n                lambda value: self.printer_info.mark_for_send(), weak=False)\n\n        self.printer_info.became_valid_signal.connect(\n            self._printer_info_became_valid)\n\n        # Other stuff\n\n        self.job_id = WatchedItem(\n            \"job_id\",\n            gather_function=self._get_job_id,\n            write_function=self._set_job_id,\n        )\n        self.job_id.became_valid_signal.connect(\n            lambda _: self._set_job_id_condition(CondState.OK), weak=False)\n        self.job_id.val_err_timeout_signal.connect(\n            lambda _: self._set_job_id_condition(CondState.NOK), weak=False)\n\n        self.print_mode = WatchedItem(\n            \"print_mode\",\n            gather_function=self._get_print_mode,\n            interval=SLOW_POLL_INTERVAL,\n        )\n\n        self.mbl = WatchedItem(\n            \"mbl\",\n            gather_function=self._get_mbl,\n            validation_function=self._validate_mbl,\n            on_fail_interval=None,\n        )\n\n        self.flash_air = WatchedItem(\n            \"flash_air\",\n            gather_function=self._get_flash_air,\n            write_function=self._set_flash_air,\n            validation_function=lambda value: isinstance(value, bool),\n        )\n        self.other_stuff = WatchedGroup([\n            self.job_id, self.print_mode, self.mbl, self.flash_air])\n\n        for item in self.other_stuff:\n            self.item_updater.add_item(item, start_tracking=False)\n\n        self.item_updater.set_value(self.flash_air, False)\n        # Make silent the default for when we fail to get the value in time\n        self.item_updater.set_value(self.print_mode, PrintMode.SILENT)\n\n        # Telemetry\n        self.speed_multiplier = WatchedItem(\n            \"speed_multiplier\",\n            gather_function=self._get_speed_multiplier,\n            write_function=self._set_speed_multiplier,\n            validation_function=self._validate_percent,\n            interval=FAST_POLL_INTERVAL)\n\n        self.flow_multiplier = WatchedItem(\n            \"flow_multiplier\",\n            gather_function=self._get_flow_multiplier,\n            write_function=self._set_flow_multiplier,\n            validation_function=self._validate_percent,\n            interval=FAST_POLL_INTERVAL)\n\n        # Print info can be autoreported or polled\n\n        # Only the progress gets an interval\n        # Its gatherer sets all the other values manually while other\n        # get set in cascade, converted from sooner acquired values\n        self.print_progress = WatchedItem(\n            \"print_progress\",\n            gather_function=self._get_print_info,\n            validation_function=self._validate_progress,\n            write_function=self._set_print_progress,\n        )\n\n        self.progress_broken = WatchedItem(\"progress_broken\")\n        self.print_progress.validation_error_signal.connect(\n            lambda _: self.set_progress_broken(True), weak=False)\n        self.print_progress.became_valid_signal.connect(\n            lambda _: self.set_progress_broken(False), weak=False)\n\n        self.time_remaining = WatchedItem(\n            \"time_remaining\",\n            validation_function=self._validate_time_till,\n            write_function=self._set_time_remaining)\n\n        self.time_broken = WatchedItem(\"time_broken\")\n        self.time_remaining.validation_error_signal.connect(\n            lambda _: self.set_time_broken(True), weak=False)\n        self.time_remaining.value_changed_signal.connect(\n            lambda _: self.set_time_broken(False), weak=False)\n\n        self.filament_change_in = WatchedItem(\n            \"filament_change_in\",\n            validation_function=self._validate_time_till,\n            write_function=self._set_filament_change_in,\n            on_fail_interval=None,\n        )\n\n        self.filament_change_in.validation_error_signal.connect(\n            lambda _: self.telemetry_passer.reset_value(\n                (\"filament_change_in\",)),\n            weak=False)\n\n        self.inaccurate_estimates = WatchedItem(\"inaccurate_estimates\")\n        self.time_broken.value_changed_signal.connect(\n            lambda _: self._infer_estimate_accuracy(), weak=False)\n        self.speed_multiplier.value_changed_signal.connect(\n            lambda _: self._infer_estimate_accuracy(), weak=False)\n        self.inaccurate_estimates.value_changed_signal.connect(\n            self._set_inaccurate_estimates,\n        )\n\n        # M27 results\n        # These are sometimes auto reported, but due to some technical\n        # limitations, I'm not able to read them when auto reported\n        self.print_state = WatchedItem(\"print_state\",\n                                       gather_function=self._get_m27,\n                                       interval=FAST_POLL_INTERVAL,\n                                       on_fail_interval=SLOW_POLL_INTERVAL)\n\n        # short (8.3) folder names, long file name (52 chars)\n        self.mixed_path = WatchedItem(\"mixed_path\")\n\n        self.byte_position = WatchedItem(\"byte_position\")\n\n        self.progress_from_bytes = WatchedItem(\n            \"progress_from_bytes\",\n            write_function=self._set_progress_from_bytes)\n        self.byte_position.value_changed_signal.connect(\n            self._get_progress_from_byte_position)\n\n        self.sd_seconds_printing = WatchedItem(\n            \"sd_seconds_printing\",\n            write_function=self._set_sd_seconds_printing)\n\n        self.time_remaining_guesstimate = WatchedItem(\n            \"time_remaining_guesstimate\",\n            write_function=self._set_time_remaining_guesstimate)\n        self.byte_position.value_changed_signal.connect(\n            self._guess_time_remaining)\n        self.sd_seconds_printing.value_changed_signal.connect(\n            self._guess_time_remaining)\n\n        self.total_filament = WatchedItem(\n            \"total_filament\",\n            gather_function=self._get_total_filament,\n            write_function=self._set_total_filament,\n            on_fail_interval=SLOW_POLL_INTERVAL)\n\n        self.total_print_time = WatchedItem(\n            \"total_print_time\",\n            gather_function=self._get_total_print_time,\n            write_function=self._set_total_print_time,\n            on_fail_interval=SLOW_POLL_INTERVAL)\n\n        self.telemetry = WatchedGroup([\n            self.speed_multiplier,\n            self.flow_multiplier,\n            self.print_progress,\n            self.time_remaining,\n            self.filament_change_in,\n            self.print_state,\n            self.mixed_path,\n            self.byte_position,\n            self.progress_from_bytes,\n            self.time_remaining_guesstimate,\n            self.sd_seconds_printing,\n            self.total_filament,\n            self.total_print_time,\n            self.progress_broken,\n            self.time_broken,\n            self.inaccurate_estimates,\n        ])\n\n        for item in self.telemetry:\n            self.item_updater.add_item(item, start_tracking=False)\n\n        self.invalidate_printer_info()\n\n    def start(self):\n        \"\"\"Starts the item updater\"\"\"\n        self.item_updater.start()\n\n    def stop(self):\n        \"\"\"Stops the item updater\"\"\"\n        self.item_updater.stop()\n\n    def wait_stopped(self):\n        \"\"\"Waits for the item updater to stop\"\"\"\n        self.item_updater.wait_stopped()\n\n    def invalidate_printer_info(self):\n        \"\"\"Invalidates all unnecessary watched items\"\"\"\n        for item in itertools.chain(self.telemetry, self.other_stuff,\n                                    self.printer_info):\n            self.item_updater.disable(item)\n        self.item_updater.disable(self.mmu_version)\n\n        self.item_updater.enable(self.printer_type)\n\n    def invalidate_network_info(self):\n        \"\"\"Invalidates just the network info\"\"\"\n        self.item_updater.invalidate(self.network_info)\n\n    def invalidate_serial_number(self):\n        \"\"\"Invalidates just the serial number\"\"\"\n        self.item_updater.invalidate(self.serial_number)\n\n    def invalidate_mbl(self):\n        \"\"\"Invalidates the mbl_data, so it will get updated.\"\"\"\n        self.item_updater.invalidate(self.mbl)\n\n    def invalidate_statistics(self):\n        \"\"\"Invalidates the statistics, so they get updated.\"\"\"\n        self.item_updater.invalidate(self.total_filament)\n        self.item_updater.invalidate(self.total_print_time)\n\n    def schedule_printer_type_invalidation(self):\n        \"\"\"Marks printer_type gor gathering in X seconds\"\"\"\n        self.item_updater.schedule_invalidation(self.printer_type,\n                                                SLOW_POLL_INTERVAL)\n\n    def _change_interval(self, item: WatchedItem, interval):\n        \"\"\"Changes the item interval and schedules depending on the new one\"\"\"\n        item.interval = interval\n        if interval is None:\n            self.item_updater.cancel_scheduled_invalidation(item)\n        else:\n            self.item_updater.schedule_invalidation(item)\n\n    def polling_not_ok(self):\n        \"\"\"Stops polling of some values\"\"\"\n        self._change_interval(self.nozzle_diameter, None)\n        self._change_interval(self.flow_multiplier, SLOW_POLL_INTERVAL)\n        self._change_interval(self.speed_multiplier, SLOW_POLL_INTERVAL)\n        self._change_interval(self.print_progress, SLOW_POLL_INTERVAL)\n        self._change_interval(self.sheet_settings, None)\n        self._change_interval(self.active_sheet, None)\n        self._change_interval(self.flash_air, None)\n        self._change_interval(self.printer_type, None)\n\n    def polling_ok(self):\n        \"\"\"Re-starts polling of some values\"\"\"\n        self._change_interval(self.nozzle_diameter, SLOW_POLL_INTERVAL)\n        self._change_interval(self.flow_multiplier, FAST_POLL_INTERVAL)\n        self._change_interval(self.speed_multiplier, FAST_POLL_INTERVAL)\n        self._change_interval(self.print_progress, None)\n        self._change_interval(self.sheet_settings, VERY_SLOW_POLL_INTERVAL)\n        self._change_interval(self.active_sheet, SLOW_POLL_INTERVAL)\n        self._change_interval(self.flash_air, VERY_SLOW_POLL_INTERVAL)\n        self._change_interval(self.printer_type, VERY_SLOW_POLL_INTERVAL)\n\n    def ensure_job_id(self):\n        \"\"\"This is an oddball, I don't have anything able to ensure the job_id\n        stays in sync, I cannot wait for it, that would block the read thread\n        I cannot just write it either, I wouldn't know if it failed.\"\"\"\n        def job_became_valid(item):\n            self.job_id.became_valid_signal.disconnect(job_became_valid)\n            if self.model.job.job_id != item.value:\n                log.warning(\n                    \"Job id on the printer: %s differs from the local\"\n                    \" one: %s!\", item.value, self.model.job.job_id)\n                self.job.write()\n                self.ensure_job_id()\n\n        self.item_updater.schedule_invalidation(self.job_id, interval=1)\n        self.job_id.became_valid_signal.connect(job_became_valid)\n\n    # -- Gather --\n    def should_wait(self):\n        \"\"\"Gather helper returning if the component is still running\"\"\"\n        return self.item_updater.running\n\n    def do_matchable(self, gcode, regex, to_front=False, has_to_match=True):\n        \"\"\"Analog to the command one, as the getters do this\n        over and over again\"\"\"\n        instruction = enqueue_matchable(self.serial_queue,\n                                        gcode,\n                                        regex,\n                                        to_front=to_front,\n                                        has_to_match=has_to_match)\n        wait_for_instruction(instruction, self.should_wait)\n        match = instruction.match()\n        if match is None:\n            raise RuntimeError(\"Printer responded with something unexpected\")\n        return match\n\n    def do_multimatch(self, gcode, regex, to_front=False):\n        \"\"\"Send an instruction with multiple lines as output\"\"\"\n        instruction = enqueue_matchable(\n            self.serial_queue, gcode, regex, to_front=to_front)\n        wait_for_instruction(instruction, self.should_wait)\n        matches = instruction.get_matches()\n        if not matches:\n            raise RuntimeError(f\"There are no matches for {gcode}. \"\n                               f\"That is weird.\")\n        return matches\n\n    def _get_network_info(self):\n        \"\"\"Gets the mac and ip addresses and packages them into an object.\"\"\"\n        network_info = NetworkInfo()\n        ip_data = self.model.ip_updater\n        if ip_data.local_ip is not None:\n            if ip_data.is_wireless:\n                log.debug(\"WIFI - mac: %s\", ip_data.mac)\n                network_info.wifi_ipv4 = ip_data.local_ip\n                network_info.wifi_ipv6 = ip_data.local_ip6\n                network_info.wifi_mac = ip_data.mac\n                network_info.wifi_ssid = ip_data.ssid\n                network_info.lan_ipv4 = None\n                network_info.lan_ipv6 = None\n                network_info.lan_mac = None\n            else:\n                log.debug(\"LAN - mac: %s\", ip_data.mac)\n                network_info.lan_ipv4 = ip_data.local_ip\n                network_info.lan_ipv6 = ip_data.local_ip6\n                network_info.lan_mac = ip_data.mac\n                network_info.wifi_ipv4 = None\n                network_info.wifi_ipv6 = None\n                network_info.wifi_mac = None\n                network_info.wifi_ssid = None\n\n            network_info.hostname = ip_data.hostname\n            network_info.username = ip_data.username\n            network_info.digest = ip_data.digest\n\n        return network_info.dict()\n\n    def _get_printer_type(self):\n        \"\"\"Gets the printer code using the M862.2 Q gcode.\"\"\"\n        match = self.do_matchable(\"M862.2 Q\",\n                                  PRINTER_TYPE_REGEX,\n                                  to_front=True)\n        code = int(match.group(\"code\"))\n        mmu_connected = code == MMU3_TYPE_CODE\n        self.item_updater.set_value(self.mmu_connected, mmu_connected)\n        return code\n\n    def _get_firmware_version(self):\n        \"\"\"Try to get firmware version from the printer.\"\"\"\n        match = self.do_matchable(\"PRUSA Fir\", FW_REGEX, to_front=True)\n        return match.group(\"version\")\n\n    def _get_nozzle_diameter(self):\n        \"\"\"Gets the printers nozzle diameter using M862.1 Q\"\"\"\n        match = self.do_matchable(\"M862.1 Q\", NOZZLE_REGEX, to_front=True)\n        return float(match.group(\"size\"))\n\n    def _get_serial_number(self):\n        \"\"\"Returns the SN regex match\"\"\"\n        # If we're connected through USB and we know the SN, use that one\n        serial_port = self.model.serial_adapter.using_port\n        if serial_port is not None and serial_port.sn is not None:\n            try:\n                if self._validate_serial_number(serial_port.sn):\n                    return serial_port.sn\n            except RuntimeError:\n                pass\n        # Do not ask MK2.5 for its SN, it would break serial communications\n        if self.printer.type in MK25_PRINTERS | {None}:\n            return \"\"\n        match = self.do_matchable(\"PRUSA SN\", SN_REGEX, to_front=True)\n        return match.group(\"sn\")\n\n    def _get_sheet_settings(self) -> List[Sheet]:\n        \"\"\"Gets all the sheet settings from the EEPROM\"\"\"\n        # TODO: How do we deal with default settings?\n        matches = self.do_multimatch(\n            get_d3_code(*EEPROMParams.SHEET_SETTINGS.value),\n            D3_OUTPUT_REGEX, to_front=True)\n\n        sheets: List[Sheet] = []\n        str_data = \"\"\n        for match in matches:\n            str_data += match.group(\"data\").replace(\" \", \"\")\n\n        data = bytes.fromhex(str_data)\n        for i in range(0, 8*11, 11):\n            sheet_data = data[i:i+11]\n\n            z_offset_u16 = struct.unpack(\"H\", sheet_data[7:9])[0]\n            max_uint16 = 2**16-1\n            if z_offset_u16 in {0, max_uint16}:\n                z_offset_workaround = max_uint16\n            else:\n                z_offset_workaround = z_offset_u16 - 1\n            z_offset = (z_offset_workaround-max_uint16)/400\n\n            sheets.append(Sheet(\n                name=sheet_data[:7].decode(\"ascii\"),\n                z_offset=z_offset,\n                bed_temp=struct.unpack(\"B\", sheet_data[9:10])[0],\n                pinda_temp=struct.unpack(\"B\", sheet_data[10:11])[0],\n            ))\n\n        return sheets\n\n    def get_active_sheet(self):\n        \"\"\"Gets the active sheet from the EEPROM\"\"\"\n        matches = self.do_matchable(\n            get_d3_code(*EEPROMParams.ACTIVE_SHEET.value),\n            D3_OUTPUT_REGEX, to_front=True)\n\n        str_data = matches.group(\"data\").replace(\" \", \"\")\n        data = bytes.fromhex(str_data)\n        active_sheet = struct.unpack(\"B\", data)[0]\n        return active_sheet\n\n    def _get_mmu_version(self):\n        \"\"\"Gets the mmu_version\"\"\"\n        major_match = self.do_matchable(\n            \"M707 A0x00\", MMU_MAJOR_REGEX, has_to_match=False)\n        minor_match = self.do_matchable(\n            \"M707 A0x01\", MMU_MINOR_REGEX, has_to_match=False)\n        revision_match = self.do_matchable(\n            \"M707 A0x02\", MMU_REVISION_REGEX, has_to_match=False)\n        build_match = self.do_matchable(\n            \"M707 A0x03\", MMU_BUILD_REGEX, has_to_match=False)\n        matches = [major_match, minor_match, revision_match, build_match]\n        numbers = list(map(lambda match: str(int(match.group(\"number\"), 16)),\n                           matches))\n        return \".\".join(numbers[:-1]) + \"+\" + numbers[-1]\n\n    def _get_job_id(self):\n        \"\"\"Gets the current job_id from the printer\"\"\"\n        match = self.do_matchable(\n            get_d3_code(*EEPROMParams.JOB_ID.value),\n            D3_OUTPUT_REGEX, to_front=True)\n        return int(match.group(\"data\").replace(\" \", \"\"), base=16)\n\n    def _get_mbl(self):\n        \"\"\"Gets the current MBL data\"\"\"\n        matches = self.do_multimatch(\"M420\", MBL_REGEX, to_front=True)\n        groups = matches[0].groupdict()\n\n        data = {}\n        if groups[\"no_mbl\"] is None:\n            num_x = int(groups[\"num_x\"])\n            num_y = int(groups[\"num_y\"])\n            data[\"shape\"] = (num_x, num_y)\n            data[\"data\"] = []\n            for i, match in enumerate(matches):\n                if i == 0:\n                    continue\n                line = match.group(\"mbl_row\")\n                str_values = line.split()\n                values = [float(val) for val in str_values]\n                data[\"data\"].extend(values)\n        return data\n\n    def _get_flash_air(self):\n        \"\"\"Determines if the Flash Air functionality is on\"\"\"\n        match = self.do_matchable(\n            get_d3_code(*EEPROMParams.FLASH_AIR.value), D3_OUTPUT_REGEX)\n        return match.group(\"data\") == \"01\"\n\n    def _get_print_mode(self):\n        \"\"\"Gets the print mode from the printer\"\"\"\n        match = self.do_matchable(\n            get_d3_code(*EEPROMParams.PRINT_MODE.value),\n            D3_OUTPUT_REGEX, to_front=True)\n        index = int(match.group(\"data\").replace(\" \", \"\"), base=16)\n        return PRINT_MODE_ID_PAIRING[index]\n\n    def _get_speed_multiplier(self):\n        match = self.do_matchable(\"M220\", PERCENT_REGEX)\n        return int(match.group(\"percent\"))\n\n    def _get_flow_multiplier(self):\n        match = self.do_matchable(\"M221\", PERCENT_REGEX)\n        return int(match.group(\"percent\"))\n\n    def _get_print_info(self):\n        \"\"\"Polls the print info, but instead of returning it, it uses\n        another method, that will eventually set it\"\"\"\n        matches = self.do_multimatch(\"M73\", PRINT_INFO_REGEX)\n        self.print_info_handler(self, matches)\n\n        raise SideEffectOnly()\n\n    def _get_m27(self):\n        \"\"\"Polls M27, sets all values got from it manually,\n        and returns its own\"\"\"\n        matches = self.do_multimatch(\"M27 P\", M27_OUTPUT_REGEX,\n                                     to_front=True)\n\n        if len(matches) >= 3:\n            third_match = matches[2]\n            self._parse_sd_seconds_printing(third_match.groupdict())\n\n        if len(matches) >= 2:\n            second_match = matches[1]\n            self._parse_byte_position(second_match.groupdict())\n\n        if len(matches) >= 1:\n            first_match = matches[0]\n            self._parse_mixed_path(first_match.groupdict())\n            return self._parse_print_state(first_match.groupdict())\n\n        raise RuntimeError(\"Failed to gather print info\")\n\n    @staticmethod\n    def _parse_print_state(groups):\n        \"\"\"Parse a printer tracked state depending on which match group\n        is present\"\"\"\n        for group, state in PRINT_STATE_PAIRING.items():\n            if groups[group] is not None:\n                return state\n        return None\n\n    def _parse_mixed_path(self, groups):\n        \"\"\"Here we get a printer print state and if printing\n        a mixed length path of the file being printed from the SD card\"\"\"\n        if groups[\"sdn_lfn\"] is not None:\n            self.item_updater.set_value(self.mixed_path, groups[\"sdn_lfn\"])\n\n    def _parse_byte_position(self, groups):\n        \"\"\"Gets the byte position of the file being sd printed\"\"\"\n        byte_position = (int(groups[\"current\"]), int(groups[\"sum\"]))\n        self.item_updater.set_value(self.byte_position, byte_position)\n\n    def _parse_sd_seconds_printing(self, groups):\n        \"\"\"Gets the time for which we've been printing already\"\"\"\n        printing_time = timedelta(hours=int(groups[\"hours\"]),\n                                  minutes=int(groups[\"minutes\"]))\n        self.item_updater.set_value(self.sd_seconds_printing,\n                                    printing_time.seconds)\n\n    def _get_progress_from_byte_position(self, value):\n        \"\"\"Gets a progress value out of byte position\"\"\"\n        current, total = value\n        progress = int((current / total) * 100)\n        self.item_updater.set_value(self.progress_from_bytes, progress)\n\n    def _guess_time_remaining(self, _):\n        \"\"\"Tracking is nonexistant, guess a time_remaining value\n        I'd just write out \"On Friday\" but people don't like that\"\"\"\n        if not self.time_broken.value:\n            return\n        if not self.sd_seconds_printing.valid:\n            return\n        sd_seconds_printing = self.sd_seconds_printing.value\n        if self.progress_broken.value:\n            if not self.progress_from_bytes.valid:\n                return\n            progress = self.progress_from_bytes.value\n        else:\n            if not self.print_progress.valid:\n                return\n            progress = self.print_progress.value\n        if progress == 0:\n            return\n        percent_remaining = 100 - progress\n        multiplier = percent_remaining / progress\n        guesstimation = sd_seconds_printing * multiplier\n        self.item_updater.set_value(self.time_remaining_guesstimate,\n                                    guesstimation)\n\n    def print_info_handler(self, sender, matches: List[re.Match]):\n        \"\"\"One special handler supporting polling and spontaneous\n        unsolicited reporting of progress and minutes remaining\"\"\"\n        assert sender is not None\n\n        class PrintInfo:\n            \"\"\"A shell for print stat data\"\"\"\n            def __init__(self):\n                self.valid = False\n                self.progress = -1\n                self.remaining = -1\n                self.filament_change_in = -1\n\n        silent, normal = PrintInfo(), PrintInfo()\n        for match in matches:\n            groups = match.groupdict()\n            info = PrintInfo()\n            info.progress = int(groups[\"progress\"])\n            # Convert both time values to seconds and adjust by print speed\n            secs_remaining_unadjusted = int(groups[\"remaining\"]) * 60\n            info.remaining = self._speed_adjust_time_value(\n                secs_remaining_unadjusted)\n            secs_change_in_unadjusted = int(groups[\"change_in\"]) * 60\n            info.filament_change_in = self._speed_adjust_time_value(\n                secs_change_in_unadjusted)\n\n            try:\n                info.valid = self._validate_progress(info.progress)\n            except ValueError:\n                pass\n\n            if match.group(\"mode\") == PrintMode.SILENT.value:\n                silent = info\n            elif match.group(\"mode\") == PrintMode.NORMAL.value:\n                normal = info\n\n        use_normal = False\n\n        if self.print_mode.value == PrintMode.NORMAL:\n            if not normal.valid and silent.valid:\n                log.warning(\"We are in normal mode but only silent print \"\n                            \"tracking info is valid. That's weird\")\n            else:\n                use_normal = True\n        elif not silent.valid:\n            # The file must have been sliced in a semi-compatible slicer\n            use_normal = True\n        # Yes, this solution ignores MK25 auto mode. Sorry\n\n        # Gladly reports even the wrong values\n        # just to set off handlers that depend on the validation failing\n        if use_normal:\n            self.item_updater.set_value(self.print_progress, normal.progress)\n            self.item_updater.set_value(self.time_remaining, normal.remaining)\n            self.item_updater.set_value(self.filament_change_in,\n                                        normal.filament_change_in)\n        else:\n            self.item_updater.set_value(self.print_progress, silent.progress)\n            self.item_updater.set_value(self.time_remaining, silent.remaining)\n            self.item_updater.set_value(self.filament_change_in,\n                                        silent.filament_change_in)\n\n    # -- From other watched items --\n    def _speed_adjust_time_value(self, value):\n        \"\"\"Multiplies tha value by the inverse of the speed multiplier\"\"\"\n        if self.model.latest_telemetry.speed is not None:\n            speed_multiplier = self.model.latest_telemetry.speed / 100\n        else:\n            speed_multiplier = 1\n        inverse_speed_multiplier = 1 / speed_multiplier\n\n        adjusted_value = int(value * inverse_speed_multiplier)\n        log.debug(\"Secs without speed scaling %s, secs otherwise %s\",\n                  value, adjusted_value)\n        return adjusted_value\n\n    def _eeprom_little_endian_uint32(self, dcode):\n        \"\"\"Reads and decodes the D-Code specified little-endian uint32_t\n        eeprom variable\"\"\"\n        match = self.do_matchable(dcode,\n                                  D3_OUTPUT_REGEX,\n                                  to_front=True)\n        return _parse_little_endian_uint32(match)\n\n    def _get_total_filament(self):\n        \"\"\"Gets the total filament used from the eeprom\"\"\"\n        total_filament = self._eeprom_little_endian_uint32(\n            get_d3_code(*EEPROMParams.TOTAL_FILAMENT.value))\n        return total_filament * 1000\n\n    def _get_total_print_time(self):\n        \"\"\"Gets the total print time from the eeprom\"\"\"\n        total_minutes = self._eeprom_little_endian_uint32(\n            get_d3_code(*EEPROMParams.TOTAL_PRINT_TIME.value))\n        return total_minutes * 60\n\n    # -- Validate --\n\n    def _validate_serial_number(self, value):\n        \"\"\"Validates the serial number, throws error because a more\n        descriptive error message can be shown this way\"\"\"\n        if VALID_SN_REGEX.match(value) is None:\n            return False\n\n        if self.printer.sn is not None and value != self.printer.sn:\n            log.error(\"The new serial number is different from the old one!\")\n            raise RuntimeError(f\"Serial numbers differ. Original: \"\n                               f\"{self.printer.sn} new one: {value}.\")\n        return True\n\n    def _validate_printer_type(self, value):\n        \"\"\"Validates the printer type, throws error because a more\n        descriptive error message can be shown this way\"\"\"\n        if value not in PRINTER_TYPES:\n            raise ValueError(f\"The printer with type {value} is not supported\")\n\n        printer_type = PRINTER_TYPES[value]\n        if self.printer.type is not None and printer_type != self.printer.type:\n            log.error(\"The printer type changed while running.\")\n            raise RuntimeError(f\"Printer type cannot change! Original: \"\n                               f\"{self.printer.type} current: {value}.\")\n\n        return True\n\n    @staticmethod\n    def _validate_fw_version(value):\n        \"\"\"Validates that the printer fw version is up to date enough\"\"\"\n        without_buildnumber = value.split(\"-\")[0]\n        if Version(without_buildnumber) < MINIMAL_FIRMWARE:\n            raise ValueError(\"The printer firmware is outdated\")\n        return True\n\n    @staticmethod\n    def _validate_mbl(value):\n        \"\"\"Validates the mesh bed leveling data\"\"\"\n        num_x, num_y = value[\"shape\"]\n        number_of_points = num_x * num_y\n        data = value[\"data\"]\n        if len(data) != number_of_points:\n            raise ValueError(f\"The mbl data matrix was reported to have \"\n                             f\"{num_x} x {num_y} values, but \"\n                             f\"{len(data)} were observed.\")\n        return True\n\n    @staticmethod\n    def _validate_percent(value):\n        \"\"\"Validates the speed multiplier as well as the flow rate\"\"\"\n        if not 0 <= value <= 999:\n            raise ValueError(\"The speed multiplier or flow rate is not \"\n                             \"between 0 and 999\")\n        return True\n\n    @staticmethod\n    def _validate_progress(value):\n        \"\"\"Validates progress\"\"\"\n        if not 0 <= value <= 100:\n            raise ValueError(\"The progress value is outside 0 and 100, this is\"\n                             \" usually a perfectly normal behaviour\")\n        return True\n\n    @staticmethod\n    def _validate_time_till(value):\n        \"\"\"Validates both time values because negative time till something\n        is impossible\"\"\"\n        if value < 0:\n            raise ValueError(\"There cannot be negative time till something\")\n        return True\n\n    # -- Write --\n    def _set_network_info(self, value):\n        \"\"\"Sets network info\"\"\"\n        self.printer.network_info = value\n\n    def _set_printer_type(self, value):\n        \"\"\"Do not try and overwrite the printer type, that would\n        raise an error\"\"\"\n        if self.printer.type is None:\n            self.printer.type = PRINTER_TYPES[value]\n\n    def _set_firmware_version(self, value):\n        \"\"\"It's a setter, what am I expected to write here?\n        Sets the firmware version duh\"\"\"\n        self.printer.firmware = value\n\n    def _set_nozzle_diameter(self, value):\n        \"\"\"Sets the nozzle diameter\"\"\"\n        self.printer.nozzle_diameter = value\n\n    def _set_serial_number(self, value):\n        \"\"\"Set serial number and fingerprint\"\"\"\n        if self.printer.sn is None:\n            self.printer.sn = value\n            self.printer.fingerprint = make_fingerprint(value)\n\n    def _set_job_id(self, value):\n        \"\"\"Set the job id\"\"\"\n        self.job.job_id_from_eeprom(value)\n\n    def _set_flash_air(self, value):\n        \"\"\"Passes the flash air value to sd updater\"\"\"\n        self.sd_card.set_flash_air(value)\n\n    def _set_speed_multiplier(self, value):\n        \"\"\"Write the speed multiplier to model\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(speed=value))\n\n    def _set_flow_multiplier(self, value):\n        \"\"\"Write the flow multiplier to model\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(flow=value))\n\n    def _set_print_progress(self, value):\n        \"\"\"Write the progress\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(progress=value))\n\n    def _set_time_remaining(self, value):\n        \"\"\"Sets the time remaining adjusted for speed\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(time_remaining=value))\n\n    def _set_filament_change_in(self, value):\n        \"\"\"Write the filament change in\"\"\"\n        self.telemetry_passer.set_telemetry(\n            Telemetry(filament_change_in=value))\n\n    def _set_sd_seconds_printing(self, value):\n        \"\"\"sets the time we've been printing\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(time_printing=value))\n\n    def _set_progress_from_bytes(self, value):\n        \"\"\"Sets the progress gathered from the byte position,\n        But only if it's broken in the printer\"\"\"\n        if self.progress_broken.value:\n            log.debug(\n                \"SD print has no inbuilt percentage tracking, \"\n                \"falling back to getting progress from byte \"\n                \"position in the file. \"\n                \"Progress: %s%% Byte %s/%s\", value,\n                self.byte_position.value[0], self.byte_position.value[1])\n            self.telemetry_passer.set_telemetry(Telemetry(progress=value))\n\n    def _set_time_remaining_guesstimate(self, value):\n        \"\"\"Set the guesstimated time remaining if the real one's broken\"\"\"\n        if self.time_broken.value:\n            log.debug(\"SD print has no time remaining tracking. \"\n                      \"Guesstimating\")\n            self.telemetry_passer.set_telemetry(\n                Telemetry(time_remaining=value))\n\n    def _set_total_filament(self, value):\n        \"\"\"Write the total filament used into the model\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(total_filament=value))\n\n    def _set_total_print_time(self, value):\n        \"\"\"Write the total print time into the model\"\"\"\n        self.telemetry_passer.set_telemetry(Telemetry(total_print_time=value))\n\n    def _set_inaccurate_estimates(self, value):\n        \"\"\"Write whether out time estimates are inaccurate into the model\"\"\"\n        self.telemetry_passer.set_telemetry(\n            Telemetry(inaccurate_estimates=value))\n\n    # -- Signal handlers --\n\n    def set_progress_broken(self, value: bool):\n        \"\"\"Sets progress as being broken or functioning normally\"\"\"\n        self.item_updater.set_value(self.progress_broken, value)\n\n    def set_time_broken(self, value: bool):\n        \"\"\"Sets time_remaining as being broken or functioning normally\"\"\"\n        self.item_updater.set_value(self.time_broken, value)\n\n    @staticmethod\n    def _set_sn_condition(state: CondState):\n        \"\"\"Needs to exist because we cannot assign in lambdas\"\"\"\n        SN.state = state\n\n    @staticmethod\n    def _set_id_condition(state: CondState):\n        \"\"\"Needs to exist because we cannot assign in lambdas\"\"\"\n        ID.state = state\n\n    @staticmethod\n    def _set_fw_condition(state: CondState):\n        \"\"\"Needs to exist because we cannot assign in lambdas\"\"\"\n        FW.state = state\n\n    @staticmethod\n    def _set_job_id_condition(state: CondState):\n        \"\"\"Needs to exist because we cannot assign in lambdas\"\"\"\n        JOB_ID.state = state\n\n    def _printer_type_became_valid(self, _):\n        \"\"\"Printer type became valid,\n        set the condition and enable the fw check\"\"\"\n        self.item_updater.enable(self.firmware_version)\n        self._set_id_condition(CondState.OK)\n\n    def _firmware_version_became_valid(self, _):\n        \"\"\"Firmware version became valid,\n        enable polling of the rest of the info\"\"\"\n        for item in self.printer_info:\n            self.item_updater.enable(item)\n        self._set_fw_condition(CondState.OK)\n\n    def _mmu_connected_became_valid(self, _):\n        \"\"\"MMU connected became valid, enable polling of its version\"\"\"\n        if self.mmu_connected.value:\n            self.item_updater.enable(self.mmu_version)\n        else:\n            self.item_updater.set_value(self.mmu_version, None)\n            self.item_updater.disable(self.mmu_version)\n\n    def _printer_info_became_valid(self, _):\n        \"\"\"Printer info became valid, we can start looking at telemetry\n        and other stuff\n\n        Also activated when the mmu version becomes valid\n        This only works because the mmu_version cannot become valide unless\n        the printer_info is valid already\n        \"\"\"\n\n        if self.mmu_connected.value:\n            if not self.mmu_version.valid:\n                return  # We'll get here again when it becomes valid\n\n        self._send_info_if_changed()\n        for item in itertools.chain(self.telemetry, self.other_stuff):\n            self.item_updater.enable(item)\n\n    def _send_info_if_changed(self):\n        \"\"\"Sends printer info if a value change marked it for sending\"\"\"\n        # This relies on update being called after became_valid_signal\n        if self.printer_info.valid and self.printer_info.to_send:\n            self.printer.event_cb(**self.printer.get_info())\n            self.printer_info.to_send = False\n\n    def _infer_estimate_accuracy(self):\n        \"\"\"Looks at the current state of things and infers whether the\n        time estimates are accurate or not\"\"\"\n        if self.time_broken.value in {None, True}:\n            self.item_updater.set_value(self.inaccurate_estimates, True)\n        elif self.speed_multiplier.value != 100:\n            self.item_updater.set_value(self.inaccurate_estimates, True)\n        else:\n            self.item_updater.set_value(self.inaccurate_estimates, False)\n"
  },
  {
    "path": "prusa/link/printer_adapter/prusa_link.py",
    "content": "\"\"\"Implements the PrusaLink class\"\"\"\nimport logging\nimport os\nimport re\nfrom enum import Enum\nfrom threading import Event\nfrom threading import enumerate as enumerate_threads\nfrom time import sleep\nfrom typing import Any, Dict, List, Optional, Type\n\nfrom prusa.connect.printer import Command as SDKCommand\nfrom prusa.connect.printer import DownloadMgr\nfrom prusa.connect.printer.camera_configurator import CameraConfigurator\nfrom prusa.connect.printer.camera_driver import CameraDriver\nfrom prusa.connect.printer.conditions import API, CondState\nfrom prusa.connect.printer.const import MMU_SLOT_COUNTS, MMUType, Source, State\nfrom prusa.connect.printer.const import Command as CommandType\nfrom prusa.connect.printer.const import Event as EventType\nfrom prusa.connect.printer.files import File\nfrom prusa.connect.printer.models import Sheet as SDKSheet\n\nfrom .. import __version__\nfrom ..camera_governor import CameraGovernor\nfrom ..cameras.picamera_driver import PiCameraDriver\nfrom ..cameras.v4l2_driver import V4L2Driver\nfrom ..conditions import HW, ROOT_COND, UPGRADED, use_connect_errors\nfrom ..config import Config, Settings\nfrom ..const import (\n    BASE_STATES,\n    MK25_PRINTERS,\n    PATH_WAIT_TIMEOUT,\n    PRINTER_CONF_TYPES,\n    PRINTER_TYPES,\n    PRINTING_STATES,\n    SD_STORAGE_NAME,\n)\nfrom ..interesting_logger import InterestingLogRotator\nfrom ..sdk_augmentation.printer import MyPrinter\nfrom ..serial.helpers import enqueue_instruction, enqueue_matchable\nfrom ..serial.serial import SerialException\nfrom ..serial.serial_adapter import SerialAdapter\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom ..serial.serial_queue import MonitoredSerialQueue\nfrom ..service_discovery import ServiceDiscovery\nfrom ..util import (\n    get_print_stats_gcode,\n    is_potato_cpu,\n    make_fingerprint,\n    power_panic_delay,\n    prctl_name,\n)\nfrom .auto_telemetry import AutoTelemetry\nfrom .command_handlers import (\n    CancelReady,\n    DisableResets,\n    EnableResets,\n    ExecuteGcode,\n    JobInfo,\n    LoadFilament,\n    PausePrint,\n    PPRecovery,\n    RePrint,\n    ResetPrinter,\n    ResumePrint,\n    SetReady,\n    StartPrint,\n    StopPrint,\n    UnloadFilament,\n    UpgradeLink,\n)\nfrom .command_queue import CommandQueue, CommandResult\nfrom .file_printer import FilePrinter\nfrom .filesystem.sd_card import SDState\nfrom .filesystem.storage_controller import StorageController\nfrom .ip_updater import IPUpdater\nfrom .job import Job, JobState\nfrom .keepalive import Keepalive\nfrom .lcd_printer import LCDPrinter\nfrom .mmu_observer import MMUObserver\nfrom .model import Model\nfrom .print_stat_doubler import PrintStatDoubler\nfrom .printer_polling import PrinterPolling\nfrom .special_commands import SpecialCommands\nfrom .state_manager import StateChange, StateManager\nfrom .structures.item_updater import WatchedItem\nfrom .structures.model_classes import (\n    PrintState,\n    Telemetry,\n)\nfrom .structures.module_data_classes import Sheet\nfrom .structures.regular_expressions import (\n    LCD_UPDATE_REGEX,\n    MBL_TRIGGER_REGEX,\n    NOT_READY_REGEX,\n    PAUSE_PRINT_REGEX,\n    POWER_PANIC_REGEX,\n    PP_AUTO_RECOVER_REGEX,\n    PP_RECOVER_REGEX,\n    PRINTER_BOOT_REGEX,\n    READY_REGEX,\n    REPRINT_REGEX,\n    RESUME_PRINT_REGEX,\n    TM_CAL_END_REGEX,\n    TM_CAL_START_REGEX,\n    TM_ERROR_LOG_REGEX,\n)\nfrom .telemetry_passer import TelemetryPasser\nfrom .updatable import Thread\n\nlog = logging.getLogger(__name__)\n\n# pylint: disable=too-many-lines\n\n\n# pylint: disable=too-many-lines\n\nclass TransferCallbackState(Enum):\n    \"\"\"Return values form download_finished_cb.\"\"\"\n    SUCCESS = 0\n    NOT_IN_TREE = 1\n    ANOTHER_PRINTING = 2\n    PRINTER_IN_ATTENTION = 3\n\n\nclass PrusaLink:\n    \"\"\"\n    This class is the controller for PrusaLink, more specifically the part\n    that communicates with the printer.\n\n    It connects signals with their handlers\n    \"\"\"\n\n    def __init__(self, cfg: Config, settings: Settings) -> None:\n        # pylint: disable=too-many-statements\n        self.cfg: Config = cfg\n        log.info('Starting adapter for port %s', self.cfg.printer.port)\n        self.settings: Settings = settings\n\n        use_connect_errors(self.settings.use_connect())\n\n        self.quit_evt = Event()\n        self.stopped_event = Event()\n        HW.state = CondState.OK\n        self.model = Model()\n\n        # These start by themselves\n        self.service_discovery = ServiceDiscovery(self.cfg.http.port)\n\n        self.serial_parser = ThreadedSerialParser()\n\n        # Wait for power panic recovery to reach a stable state\n        power_panic_delay(cfg)\n\n        self.serial = SerialAdapter(\n            self.serial_parser,\n            self.model,\n            configured_port=cfg.printer.port,\n            baudrate=cfg.printer.baudrate,\n            reset_disabling=cfg.printer.reset_disabling)\n\n        self.serial_queue = MonitoredSerialQueue(\n            serial_adapter=self.serial,\n            serial_parser=self.serial_parser,\n            threshold_path=self.cfg.daemon.threshold_file)\n        # -----\n\n        self.keepalive = Keepalive(self.serial_queue)\n        self.keepalive.set_use_connect(self.settings.use_connect())\n\n        self.printer = MyPrinter()\n        self.printer.software = __version__\n\n        drivers: List[Type[CameraDriver]] = [V4L2Driver]\n        if PiCameraDriver.supported:\n            drivers.append(PiCameraDriver)\n\n        self.camera_configurator = CameraConfigurator(\n            config=self.settings,\n            config_file_path=self.cfg.printer.settings,\n            camera_controller=self.printer.camera_controller,\n            drivers=drivers,\n            auto_detect=self.cfg.cameras.auto_detect,\n        )\n        self.camera_governor = CameraGovernor(self.camera_configurator,\n                                              self.printer.camera_controller)\n\n        self.printer.register_handler = self.printer_registered\n        self.printer.connection_from_settings(settings)\n\n        # Set download callbacks\n        self.printer.printed_file_cb = self.printed_file_cb\n        self.printer.download_mgr.download_finished_cb \\\n            = self.download_finished_cb\n\n        # Bind command handlers\n        self.printer.set_handler(CommandType.GCODE, self.execute_gcode)\n        self.printer.set_handler(CommandType.PAUSE_PRINT, self.pause_print)\n        self.printer.set_handler(CommandType.RESET_PRINTER, self.reset_printer)\n        self.printer.set_handler(CommandType.UPGRADE, self.upgrade_link)\n        self.printer.set_handler(CommandType.RESUME_PRINT, self.resume_print)\n        self.printer.set_handler(CommandType.START_PRINT, self.start_print)\n        self.printer.set_handler(CommandType.STOP_PRINT, self.stop_print)\n        self.printer.set_handler(CommandType.SEND_JOB_INFO, self.job_info)\n        self.printer.set_handler(CommandType.LOAD_FILAMENT, self.load_filament)\n        self.printer.set_handler(CommandType.UNLOAD_FILAMENT,\n                                 self.unload_filament)\n        self.printer.set_handler(CommandType.SET_PRINTER_READY,\n                                 self.set_printer_ready)\n        self.printer.set_handler(CommandType.CANCEL_PRINTER_READY,\n                                 self.cancel_printer_ready)\n\n        self.serial_parser.add_decoupled_handler(\n            PAUSE_PRINT_REGEX, lambda sender, match: self.fw_pause_print())\n        self.serial_parser.add_decoupled_handler(\n            RESUME_PRINT_REGEX, lambda sender, match: self.fw_resume_print())\n        self.serial_parser.add_decoupled_handler(\n            READY_REGEX, lambda sender, match: self.fw_set_ready())\n        self.serial_parser.add_decoupled_handler(\n            NOT_READY_REGEX, lambda sender, match: self.fw_cancel_ready())\n        self.serial_parser.add_decoupled_handler(\n            REPRINT_REGEX, lambda sender, match: self.fw_reprint())\n\n        # Init components first, so they all exist for signal binding stuff\n        # TODO: does not need printer, the transfer object should be\n        #  viewable from elsewhere imo\n        self.lcd_printer = LCDPrinter(self.serial_queue, self.model,\n                                      self.settings, self.printer,\n                                      self.cfg.daemon.printer_number)\n        self.serial_parser.add_decoupled_handler(\n            LCD_UPDATE_REGEX, self.lcd_printer.lcd_updated)\n\n        self.job = Job(self.serial_queue, self.model,\n                       self.printer)\n        self.state_manager = StateManager(self.serial_parser, self.model)\n\n        self.file_printer = FilePrinter(self.serial_queue, self.serial_parser,\n                                        self.model, self.cfg)\n        self.storage_controller = StorageController(cfg, self.serial_queue,\n                                                    self.serial_parser,\n                                                    self.model)\n        self.ip_updater = IPUpdater(self.model, self.serial_queue)\n        self.telemetry_passer = TelemetryPasser(self.model, self.printer)\n        self.printer_polling = PrinterPolling(self.serial_queue,\n                                              self.serial_parser, self.printer,\n                                              self.model,\n                                              self.telemetry_passer, self.job,\n                                              self.storage_controller.sd_card)\n        self.command_queue = CommandQueue()\n        self.special_commands = SpecialCommands(self.serial_parser,\n                                                self.command_queue)\n\n        # Set Transfer callbacks\n        self.printer.transfer.started_cb = self.transfer_activity_observed\n        self.printer.transfer.progress_cb = self.transfer_activity_observed\n        self.printer.transfer.stopped_cb = self.transfer_activity_observed\n        for state in ROOT_COND:\n            state.add_broke_handler(lambda *_: self.lcd_printer.notify())\n            state.add_fixed_handler(lambda *_: self.lcd_printer.notify())\n\n        self.serial_parser.add_decoupled_handler(\n            MBL_TRIGGER_REGEX,\n            lambda sender, match: self.printer_polling.invalidate_mbl())\n        self.serial_parser.add_decoupled_handler(\n            TM_CAL_START_REGEX, self.block_serial_queue)\n        self.serial_parser.add_decoupled_handler(\n            TM_CAL_END_REGEX, self.unblock_serial_queue)\n        self.serial_parser.add_decoupled_handler(\n            POWER_PANIC_REGEX, self.power_panic_observed)\n        self.serial_parser.add_decoupled_handler(\n            PP_RECOVER_REGEX, self.recover_from_pp)\n        self.serial_parser.add_decoupled_handler(\n            PP_AUTO_RECOVER_REGEX, self.recover_from_pp)\n\n        self.print_stat_doubler = PrintStatDoubler(self.serial_parser,\n                                                   self.printer_polling)\n\n        # Bind signals\n        self.serial_queue.serial_queue_failed.connect(self.serial_queue_failed)\n\n        self.serial.failed_signal.connect(self.serial_failed)\n        self.serial.renewed_signal.connect(self.serial_renewed)\n        self.serial_queue.instruction_confirmed_signal.connect(\n            self.instruction_confirmed)\n        self.serial_queue.message_number_changed.connect(\n            self.serial_message_number_changed)\n        self.serial_parser.add_decoupled_handler(PRINTER_BOOT_REGEX,\n                                                 self.printer_reconnected)\n        self.serial_parser.add_decoupled_handler(TM_ERROR_LOG_REGEX,\n                                                 self.log_tm_error)\n\n        # Set up the signals for special menu handling\n        # And for passthrough\n        self.special_commands.open_result_signal.connect(self.job.file_opened)\n        self.special_commands.start_print_signal.connect(\n            lambda _, match: self.state_manager.printing(), weak=False)\n        self.special_commands.print_done_signal.connect(\n            lambda _, match: self.state_manager.finished(), weak=False)\n        self.storage_controller.menu_found_signal.connect(\n            self.special_commands.menu_folder_found)\n        self.storage_controller.sd_detached_signal.connect(\n            self.special_commands.menu_folder_gone)\n\n        self.printer.command.stop_cb = self.command_queue.clear_queue\n\n        self.job.job_info_updated_signal.connect(self.job_info_updated)\n        self.job.job_id_updated_signal.connect(self.job_id_updated)\n        self.state_manager.pre_state_change_signal.connect(\n            self.pre_state_change)\n        self.state_manager.post_state_change_signal.connect(\n            self.post_state_change)\n        self.state_manager.state_changed_signal.connect(self.state_changed)\n        self.state_manager.pause_signal.connect(\n            lambda match: self.file_printer.pause(), weak=False)\n        self.file_printer.time_printing_signal.connect(\n            self.time_printing_updated)\n        self.file_printer.new_print_started_signal.connect(\n            self.file_printer_started_printing)\n        self.file_printer.print_stopped_signal.connect(\n            self.file_printer_stopped_printing)\n        self.file_printer.print_finished_signal.connect(\n            self.file_printer_finished_printing)\n        self.file_printer.byte_position_signal.connect(\n            self.byte_position_changed)\n        self.file_printer.layer_trigger_signal.connect(self.layer_trigger)\n        self.file_printer.recovery_done_signal.connect(\n            lambda _: self.lcd_printer.notify(), weak=False)\n        self.storage_controller.folder_attached_signal.\\\n            connect(self.folder_attach)\n        self.storage_controller.folder_detached_signal.\\\n            connect(self.folder_detach)\n        self.storage_controller.sd_attached_signal.connect(self.sd_attach)\n        self.storage_controller.sd_detached_signal.connect(self.sd_detach)\n        self.printer_polling.printer_type.became_valid_signal.connect(\n            self.printer_type_changed)\n        self.printer_polling.print_state.became_valid_signal.connect(\n            self.print_state_changed)\n        self.printer_polling.byte_position.value_changed_signal.connect(\n            lambda value: self.byte_position_changed(self.printer_polling,\n                                                     value[0], value[1]))\n        self.printer_polling.mixed_path.value_changed_signal.connect(\n            self.mixed_path_changed)\n        self.printer_polling.progress_broken.value_changed_signal.connect(\n            self.progress_broken)\n        self.printer_polling.mbl.value_changed_signal.connect(\n            self.mbl_data_changed)\n        self.printer_polling.sheet_settings.value_changed_signal.connect(\n            self.sheet_settings_changed)\n        self.printer_polling.active_sheet.value_changed_signal.connect(\n            self.active_sheet_changed)\n        self.printer_polling.mmu_connected.value_changed_signal.connect(\n            self.mmu_connection_changed)\n        self.printer_polling.mmu_version.value_changed_signal.connect(\n            self.mmu_info_changed)\n        self.printer_polling.speed_multiplier.value_changed_signal.connect(\n            lambda val: self.lcd_printer.notify(), weak=False)\n\n        API.add_fixed_handler(self.connection_renewed)\n\n        # get the ip, then poll the rest of the network info\n        self.ip_updater.update()\n        self.ip_updater.updated_signal.connect(self.ip_updated)\n\n        self.camera_governor.start()\n\n        # Leave the non-polled telemetry split from the rest\n        self.auto_telemetry = AutoTelemetry(self.serial_parser,\n                                            self.serial_queue, self.model,\n                                            self.telemetry_passer)\n        self.auto_telemetry.start()\n\n        self.mmu_observer = MMUObserver(self.serial_parser,\n                                        self.model, self.printer,\n                                        self.telemetry_passer)\n\n        self.mmu_observer.error_changed_signal.connect(self.mmu_error_changed)\n\n        self.keepalive.start()\n        self.printer_polling.start()\n        self.storage_controller.start()\n        self.ip_updater.start()\n        self.lcd_printer.start()\n        self.command_queue.start()\n        self.telemetry_passer.start()\n        self.printer.start()\n\n        log.debug(\"Initialization done\")\n\n        debug = False\n        if debug:\n            Thread(target=self.debug_shell, name=\"debug_shell\",\n                   daemon=True).start()\n\n    # pylint: disable=too-many-branches\n    def debug_shell(self) -> None:\n        \"\"\"\n        Calling this in a thread that receives stdin enables th user to\n        give PrusaLink commands through the terminal\n        \"\"\"\n        print(\"Debug shell\")\n        while not self.quit_evt.is_set():\n            try:\n                command = input(\"[PrusaLink]: \")\n                result: Any = \"\"\n                if command == \"pause\":\n                    result = self.command_queue.do_command(PausePrint())\n                elif command == \"reprint\":\n                    result = self.command_queue.do_command(RePrint())\n                elif command == \"resume\":\n                    result = self.command_queue.do_command(ResumePrint())\n                elif command == \"stop\":\n                    result = self.command_queue.do_command(StopPrint())\n                elif command.startswith(\"gcode\"):\n                    result = self.command_queue.do_command(\n                        ExecuteGcode(command.split(\" \", 1)[1]))\n                elif command.startswith(\"print\"):\n                    result = self.command_queue.do_command(\n                        StartPrint(command.split(\" \", 1)[1]))\n                elif command.startswith(\"trigger\"):\n                    InterestingLogRotator.trigger(\"a debugging command\")\n                elif command.startswith(\"faststop\"):\n                    self.stop(True)\n                elif command == \"break comms\":\n                    result = enqueue_matchable(\n                        self.serial_queue, \"M117 Breaking\",\n                        re.compile(r\"something the printer will not tell us\"))\n                if result:\n                    print(result)\n            # pylint: disable=bare-except\n            except:  # noqa: E722\n                log.exception(\"Debug console errored out\")\n\n    def stop(self, fast: bool = False) -> None:\n        \"\"\"\n        Calls stop on every module containing a thread, for debugging prints\n        out all threads which are still running and sets an event to signalize\n        that PrusaLink has stopped.\n        \"\"\"\n        # pylint: disable=too-many-statements\n        log.debug(\"Stop start%s\", ' fast' if fast else '')\n\n        was_printing = self.model.file_printer.printing\n        was_sd_printing = (self.printer_polling.print_state\n                           == PrintState.SD_PRINTING)\n\n        self.quit_evt.set()\n        self.camera_governor.stop()\n        self.file_printer.stop()\n        self.command_queue.stop()\n        self.telemetry_passer.stop()\n        self.printer.stop_loop()\n        self.printer.indicate_stop()\n        self.printer_polling.stop()\n        self.storage_controller.stop()\n        self.keepalive.stop()\n        self.lcd_printer.stop(fast)\n        # This is for pylint to stop complaining, I'd like stop(fast) more\n        if fast:\n            self.ip_updater.stop()\n            self.auto_telemetry.stop()\n        else:\n            self.ip_updater.proper_stop()\n            self.auto_telemetry.proper_stop()\n\n        if was_sd_printing:\n            self.serial.enable_dtr_resets()\n\n        self.serial_queue.stop()\n        self.serial_parser.stop()\n\n        if was_printing and not fast:\n            try:\n                self.serial.write(b\"M603\\n\")\n            except SerialException:\n                pass\n\n        self.serial.stop()\n        log.debug(\"Stop signalled\")\n\n        if not fast:\n            self.service_discovery.unregister()\n            self.file_printer.wait_stopped()\n            self.telemetry_passer.wait_stopped()\n            self.printer.wait_stopped()\n            self.printer_polling.wait_stopped()\n            self.storage_controller.wait_stopped()\n            self.keepalive.wait_stopped()\n            self.lcd_printer.wait_stopped()\n            self.ip_updater.wait_stopped()\n            self.camera_governor.wait_stopped()\n            self.auto_telemetry.wait_stopped()\n            self.serial_queue.wait_stopped()\n            self.serial_parser.wait_stopped()\n            self.serial.wait_stopped()\n\n            log.debug(\"Remaining threads, that might prevent stopping:\")\n            for thread in enumerate_threads():\n                log.debug(thread)\n        self.stopped_event.set()\n        log.info(\"Stop completed%s\", ' fast!' if fast else '')\n\n    # --- Download callbacks ---\n    def printed_file_cb(self) -> Optional[str]:\n        \"\"\"Return absolute path of the currently printed file.\"\"\"\n        if self.job.data.job_state == JobState.IN_PROGRESS:\n            return self.job.data.selected_file_path\n        return None\n\n    # Not type annotated, has problems\n    def download_finished_cb(self, transfer):\n        \"\"\"Called when download is finished successfully\"\"\"\n        if not transfer.to_print:\n            return TransferCallbackState.SUCCESS\n\n        if self.printer.state == State.ATTENTION:\n            return TransferCallbackState.PRINTER_IN_ATTENTION\n\n        if self.job.data.job_state == JobState.IDLE:\n            self.job.deselect_file()\n            if not self.printer.fs.wait_until_path(transfer.path,\n                                                   PATH_WAIT_TIMEOUT):\n                log.warning(\"Transferred file %s not found in tree\",\n                            transfer.path)\n                return TransferCallbackState.NOT_IN_TREE\n\n            self.job.select_file(transfer.path)\n            self.command_queue.do_command(\n                StartPrint(self.job.data.selected_file_path))\n            return TransferCallbackState.SUCCESS\n\n        log.warning(\"Printer is printing another file.\")\n        return TransferCallbackState.ANOTHER_PRINTING\n\n    # --- Command handlers ---\n\n    def execute_gcode(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to exectue gcode from CONNECT with its handler\n        \"\"\"\n        assert caller.kwargs\n        command = ExecuteGcode(gcode=caller.kwargs[\"gcode\"],\n                               force=caller.force,\n                               command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def start_print(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to start print from CONNECT with its handler\n        \"\"\"\n        assert caller.kwargs\n        command = StartPrint(path=caller.kwargs[\"path\"],\n                             command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def pause_print(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to pause print from CONNECT with its handler\n        \"\"\"\n        command = PausePrint(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def resume_print(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to resume print from CONNECT with its handler\n        \"\"\"\n        command = ResumePrint(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def stop_print(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to stop print from CONNECT with its handler\n        \"\"\"\n        command = StopPrint(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def reset_printer(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to reset printer from CONNECT with its handler\n        \"\"\"\n        command = ResetPrinter(command_id=caller.command_id)\n        return self.command_queue.force_command(command)\n\n    def upgrade_link(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to upgrade link from CONNECT with its handler\n        \"\"\"\n        command = UpgradeLink(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def job_info(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"\n        Connects the command to send job info from CONNECT with its handler\n        \"\"\"\n        command = JobInfo(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def load_filament(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"Load filament\"\"\"\n        command = LoadFilament(parameters=caller.kwargs,\n                               command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def unload_filament(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"Unload filament\"\"\"\n        command = UnloadFilament(parameters=caller.kwargs,\n                                 command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def set_printer_ready(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"Set printer ready\"\"\"\n        command = SetReady(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    def cancel_printer_ready(self, caller: SDKCommand) -> CommandResult:\n        \"\"\"Cancel printer ready\"\"\"\n        command = CancelReady(command_id=caller.command_id)\n        return self.command_queue.do_command(command)\n\n    # --- FW Command handlers ---\n\n    def fw_pause_print(self) -> None:\n        \"\"\"\n        Pauses the print, when fw asks to through serial\n        This is activated by the user most of the time\n        \"\"\"\n        # FIXME: The source is wrong for the LCD pause\n        prctl_name()\n        command = PausePrint(source=Source.FIRMWARE)\n        self.command_queue.enqueue_command(command)\n\n    def fw_resume_print(self) -> None:\n        \"\"\"\n        Pauses the print, when fw asks to through serial\n        This happens, when the user presses resume on the LCD\n        \"\"\"\n        prctl_name()\n        command = ResumePrint(source=Source.USER)\n        self.command_queue.enqueue_command(command)\n\n    def fw_set_ready(self) -> None:\n        \"\"\"Set printer ready from the printer LCD menu\"\"\"\n        prctl_name()\n        command = SetReady(source=Source.USER)\n        self.command_queue.enqueue_command(command)\n\n    def fw_cancel_ready(self) -> None:\n        \"\"\"Cancel printer ready from the printer LCD menu\"\"\"\n        prctl_name()\n        command = CancelReady(source=Source.USER)\n        self.command_queue.enqueue_command(command)\n\n    def fw_reprint(self) -> None:\n        \"\"\"Prints the last job again, activated from the printer LCD screen\"\"\"\n        prctl_name()\n        command = RePrint(source=Source.USER)\n        self.command_queue.enqueue_command(command)\n\n    # --- Signal handlers ---\n    def layer_trigger(self, _):\n        \"\"\"Passes the call to trigger to the camera controller\"\"\"\n        self.printer.camera_controller.layer_trigger()\n\n    def mbl_data_changed(self, data) -> None:\n        \"\"\"Sends the mesh bed leveling data to Connect\"\"\"\n        self.printer.mbl = data[\"data\"]\n        self.printer.event_cb(event=EventType.MESH_BED_DATA,\n                              source=Source.MARLIN,\n                              mbl=data[\"data\"])\n\n    def sheet_settings_changed(self, printer_sheets: List[Sheet]) -> None:\n        \"\"\"Sends the new sheet settings\"\"\"\n        sdk_sheets: List[SDKSheet] = []\n        sheet: Sheet\n        for sheet in printer_sheets:\n            sdk_sheets.append({\n                \"name\": sheet.name,\n                \"z_offset\": sheet.z_offset,\n            })\n        self.printer.sheet_settings = sdk_sheets\n\n        if not self.printer.is_initialised():\n            return\n        self.printer.event_cb(event=EventType.INFO,\n                              source=Source.USER,\n                              sheet_settings=sdk_sheets)\n\n    def active_sheet_changed(self, active_sheet) -> None:\n        \"\"\"Sends the new active sheet\"\"\"\n        self.printer.active_sheet = active_sheet\n\n        if not self.printer.is_initialised():\n            return\n        self.printer.event_cb(event=EventType.INFO,\n                              source=Source.USER,\n                              active_sheet=active_sheet)\n\n    def mmu_connection_changed(self, _) -> None:\n        \"\"\"Notifies the telemetry passer about the new state\n        of the mmu connection ans continues ba calling the info sending method\n        \"\"\"\n        self.telemetry_passer.state_changed()\n        self.mmu_info_changed(_)\n\n    def mmu_info_changed(self, _) -> None:\n        \"\"\"Sends the mmu connection status\"\"\"\n        mmu_connected = self.printer_polling.mmu_connected.value\n        mmu_version = self.printer_polling.mmu_version.value\n        if not mmu_connected:\n            mmu_version = None\n\n        self.printer.mmu_enabled = mmu_connected\n        # Hardcoded MMU3, sorry\n        self.printer.mmu_type = MMUType.MMU3 if mmu_connected else None\n\n        if not self.printer_polling.mmu_version.valid:\n            return\n\n        self.printer.mmu_fw = mmu_version\n\n        if not self.printer.is_initialised():\n            return\n\n        kwargs: Dict[str, Any] = {}\n        mmu = {\"enabled\": mmu_connected}\n        if mmu_connected and self.printer.mmu_type is not None:\n            mmu[\"version\"] = mmu_version\n            kwargs[\"slots\"] = MMU_SLOT_COUNTS[self.printer.mmu_type]\n        kwargs[\"mmu\"] = mmu\n\n        self.printer.event_cb(event=EventType.INFO,\n                              source=Source.FIRMWARE,\n                              **kwargs)\n\n    def mmu_error_changed(self, _) -> None:\n        \"\"\"Connect the mmu error code changing to the state manager\"\"\"\n        self.state_manager.mmu_error_changed()\n\n    def job_info_updated(self, _) -> None:\n        \"\"\"On job info update, sends the updated job info to the Connect\"\"\"\n        # pylint: disable=unsupported-assignment-operation,not-a-mapping\n        try:\n            job_info: Dict[str, Any] = self.command_queue.do_command(JobInfo())\n        except Exception:  # pylint: disable=broad-except\n            log.warning(\"Job update could not get job info\")\n        else:\n            job_info[\"source\"] = Source.FIRMWARE\n            self.printer.event_cb(**job_info)\n\n    def job_id_updated(self, _, job_id: int) -> None:\n        \"\"\"Passes the job_id into the SDK\"\"\"\n        self.printer.job_id = job_id\n        self.printer_polling.ensure_job_id()\n\n    def printer_type_changed(self, item: WatchedItem) -> None:\n        \"\"\"Watches for printer type mismatches\"\"\"\n        if not self.settings.printer.type:\n            return\n\n        settings_type = PRINTER_CONF_TYPES[self.settings.printer.type]\n        detected_type = PRINTER_TYPES[item.value]\n        if not settings_type or settings_type == detected_type:\n            UPGRADED.state = CondState.OK\n            return\n\n        if self.settings.use_connect():\n            log.warning(\"Configured printer type does not match the one \"\n                        \"of the printer\")\n            UPGRADED.state = CondState.NOK\n            # Keep this getter spinning, so we get called again\n            self.printer_polling.schedule_printer_type_invalidation()\n        else:\n            # If not using connect, update the type straight away\n            self.settings.printer.type = PRINTER_CONF_TYPES.inverse[\n                detected_type]\n            self.settings.update_sections(connect_skip=True)\n            with open(self.cfg.printer.settings, 'w', encoding='utf-8') as ini:\n                self.settings.write(ini)\n\n            UPGRADED.state = CondState.OK\n\n    def print_state_changed(self, item: WatchedItem) -> None:\n        \"\"\"Handles the newly observed print state\"\"\"\n        assert item.value is not None\n        state_to_handler = {\n            PrintState.SD_PRINTING: self.observed_print,\n            PrintState.NOT_SD_PRINTING: self.observed_no_print,\n            PrintState.SD_PAUSED: self.observed_sd_pause,\n            PrintState.SERIAL_PAUSED: self.observed_serial_pause,\n        }\n        state_to_handler[item.value]()\n\n    def observed_print(self) -> None:\n        \"\"\"\n        The telemetry can observe some states, this method connects\n        it observing a print in progress to the state manager\n        \"\"\"\n        self.command_queue.enqueue_command(DisableResets())\n        self.state_manager.expect_change(\n            StateChange(to_states={State.PRINTING: Source.FIRMWARE}))\n        self.state_manager.printing()\n        self.state_manager.stop_expecting_change()\n\n    def observed_sd_pause(self) -> None:\n        \"\"\"\n        Connects telemetry observing a paused sd print to the state manager\n        \"\"\"\n        self.state_manager.expect_change(\n            StateChange(to_states={State.PAUSED: Source.FIRMWARE}))\n        self.state_manager.paused()\n        self.state_manager.stop_expecting_change()\n\n    def observed_serial_pause(self) -> None:\n        \"\"\"\n        If the printer says the serial print is paused, but we're not serial\n        printing at all, we'll resolve it by stopping whatever was going on\n        before.\n        If the serial print is recovering, we tell that to connnect\n        \"\"\"\n        if self.model.file_printer.recovering:\n            self.state_manager.expect_change(\n                StateChange(to_states={State.PAUSED: Source.FIRMWARE},\n                            reason=\"Waiting for the user to recover the print \"\n                                   \"after a power failure.\"))\n            self.state_manager.paused()\n            self.state_manager.stop_expecting_change()\n\n    def observed_no_print(self) -> None:\n        \"\"\"\n        Useful only when not serial printing. Connects telemetry\n        observing there's no print in progress to the state_manager\n        \"\"\"\n        self.command_queue.enqueue_command(EnableResets())\n        # When serial printing, the printer reports not printing\n        # Let's ignore it in that case\n        if not self.model.file_printer.printing:\n            self.state_manager.expect_change(\n                StateChange(from_states={State.PRINTING: Source.FIRMWARE}))\n            self.state_manager.stopped_or_not_printing()\n            self.state_manager.stop_expecting_change()\n\n    def progress_broken(self, progress_broken: bool) -> None:\n        \"\"\"\n        Connects telemetry, which can see the progress returning garbage\n        values to the job component\n        \"\"\"\n        self.job.progress_broken(progress_broken)\n\n    def byte_position_changed(self, _, current: int, total: int) -> None:\n        \"\"\"Passes byte positions to the job component\"\"\"\n        self.job.file_position(current=current, total=total)\n\n    def mixed_path_changed(self, path: str) -> None:\n        \"\"\"Connects telemetry observed file path to the job component\"\"\"\n        self.job.process_mixed_path(path)\n\n    def _reset_print_stats(self) -> None:\n        \"\"\"Reset print stats on the printer and in telemetry\"\"\"\n        gcode = get_print_stats_gcode()\n        enqueue_instruction(self.serial_queue, gcode)\n\n        self.telemetry_passer.set_telemetry(Telemetry(\n            time_printing=0,\n            time_remaining=0,\n            filament_change_in=0,\n        ))\n\n    def file_printer_started_printing(self, _) -> None:\n        \"\"\"Tells the state manager about a new print job starting\"\"\"\n        self.state_manager.file_printer_started_printing()\n\n    def file_printer_stopped_printing(self, _) -> None:\n        \"\"\"Connects file printer stopping with state manager\"\"\"\n        self.state_manager.stopped()\n\n    def file_printer_finished_printing(self, _) -> None:\n        \"\"\"Connects file printer finishing a print with state manager\"\"\"\n        self.state_manager.finished()\n\n    def serial_failed(self, _) -> None:\n        \"\"\"Connects serial errors with state manager\"\"\"\n        self.state_manager.serial_error()\n        self.file_printer.stop_print()\n\n    def serial_renewed(self, _) -> None:\n        \"\"\"Connects serial recovery with state manager\"\"\"\n        self.state_manager.serial_error_resolved()\n        self.printer_reconnected()\n\n    def set_sn(self, _, serial_number: str) -> None:\n        \"\"\"Set serial number and fingerprint\"\"\"\n        # Only do it if the serial number is missing\n        # Setting it for a second time raises an error for some reason\n        if self.printer.sn is None:\n            self.printer.sn = serial_number\n            self.printer.fingerprint = make_fingerprint(serial_number)\n        elif self.printer.sn != serial_number:\n            log.error(\"The new serial number is different from the old one!\")\n            raise RuntimeError(f\"Serial numbers differ original: \"\n                               f\"{self.printer.sn} new one: {serial_number}.\")\n\n    def printer_registered(self, token: str) -> None:\n        \"\"\"Store settings with updated token when printer was registered.\"\"\"\n        printer_type_string = PRINTER_CONF_TYPES.inverse[self.printer.type]\n        self.settings.printer.type = printer_type_string\n        self.settings.service_connect.token = token\n        self.settings.update_sections()\n        use_connect = self.settings.use_connect()\n        use_connect_errors(use_connect)\n        self.keepalive.set_use_connect(use_connect)\n        with open(self.cfg.printer.settings, 'w', encoding='utf-8') as ini:\n            self.settings.write(ini)\n\n    def ip_updated(self, _) -> None:\n        \"\"\"On every ip change from ip updater sends a new info\"\"\"\n        self.printer_polling.invalidate_network_info()\n\n    def folder_attach(self, _, path: str) -> None:\n        \"\"\"Connects a folder being attached to PrusaConnect events\"\"\"\n        self.printer.attach(path, os.path.basename(path))\n\n    def folder_detach(self, _, path: str) -> None:\n        \"\"\"Connects a folder being detached to PrusaConnect events\"\"\"\n        self.printer.detach(os.path.basename(path))\n\n    def sd_attach(self, _, files: File) -> None:\n        \"\"\"Connects the sd being attached to PrusaConnect events\"\"\"\n        self.printer.fs.attach(SD_STORAGE_NAME, files, \"\", use_inotify=False)\n\n    def sd_detach(self, _) -> None:\n        \"\"\"Connects the sd being detached to PrusaConnect events\"\"\"\n        self.printer.fs.detach(SD_STORAGE_NAME)\n\n    def instruction_confirmed(self, _) -> None:\n        \"\"\"\n        Connects instruction confirmation from serial queue to state manager\n        \"\"\"\n        self.state_manager.instruction_confirmed()\n\n    def serial_message_number_changed(self, message_number):\n        \"\"\"Connects serial message number change to file printer\n        for power panic to work\"\"\"\n        self.file_printer.serial_message_number_changed(message_number)\n\n    def block_serial_queue(self, *_, **__) -> None:\n        \"\"\"Blocks the serial queue\"\"\"\n        self.serial_queue.block_sending()\n\n    def unblock_serial_queue(self, *_, **__) -> None:\n        \"\"\"Unblocks the serial queue\"\"\"\n        self.serial_queue.unblock_sending()\n\n    def power_panic_observed(self, *_, **__):\n        \"\"\"Routes a power panic message to components\"\"\"\n        self.file_printer.power_panic()\n        self.state_manager.power_panic_observed()\n        self.serial.power_panic_observed()\n        self.state_manager.paused()\n        # This is normally a bad idea in a serial handler\n        # But as we are holding the serial disconnected anyways, it's OK\n        sleep(10)\n        self.serial.power_panic_unblock()\n\n    def recover_from_pp(self, *_, **__) -> None:\n        \"\"\"Recover from power panic\"\"\"\n        self.command_queue.enqueue_command(PPRecovery())\n\n    def printer_reconnected(self, *_, **__) -> None:\n        \"\"\"\n        Connects the printer reconnect (reset) to many other components.\n        Stops serial prints, flushes the serial queue, updates the state and\n        tries to send its info again.\n        \"\"\"\n        was_printing = self.state_manager.get_state() in PRINTING_STATES\n        was_power_panic = self.state_manager.in_power_panic\n        self.file_printer.stop_print()\n        self.file_printer.wait_stopped()\n        self.serial_queue.printer_reconnected(was_printing, was_power_panic)\n        self.command_queue.enqueue_command(CancelReady(source=Source.SERIAL))\n\n        # file printer stop print needs to happen before this\n        self.state_manager.reset()\n        self.lcd_printer.reset_error_grace()\n        self.printer_polling.invalidate_printer_info()\n        # Don't wait for the instruction confirmation, we'd be blocking the\n        # thread supposed to provide it\n        self.ip_updater.send_ip_to_printer(timeout=0)\n        self.telemetry_passer.wipe_telemetry()\n\n        # Re-set the power panic flag once we-re done\n        self.state_manager.reset_power_panic()\n\n    @property\n    def sd_ready(self) -> bool:\n        \"\"\"Returns if sd_state is PRESENT.\"\"\"\n        return self.model.sd_card.sd_state == SDState.PRESENT\n\n    def pre_state_change(self, _, command_id: int):\n        \"\"\"\n        First step of a two step process. Connects the state change to the\n        job module. Explanation is(will be) in the job module\n        \"\"\"\n        self.job.state_changed(command_id=command_id)\n\n    def post_state_change(self, _) -> None:\n        \"\"\"Second step of a two step process. Connects the state change to the\n        job module. Explanation is(will be) in the job module\"\"\"\n        self.job.tick()\n\n    # pylint: disable=too-many-arguments\n    # Fix SDK download manager throttle to float, then type annotate\n    def state_changed(self,\n                      _,\n                      from_state,\n                      to_state,\n                      source=None,\n                      command_id=None,\n                      reason=None,\n                      ready=False):\n        \"\"\"Connects the state manager state change to PrusaConnect\"\"\"\n        assert from_state is not None\n        assert to_state is not None\n        if source is None:\n            source = Source.WUI\n            InterestingLogRotator.trigger(\"by an unexpected state change.\")\n            log.warning(\"State change had no source %s\", to_state.value)\n\n        if to_state == State.ERROR:\n            InterestingLogRotator.trigger(\n                \"the printer entering the ERROR state.\")\n            self.file_printer.stop_print()\n\n        self.telemetry_passer.state_changed()\n        if from_state in PRINTING_STATES and to_state in BASE_STATES:\n            self._reset_print_stats()\n\n        # Was printing. Statistics probably changed, let's poll those now\n        if from_state == State.PRINTING:\n            self.printer_polling.invalidate_statistics()\n\n        # No other trigger exists for these older printers\n        # The printer will dip into BUSY for MBL, so lets use that\n        printer_type = None\n        if self.printer.type is not None:\n            printer_type = self.printer.type.value\n        if to_state in {State.PRINTING, State.IDLE} and \\\n                printer_type in MK25_PRINTERS:\n            self.printer_polling.invalidate_mbl()\n\n        # The states should be completely re-done i'm told. So this janky\n        # stuff is what we're going to deal with for now\n        if to_state in {State.PRINTING, State.ATTENTION, State.ERROR}:\n            self.printer_polling.polling_not_ok()\n        if to_state not in {State.PRINTING, State.ATTENTION, State.ERROR}:\n            self.printer_polling.polling_ok()\n\n        # Set download throttling depending on printer state and cpu count\n        if to_state == State.PRINTING and is_potato_cpu():\n            self.printer.download_mgr.buffer_size = DownloadMgr.SMALL_BUFFER\n            self.printer.download_mgr.throttle = 0.03\n        else:\n            self.printer.download_mgr.buffer_size = DownloadMgr.BIG_BUFFER\n            self.printer.download_mgr.throttle = 0\n\n        extra_data = {}\n        if reason is not None:\n            extra_data[\"reason\"] = reason\n\n        self.printer.set_state(to_state,\n                               command_id=command_id,\n                               source=source,\n                               job_id=self.model.job.get_job_id_for_api(),\n                               ready=ready,\n                               **extra_data)\n\n    def time_printing_updated(self, _, time_printing: int) -> None:\n        \"\"\"Connects the serial-print print-timer with telemetry\"\"\"\n        self.telemetry_passer.set_telemetry(new_telemetry=Telemetry(\n            time_printing=time_printing))\n\n    def serial_queue_failed(self, _) -> None:\n        \"\"\"Handles the serial queue failure by resetting the printer\"\"\"\n        reset_command = ResetPrinter()\n        self.state_manager.serial_error()\n        try:\n            self.command_queue.do_command(reset_command)\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Failed to reset the printer. Oh my god... \"\n                          \"my attempt at safely failing has failed.\")\n\n    def connection_renewed(self, *_) -> None:\n        \"\"\"Reacts to the connection with connect being ok again\"\"\"\n        self.telemetry_passer.resend_latest_telemetry()\n\n    def transfer_activity_observed(self, *_) -> None:\n        \"\"\"Notifies PrusaLink components about a transfer happening\"\"\"\n        self.telemetry_passer.activity_observed()\n        self.lcd_printer.notify()\n\n    def log_tm_error(self, _, match: re.Match) -> None:\n        \"\"\"Logs the temperature model errors\"\"\"\n        groups = match.groupdict()\n        deviation = float(groups[\"deviation\"])\n        threshold = float(groups[\"threshold\"])\n        log.warning(\"The hot-end temperature differs from the expected one. \"\n                    \"|%s|>%s\", deviation, threshold)\n"
  },
  {
    "path": "prusa/link/printer_adapter/py.typed",
    "content": ""
  },
  {
    "path": "prusa/link/printer_adapter/special_commands.py",
    "content": "\"\"\"An implementation of a hidden menu logic\"\"\"\nimport logging\nimport re\nfrom time import time\n\nfrom blinker import Signal  # type:ignore\n\nfrom ..interesting_logger import InterestingLogRotator\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom .command import CommandFailed\nfrom .command_handlers import SetReady\nfrom .command_queue import CommandQueue\nfrom .structures.regular_expressions import (\n    OPEN_RESULT_REGEX,\n    PRINT_DONE_REGEX,\n    START_PRINT_REGEX,\n)\n\nlog = logging.getLogger(__name__)\n\nCMD_TIMEOUT = 1\n\n\nclass SpecialCommands:\n    \"\"\"Filter print start related serial output and catch special menu item\n    related ones\"\"\"\n\n    def __init__(self, serial_parser: ThreadedSerialParser,\n                 command_queue: CommandQueue):\n        self.command_queue = command_queue\n\n        self.commands = {\"setready.g\": self.set_ready}\n        self.detected_at = 0\n        self.menu_folder_sfn = None\n        self.current = None\n\n        self.open_result_signal = Signal()  # kwargs - match: re.Match\n        self.start_print_signal = Signal()\n        self.print_done_signal = Signal()\n\n        serial_parser.add_decoupled_handler(\n                OPEN_RESULT_REGEX, self.handle_file)\n        serial_parser.add_decoupled_handler(\n                START_PRINT_REGEX, self.handle_start)\n        serial_parser.add_decoupled_handler(\n                PRINT_DONE_REGEX, self.handle_done)\n\n    def menu_folder_found(self, _, menu_sfn):\n        \"\"\"An SD with the special menu has been inserted\"\"\"\n        log.debug(\"Registered a menu folder %s\", menu_sfn)\n        self.menu_folder_sfn = menu_sfn\n\n    def menu_folder_gone(self, _):\n        \"\"\"The special menu was ejected with its SD card\"\"\"\n        log.debug(\"De-registered a menu folder %s\", self.menu_folder_sfn)\n        self.menu_folder_sfn = None\n\n    def _open_is_special(self, match):\n        \"\"\"Does this match correspond to one of our special menu item files?\"\"\"\n        sdn_lfn = match.group(\"sdn_lfn\")\n        if sdn_lfn is None:\n            return False\n        if self.menu_folder_sfn is None:\n            return False\n        path = sdn_lfn.lower()\n        parts = path.rsplit(\"/\", 2)\n        if len(parts) < 2:\n            return False\n        if parts[-2] != self.menu_folder_sfn:\n            return False\n        return parts[-1] in self.commands\n\n    def handle_file(self, _, match):\n        \"\"\"A file has been opened, should we pass along that info,\n        or should we prepare our special command\"\"\"\n        if self._open_is_special(match):\n            path = match.group(\"sdn_lfn\").lower()\n            parts = path.rsplit(\"/\", 2)\n            self.current = self.commands[parts[-1]]\n            self.detected_at = time()\n        else:\n            self.open_result_signal.send(match=match)\n\n    def handle_start(self, _, match: re.Match):\n        \"\"\"If a command is prepared, prolong it's lifetime,\n        otherwise pass through\"\"\"\n        assert match is not None\n        since_detected = time() - self.detected_at\n        if self.current is not None and since_detected < CMD_TIMEOUT:\n            self.detected_at = time()\n        else:\n            self.current = None\n            self.start_print_signal.send(match=match)\n\n    def handle_done(self, _, match: re.Match):\n        \"\"\"If a command is prepared and the placeholder file print has been\n        done, execute the command\"\"\"\n        since_detected = time() - self.detected_at\n        if self.current is not None and since_detected < CMD_TIMEOUT:\n            self.current()\n        else:\n            self.print_done_signal.send(match=match)\n        self.current = None\n\n    def set_ready(self):\n        \"\"\"A command handler to set the printer into READY\"\"\"\n        try:\n            self.command_queue.do_command(SetReady())\n        except CommandFailed:\n            InterestingLogRotator.trigger(\"Attempt to set the printer ready\")\n            log.exception(\"Setting the printer to READY has failed\")\n"
  },
  {
    "path": "prusa/link/printer_adapter/state_manager.py",
    "content": "\"\"\"Contains implementation of the  the StateManager and StateChange classes\"\"\"\nimport logging\nimport re\nfrom collections import deque\nfrom threading import Event, RLock, Thread, Timer\nfrom time import monotonic\nfrom typing import Dict, Optional, Union\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.conditions import Condition, CondState\nfrom prusa.connect.printer.const import Source, State\n\nfrom ..conditions import HW, SERIAL\nfrom ..const import ERROR_REASON_TIMEOUT, STATE_HISTORY_SIZE, \\\n    ATTENTION_CLEAR_INTERVAL, PRINT_END_TIMEOUT\nfrom ..serial.serial_parser import ThreadedSerialParser\nfrom .model import Model\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.module_data_classes import StateManagerData\nfrom .structures.regular_expressions import (ATTENTION_REASON_REGEX,\n                                             ATTENTION_REGEX, BUSY_REGEX,\n                                             CANCEL_REGEX, ERROR_REASON_REGEX,\n                                             ERROR_REGEX, FAN_ERROR_REGEX,\n                                             FAN_REGEX, PAUSED_REGEX,\n                                             RESUMED_REGEX, TM_ERROR_CLEARED)\n\nlog = logging.getLogger(__name__)\n\n\nclass StateChange:\n    \"\"\"\n    Represents a set of state changes that can happen\n    Used for assigning info to observed state changes\n    \"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self,\n                 command_id=None,\n                 to_states: Optional[Dict[State, Union[Source, None]]] = None,\n                 from_states: Optional[Dict[State, Union[Source, None]]] = None,\n                 default_source: Optional[Source] = None,\n                 reason: Optional[str] = None,\n                 ready: bool = False):\n\n        self.reason = reason\n        self.to_states: Dict[State, Union[Source, None]] = {}\n        self.from_states: Dict[State, Union[Source, None]] = {}\n\n        if from_states is not None:\n            self.from_states = from_states\n        if to_states is not None:\n            self.to_states = to_states\n\n        self.command_id = command_id\n        self.default_source = default_source\n        self.ready = ready\n\n\ndef state_influencer(state_change: Optional[StateChange] = None):\n    \"\"\"\n    This decorator makes it possible for each state change to have default\n    expected sources\n    This can be overridden by notifying the state manager about an\n    oncoming state change through expect_change\n    \"\"\"\n\n    def inner(func):\n        \"\"\"It's just how decorators work man\"\"\"\n\n        def wrapper(self, *args, **kwargs):\n            \"\"\"By nesting function definitions. Shut up Travis!\"\"\"\n            with self.state_lock:\n                has_set_expected_change = False\n                if self.expected_state_change is None and \\\n                        state_change is not None:\n                    has_set_expected_change = True\n                    self.expect_change(state_change)\n\n                else:\n                    log.debug(\"Default expected state change is overridden\")\n\n                func(self, *args, **kwargs)\n                self.state_may_have_changed()\n\n                if has_set_expected_change:\n                    self.stop_expecting_change()\n\n        return wrapper\n\n    return inner\n\n\nclass StateManager(metaclass=MCSingleton):\n    \"\"\"\n    Keeps track of the printer states by observing the serial and by listening\n    to other PrusaLink components\n    \"\"\"\n\n    # pylint: disable=too-many-instance-attributes,\n    # pylint: disable=too-many-public-methods\n    # pylint: disable=too-many-arguments\n    def __init__(self, serial_parser: ThreadedSerialParser, model: Model):\n\n        self.serial_parser: ThreadedSerialParser = serial_parser\n        self.model: Model = model\n\n        self.pre_state_change_signal = Signal()  # kwargs: command_id: int\n        self.post_state_change_signal = Signal()\n        self.state_changed_signal = Signal()  # kwargs:\n        #                                           from_state: State\n        #                                           to_state: State\n        #                                           command_id: int,\n        #                                           source: Sources\n        #                                           reason: str\n        #                                           ready: bool\n\n        self.pause_signal = Signal()\n\n        self.model.state_manager = StateManagerData(\n            # The ACTUAL states considered when reporting\n            base_state=State.BUSY,\n            printing_state=None,\n            override_state=None,\n            # Reported state history\n            state_history=deque(maxlen=STATE_HISTORY_SIZE),\n            last_state=State.BUSY,\n            current_state=State.BUSY,\n            awaiting_error_reason=False)\n        self.data = self.model.state_manager\n\n        # Prevent multiple threads changing the state at once\n        self.state_lock = RLock()\n\n        # Another anti-ideal thing is, that with this observational\n        # approach to state detection we cannot correlate actions with\n        # reactions nicely. My first approach is to have an action,\n        # that's supposed to change the state and to which state that shall be\n        # if we observe such a transition, we'll say the action\n        # caused the state change\n        self.expected_state_change: Union[None, StateChange] = None\n\n        # The fan error doesn't fit into this mechanism\n        # When this value isn't none, a fan error has been observed\n        # but not yet reported, the value shall be the name of the fan which\n        # caused the error\n        # New: clear once the error is known resolved\n        self.fan_error_name = None\n\n        # A thing to detect a false positive attention\n        self.resuming_from_fan_error = False\n\n        # At startup, we must avoid going to the IDLE state, until\n        # we are sure about not printing\n        self.unsure_whether_printing = True\n\n        # Errors are a fun bunch, sometimes, the explanation of what has\n        # happened comes before and sometimes after the stop() or kill()\n        # call. Let's start a timer when an unexplained kill() or stop() comes\n        # and if an explanation comes, let's send that as reason, otherwise\n        # do the error state without a reason.\n        self.error_reason_thread: Optional[Thread] = None\n        self.error_reason_event = Event()\n\n        # Workaround for a bug, where on a start of a SD print from the LCD,\n        # the printer announces it will be printing a file, then says it's not\n        # printing anything and then announces printing the same file again\n        # This makes us ask the user to remove the print while printing\n        # Stopping on the first layer potentially damaging the build plate\n        self.believe_not_printing = False\n\n        # Another special case - need to ignore a pause when we're\n        # in temperature model triggered error\n        self.tm_ignore_pause = False\n\n        # There are attention states that end in a BUSY state,\n        # so the attention does not get cleared.\n        # Let's clear it on a timer instead\n        self.attention_clearing_timer = self.new_attention_timer()\n\n        # We need to stay in the STOPPED and FINISHED states for a while\n        # for Connect to take and save the last print photo\n        self.print_ended_at = None\n\n        # Flag to keep track of power panic.\n        # If the printer re-sets because of power panic, we don't want to\n        # send an M603, the flag has to be re-set manually\n        self.in_power_panic = False\n\n        regex_handlers = {\n            BUSY_REGEX: lambda sender, match: self.busy(),\n            ATTENTION_REGEX: lambda sender, match: self.attention(),\n            PAUSED_REGEX: lambda sender, match: self.filter_pause_events(),\n            RESUMED_REGEX: lambda sender, match: self.resumed(),\n            CANCEL_REGEX: lambda sender, match: self.stopped_or_not_printing(),\n            ERROR_REGEX: lambda sender, match: self.error_handler(),\n            ERROR_REASON_REGEX: self.error_reason_handler,\n            ATTENTION_REASON_REGEX: self.attention_reason_handler,\n            FAN_ERROR_REGEX: self.fan_error,\n            TM_ERROR_CLEARED: self.clear_tm_error,\n        }\n\n        for regex, handler in regex_handlers.items():\n            self.serial_parser.add_decoupled_handler(regex, handler)\n\n        for state in SERIAL:\n            state.add_broke_handler(self.link_error_detected)\n            state.add_fixed_handler(self.link_error_resolved)\n\n        super().__init__()\n\n    def new_attention_timer(self):\n        \"\"\"Creates a new attention clearing timer object\"\"\"\n        timer = Timer(\n            interval=ATTENTION_CLEAR_INTERVAL,\n            function=self._attention_timer_handler,\n        )\n        timer.daemon = True\n        return timer\n\n    def start_attention_timer(self):\n        \"\"\"Clears the previous timer and starts a new one\"\"\"\n        with self.state_lock:\n            self.stop_attention_timer()\n            self.attention_clearing_timer = self.new_attention_timer()\n            self.attention_clearing_timer.start()\n\n    def stop_attention_timer(self):\n        \"\"\"Clears the attention clearing timer if it's running\"\"\"\n        with self.state_lock:\n            if self.attention_clearing_timer.is_alive():\n                self.attention_clearing_timer.cancel()\n\n    def link_error_detected(self, condition: Condition, old_value: CondState):\n        \"\"\"increments an error counter once an error gets detected\"\"\"\n        if old_value == CondState.OK:\n            log.debug(\"Condition %s broke, causing an ERROR state\",\n                      condition.name)\n            if self.expected_state_change is None:\n                self.expect_change(\n                    StateChange(to_states={State.ERROR: Source.SERIAL},\n                                reason=condition.short_msg))\n            self.error()\n\n    def link_error_resolved(self, condition: Condition, old_value: CondState):\n        \"\"\"decrements an error counter once an error gets resolved\"\"\"\n        if old_value == CondState.NOK:\n            log.debug(\"Condition %s fixed\", condition.name)\n            if SERIAL.successors_ok():\n                log.debug(\"All printer conditions are OK\")\n                self.error_resolved()\n\n    def file_printer_started_printing(self):\n        \"\"\"\n        If the file printer truly is printing and we don't know about it\n        yet, let's change our state to PRINTING.\n        \"\"\"\n        if (self.model.file_printer.printing\n                and self.data.printing_state != State.PRINTING):\n            self.printing()\n\n    def get_state(self):\n        \"\"\"\n        State manager has three levels of importance, the most important state\n        is the one returned. The least important is the base state,\n        followed by printing state and then the override state.\n        \"\"\"\n        if self.data.override_state is not None:\n            return self.data.override_state\n        if self.data.printing_state is not None:\n            return self.data.printing_state\n        return self.data.base_state\n\n    def expect_change(self, change: StateChange):\n        \"\"\"\n        Pairing state changes with events that could've caused them\n        is done through expected state changes. This method sets it\n        \"\"\"\n        with self.state_lock:\n            self.expected_state_change = change\n\n    def stop_expecting_change(self):\n        \"\"\"Resets the expected state change\"\"\"\n        with self.state_lock:\n            self.expected_state_change = None\n\n    def is_expected(self):\n        \"\"\"Figure out if the state change we are experiencing was expected\"\"\"\n        with self.state_lock:\n            state_change = self.expected_state_change\n            expecting_change = state_change is not None\n            if expecting_change:\n                # flake8: noqa\n                expected_to = self.data.current_state in state_change.to_states\n                expected_from = self.data.last_state in state_change.from_states\n                has_default_source = state_change.default_source is not None\n                return expected_to or expected_from or has_default_source\n            return False\n\n    def get_expected_source(self):\n        \"\"\"\n        Figures out who or what could have caused the state change\n        :return:\n        \"\"\"\n        with self.state_lock:\n            # No change expected,\n            if self.expected_state_change is None:\n                return None\n\n            state_change = self.expected_state_change\n\n            # Get the expected sources\n            source_from = None\n            source_to = None\n            if self.data.last_state in state_change.from_states:\n                source_from = state_change.from_states[self.data.last_state]\n            if self.data.current_state in state_change.to_states:\n                source_to = state_change.to_states[self.data.current_state]\n\n            # If there are conflicting sources, pick the one, paired with\n            # from_state as this is useful for leaving states like\n            # ATTENTION and ERROR\n            if (source_from is not None and source_to is not None\n                    and source_to != source_from):\n                source = source_from\n            else:\n                # no conflict here, the sources are the same,\n                # or one or both of them are None\n                try:\n                    # make a list throwing out Nones and get the next item\n                    # (the first one)\n                    source = next(item for item in [source_from, source_to]\n                                  if item is not None)\n                except StopIteration:  # tried to get next from an empty list\n                    source = None\n\n            if source is None:\n                source = state_change.default_source\n\n            log.debug(\n                \"Source has been determined to be %s. Default was: %s, \"\n                \"from: %s, to: %s\", source, state_change.default_source,\n                source_from, source_to)\n\n            return source\n\n    def state_may_have_changed(self):\n        \"\"\"\n        Should be called after every internal state change. If the internal\n        state change changed the external reported state, updates the state\n        history and lets everyone know the state change details.\n        \"\"\"\n        with self.state_lock:\n            # Did our internal state change cause a reported state change?\n            # If yes, update state stuff\n            if self.get_state() != self.data.current_state:\n                self.believe_not_printing = False\n                self.data.last_state = self.data.current_state\n                self.data.current_state = self.get_state()\n                self.data.state_history.append(self.data.current_state)\n                log.debug(\"Changing state from %s to %s\", self.data.last_state,\n                          self.data.current_state)\n\n                # Now let's find out if the state change was expected\n                # and what parameters can we deduce from that\n                command_id = None\n                source = None\n                reason = None\n                ready = False\n\n                if self.data.printing_state is not None:\n                    log.debug(\"We are printing - %s\", self.data.printing_state)\n\n                if self.data.override_state is not None:\n                    log.debug(\"State is overridden by %s\",\n                              self.data.override_state)\n\n                # If the state changed to something expected,\n                # then send the information about it\n                if self.is_expected():\n                    if self.expected_state_change.command_id is not None:\n                        command_id = self.expected_state_change.command_id\n                    source = self.get_expected_source()\n                    reason = self.expected_state_change.reason\n                    ready = self.expected_state_change.ready\n                    if reason is not None:\n                        log.debug(\"Reason for %s: %s\", self.get_state(),\n                                  reason)\n                else:\n                    log.debug(\"Unexpected state change. This is weird\")\n                self.expected_state_change = None\n\n                self.pre_state_change_signal.send(self, command_id=command_id)\n\n                self.state_changed_signal.send(\n                    self,\n                    from_state=self.data.last_state,\n                    to_state=self.data.current_state,\n                    command_id=command_id,\n                    source=source,\n                    reason=reason,\n                    ready=ready)\n                self.post_state_change_signal.send(self)\n\n    def fan_error(self, sender, match: re.Match):\n        \"\"\"\n        Even though using these two callables is more complicated,\n        I think the majority of the implementation got condensed into here\n        \"\"\"\n        assert sender is not None\n        self.fan_error_name = match.group(\"fan_name\")\n        self.serial_parser.add_decoupled_handler(FAN_REGEX, self.fan_error_resolver)\n\n        log.debug(\"%s fan error has been observed.\", self.fan_error_name)\n        self.expect_change(\n            StateChange(to_states={State.ATTENTION: Source.FIRMWARE},\n                        reason=f\"{self.fan_error_name} fan error\"))\n\n        state = self.get_state()\n        if state not in {State.PRINTING, State.ERROR}:\n            self.attention()\n\n    def mmu_error_changed(self):\n        \"\"\"\n        If the MMU error has changed, enter attention if the error is not None,\n        attempt to leave attention otherwise\n        \"\"\"\n        current_error_code = self.model.mmu_observer.current_error_code\n        if current_error_code is None:\n            self.expect_change(\n                StateChange(to_states={State.ATTENTION: Source.SLOT},\n                            reason=current_error_code))\n            self._clear_attention()\n        else:\n            self.expect_change(\n                StateChange(to_states={State.ATTENTION: Source.SLOT},\n                            reason=current_error_code))\n            self.attention()\n        self.stop_expecting_change()\n\n\n    def fan_error_resolver(self, sender, match):\n        \"\"\"\n        If the fan speeds are indicative of a fan error being resolved\n        clears the fan error\n\n        This is very rudimentary, it only counts with one fan\n        failing at a time, and it will quit the attention only if\n        the firmware/user spins up the fan that's been reported\n        or on print resume and stop\n        weird edge cases expected\"\"\"\n        assert sender is not None\n\n        hotend_fan_rpm = int(match.group(\"hotend_rpm\"))\n        hotend_fan_power = int(match.group(\"hotend_power\"))\n        print_fan_rpm = int(match.group(\"print_rpm\"))\n        print_fan_power = int(match.group(\"print_power\"))\n\n        hotend_fan_works = hotend_fan_rpm > hotend_fan_power > 0\n        print_fan_works = print_fan_rpm > print_fan_power > 0\n        fan_name = self.fan_error_name\n\n        if (fan_name in {\"Extruder\", \"Hotend\"} and hotend_fan_works) or \\\n                (fan_name == \"Print\" and print_fan_works):\n            self.expect_change(\n                StateChange(from_states={State.ATTENTION: Source.USER},\n                            reason=f\"{fan_name} fan error resolved\"))\n            self._cancel_fan_error()\n            self.clear_attention()\n            if self.data.printing_state == State.PAUSED:\n                self.resuming_from_fan_error = True\n\n    def _cancel_fan_error(self):\n        \"\"\"Removes the fan error\"\"\"\n        self.fan_error_name = None\n        self.serial_parser.remove_handler(FAN_ERROR_REGEX,\n                                          self.fan_error_resolver)\n\n    def error_handler(self):\n        \"\"\"\n        Handle a generic error message. Start waiting for a reason an error\n        was raised. If that times out, sets just a generic error\n        \"\"\"\n        if self.data.override_state != State.ERROR:\n            self.data.awaiting_error_reason = True\n            self.error_reason_thread = Thread(target=self.error_reason_waiter,\n                                              daemon=True)\n            self.error_reason_thread.start()\n\n    def error_reason_handler(self, sender, match: re.Match):\n        \"\"\"\n        Handle a specific error, which requires printer reset\n        \"\"\"\n        assert sender is not None\n        groups = match.groupdict()\n        # End the previous reason waiting thread\n        self.error_reason_event.set()\n        self.error_reason_event.clear()\n\n        reason = self.parse_error_reason(groups)\n        self.expect_change(\n            StateChange(to_states={State.ERROR: Source.MARLIN}, reason=reason))\n\n        HW.state = CondState.NOK\n\n    def attention_reason_handler(self, sender, match: re.Match):\n        \"\"\"\n        Handle a message, that is sure to cause an ATTENTION state\n        use it as the reason for going into that state\n        \"\"\"\n        assert sender is not None\n        groups = match.groupdict()\n\n        reason = \"unknown\"\n        if groups[\"mbl_didnt_trigger\"]:\n            reason = \"Bed leveling failed. Sensor didn't trigger. \" \\\n                     \"Is there debris on the nozzle?\"\n        elif groups[\"mbl_too_high\"]:\n            reason = \"Bed leveling failed. Sensor triggered too high. \"\n        elif groups[\"tm_error\"]:\n\n            end_text = \"Resolve the error and reset the printer.\"\n            if self.data.printing_state == State.PRINTING:\n                end_text = \"Print paused.\"\n            reason = f\"The nozzle temperature has deviated too far \" \\\n                     f\"from the expected one. {end_text}\"\n            self.tm_ignore_pause = True\n\n        self.expect_change(\n            StateChange(to_states={State.ATTENTION: Source.MARLIN},\n                        reason=reason))\n\n    def filter_pause_events(self):\n        \"\"\"Filters the action: paused events, notifies the rest\n\n        This is a giant workaround, this state machine should be\n        separated from the state manager\"\"\"\n        if self.tm_ignore_pause:\n            return\n\n        self.pause_signal.send()\n        self.paused()\n\n    def clear_tm_error(self, _, match: re.Match):\n        \"\"\"Clear the TM error flag\"\"\"\n        assert match is not None\n        self.tm_ignore_pause = False\n\n    def power_panic_observed(self):\n        \"\"\"Set the power panic flag\"\"\"\n        self.in_power_panic = True\n\n    def reset_power_panic(self):\n        \"\"\"Reset the power panic flag\"\"\"\n        self.in_power_panic = False\n\n    @staticmethod\n    def parse_error_reason(groups):\n        \"\"\"\n        Provided error parsed groups, put together a reason explaining\n        why it occurred\n        :param groups: re match group dictionary\n        :return: a reason string\n        \"\"\"\n        reason = \"\"\n        if groups[\"temp\"] is not None:\n            if groups[\"mintemp\"] is not None:\n                reason += \"Mintemp\"\n            elif groups[\"maxtemp\"] is not None:\n                reason += \"Maxtemp\"\n            reason += \" triggered by the \"\n            if groups[\"bed\"] is not None:\n                reason += \"heatbed thermistor.\"\n            else:\n                reason += \"hotend thermistor.\"\n        elif groups[\"runaway\"] is not None:\n            if groups[\"hotend_runaway\"] is not None:\n                reason = \"Hotend\"\n            elif groups[\"heatbed_runaway\"] is not None:\n                reason = \"Heatbed\"\n            elif groups[\"preheat_hotend\"] is not None:\n                reason = \"Hotend preheat\"\n            elif groups[\"preheat_heatbed\"] is not None:\n                reason = \"Heatbed preheat\"\n            reason += \" thermal runaway.\"\n        reason += \" Manual restart required!\"\n        return reason\n\n    def error_reason_waiter(self):\n        \"\"\"\n        Waits for an error reason to be provided\n        If it times out, it will warn the user and send \"404 reason not found\"\n        as the reason.\n        \"\"\"\n        if not self.error_reason_event.wait(ERROR_REASON_TIMEOUT):\n            log.warning(\"Did not capture any explanation for the error state\")\n            self.expect_change(\n                StateChange(to_states={State.ERROR: Source.MARLIN},\n                            reason=\"404 Reason not found\"))\n            HW.state = CondState.NOK\n        self.data.awaiting_error_reason = False\n\n    # --- State changing methods ---\n\n    def stopped_or_not_printing(self):\n        \"\"\"\n        Depending on state, clears the printing state or sets the printing\n        state to STOPPED\n        \"\"\"\n        if self.believe_not_printing:\n            if self.data.printing_state in (State.PRINTING, State.PAUSED):\n                self.stopped()\n            else:\n                self.not_printing()\n        else:\n            self.believe_not_printing = True\n\n    def reset(self):\n        \"\"\"\n        On printer reset, the printer is not idle yet, so set the base state\n        to busy. After reset it surely can't carry on printing so take care of\n        that as well\n        :return:\n        \"\"\"\n        HW.state = CondState.OK\n        self.busy()\n        self.stopped_or_not_printing()\n\n    # This state change can change the state to \"PRINTING\"\n    @state_influencer(StateChange(to_states={State.PRINTING: Source.USER}))\n    def printing(self):\n        \"\"\"\n        If not printing or paused, sets printing state to PRINTING\n        :return:\n        \"\"\"\n        log.debug(\"Should be PRINTING\")\n        if self.data.printing_state is None or \\\n                self.data.printing_state == State.PAUSED:\n            self.unsure_whether_printing = False\n            self.data.printing_state = State.PRINTING\n        else:\n            log.debug(\"Ignoring switch to PRINTING base: %s, printing: %s\",\n                      self.data.base_state, self.data.printing_state)\n\n    @state_influencer(\n        StateChange(from_states={\n            State.PRINTING: Source.MARLIN,\n            State.PAUSED: Source.MARLIN,\n        }))\n    def not_printing(self):\n        \"\"\"\n        We know we're not printing, keeps FINISHED and STOPPED because\n        the user needs to confirm those manually now\n        \"\"\"\n        self.unsure_whether_printing = False\n        if self.data.printing_state not in {State.FINISHED, State.STOPPED}:\n            self.data.printing_state = None\n\n    @state_influencer(StateChange(to_states={State.FINISHED: Source.MARLIN}))\n    def finished(self):\n        \"\"\"Sets the printing state to FINISHED if we are printing\"\"\"\n        if self.data.printing_state == State.PRINTING:\n            self.print_ended_at = monotonic()\n            self.data.printing_state = State.FINISHED\n\n    @state_influencer(StateChange(to_states={State.READY: Source.USER}))\n    def ready(self):\n        \"\"\"If we were IDLE, sets te base state to READY\"\"\"\n        if self.data.base_state == State.IDLE:\n            self.data.base_state = State.READY\n\n    @state_influencer(StateChange(to_states={State.IDLE: Source.USER}))\n    def idle(self):\n        \"\"\"If we were READY, sets te base state to IDLE\"\"\"\n        if self.data.base_state == State.READY:\n            self.data.base_state = State.IDLE\n\n    @state_influencer(StateChange(to_states={State.BUSY: Source.MARLIN}))\n    def busy(self):\n        \"\"\"If we were idle, sets te base state to BUSY\"\"\"\n        if self.data.base_state in {State.IDLE, State.READY}:\n            self.data.base_state = State.BUSY\n\n    # Cannot distinguish pauses from the user and the gcode\n    @state_influencer(StateChange(to_states={State.PAUSED: Source.USER}))\n    def paused(self):\n        \"\"\"If we were printing, sets the printing state to PAUSED\"\"\"\n        if self.data.printing_state in {State.PRINTING, None}:\n            self.unsure_whether_printing = False\n            self.data.printing_state = State.PAUSED\n\n        if self.fan_error_name is not None:\n            self.data.override_state = State.ATTENTION\n\n    @state_influencer(StateChange(to_states={State.PRINTING: Source.USER}))\n    def resumed(self):\n        \"\"\"If we were paused, sets the printing state to PRINTING\"\"\"\n        if self.data.printing_state == State.PAUSED:\n            self.unsure_whether_printing = False\n            self.data.printing_state = State.PRINTING\n\n        if self.fan_error_name is not None:\n            self._cancel_fan_error()\n\n        if self.resuming_from_fan_error:\n            self.resuming_from_fan_error = False\n\n    @state_influencer(StateChange(from_states={State.PRINTING: Source.USER}))\n    def stopped(self):\n        \"\"\"\n        If we were printing or paused, sets the printing state to STOPPED\n        \"\"\"\n        if self.data.printing_state in {State.PRINTING, State.PAUSED}:\n            self.unsure_whether_printing = False\n            self.print_ended_at = monotonic()\n            self.data.printing_state = State.STOPPED\n\n        if self.fan_error_name is not None:\n            self._cancel_fan_error()\n\n    @state_influencer(\n        StateChange(to_states={State.IDLE: Source.MARLIN},\n                    from_states={\n                        State.ATTENTION: Source.USER,\n                        State.ERROR: Source.MARLIN,\n                        State.BUSY: Source.HW,\n                        State.FINISHED: Source.MARLIN,\n                        State.STOPPED: Source.MARLIN,\n                    },\n                    ready=False))\n    def instruction_confirmed(self):\n        \"\"\"\n        Instruction confirmation shall clear all temporary states\n        Starts at the least important so it generates only one state change\n        \"\"\"\n        if self.unsure_whether_printing:\n            return\n\n        if self.data.base_state == State.BUSY:\n            self.data.base_state = State.IDLE\n\n        if self.data.printing_state in {State.STOPPED, State.FINISHED} and \\\n                self.data.override_state is not State.ATTENTION:\n            if monotonic() > self.print_ended_at + PRINT_END_TIMEOUT:\n                self.data.printing_state = None\n\n                # Make sure that if we just finished a print, or we\n                # stopped one, we return to IDLE\n                if self.data.base_state == State.READY:\n                    self.data.base_state = State.IDLE\n\n        self._clear_attention()\n\n    def _attention_timer_handler(self):\n        \"\"\"Handles the attention timer running out.\"\"\"\n        with self.state_lock:\n            self.expect_change(\n                StateChange(from_states={State.ATTENTION: Source.MARLIN},\n                            reason=\"The ATTENTION state has stopped being \"\n                                   \"reported by the printer\"))\n            self.clear_attention()\n\n    def _clear_attention(self):\n        \"\"\"Clears the ATTENTION state, if the conditions are right\"\"\"\n        if self.data.override_state != State.ATTENTION:\n            return\n        if self.fan_error_name is not None:\n            return\n        if self.model.mmu_observer.current_error_code is not None:\n            return\n\n        log.debug(\"Clearing ATTENTION\")\n        self.data.override_state = None\n        self.stop_attention_timer()\n\n    @state_influencer(StateChange(from_states={State.ATTENTION: Source.USER}))\n    def clear_attention(self):\n        \"\"\"Calls the internal method for clearing the attention state\"\"\"\n        self._clear_attention()\n\n    @state_influencer(StateChange(to_states={State.ATTENTION: Source.USER}))\n    def attention(self):\n        \"\"\"\n        Sets the override state to ATTENTION\n        \"\"\"\n        if self.resuming_from_fan_error:\n            self.expect_change(\n                StateChange(to_states={State.ATTENTION: Source.MARLIN},\n                            reason=\"Most likely a false positive. \"\n                            \"Sorry about that 😅\"))\n        self.start_attention_timer()\n\n        log.debug(\"Overriding the state with ATTENTION\")\n        log.warning(\"State was %s\", self.get_state())\n        self.data.override_state = State.ATTENTION\n\n    @state_influencer(StateChange(to_states={State.ERROR: Source.WUI}))\n    def error(self):\n        \"\"\"Sets the override state to ERROR\"\"\"\n        log.debug(\"Overriding the state with ERROR\")\n        self.data.override_state = State.ERROR\n\n    @state_influencer(StateChange(from_states={State.ERROR: Source.USER}))\n    def error_resolved(self):\n        \"\"\"Removes the override ERROR state\"\"\"\n        if self.data.override_state == State.ERROR and \\\n                SERIAL.successors_ok():\n            log.debug(\"Cancelling the ERROR state override\")\n            self.data.override_state = None\n\n    @state_influencer(\n        StateChange(to_states={State.ERROR: Source.SERIAL},\n                    reason=\"Communication with the printer has failed\"))\n    def serial_error(self):\n        \"\"\"\n        Also sets the override state to ERROR but has a different\n        default source\n        \"\"\"\n        log.debug(\"Serial ERROR overrode state\")\n        self.data.override_state = State.ERROR\n\n    @state_influencer(\n        StateChange(to_states={State.IDLE: Source.SERIAL},\n                    reason=\"Re-established the communication \"\n                    \"with the printer\"))\n    def serial_error_resolved(self):\n        \"\"\"Resets the error state if there is any\"\"\"\n        if self.data.override_state == State.ERROR:\n            log.debug(\"Serial ERROR resolved, removing override\")\n            self.data.override_state = None\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/printer_adapter/structures/carousel.py",
    "content": "\"\"\"Implements the helper classes for LCD printer.\nDoes not depend on the rest of the PrusaLink app\"\"\"\nimport math\nfrom collections import deque\nfrom copy import copy\nfrom time import time\nfrom typing import Deque, List, Optional, Set\n\n\nclass LCDLine:\n    \"\"\"Info about the text to show and the chime to play\"\"\"\n\n    def __init__(self, text: str, delay: float = 5.0,\n                 resets_idle: bool = False,\n                 chime_gcode: Optional[List[str]] = None) -> None:\n        self.text: str = text\n        self.delay: float = delay\n        self.chime_gcode: List[str] = []\n        if chime_gcode is not None:\n            self.chime_gcode = chime_gcode\n        self.resets_idle = resets_idle\n        self.ends_at = time() + self.delay\n\n    def reset_end(self):\n        \"\"\"Resets the message's end time, used, so the delay is the minimum\n        time the message is shown on screen\"\"\"\n        self.ends_at = time() + self.delay\n\n\nclass Screen:\n    \"\"\"A Screen - like an error screen, or an easter egg scrolling screen\"\"\"\n\n    def __init__(self, resets_idle=True, chime_gcode=None, order=0):\n        \"\"\"\n        :param resets_idle: Do the messages from this screen reset the idle\n                            timer?\n        :param chime_gcode: The gcode to play when this screen is enabled\n        :param order: This is static, but could be made dynamic.\n                      The order of screens in case there's more with the\n                      same priority. Smallest goes first\n        \"\"\"\n        self.resets_idle = resets_idle\n        self.chime_gcode = []\n        if chime_gcode is not None:\n            self.chime_gcode = chime_gcode\n\n        self.conditions = {}\n        self.changed = False\n\n        self.text = \"\"\n        self.scroll_delay = 2.0\n        self.first_line_extra = 2.0\n        self.scroll_amount = 10\n        self.last_line_extra = 1.0\n\n        # only the things with the highest priority get displayed\n        self.priority = 0\n        # if there are more than one, they get ordered by this number\n        self.order = order\n        self.enabled = False\n        self.to_chime = False\n\n    def __str__(self):\n        return f\"A Screen saying {self.text}\"\n\n    def lines(self):\n        \"\"\"The status display has 19 usable chars (20)\n        Iterating over this cuts the text into displayable messages (lines)\n        to output, so a scrolling or paginated appearance can be achieved\"\"\"\n        remaining_text = self.text\n        while (last_index := len(remaining_text) - 19) > 0:\n            line = LCDLine(remaining_text[:19],\n                           delay=self.scroll_delay,\n                           resets_idle=self.resets_idle)\n            if remaining_text == self.text:\n                line.delay += self.first_line_extra\n            actual_scroll_amount = min(self.scroll_amount, last_index)\n            remaining_text = remaining_text[actual_scroll_amount:]\n            yield line\n        yield LCDLine(remaining_text[:19],\n                      delay=self.scroll_delay + self.last_line_extra)\n\n\nclass Carousel:\n    \"\"\"Manages Screens and spurious messages\n    Ignores the timing, focuses just on what line and screen to show if asked\n    \"\"\"\n\n    def __init__(self, screens: List[Screen]):\n        self.screens = set(screens)\n        self.enabled_screens: Set[Screen] = set()\n        self.active_set: Set[Screen] = set()\n        self.active_screens: List[Screen] = []\n\n        self.current_screen = None\n\n        self.to_rewind = False\n        self.messages: Deque[LCDLine] = deque()\n\n        self.line_generator = self._lines()\n\n    def _lines(self):\n        \"\"\"Iterating over this goes over every enabled screen with the highest\n        priority. More screens on the same priority are supported\"\"\"\n        for self.current_screen in copy(self.active_screens):\n            for line in self.current_screen.lines():\n                if self.to_rewind:\n                    self.current_screen = None\n                    return\n\n                if self.current_screen.to_chime:\n                    self.current_screen.to_chime = False\n                    line.chime_gcode = self.current_screen.chime_gcode\n                line.resets_idle = self.current_screen.resets_idle\n                yield line\n\n    def get_next(self):\n        \"\"\"Handles giving out lines to show. The spurious messages\n         have priority\"\"\"\n        if self.messages:\n            self.set_rewind()\n            return self.messages.popleft()\n        try:\n            return next(self.line_generator)\n        except (StopIteration, TypeError):\n            self._rewind()\n        try:\n            return next(self.line_generator)\n        except (StopIteration, TypeError):\n            return None  # nothing to show\n\n    def _rewind(self):\n        \"\"\"Re-winds to the start. This updates what lines will get output\"\"\"\n        self.to_rewind = False\n        self.line_generator = self._lines()\n\n    def set_rewind(self):\n        \"\"\"Marks the carousel for a re-wind. Next time a line will be\n        requested, the carousel will start from the first Line on the first\n        Screen\"\"\"\n        self.to_rewind = True\n\n    def add_message(self, line: LCDLine):\n        \"\"\"Adds a \"spurious\" message to be displayed.\n        Long ones (over 19 chars) aren't supported\"\"\"\n        self.messages.append(line)\n\n    def verify_tracked(self, screen):\n        \"\"\"If the screen isn't tracked, complains\"\"\"\n        if screen not in self.screens:\n            raise ValueError(\"This screen is not in the carousel\")\n\n    # pylint: disable=too-many-arguments\n    def set_text(self,\n                 screen,\n                 text,\n                 scroll_delay=2.0,\n                 first_line_extra=2.0,\n                 scroll_amount=10,\n                 last_line_extra=1.0):\n        \"\"\"\n        Given text and parameters, it sets up the \"screen\" with your text\n\n        text: Text longer than 19 character gets converted into multiple lines\n        scroll delay: each screen will wait this amount before scrolling again\n        first_line_extra: Extra seconds to wait on the first screen\n        scroll_amount: How many characters to scroll > 0\n        last_line_extra: How much longer to wait on the last screen\n\n        If the text fits on a one line, set the extra delays to 0 and use\n        just the scroll delay. Anything else is undefined\n\n        The splitting functionality is in the Screen itself\n\n        Setting text to an active screen rewinds the carousel.\n        No way to rewind just the current screen as of now\n        \"\"\"\n        self.verify_tracked(screen)\n        screen.changed = True\n        screen.text = text\n        screen.scroll_delay = scroll_delay\n        screen.first_line_extra = first_line_extra\n        screen.scroll_amount = scroll_amount\n        screen.last_line_extra = last_line_extra\n\n        self._react()\n\n    def enable(self, screen: Screen, silent=False):\n        \"\"\"Enables a screen, if it's a one with a greater or equal priority\n        than those currently shown, it will get shown\"\"\"\n        self.verify_tracked(screen)\n        if screen in self.enabled_screens:\n            return  # Has no effect\n\n        screen.to_chime = not silent\n        self.enabled_screens.add(screen)\n        self._react()\n\n    def set_priority(self, screen: Screen, priority):\n        \"\"\"Sets a priority to a screen, if it ends up with a higher or equal\n        one, than the ones shown, and is enabled, it will get shown\"\"\"\n        self.verify_tracked(screen)\n        if priority == screen.priority:\n            return  # has no effect\n\n        screen.priority = priority\n        self._react()\n\n    def disable(self, screen: Screen):\n        \"\"\"Disables a Screen, if currently being shown, gets hidden\"\"\"\n        self.verify_tracked(screen)\n        if screen not in self.enabled_screens:\n            return  # has no effect\n\n        self.enabled_screens.remove(screen)\n        self._react()\n\n    def is_enabled(self, screen: Screen):\n        \"\"\"Is the specified screen enabled?\"\"\"\n        return screen in self.enabled_screens\n\n    def get_set_to_show(self):\n        \"\"\"What screens should get shown according to the current state\"\"\"\n        try:\n            priority_item = max(self.enabled_screens, key=lambda i: i.priority)\n            max_priority = priority_item.priority\n        except ValueError:\n            max_priority = -1 * math.inf\n        return {s for s in self.enabled_screens if s.priority == max_priority}\n\n    def _react(self):\n        \"\"\"Reacts to the changes in Screen settings.\n        Sets active screens according to the Screen settings/state\"\"\"\n        if (new_set := self.get_set_to_show()) != self.active_set or \\\n                any((s.changed for s in self.active_set)):\n            self.set_rewind()\n\n            self.active_set = new_set\n            for screen in self.active_set:\n                screen.changed = False\n            self.active_screens = sorted(new_set, key=lambda i: i.order)\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/heap.py",
    "content": "\"\"\"\nContains implementation of the HeapItem, MinHeap and MaxHeap classes\nI HAVE COPIED THIS FROM THE INTERNET!\nIt turns out that popping the last item in the queue was broken\n\"\"\"\nfrom typing import List\n\n\nclass HeapItem:\n    \"\"\"An item in the heap. Needs to be comparable\"\"\"\n    def __init__(self, value):\n        self.value = value\n        self.heap_value = None\n        self.heap_index = None\n\n    def __gt__(self, other):\n        if isinstance(other, HeapItem):\n            return self.heap_value > other.heap_value\n        raise TypeError(\"HeapItems can be compared only with each other\")\n\n    def __ge__(self, other):\n        if isinstance(other, HeapItem):\n            return self.heap_value >= other.heap_value\n        raise TypeError(\"HeapItems can be compared only with each other\")\n\n    def __lt__(self, other):\n        if isinstance(other, HeapItem):\n            return self.heap_value < other.heap_value\n        raise TypeError(\"HeapItems can be compared only with each other\")\n\n    def __le__(self, other):\n        if isinstance(other, HeapItem):\n            return self.heap_value <= other.heap_value\n        raise TypeError(\"HeapItems can be compared only with each other\")\n\n    def __eq__(self, other):\n        if isinstance(other, HeapItem):\n            return self.heap_value == other.heap_value\n        raise TypeError(\"HeapItems can be compared only with each other\")\n\n    def __hash__(self):\n        return hash(self.heap_value)\n\n\nclass MinHeap:\n    \"\"\"Min heap implementation with element adding and removing\"\"\"\n    def __init__(self) -> None:\n        self.heap: List[HeapItem] = []\n\n    def __len__(self):\n        return len(self.heap)\n\n    def __bool__(self):\n        return bool(self.heap)\n\n    def __getitem__(self, key):\n        return self.heap[key]\n\n    def __setitem__(self, key, value):\n        self.heap[key] = value\n\n    def push(self, item: HeapItem):\n        \"\"\"Ads an element to the heap\"\"\"\n        item.heap_value = item.value\n        self._push(item)\n\n    def _push(self, item):\n        \"\"\"\n        Adds an element to the heap\n\n        In min heaps this is done by adding the element at the end, then\n        switching places with parents larger than it\n        \"\"\"\n        self.heap.append(item)\n\n        initial_index = len(self.heap) - 1\n        self.sift_down(0, initial_index)\n\n    def pop(self, index: int = 0) -> HeapItem:\n        \"\"\"\n        Removes an element from the heap\n\n        In min heaps this is done by swapping the element with the last\n        element in the heap, then removing the last element, (which is now\n        the thing we wanted to delete). After that depending on the value of\n        the element that replaced the deleted one sifting it up or down\n        \"\"\"\n\n        old_item: HeapItem = self.heap[index]\n        old_value = old_item.heap_value\n\n        if old_item.heap_index != index:\n            raise RuntimeError(\"Item index and actual index differ. NOOOOO!\")\n\n        new_item = self.heap.pop()\n\n        # The first one checks if we didn't remove the last item from the\n        # heap, if we did, there is nothing else that needs to be done\n        if index != len(self) and self:\n            new_value = new_item.heap_value\n\n            self.heap[index] = new_item\n            if new_value > old_value:\n                self.sift_up(index)\n            else:\n                self.sift_down(0, index)\n\n        return old_item\n\n    def sift_up(self, pos):\n        \"\"\"\n        Compares an element with its children, if the element is larger,\n        its position gets swapped with the smaller child. Continues until\n        there are no children smaller than the element\n        \"\"\"\n        endpos = len(self.heap)\n        startpos = pos\n        newitem = self.heap[pos]\n        # Bubble up the smaller child until hitting a leaf.\n        childpos = 2 * pos + 1  # leftmost child position\n        while childpos < endpos:\n            # Set childpos to index of smaller child.\n            rightpos = childpos + 1\n            if rightpos < endpos and \\\n                    not self.heap[childpos] < self.heap[rightpos]:\n                childpos = rightpos\n            # Move the smaller child up.\n            self.heap[pos] = self.heap[childpos]\n            self.heap[pos].heap_index = pos\n            pos = childpos\n            childpos = 2 * pos + 1\n        # The leaf at pos is empty now. Put newitem there, and bubble it up\n        # to its final resting place (by sifting its parents down).\n        self.heap[pos] = newitem\n        self.heap[pos].heap_index = pos\n        self.sift_down(startpos, pos)\n\n    def sift_down(self, startpos, pos):\n        \"\"\"\n        The element gets compared with its parent, if it's smaller, they get\n        swapped. Continues until it finds a parent smaller than itself,\n        or the element becomes the root of the heap\n        :param startpos:\n        :param pos:\n        :return:\n        \"\"\"\n        newitem = self.heap[pos]\n        # Follow the path to the root, moving parents down until finding\n        # a place newitem fits.\n        while pos > startpos:\n            parentpos = (pos - 1) >> 1\n            parent = self.heap[parentpos]\n            if newitem < parent:\n                self.heap[pos] = parent\n                parent.heap_index = pos\n                pos = parentpos\n                continue\n            break\n        self.heap[pos] = newitem\n        newitem.heap_index = pos\n\n\nclass MaxHeap(MinHeap):\n    \"\"\"\n    Lazily implemented max heap by using the min heap, just inverting\n    the heap value\n    \"\"\"\n    def push(self, item: HeapItem):\n        item.heap_value = -item.value\n        self._push(item)\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/item_updater.py",
    "content": "\"\"\"Implements classes for monitoring and updating arbitrary values\"\"\"\n\nimport logging\nfrom math import inf\nfrom multiprocessing import Event\nfrom queue import Empty, PriorityQueue, Queue\nfrom threading import RLock, Thread\nfrom time import time\nfrom typing import Any, Callable, Iterable, Optional, Set\n\nfrom blinker import Signal  # type: ignore\n\nfrom ...util import prctl_name\n\nlog = logging.getLogger(__name__)\n\n\nclass SideEffectOnly(Exception):\n    \"\"\"An exception to raise in a gatherer that has nothing to return,\n    but its side effects succeeded in setting a value or\n    have otherwise ensured that the value would be received eventually\"\"\"\n\n\nclass Watchable:\n    \"\"\"Encapsulates the common stuff between watched values and groups\"\"\"\n\n    def __init__(self):\n\n        self.valid = False\n\n        self.became_valid_signal = Signal()\n        self.became_invalid_signal = Signal()\n\n\nclass WatchedItem(Watchable):\n    \"\"\"\n    A value, that can be polled or set.\n    Can be tracked in the info updater\n    \"\"\"\n    # Set to None to disable automatic refreshes on read/validation fails\n    default_on_fail_interval = 5\n\n    # pylint: disable=too-many-arguments\n    def __init__(self,\n                 name,\n                 gather_function: Optional[Callable[[], Any]] = None,\n                 write_function: Optional[Callable[[Any], None]] = None,\n                 validation_function: Optional[Callable[[Any], bool]] = None,\n                 interval=None,\n                 timeout=None,\n                 on_fail_interval=default_on_fail_interval):\n        super().__init__()\n        self.name = name\n        self.value: Any = None\n        self.lock = RLock()\n\n        self.in_groups: Set[\"WatchedGroup\"] = set()\n\n        self.scheduled = False  # Are we scheduled for a value refresh\n        # Imprecise timing intended\n        self.interval = interval  # If set, gets invalidated each interval\n        self.disabled = False  # If True, the interval is overridden with None\n\n        self.on_fail_interval = on_fail_interval  # Refresh reschedule timeout\n        self.timeout = timeout  # How long can we be invalid, before timing out\n\n        # internal timestamps\n        self.invalidate_at = inf\n        self.times_out_at = inf\n\n        # pylint: disable=unused-argument\n        def _default_validation(value):\n            return True\n\n        # pylint: disable=unused-argument\n        def _default_write(value):\n            ...\n\n        if validation_function is None:\n            validation_function = _default_validation\n\n        if write_function is None:\n            write_function = _default_write\n\n        # A function that returns a value, or throws an error\n        # If it returns None, The value is not written and the item gets\n        # re-scheduled\n        self.gather_function: Optional[Callable[[], Any]] = gather_function\n        # If valid, returns Ture, if not, throws an error or returns False\n        self.validation_function: Callable[[Any], bool] = validation_function\n        # Takes care of putting the value in the right places\n        # Shall not throw anything EVER!\n        self.write_function: Callable[[\"WatchedItem\"], None] = write_function\n\n        # -- Signals --\n\n        self.timed_out_signal = Signal()\n        self.error_refreshing_signal = Signal()\n        self.validation_error_signal = Signal()  # kwargs: validation exception\n        self.value_changed_signal = Signal()  # sender is the value\n        # Combined gather error signal\n        self.val_err_timeout_signal = Signal()\n\n    def __repr__(self):\n        return super().__repr__() + \": \" + self.name\n\n    def __lt__(self, other):\n        if not isinstance(other, WatchedItem):\n            return NotImplemented\n        return self.name < other.name\n\n    def __eq__(self, other):\n        if not isinstance(other, WatchedItem):\n            return NotImplemented\n        return self.name == other.name\n\n    def __hash__(self):\n        return hash(self.name)\n\n\nclass WatchedGroup(Watchable):\n    \"\"\"\n    A group of watched items.\n    Aggregates the validity signals from its members\n    \"\"\"\n\n    def __init__(self, items: Iterable[WatchedItem]):\n        super().__init__()\n\n        if not items:\n            raise ValueError(\n                \"Supply at least one item, or group to be watched\")\n\n        self.all_items = list(items)\n        self.valid_items = set()\n        self.invalid_items = set()\n\n        for item in items:\n            # Tracking using these signals,\n            item.in_groups.add(self)\n\n            if item.valid:\n                self.valid_items.add(item)\n            else:\n                self.invalid_items.add(item)\n\n        if not self.invalid_items:\n            self.valid = True\n\n    def __iter__(self):\n        return self.all_items.__iter__()\n\n    def invalid_handler(self, item):\n        \"\"\"\n        A member became invalid. Moves the member to the invalid pile\n        If the group was valid, it's not anymore and that gets signalled\n        \"\"\"\n        self.valid_items.remove(item)\n        self.invalid_items.add(item)\n\n        if self.valid:\n            self.valid = False\n            self.became_invalid_signal.send(self)\n\n    def valid_handler(self, item):\n        \"\"\"\n        A member became valid. Moves the member to the valid pile\n        If all members are valid, sends a signal\n        \"\"\"\n        self.invalid_items.remove(item)\n        self.valid_items.add(item)\n\n        if not self.valid and not self.invalid_items:\n            self.valid = True\n            self.became_valid_signal.send(self)\n\n\nclass ItemUpdater:\n    \"\"\"\n    This governs some defined variables\n\n    Variables can be made to be refreshed manually, or on a timer\n    Variable getters can time out, which sends out a signal\n    Variables can be validated\n    On validation or read error, variable refresh can be re-scheduled\n    automatically on a timer\n    \"\"\"\n\n    def __init__(self, quit_interval=0.2):\n        self.quit_interval = quit_interval\n\n        self.running = True\n\n        self.invalidate_timers = PriorityQueue()\n        self.invalidate_queue_event = Event()\n        self.timeout_timers = PriorityQueue()\n        self.timeout_queue_event = Event()\n        self.refresh_queue = Queue()\n\n        self.refresher_thread = Thread(target=self._refresher,\n                                       name=\"polling\",\n                                       daemon=True)\n        self.invalidator_thread = Thread(target=self._process_invalidations,\n                                         name=\"item_invalidator\",\n                                         daemon=True)\n        self.timeout_thread = Thread(target=self._process_timeouts,\n                                     name=\"polling_timeout\",\n                                     daemon=True)\n\n        self.items = set()\n\n    def start(self):\n        \"\"\"Starts up the governing threads\"\"\"\n        self.refresher_thread.start()\n        self.invalidator_thread.start()\n        self.timeout_thread.start()\n\n    def stop(self):\n        \"\"\"Stops the value tracker\"\"\"\n        self.running = False\n        self.invalidate_queue_event.set()\n        self.timeout_queue_event.set()\n\n    def wait_stopped(self):\n        \"\"\"waits for the value tracker to quit\"\"\"\n        self.invalidator_thread.join()\n        self.timeout_thread.join()\n        self.refresher_thread.join()\n\n    def add_item(self, item: WatchedItem, start_tracking=True):\n        \"\"\"\n        Only invalid items can be added for now\n        :param item: The item to add to watched ones\n        :param start_tracking: Whether to invalidate the item.\n            Without this, the item does not gather its value and has to be\n            invalidated manually\n        \"\"\"\n        if not issubclass(type(item), WatchedItem):\n            raise TypeError(\"Can't track something, that isn't a WatchedItem.\")\n        self.items.add(item)\n        if start_tracking:\n            self.invalidate(item)\n\n    def invalidate_group(self, group: WatchedGroup):\n        \"\"\"\n        Invalidates every item of the supplied WatchedGroup\n        \"\"\"\n        for group_item in group:\n            self.invalidate(group_item)\n\n    def invalidate(self, item: WatchedItem):\n        \"\"\"\n        Invalidates the item, putting it into the queue for validation\n        If the object has a timeout, sets up the timer for it\n\n        Calling repeatedly should not affect anything,\n        the first invalidation matters\n\n        If the item already is invalidated but is not scheduled for a refresh,\n        it gets scheduled\n        \"\"\"\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            if item.disabled:\n                log.debug(\"Will not invalidate item %s because it's disabled.\",\n                          item.name)\n                return\n            log.debug(\"Item %s has been invalidated\", item.name)\n            item.invalidate_at = inf\n            if item.valid:\n                item.valid = False\n                for group in item.in_groups:\n                    group.invalid_handler(item)\n                item.became_invalid_signal.send(item)\n\n            if not item.scheduled:\n                self._enqueue_refresh(item)\n\n    def disable(self, item: WatchedItem):\n        \"\"\"Disables the item polling without changing its interval\"\"\"\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            if item.disabled:\n                return\n            item.disabled = True\n            self.cancel_scheduled_invalidation(item)\n\n    def enable(self, item: WatchedItem):\n        \"\"\"Enables the item polling without changing its interval\"\"\"\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            if not item.disabled:\n                return\n            item.disabled = False\n            self.invalidate(item)\n\n    def set_value(self, item: WatchedItem, value):\n        \"\"\"\n        Validates the value and writes it\n\n        Forcefully re-schedules invalidation. This can be used to enable\n        polling, when auto reporting stops for example\n        \"\"\"\n\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            try:\n                if not item.validation_function(value):\n                    raise ValueError(f\"Invalid value for {item.name}: {value}\")\n            # pylint: disable=broad-except\n            except Exception:\n                log.debug(\"Validation of item %s has failed\", item.name)\n                item.validation_error_signal.send(item)\n                item.val_err_timeout_signal.send(item)\n\n                # If the item is valid, do not schedule a gather, as this\n                # probably was a setter from the outside with a bad value\n                if not item.valid:\n                    self._gather_error_reschedule(item)\n            else:\n                log.debug(\"Value of item %s has been determined to be %s\",\n                          item.name, value)\n                self._set_value(item, value)\n\n    def schedule_invalidation(self, item: WatchedItem, interval=None,\n                              reschedule=False):\n        \"\"\"\n        Schedules an item invalidation at a certain time\n        Will not shift already scheduled invalidation unless forced to\n\n        If an already invalid item is scheduled for example after a\n        gather/validation error, it is just added to the refresh queue without\n        emitting any additional signals\n        :param item: The item to schedule invalidation for.\n        :param interval: How long in the future should we invalidate?\n                         If left empty, the default is used, if that's None\n                         an error will be raised\n        :param reschedule: If an invalidation is already scheduled,\n                           it won't get re-scheduled unless this is True\n\n        \"\"\"\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            if item.disabled:\n                log.debug(\"Will not schedule item %s because it is disabled.\",\n                          item.name)\n                return\n            if item.invalidate_at != inf and not reschedule:\n                log.debug(\n                    \"Will not schedule an invalidation for item %s because \"\n                    \"another is already scheduled\", item.name)\n                return\n\n            if interval is None:\n                interval = item.interval\n\n            if interval is None:\n                raise AttributeError(f\"No interval specified for item \"\n                                     f\"{item.name} has no default and none\"\n                                     f\" has been provided!\")\n\n            log.debug(\n                \"Scheduling invalidation of item %s for %ss in \"\n                \"the future\", item.name, interval)\n            item.invalidate_at = time() + interval\n            self.invalidate_timers.put((item.invalidate_at, item))\n            self.invalidate_queue_event.set()\n\n    def cancel_scheduled_invalidation(self, item: WatchedItem):\n        \"\"\"\n        Cancels the scheduled invalidation. The timer itself cannot\n        be cancelled, but the invalidate_at value has to match before\n        anything is executed. Changing it to infinity will accomplish\n        that nicely\n        \"\"\"\n        self._validate_is_tracked(item)\n\n        with item.lock:\n            if item.invalidate_at == inf:\n                return\n            log.debug(\"Cancelling scheduled invalidation of item %s \",\n                      item.name)\n            item.invalidate_at = inf\n\n    # -- Private --\n\n    @staticmethod\n    def _time_out(item: WatchedItem):\n        \"\"\"\n        Times out the item, notifying everyone of the fail\n        :return:\n        \"\"\"\n\n        with item.lock:\n            log.warning(\"Timed out when getting item %s\", item.name)\n            item.times_out_at = inf\n            item.timed_out_signal.send(item)\n            item.val_err_timeout_signal.send(item)\n\n    def _validate_is_tracked(self, item: WatchedItem):\n        if item not in self.items:\n            raise ValueError(\n                f\"Item {item.name} is not tracked by this instance.\")\n\n    def _gather(self, item: WatchedItem):\n        \"\"\"\n        Refreshes the item value, if the item has a refresh interval,\n        sets up the timed invalidation\n\n        If the value gathering throws an error, it re-schedules its refresh\n        and notifies of a fail\n        \"\"\"\n        if item.valid:\n            return\n\n        # Items without gather functions have no point in spinning,\n        # something else needs to take care of them\n        if item.gather_function is None:\n            return\n\n        log.debug(\"Gathering new value for item %s\", item.name)\n        try:\n            value = item.gather_function()\n        # pylint: disable=broad-except\n        except SideEffectOnly:\n            # Special case for gatherers with just side effects\n            # Useful for when the value is autoreported and gather needs\n            # to only turn the reporting on\n\n            # If the gatherer sets its own items value, then let's not\n            # re-schedule anything\n            if not item.valid:\n                # Counting on set_item cancelling the re-schedule\n                self._gather_error_reschedule(item)\n\n        except Exception:\n            with item.lock:\n                log.exception(\"Gather of %s has failed\", item.name)\n                item.error_refreshing_signal.send(item)\n                item.val_err_timeout_signal.send(item)\n                self._gather_error_reschedule(item)\n        else:\n            with item.lock:\n                self.set_value(item, value)\n\n    def _gather_error_reschedule(self, item):\n        \"\"\"\n        Reschedules the value refresh on gather or validation errors\n        Reschedules only if the reschedule interval is set (default = 5s)\n        \"\"\"\n        with item.lock:\n            if item.on_fail_interval is not None:\n                log.debug(\n                    \"Rescheduling gather of item %s for \"\n                    \"%ss in the future\", item.name, item.on_fail_interval)\n                self.schedule_invalidation(item, item.on_fail_interval)\n\n    def _set_value(self, item, value):\n        \"\"\"\n        Internal, only sets the value without validation\n        Should be pre-validate before this gets called\n        \"\"\"\n        with item.lock:\n            changed = value != item.value\n            if changed:\n                log.debug(\"Item %s got a new value! old: %s new: %s\",\n                          item.name, item.value, value)\n            item.value = value\n            item.write_function(value)\n            was_invalid = not item.valid\n            item.valid = True\n            item.times_out_at = inf\n            if item.interval is not None:\n                self.schedule_invalidation(item, reschedule=True)\n            if was_invalid:\n                for group in item.in_groups:\n                    group.valid_handler(item)\n                item.became_valid_signal.send(item)\n            if changed:\n                item.value_changed_signal.send(value)\n\n    def _enqueue_refresh(self, item):\n        \"\"\"\n        Forcefully enqueues the item for refresh\n        Does not re-schedule the time out. If the item failed to gather for\n        example, it gets re-scheduled. But has to time out in the set time\n        since it is invalid for more than X seconds\n        :param item:\n        :return:\n        \"\"\"\n        with item.lock:\n            if item.timeout is not None and item.times_out_at == inf:\n                item.times_out_at = time() + item.timeout\n                self.timeout_timers.put((item.times_out_at, item))\n\n            item.scheduled = True\n            self.refresh_queue.put(item)\n\n    def _refresher(self):\n        \"\"\"\n        Processes all values queued up for refreshing\n        \"\"\"\n        prctl_name()\n        while self.running:\n            try:\n                item = self.refresh_queue.get(timeout=self.quit_interval)\n            except Empty:\n                pass\n            else:\n                with item.lock:\n                    item.scheduled = False\n                self._gather(item)\n\n    def _process_invalidations(self):\n        \"\"\"\n        Processes the invalidation queue.\n        If a timer is checked and does not match with the set timer on an\n        item, it is discarded, so only valid timers call their callbacks\n        :return:\n        \"\"\"\n        prctl_name()\n        while self.running:\n            try:\n                invalidate_at, item = self.invalidate_timers.get(\n                    timeout=self.quit_interval)\n            except Empty:\n                pass\n            else:\n                # Check if the timer is valid\n                if invalidate_at != item.invalidate_at:\n                    continue\n\n                current_time = time()\n                if invalidate_at > current_time:\n                    self.invalidate_timers.put((invalidate_at, item))\n                    self.invalidate_queue_event.wait(invalidate_at -\n                                                     current_time)\n                    self.invalidate_queue_event.clear()\n                else:\n                    self.invalidate(item)\n\n    def _process_timeouts(self):\n        \"\"\"\n        Same as invalidators, except its timeouts\n        \"\"\"\n        prctl_name()\n        while self.running:\n            try:\n                times_out_at, item = self.timeout_timers.get(\n                    timeout=self.quit_interval)\n            except Empty:\n                pass\n            else:\n                # Check if the timer is valid\n                if times_out_at != item.times_out_at:\n                    continue\n\n                current_time = time()\n                if times_out_at > current_time:\n                    self.timeout_timers.put((times_out_at, item))\n                    self.timeout_queue_event.wait(times_out_at - current_time)\n                    self.timeout_queue_event.clear()\n                else:\n                    self._time_out(item)\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/mc_singleton.py",
    "content": "\"\"\"Contains implementation of the MCSingleton class\"\"\"\n\n\nclass MCSingleton(type):\n    \"\"\"\n    Classes that use this metaclass are singletons\n    \"\"\"\n    def __init__(cls, name, bases, dic):\n        cls.__instance = None\n        cls.get_instance = lambda: cls.__instance\n        super().__init__(name, bases, dic)\n\n    def __call__(cls, *args, **kwargs):\n        if cls.__instance is not None:\n            raise RuntimeError(\"There can be only one singleton in existence\")\n\n        instance = cls.__new__(cls)\n        instance.__init__(*args, **kwargs)\n        cls.__instance = instance\n        return instance\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/model_classes.py",
    "content": "\"\"\"\nContains models that were originally intended for sending to the connect.\nPydantic makes a great tool for cleanly serializing simple python objects,\nwhile enforcing their type\n\"\"\"\nfrom enum import Enum\nfrom typing import Dict, Optional\n\nfrom pydantic import BaseModel\n\n\nclass IndividualSlot(BaseModel):\n    \"\"\"Support the slot number specific telemetry structure\"\"\"\n    material: Optional[str] = None\n    temp: Optional[float] = None\n    fan_hotend: Optional[int] = None\n    fan_print: Optional[int] = None\n\n\nclass Slot(BaseModel):\n    \"\"\"Support the telemetry item described here:\n    https://connect.prusa3d.com/docs/mmu (Internal doc)\"\"\"\n\n    active: Optional[int] = None\n    state: Optional[int] = None\n    progress: Optional[int] = None\n    command: Optional[str] = None\n    slots: Optional[Dict[str, IndividualSlot]] = None\n\n    def dict(self, **kwargs) -> Dict:\n        \"\"\"Override the dict method to respect the Connect telemetry API\"\"\"\n        data = super().dict(**kwargs)\n        if \"slots\" in data and data[\"slots\"] is not None:\n            slots = data.pop(\"slots\")\n            data.update(slots)\n        return data\n\n\nclass Telemetry(BaseModel):\n    \"\"\"The Telemetry model\"\"\"\n    # time_remaining is deprecated, kept for compatibility\n\n    temp_nozzle: Optional[float] = None\n    temp_bed: Optional[float] = None\n    target_nozzle: Optional[float] = None\n    target_bed: Optional[float] = None\n    axis_x: Optional[float] = None\n    axis_y: Optional[float] = None\n    axis_z: Optional[float] = None\n    fan_extruder: Optional[int] = None\n    fan_hotend: Optional[int] = None\n    fan_print: Optional[int] = None\n    target_fan_extruder: Optional[int] = None\n    target_fan_hotend: Optional[int] = None\n    target_fan_print: Optional[int] = None\n    progress: Optional[int] = None\n    filament: Optional[str] = None\n    flow: Optional[int] = None\n    speed: Optional[int] = None\n    time_printing: Optional[int] = None\n    time_transferring: Optional[int] = None\n    time_remaining: Optional[int] = None\n    odometer_x: Optional[int] = None\n    odometer_y: Optional[int] = None\n    odometer_z: Optional[int] = None\n    odometer_e: Optional[int] = None\n    material: Optional[str] = None\n    total_filament: Optional[int] = None\n    total_print_time: Optional[int] = None\n    filament_change_in: Optional[int] = None\n    inaccurate_estimates: Optional[bool] = None\n    slot: Optional[Slot] = None\n\n    def dict(self, **kwargs) -> Dict:\n        data = super().dict(**kwargs)\n        if self.slot is not None:\n            data['slot'] = self.slot.dict(**kwargs)\n        return data\n\n\nclass NetworkInfo(BaseModel):\n    \"\"\"The Network Info model\"\"\"\n\n    lan_ipv4: Optional[str] = None  # not implemented yet\n    lan_ipv6: Optional[str] = None  # not implemented yet\n    lan_mac: Optional[str] = None  # not implemented yet\n    wifi_ipv4: Optional[str] = None\n    wifi_ipv6: Optional[str] = None  # not implemented yet\n    wifi_mac: Optional[str] = None\n    wifi_ssid: Optional[str] = None  # not implemented yet\n    hostname: Optional[str] = None\n    username: Optional[str] = None\n    digest: Optional[str] = None\n\n\nclass FileType(Enum):\n    \"\"\"File type enum\"\"\"\n    FILE = \"FILE\"\n    FOLDER = \"FOLDER\"\n    STORAGE = \"STORAGE\"\n\n\nclass JobState(Enum):\n    \"\"\"Job state enum\"\"\"\n    IDLE = \"IDLE\"\n    IN_PROGRESS = \"IN_PROGRESS\"\n    ENDING = \"ENDING\"\n\n\nclass SDState(Enum):\n    \"\"\"SD State enum\"\"\"\n    PRESENT = \"PRESENT\"\n    INITIALISING = \"INITIALISING\"\n    UNSURE = \"UNSURE\"\n    ABSENT = \"ABSENT\"\n\n\nclass PrintState(Enum):\n    \"\"\"States which the printer can report on its own\"\"\"\n    SD_PRINTING = \"SD_PRINTING\"\n    SD_PAUSED = \"SD_PAUSED\"\n    SERIAL_PAUSED = \"SERIAL_PAUSED\"\n    NOT_SD_PRINTING = \"NOT_SD_PRINTING\"\n\n\nclass PrintMode(Enum):\n    \"\"\"The \"Mode\" from the printer LCD settings\"\"\"\n    SILENT = \"SILENT\"\n    NORMAL = \"NORMAL\"\n    AUTO = \"AUTO\"\n\n\nclass EEPROMParams(Enum):\n    \"\"\"List of EEPROM addresses read by PrusaLink\"\"\"\n    JOB_ID = 0x0D05, 4\n    FLASH_AIR = 0x0FBB, 1\n    PRINT_MODE = 0x0FFF, 1\n    SHEET_SETTINGS = 0x0D49, 88\n    ACTIVE_SHEET = 0x0DA1, 1\n    TOTAL_FILAMENT = 0x0FF1, 4\n    TOTAL_PRINT_TIME = 0x0FED, 4\n    EEPROM_FILE_POSITION = 0x0F91, 4\n\n\nclass PPData(BaseModel):\n    \"\"\"Not things like length or diameter,\n    just path and the command number -> gcode command number\"\"\"\n    file_path: str\n    connect_path: str\n    message_number: int  # N number on the printer\n    gcode_number: int  # From file printer\n    using_rip_port: bool = False\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/module_data_classes.py",
    "content": "\"\"\"\nDecided that keeping module data externally will aid with gathering them for\nthe api, definitions of which is what this module contains\n\"\"\"\n\nfrom typing import Any, Deque, Dict, List, Optional, Set\n\nfrom prusa.connect.printer.const import State\nfrom pydantic import BaseModel\n\nfrom .model_classes import JobState, SDState\n\n# pylint: disable=too-few-public-methods\n\n\nclass Port(BaseModel):\n    \"\"\"Data known about a port\"\"\"\n    path: str\n    is_rpi_port: bool = False\n    checked: bool = False  # False if it has not been finished checking\n    usable: bool = False  # We can probably use this port for communication\n    selected: bool = False  # PrusaLink selected to use this port\n    description: str = \"Unknown\"  # A nice human-readable status\n    baudrate: int = 115200\n    timeout: int = 2\n    sn: Optional[str] = None  # Save the USB descriptor SN if valid\n\n    def __str__(self):\n        return (f\"Port: {self.path}, \"\n                f\"Checked: {self.checked}, \"\n                f\"Usable: {self.usable}, \"\n                f\"Selected: {self.selected}, \"\n                f\"RPi port: {self.is_rpi_port}, \"\n                f\"Description: {self.description}\")\n\n\nclass SerialAdapterData(BaseModel):\n    \"\"\"Data of the SerialAdapter class\"\"\"\n    ports: List[Port] = []\n    using_port: Optional[Port]\n    reset_disabling: bool = True\n    resets_enabled: Optional[bool] = None\n\n\nclass FilePrinterData(BaseModel):\n    \"\"\"Data of the FilePrinter class\"\"\"\n    file_path: str\n    pp_file_path: str\n    printing: bool\n    recovering: bool\n    paused: bool\n    was_stopped: bool\n    power_panic: bool\n    recovery_ready: bool\n\n    # In reality Deque[Instruction] but that cannot be validated by pydantic\n    enqueued: Deque[Any]\n    gcode_number: int\n\n\nclass StateManagerData(BaseModel):\n    \"\"\"Data of the StateManager class\"\"\"\n    # The ACTUAL states considered when reporting\n    base_state: State\n    printing_state: Optional[State]\n    override_state: Optional[State]\n\n    # Reported state history\n    last_state: State\n    current_state: State\n    state_history: Deque[State]\n    awaiting_error_reason: bool\n\n\nclass JobData(BaseModel):\n    \"\"\"Data of the Job class\"\"\"\n    job_id: Optional[int]\n    job_id_offset: int\n    already_sent: Optional[bool]\n    job_start_cmd_id: Optional[int]\n    selected_file_path: Optional[str]\n    selected_file_m_timestamp: Optional[int]\n    selected_file_size: Optional[str]\n    printing_file_byte: Optional[int]\n    path_incomplete: Optional[bool]\n    from_sd: Optional[bool]\n    inbuilt_reporting: Optional[bool]\n\n    last_job_path: Optional[str]\n\n    job_state: JobState\n\n    def get_job_id_for_api(self):\n        \"\"\"\n        The API does not send None values. This function returns None when\n        no job is running, otherwise it gives the job_id\n        \"\"\"\n        if self.job_state == JobState.IDLE:\n            return None\n        return self.job_id\n\n\nclass IPUpdaterData(BaseModel):\n    \"\"\"Data of the IpUpdater class\"\"\"\n    local_ip: Optional[str]\n    local_ip6: Optional[str]\n    mac: Optional[str]\n    is_wireless: bool\n    update_ip_on: float\n    ssid: Optional[str]\n    hostname: Optional[str]\n    username: Optional[str]\n    digest: Optional[str]\n\n\nclass SDCardData(BaseModel):\n    \"\"\"Data of the SDCard class\"\"\"\n    expecting_insertion: bool\n    invalidated: bool\n    is_flash_air: bool\n    last_updated: float\n    last_checked_flash_air: float\n    sd_state: SDState\n    files: Any  # We cannot type-check SDFile, only basic ones\n    sfn_to_lfn_paths: Dict[str, str]\n    lfn_to_sfn_paths: Dict[str, str]\n    mixed_to_lfn_paths: Dict[str, str]\n\n\nclass StorageData(BaseModel):\n    \"\"\"Data of the Storage class\"\"\"\n    blacklisted_paths: List[str]\n    blacklisted_names: List[str]\n    configured_storage: Set[str]\n    attached_set: Set[str]\n\n\nclass MMUObserverData(BaseModel):\n    \"\"\"Data of the MMUObserver\"\"\"\n    current_error_code: Optional[str]\n\n\nclass PrintStatsData(BaseModel):\n    \"\"\"Data of the PrintStats class\"\"\"\n    print_time: float\n    segment_start: float\n    has_inbuilt_stats: bool\n    total_gcode_count: int  # is not computed for files containg reporting\n    #                         to speed stuff up\n    start_gcode_number: int = 0\n\n\nclass Sheet(BaseModel):\n    \"\"\"Data available for sheets in the printer EEPROM\"\"\"\n    name: str = \"\"\n    z_offset: float = 0.0\n    # temps at the time of calibration\n    bed_temp: int = 0\n    pinda_temp: int = 0\n"
  },
  {
    "path": "prusa/link/printer_adapter/structures/regular_expressions.py",
    "content": "\"\"\"Contains every regular expression used in the app as a constant\"\"\"\nimport re\n\nfrom ...const import MMU_PROGRESS_MAP\n\nOPEN_RESULT_REGEX = re.compile(\n    r\"^((?P<ok>File opened): (?P<sdn_lfn>.*) Size: (?P<size>\\d+))\"\n    r\"|(?P<nok>open failed).*\")\n\nPRINTER_TYPE_REGEX = re.compile(r\"^(?P<code>\\d{3,5})$\")\nFW_REGEX = re.compile(r\"^(?P<version>\\d+\\.\\d+\\.\\d+-.*)$\")\nSN_REGEX = re.compile(r\"^(?P<sn>^CZPX\\d{4}X\\d{3}X.\\d{5})|\"\n                      r\"(?P<invalid>SN invalid)|(?P<gibberish>.*)$\")\nVALID_SN_REGEX = re.compile(r\"^(?P<sn>^CZPX\\d{4}X\\d{3}X.\\d{5})$\")\nNEW_SN_REGEX = re.compile(\n    r\"^(?P<sn>^SN(?!20)[2-9][0-9](004|017|022|023|024|025)[K,C]\\d{6})$\")\nNOZZLE_REGEX = re.compile(r\"^(?P<size>\\d\\.\\d+)$\")\nPERCENT_REGEX = re.compile(r\"^(?P<percent>\\d{0,3})%$\")\n\nVALID_USERNAME_REGEX = re.compile(r\"^[!#-9;-~][ -!#-9;-~]{1,254}[!#-9;-~]$\")\n\n# Three options of the password format\n# >= 8 chars, one lowercase letter, one uppercase letter, one number\nPASS_OPT1 = r\"((?=.*[a-z])(?=.*[A-Z])(?=.*\\d))[\\w]{8,}$\"\n# >= 8 chars, one non-alphanumeric character\nPASS_OPT2 = r\"((?=.*\\W)(?=.*[\\w])[\\w\\W]{8,})$\"\n# >= 15 chars\nPASS_OPT3 = r\"[\\w\\W]{15,}$\"\n\nVALID_PASSWORD_REGEX = re.compile(f\"^({PASS_OPT1}|{PASS_OPT2}|{PASS_OPT3})\")\n\nLFN_CAPTURE = re.compile(\n    r\"^(?P<begin>Begin file list)|\"\n    r\"(?P<dir_enter>DIR_ENTER: (?P<sdn>/[^ ]*/) \\\"(?P<ldn>[^\\\"]*)\\\")|\"\n    r\"(?P<file>(?P<sfn>.*\\.(?P<extension>GCO|G)) \"\n    r\"((0x(?P<m_time>[0-9a-fA-F]+) ?)|(?P<size>\\d+ ?)|\"\n    r\"(\\\"(?P<lfn>[^\\\"]*)\\\") ?)*)|\"\n    r\"(?P<dir_exit>DIR_EXIT)|\"\n    r\"(?P<end>End file list)$\")\n\nSD_PRESENT_REGEX = re.compile(r\"^(?P<ok>echo:SD card ok)|\"\n                              r\"(?P<fail>(echo:SD init fail)|\"\n                              r\"(Error:volume\\.init failed)|\"\n                              r\"(Error:openRoot failed))$\")\nSD_EJECTED_REGEX = re.compile(r\"^(echo:SD card released)$\")\n\nANY_REGEX = re.compile(r\".*\")\nCONFIRMATION_REGEX = re.compile(\n    r\"^(ok.*)|(Done saving file\\.)$\")  # highest priority\n\n# ---CAUTION---\n# These are handled by special_commands component\n# If you use them without, you'll get false positive print starts\n# when the special menu is used\nFILE_OPEN_REGEX = re.compile(r\"^echo:enqueing \\\"M23 (?P<sfn>[^\\\"]+)\\\"$\")\nSTART_PRINT_REGEX = re.compile(r\"^echo:enqueing \\\"M24\\\"$\")\nPRINT_DONE_REGEX = re.compile(r\"^Done printing file$\")\n# ----------------------------------------\n\nREJECTION_REGEX = re.compile(\n    r\"^(?P<unknown>(echo:Unknown command: (\\\"[^\\\"]*\\\"))|\"\n    r\"(Unknown \\S code: .*))|\"\n    r\"(?P<cold>echo: cold extrusion prevented)$\")\n\nBUSY_REGEX = re.compile(\"^echo:busy: processing$\")\nATTENTION_REGEX = re.compile(\"^echo:busy: paused for user$\")\nPAUSE_PRINT_REGEX = re.compile(r\"^// ?action:pause$\")\nPAUSED_REGEX = re.compile(r\"^// ?action:paused$\")\nRESUME_PRINT_REGEX = re.compile(\"^// ?action:resume$\")\nRESUMED_REGEX = re.compile(\"^// ?action:resumed$\")\nCANCEL_REGEX = re.compile(\"^// ?action:cancel$\")\nREADY_REGEX = re.compile(\"^// ?action:ready$\")\nNOT_READY_REGEX = re.compile(\"^// ?action:not_ready$\")\nREPRINT_REGEX = re.compile(\"^// ?action:start$\")\n# This girthy regexp tries to capture all error messages requiring printer\n# reset using M999 or manual button, with connect, only manual reset shall\n# be accepted\n\nERROR_REGEX = re.compile(\n    r\"(Error:(\"\n    r\"(?P<kill>Printer halted\\. kill\\(\\) called!)|\"\n    # There's another one ending in Supervision required\n    r\"(?P<stop>Printer stopped due to errors\\. Fix.*)))\")\n\nERROR_REASON_REGEX = re.compile(\n    # flake8: noqa\n    r\"(Error:(\"\n    r\"(?P<temp>(0: )?Heaters switched off\\. \"\n    r\"M((?P<mintemp>IN)|(?P<maxtemp>AX))TEMP (?P<bed>BED )?triggered!)|\"\n    r\"(?P<runaway>( ((?P<hotend_runaway>HOTEND)|\"\n    r\"(?P<heatbed_runaway>HEATBED)))? THERMAL RUNAWAY( \\( ?PREHEAT \"\n    r\"((?P<preheat_hotend>HOTEND)|(?P<preheat_heatbed>HEATBED))\\))?)))\")\n\nATTENTION_REASON_REGEX = re.compile(\n    r\"(?P<mbl_too_high>Bed leveling failed. Sensor triggered too high)|\"\n    r\"(?P<mbl_didnt_trigger>Bed leveling failed\\. Sensor didn't trigger\\. \"\n    r\"Debris on nozzle\\? Waiting for reset\\.)|\"\n    r\"(?P<tm_error>TM: error triggered!)\")\n\nTEMPERATURE_REGEX = re.compile(\n    r\"^T:(?P<ntemp>-?\\d+\\.\\d+) /(?P<set_ntemp>-?\\d+\\.\\d+) \"\n    r\"B:(?P<btemp>-?\\d+\\.\\d+) /(?P<set_btemp>-?\\d+\\.\\d+) \"\n    r\"T0:(-?\\d+\\.\\d+) /(-?\\d+\\.\\d+) @:(?P<tpwm>-?\\d+) B@:(?P<bpwm>-?\\d+) \"\n    r\"P:(?P<ptemp>-?\\d+\\.\\d+)( A:(?P<atemp>-?\\d+\\.\\d+))?$\")\nPOSITION_REGEX = re.compile(\n    r\"^X:(?P<x>-?\\d+\\.\\d+) Y:(?P<y>-?\\d+\\.\\d+) Z:(?P<z>-?\\d+\\.\\d+) \"\n    r\"E:(?P<e>-?\\d+\\.\\d+) Count X: (?P<count_x>-?\\d+\\.\\d+) \"\n    r\"Y:(?P<count_y>-?\\d+\\.\\d+) Z:(?P<count_z>-?\\d+\\.\\d+) \"\n    r\"E:(?P<count_e>-?\\d+\\.\\d+)$\")\nFAN_REGEX = re.compile(\n    r\"E0:(?P<hotend_rpm>\\d+) RPM PRN1:(?P<print_rpm>\\d+) RPM \"\n    r\"E0@:(?P<hotend_power>\\d+) PRN1@:(?P<print_power>\\d+)\")\n# This one takes some explaining\n# I cannot assign multiple regular expressions to a single instruction\n# The `M27 P` has more lines, the first one containing a status report or\n# a file path. The optional second line contains info about\n# which byte is being printed and the last one contains the print timer\n# Expressions below shall be in the order they appear in the output\nM27_OUTPUT_REGEX = re.compile(\n    r\"^(?P<sdn_lfn>/.*\\..*)|(?P<no_print>Not SD printing)|\"\n    r\"(?P<serial_paused>Print saved)|(?P<sd_paused>SD print paused)|\"\n    r\"(?P<byte_pos>SD printing byte (?P<current>\\d+)/(?P<sum>\\d+))|\"\n    r\"(?P<printing_time>(?P<hours>\\d+):(?P<minutes>\\d{2}))$\")\nPRINT_INFO_REGEX = re.compile(\n    r\"^(?P<mode>(SILENT)|(NORMAL)) MODE: \"\n    r\"Percent done: (?P<progress>-?\\d+); \"\n    r\"[pP]rint time remaining in mins: (?P<remaining>-?\\d+); \"\n    r\"Change in mins: (?P<change_in>-?\\d+)\")\nHEATING_REGEX = re.compile(\n    r\"^T:(?P<ntemp>\\d+\\.\\d+) E:\\d+ B:(?P<btemp>\\d+\\.\\d+)$\")\nHEATING_HOTEND_REGEX = re.compile(\n    r\"^T:(?P<ntemp>\\d+\\.\\d+) E:([?]|\\d+) W:([?]|\\d+)$\")\n\nRESEND_REGEX = re.compile(r\"^Resend: ?(?P<cmd_number>\\d+)$\")\nPRINTER_BOOT_REGEX = re.compile(r\"^start$\")\nPOWER_PANIC_REGEX = re.compile(r\"^INT4$\")\nLCD_UPDATE_REGEX = re.compile(r\"^LCD status changed$\")\nM110_REGEX = re.compile(r\"^(N\\d+)? *M110 ?N(?P<cmd_number>-?\\d*)$\")\nFAN_ERROR_REGEX = re.compile(\n    r\"^(?P<fan_name>Extruder|Hotend|Print) fan speed is lower than expected$\")\nD3_OUTPUT_REGEX = re.compile(\n    r\"^(?P<address>\\w{2,}) {2}(?P<data>([0-9a-fA-F]{2} ?)+)$\")\nMBL_REGEX = re.compile(r\"^(?P<no_mbl>Mesh bed leveling not active.)|\"\n                       r\"(Num X,Y: (?P<num_x>\\d+),(?P<num_y>\\d+))|\"\n                       r\"(?P<mbl_row>([ ]*-?\\d+\\.\\d+)+)$\")\nMBL_TRIGGER_REGEX = re.compile(r\"^(tmc\\d+_home_enter\\(axes_mask=0x..\\))|\"\n                               r\"(echo:enqueing \\\"G80\\\")\")\nTM_ERROR_LOG_REGEX = re.compile(r\"TM: error \\|(?P<deviation>-?\\d+\\.?\\d*)\\|\"\n                                r\"[<>](?P<threshold>-?\\d+\\.?\\d*)\")\nTM_ERROR_CLEARED = re.compile(r\"^TM: error cleared$\")\n\nURLS_FOR_WIZARD = re.compile(r\"/(\\d{1,3})?/?\")\n\nTM_CAL_START_REGEX = re.compile(r\"^TM: calibration start$\")\nTM_CAL_END_REGEX = re.compile(r\"^(TM: calibr\\. failed!)|\"\n                              r\"(Thermal Model settings:)$\")\n\nMMU_MAJOR_REGEX = re.compile(\n    r\"^echo:MMU[23]:<R0 A(?P<number>[0-9a-fA-F]+)\\*[0-9a-f]{1,2}\\.$\")\nMMU_MINOR_REGEX = re.compile(\n    r\"^echo:MMU[23]:<R1 A(?P<number>[0-9a-fA-F]+)\\*[0-9a-f]{1,2}\\.$\")\nMMU_REVISION_REGEX = re.compile(\n    r\"^echo:MMU[23]:<R2 A(?P<number>[0-9a-fA-F]+)\\*[0-9a-f]{1,2}\\.$\")\nMMU_BUILD_REGEX = re.compile(\n    r\"^echo:MMU[23]:<R3 A(?P<number>[0-9a-fA-F]+)\\*[0-9a-f]{1,2}\\.$\")\nMMU_SLOT_REGEX = re.compile(\n    r\"^echo:MMU2:MMU2tool=(?P<slot>\\d{1,2})$\")\n# This can report an error or a command in progress,\n# we don't know before parsing\nMMU_Q0_RESPONSE_REGEX = re.compile(\n    r\"^echo:MMU[23]:<(?P<command>[A-Z][0-9a-fA-F]+) \"\n    r\"(?P<progress>[EFP]([0-9a-fA-F]{0,4}))\\*[0-9a-f]{1,2}\\.$\")\nMMU_Q0_REGEX = re.compile(r\"^echo:MMU[23]:>Q0\\*[0-9a-f]{1,2}\\.$\")\n\nMMU_PROGRESS_REGEX = re.compile(\n    r\"echo:MMU2:(?P<message>\"\n    + r\"|\".join(map(re.escape, MMU_PROGRESS_MAP.keys()))\n    + r\")\"\n)\n\nRESET_ACTIVATED_REGEX = re.compile(r\"^Reset mode activated$\")\nRESET_DEACTIVATED_REGEX = re.compile(r\"^Reset mode deactivated$\")\nPP_RECOVER_REGEX = re.compile(r\"^// ?action:uvlo_recovery_ready$\")\nPP_AUTO_RECOVER_REGEX = re.compile(r\"^// ?action:uvlo_auto_recovery_ready$\")\n"
  },
  {
    "path": "prusa/link/printer_adapter/telemetry_passer.py",
    "content": "\"\"\"\nThe frequency at which we send telemetry is being determined by quite a\nlot of factore. This module takes care of monitoring, how often to\nsend telemetry and what actual telemetry to send\n\"\"\"\n\nimport logging\nfrom copy import deepcopy\nfrom enum import Enum\nfrom threading import Event, RLock, Thread\nfrom time import time\nfrom typing import Any\n\nfrom prusa.connect.printer import Printer\nfrom prusa.connect.printer.const import State\nfrom pydantic import BaseModel\nfrom pydantic.utils import deep_update\n\nfrom ..config import Settings\nfrom ..const import (\n    JITTER_THRESHOLD,\n    MMU_SLOTS,\n    PRINTING_STATES,\n    TELEMETRY_IDLE_INTERVAL,\n    TELEMETRY_PRINTING_INTERVAL,\n    TELEMETRY_REFRESH_INTERVAL,\n    TELEMETRY_SLEEP_AFTER,\n    TELEMETRY_SLEEPING_INTERVAL,\n)\nfrom ..util import loop_until, walk_dict\nfrom .model import Model\nfrom .structures.mc_singleton import MCSingleton\nfrom .structures.model_classes import Telemetry\n\nlog = logging.getLogger(__name__)\n\n# beyond this many things waiting to get sent by the SDK,\n# we'll stop sending telemetry\nQUEUE_LENGTH_LIMIT = 4\n\n\nclass Modifier(Enum):\n    \"\"\"The modifiers for telemetry\"\"\"\n    FILTER_IDLE = \"FILTER_IDLE\"  # Filtered when idle\n    FILTER_PRINTING = \"FILTER_PRINTING\"  # Filtered when printing\n    FILTER_MMU_OFF = \"FILTER_MMU_OFF\"  # Filtered when MMU is disconnected\n    JITTER_TEMP = \"JITTER_TEMP\"  # Temperature jitter filtr preset\n    ACTIVATE_IDLE = \"ACTIVATE_IDLE\"  # Wakes up fast telemetry when idle\n    ACTIVATE_PRINTING = \"ACTIVATE_PRINTING\"  # Same but when printing\n\n\n# Important - all filter paths are in the dict format\n# model is different in structure, the paths are mapped using a mapping below\nMODIFIERS: dict[tuple[str, ...], set[Modifier]] = {\n    (\"target_nozzle\",): {Modifier.ACTIVATE_IDLE},\n    (\"target_bed\",): {Modifier.ACTIVATE_IDLE},\n    (\"axis_x\",): {Modifier.ACTIVATE_IDLE, Modifier.FILTER_PRINTING},\n    (\"axis_y\",): {Modifier.ACTIVATE_IDLE, Modifier.FILTER_PRINTING},\n    (\"axis_z\",): {Modifier.ACTIVATE_IDLE},\n    (\"target_fan_print\",): {Modifier.ACTIVATE_IDLE},\n    (\"speed\",): {Modifier.ACTIVATE_IDLE, Modifier.ACTIVATE_PRINTING},\n    (\"temp_nozzle\",): {Modifier.JITTER_TEMP},\n    (\"temp_bed\",): {Modifier.JITTER_TEMP},\n    (\"time_printing\",): {Modifier.FILTER_IDLE},\n    (\"time_remaining\",): {Modifier.FILTER_IDLE},\n    (\"progress\",): {Modifier.FILTER_IDLE},\n    (\"inaccurate_estimates\",): {Modifier.FILTER_IDLE},\n    (\"slot\",): {Modifier.FILTER_MMU_OFF},\n    # (\"a\", \"b\") - applies to a key b in a subtree a\n    # (\"a\") - applies to \"a\", so if it's filtered, its children are too\n}\n\nMAPPING = {  # type: ignore\n    \"slot\": {},\n}\n\nfor i_ in range(1, MMU_SLOTS+1):\n    # Map slots from orm to the dict representation\n    MAPPING[\"slot\"][str(i_)] = (\"slot\", \"slots\", str(i_))\n    # Add jitter temps to every slot temp value\n    MODIFIERS[(\"slot\", str(i_), \"temp\")] = {Modifier.JITTER_TEMP}\n\n\nclass TelemetryPasser(metaclass=MCSingleton):\n    \"\"\"Tasked with passing the correct telemetry with the correct timing\"\"\"\n\n    def __init__(self, model: Model, printer: Printer):\n        self.model: Model = model\n        self.printer: Printer = printer\n\n        self.lock = RLock()\n        self.notify_evt: Event = Event()\n        self.running = True\n        self.sleeping = False\n        self.telemetry_interval = TELEMETRY_SLEEPING_INTERVAL\n        self.thread = Thread(target=self._keep_updating,\n                             name=\"telemetry_passer\")\n        self.full_refresh_at = 0\n\n        self._active_filters: set[Any] = set()\n\n        self._last_sent: dict[str, Any] = {}\n        self._to_send: dict[str, Any] = {}\n        self._latest_full = Telemetry()\n        self.model.latest_telemetry = Telemetry()\n\n        self.last_activity_at = time()\n\n    def start(self):\n        \"\"\"Starts the passer\"\"\"\n        self.thread.start()\n\n    def stop(self):\n        \"\"\"Stops the passer\"\"\"\n        self.running = False\n        self.notify_evt.set()\n\n    def wait_stopped(self):\n        \"\"\"Wait for the passer to stop\"\"\"\n        self.thread.join()\n\n    def _keep_updating(self):\n        \"\"\"keeps spinning until supposed to stop\n\n        The loop here facilitates the instant wakeup of the telemetry passer\n        after activity is observed\"\"\"\n        while self.running:\n            self.notify_evt.clear()\n            loop_until(loop_evt=self.notify_evt,\n                       run_every_sec=lambda: self.telemetry_interval,\n                       to_run=self._update)\n\n    def _update(self):\n        \"\"\"Updates how fast to send and sends the telemetry\"\"\"\n        self.sleeping = time() - self.last_activity_at > TELEMETRY_SLEEP_AFTER\n        if self.sleeping:\n            log.debug(\"Telemetry passer is sleeping... zzz\")\n            self.telemetry_interval = TELEMETRY_SLEEPING_INTERVAL\n        else:\n            state = self.model.state_manager.current_state\n            if state in PRINTING_STATES:\n                self.telemetry_interval = TELEMETRY_PRINTING_INTERVAL\n            else:\n                self.telemetry_interval = TELEMETRY_IDLE_INTERVAL\n\n        self.pass_telemetry()\n\n    def pass_telemetry(self):\n        \"\"\"Passes the telemetry to the SDK\n        and pushes the newer telemetry into the sent telemetry\"\"\"\n        if not Settings.instance.use_connect():\n            log.debug(\"Connect isn't configured -> no telemetry\")\n            return\n\n        if not self.printer.is_initialised():\n            log.debug(\"Printer isn't initialised -> no telemetry\")\n            return\n\n        if Settings.instance.is_wizard_needed():\n            log.debug(\"Wizard has not been completed yet -> no telemetry\")\n            return\n\n        if self.printer.queue.qsize() >= QUEUE_LENGTH_LIMIT:\n            log.debug(\"SDK queue looks stuck -> no telemetry\")\n            return\n\n        with self.lock:\n            # Update what we sent last time\n\n            self._last_sent = deep_update(self._last_sent, self._to_send)\n\n            telemetry = self._to_send\n            self._to_send = {}\n\n        self.printer.telemetry(**telemetry)\n\n    def _get_filtered_paths(self):\n        state = self.model.state_manager.current_state\n        if state not in PRINTING_STATES:\n            looking_for = Modifier.FILTER_IDLE\n        elif state == State.PRINTING:\n            looking_for = Modifier.FILTER_PRINTING\n        else:\n            return set()\n\n        filtered = set()\n        for key_path, filters in MODIFIERS.items():\n            if looking_for in filters:\n                filtered.add(key_path)\n\n        return filtered\n\n    def _get_modifiers(self, key_path):\n        modifiers = set()\n        for i in range(len(key_path)):\n            modifiers.update(MODIFIERS.get(key_path[:i+1], set()))\n        return modifiers\n\n    def set_telemetry(self, new_telemetry: Telemetry):\n        \"\"\"Filters jitter, state inappropriate or unchanged data\n        Updates the telemetries with new data\"\"\"\n        with self.lock:\n            new_telemetry_dict = new_telemetry.dict(exclude_none=True)\n            for key_path, value in walk_dict(new_telemetry_dict):\n                key_path = tuple(key_path)\n\n                if value is None or value == {}:\n                    continue  # ignore nones and empty dicts\n\n                modifiers = self._get_modifiers(key_path)\n\n                self._update_by_path(\n                    self._latest_full, new_telemetry, key_path)\n\n                if modifiers & self._active_filters:\n                    # Internally we need to check against none\n                    self._reset_by_path(\n                        self.model.latest_telemetry, key_path)\n                    continue\n\n                self._update_by_path(\n                    self.model.latest_telemetry, new_telemetry, key_path)\n\n                to_update = False\n                if self._get_by_path(self._last_sent, key_path) is None:\n                    to_update = True\n                elif Modifier.JITTER_TEMP in modifiers:\n                    old = self._get_by_path(self._last_sent, key_path)\n                    new = value\n                    assert new is not None\n                    if old is None:\n                        to_update = True\n                    else:\n                        assert isinstance(new, float)\n                        assert isinstance(old, float)\n                        if abs(old - new) > JITTER_THRESHOLD:\n                            to_update = True\n                elif value != self._get_by_path(self._last_sent, key_path):\n                    to_update = True\n\n                # Wake up from sleep, when specific values change\n                if to_update:\n                    if self._should_wake_up(modifiers):\n                        self.activity_observed()\n                    self._update_by_path(\n                        self._to_send, new_telemetry_dict, key_path)\n\n        self._resend_telemetry_on_timer()\n\n    def _should_wake_up(self, modifiers):\n        \"\"\"Returns true if the telemetry passer should wake up from sleep\n        based on the current state and the modifiers present\"\"\"\n        state = self.model.state_manager.current_state\n        if state in PRINTING_STATES:\n            if Modifier.ACTIVATE_PRINTING not in modifiers:\n                return False\n            if Modifier.FILTER_PRINTING in modifiers:\n                return True\n        return Modifier.ACTIVATE_IDLE in modifiers\n\n    def reset_value(self, key_path):\n        \"\"\"Resets the value for filament_change_in and nothing else\"\"\"\n        with self.lock:\n            self._reset_by_path(self._latest_full, key_path)\n            self._reset_by_path(self.model.latest_telemetry, key_path)\n\n    def _set_multi(self, structure, key, value):\n        \"\"\"Sets a value from a dictionary or a model\"\"\"\n        if isinstance(structure, dict):\n            structure[key] = value\n        elif issubclass(type(structure), BaseModel):\n            setattr(structure, key, value)\n        else:\n            raise TypeError(\"Unsupported type for traversing\")\n\n    def _get_multi(self, structure, key):\n        \"\"\"Gets a value from a dictionary or a model\"\"\"\n        if isinstance(structure, dict):\n            return structure.get(key)\n        if issubclass(type(structure), BaseModel):\n            return getattr(structure, key)\n        raise TypeError(\"Unsupported type for traversing\")\n\n    def _get_correct_path(self, structure, key_path):\n        \"\"\"Gets the correct path depending on the structure type\"\"\"\n        if isinstance(structure, dict):\n            return key_path\n        if issubclass(type(structure), BaseModel):\n            return self._path_to_model(key_path)\n        raise TypeError(\"Unsupported type for traversing\")\n\n    def _update_by_path(self, target, source, key_path,\n                        set_none=False):\n        \"\"\"Sets a value in the model,\n        allow setting none, or pushing more data\n        key path is auto mapped, provide the dict equivalent one\n\n        Some assumptions not to be broken as this is fragile AF\n        Always supply the full path, do not let it end on a sub dict\n        or sub model, full paths only\n        Supply only models or dicts\n        \"\"\"\n        if not isinstance(target, type(source)):\n            raise TypeError(\"Source and target must be of the same type\")\n        model_path = self._get_correct_path(target, key_path)\n\n        for key in model_path[:-1]:\n            if not isinstance(target, type(source)):\n                raise TypeError(\"Source and target differ in structure\")\n            next_source = self._get_multi(source, key)\n            next_target = self._get_multi(target, key)\n            if next_source is None:\n                # Source has less depth than target\n                if set_none:\n                    self._set_multi(target, key, None)\n                return\n            if next_target is None:\n                self._set_multi(target, key, deepcopy(next_source))\n                return  # We have set a subtree, we're done\n            source = next_source\n            target = next_target\n        value = self._get_multi(source, model_path[-1])\n        if value is None and not set_none:\n            return  # We don't want to set None, only add more data\n        self._set_multi(target, model_path[-1], value)\n\n    def _reset_by_path(self, target, key_path):\n        \"\"\"Resets a value in the model, does so only for the node at the end\n        of the supplied path\"\"\"\n        model_path = self._get_correct_path(target, key_path)\n\n        for key in model_path[:-1]:\n            target = self._get_multi(target, key)\n            if target is None:\n                return\n        self._set_multi(target, model_path[-1], None)\n\n    def _get_by_path(self, source, key_path):\n        \"\"\"Gets a value from model or dict\"\"\"\n        model_path = self._get_correct_path(source, key_path)\n\n        for key in model_path:\n            source = self._get_multi(source, key)\n            if source is None:\n                return None\n        return source\n\n    def _path_to_model(self, key_path) -> tuple[Any, ...]:\n        \"\"\"As the ORM is now different from the dict structure,\n        this maps the key path to the model\"\"\"\n        sub_mapping = MAPPING\n        iterable_path = iter(key_path)\n        for key in iterable_path:\n            result = sub_mapping.get(key)\n            if result is None:\n                return key_path\n            if isinstance(result, tuple):\n                break\n            sub_mapping = result\n        else:  # no break or return encountered\n            raise ValueError(\"Mapping seems to be invalid\")\n\n        return result + tuple(iterable_path)\n\n    def _resend_telemetry_on_timer(self):\n        \"\"\"If sufficient time elapsed, mark all telemetry values to be sent\"\"\"\n        if time() - self.full_refresh_at > TELEMETRY_REFRESH_INTERVAL:\n            self.full_refresh_at = time()\n            self.resend_latest_telemetry()\n\n    def state_changed(self):\n        \"\"\"When the state changes, update what keys do we filter.\n        Call the setters on any keys for which the filtered status\n        changes, to update them\"\"\"\n        with self.lock:\n            # Update the active filters\n            state = self.model.state_manager.current_state\n            if state not in PRINTING_STATES:\n                self._active_filters.add(Modifier.FILTER_IDLE)\n            elif Modifier.FILTER_IDLE in self._active_filters:\n                self._active_filters.remove(Modifier.FILTER_IDLE)\n\n            if state == State.PRINTING:\n                self._active_filters.add(Modifier.FILTER_PRINTING)\n            elif Modifier.FILTER_PRINTING in self._active_filters:\n                self._active_filters.remove(Modifier.FILTER_PRINTING)\n\n            if not self.printer.mmu_enabled:\n                self._active_filters.add(Modifier.FILTER_MMU_OFF)\n            elif Modifier.FILTER_MMU_OFF in self._active_filters:\n                self._active_filters.remove(Modifier.FILTER_MMU_OFF)\n\n            # Update the telemetry to reflect new filters\n            self.set_telemetry(self._latest_full)\n\n    def activity_observed(self):\n        \"\"\"Call if any activity that constitutes waking up from sleep occurs\"\"\"\n        self.last_activity_at = time()\n        if self.sleeping:\n            log.debug(\"Telemetry passer woke up.\")\n            self.notify_evt.set()\n\n    def wipe_telemetry(self):\n        \"\"\"Resets the telemetry, so the values don't lie\n        Paired with polling value invalidation, this will get and send\n        fresh telemetry values\"\"\"\n        with self.lock:\n            self.model.latest_telemetry = Telemetry()\n            self._last_sent = {}\n            self._to_send = {}\n\n    def resend_latest_telemetry(self):\n        \"\"\"Move the latest telemetry, so it gets sent next time.\n        Great for reconnections and other telemetry forgetting situations\"\"\"\n        with self.lock:\n            self._to_send = self.model.latest_telemetry.dict(exclude_none=True)\n        self.pass_telemetry()\n"
  },
  {
    "path": "prusa/link/printer_adapter/updatable.py",
    "content": "\"\"\"\nContains implementation of the ThreadedUpdatable class\nThere was an updatable without a thread, but it stopped being used\n\nAlso contains a thread utility function\n\"\"\"\nfrom cProfile import Profile\nfrom functools import partial\nfrom threading import Event\nfrom threading import Thread as _Thread\n\nfrom ..util import loop_until\n\n\nclass Thread(_Thread):\n    \"\"\"https://stackoverflow.com/a/1922945\"\"\"\n\n    def profile_run(self):\n        \"\"\"run method for profiling\"\"\"\n        profiler = Profile()\n        profiler.enable()\n        try:\n            return profiler.runcall(_Thread.run, self)\n        finally:\n            profiler.disable()\n            profiler.dump_stats(f'prusalink-{self.name}.profile')\n\n    @staticmethod\n    def enable_profiling():\n        \"\"\"Swap run method.\"\"\"\n        Thread.run = Thread.profile_run\n\n    @staticmethod\n    def disable_profiling():\n        \"\"\"Swap run method.\"\"\"\n        Thread.run = _Thread.run\n\n\nclass ThreadedUpdatable:\n    \"\"\"Thread for parallel update operation.\"\"\"\n    thread_name = \"updater_thread\"\n    update_interval = 1.0\n\n    def __init__(self):\n        self.quit_evt = Event()\n        target = partial(\n            loop_until,\n            loop_evt=self.quit_evt,\n            run_every_sec=lambda: self.update_interval,\n            to_run=self.update)\n\n        self.thread = Thread(target=target,\n                             name=self.thread_name)\n\n    def start(self):\n        \"\"\"Start thread.\"\"\"\n        self.thread.start()\n\n    def stop(self):\n        \"\"\"Stop the updatable\"\"\"\n        self.quit_evt.set()\n\n    def wait_stopped(self):\n        \"\"\"Wait for the updatable to be stopped\"\"\"\n        self.thread.join()\n\n    def update(self):\n        \"\"\"Put code for updating here.\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "prusa/link/sdk_augmentation/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/sdk_augmentation/command_handler.py",
    "content": "\"\"\"Contains implementation of the CommandHandler class\"\"\"\nfrom prusa.connect.printer import Command\n\nfrom ..const import QUIT_INTERVAL\nfrom ..printer_adapter.updatable import Thread\nfrom ..util import prctl_name\n\n\nclass CommandHandler:\n    \"\"\"Waits for commands from the SDK, calls their handlers\"\"\"\n\n    def __init__(self, sdk_command: Command):\n        self.sdk_command = sdk_command\n\n        # Can't start a new thread for every command.\n        # So let's recycle one in here\n        self.command_thread = Thread(target=self.handle_commands,\n                                     name=\"command_runner\",\n                                     daemon=True)\n        self.running = True\n        self.command_thread.start()\n\n    def handle_commands(self):\n        \"\"\"\n        Waits on an event, set by the SDK whenever an unprocessed command\n        gets received\n\n        Calls the sdk command class, which is overloaded and in turn calls\n        the commands handler\n        \"\"\"\n        prctl_name()\n        while self.running:\n            if self.sdk_command.new_cmd_evt.wait(QUIT_INTERVAL):\n                self.sdk_command()\n\n    def stop(self):\n        \"\"\"Stops the command handling module\"\"\"\n        self.running = False\n"
  },
  {
    "path": "prusa/link/sdk_augmentation/file.py",
    "content": "\"\"\"Contains implementation of the SDFile class which augments the SDK File\"\"\"\nfrom pathlib import Path\n\nfrom prusa.connect.printer.files import File\n\n\nclass SDFile(File):\n    \"\"\"Adds a few useful methods for adding SD Files parsed from serial\"\"\"\n\n    def add_node(self, is_dir, path: Path, name, sfn, **attrs):\n        \"\"\"\n        Adds a file/dir node to a path, can add only into an existing dir\n        node\n        \"\"\"\n        parts = Path(path).parts\n        # Ignores the first \"/\"\n        node: \"SDFile\" = self.get(parts[1:])\n        if not str(path).startswith(\"/.\"):\n            if node is None:\n                raise FileNotFoundError(f\"Can't find the node at {path} to add\"\n                                        f\" the child named {name} to.\")\n            node.add(is_dir=is_dir, name=name, read_only=True, sfn=sfn,\n                     **attrs)\n\n    def add_directory(self, path: Path, name, sfn, **attrs):\n        \"\"\"Shorthand for adding directories\"\"\"\n        self.add_node(True, path, name, sfn=sfn, **attrs)\n\n    def add_file(self, path, name, sfn, **attrs):\n        \"\"\"Shorthand for adding files\"\"\"\n        self.add_node(False, path, name, sfn=sfn, **attrs)\n"
  },
  {
    "path": "prusa/link/sdk_augmentation/printer.py",
    "content": "\"\"\"Contains implementation of the augmented Printer class from the SDK\"\"\"\n\nfrom logging import getLogger\nfrom pathlib import Path\nfrom time import sleep\nfrom typing import Any, Dict\n\nfrom gcode_metadata import FDMMetaData\nfrom prusa.connect.printer import Printer as SDKPrinter\nfrom prusa.connect.printer import const\nfrom prusa.connect.printer.command import Command\nfrom prusa.connect.printer.conditions import API, HTTP, CondState\nfrom prusa.connect.printer.const import Source\nfrom prusa.connect.printer.files import File\n\nfrom .. import __version__\nfrom ..conditions import use_connect_errors\nfrom ..const import PRINTER_CONF_TYPES\nfrom ..printer_adapter.keepalive import Keepalive\nfrom ..printer_adapter.lcd_printer import LCDPrinter\nfrom ..printer_adapter.model import Model\nfrom ..printer_adapter.structures.mc_singleton import MCSingleton\nfrom ..printer_adapter.updatable import Thread\nfrom ..util import file_is_on_sd, prctl_name\nfrom .command_handler import CommandHandler\n\nlog = getLogger(\"connect-printer\")\n\n\nclass MyPrinter(SDKPrinter, metaclass=MCSingleton):\n    \"\"\"\n    Overrides some methods of the SDK Printer to provide better support for\n    PrusaLink\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.lcd_printer = LCDPrinter.get_instance()\n        self.keepalive = Keepalive.get_instance()\n        self.download_thread = Thread(target=self.download_loop,\n                                      name=\"download\")\n        self.model = Model.get_instance()\n        self.nozzle_diameter = None\n        self.command_handler = CommandHandler(self.command)\n        self.loop_thread = Thread(target=self.loop, name=\"loop\")\n        self.__inotify_running = False\n        self.inotify_thread = Thread(target=self.inotify_loop, name=\"inotify\")\n        self.snapshot_thread = Thread(target=self.snapshot_loop,\n                                      name=\"snapshot_sender\",\n                                      daemon=True)\n\n    def parse_command(self, res):\n        \"\"\"Parse telemetry response.\n\n        When response from connect is command (HTTP Status: 200 OK), it\n        will set command object.\n        \"\"\"\n\n        if 500 > res.status_code >= 400:\n            API.state = CondState.NOK\n        elif res.status_code == 503:\n            HTTP.state = CondState.NOK\n\n        res = super().parse_command(res)\n\n        return res\n\n    def get_info(self) -> Dict[str, Any]:\n        \"\"\"Returns a dictionary containing the printers info.\"\"\"\n        info = super().get_info()\n        info[\"nozzle_diameter\"] = self.nozzle_diameter\n        info[\"files\"] = self.fs.to_dict_legacy()\n        info[\"prusa_link\"] = __version__  # TODO: remove later\n        info[\"prusalink\"] = __version__\n        return info\n\n    def connection_from_settings(self, settings):\n        \"\"\"Loads connection details from the Settings class.\"\"\"\n        self.api_key = settings.service_local.api_key\n        server = SDKPrinter.connect_url(settings.service_connect.hostname,\n                                        settings.service_connect.tls,\n                                        settings.service_connect.port)\n        token = settings.service_connect.token\n\n        self.set_connection(server, token)\n        use_connect = settings.use_connect()\n        self.keepalive.set_use_connect(use_connect)\n        use_connect_errors(use_connect)\n\n    def get_file_info(self, caller: Command) -> Dict[str, Any]:\n        \"\"\"Return file info for a given file\n        sometimes only when it exists\"\"\"\n        # pylint: disable=unused-argument\n        if not caller.kwargs:\n            raise ValueError(\"SEND_FILE_INFO requires kwargs\")\n\n        file_path_string = caller.kwargs['path']\n        path: Path = Path(file_path_string)\n        log.info(\"FILE_INFO for: %s\", path)\n        parts = path.parts\n\n        if file_is_on_sd(parts):\n            data = self.from_path(path)\n        else:\n            data = super().get_file_info(caller)\n        log.info(\"FILE_INFO: %s\", data)\n        return data\n\n    def from_path(self, path: Path):\n        \"\"\"Parses SD file metadata from its name only\"\"\"\n        string_path = str(path)\n\n        meta = FDMMetaData(string_path)\n        meta.load_from_path(string_path)\n        log.info(meta.data)\n\n        data = {\n            \"source\": Source.CONNECT,\n            \"event\": const.Event.FILE_INFO,\n            \"path\": string_path,\n        }\n\n        file: File = self.fs.get(string_path)\n        if file is not None:\n            data.update(file.attrs)\n        data.update(meta.data)\n        return data\n\n    def start(self):\n        \"\"\"Start SDK related threads.\n\n        * loop\n        * inotify\n        \"\"\"\n        self.__inotify_running = True\n        self.loop_thread.start()\n        self.inotify_thread.start()\n        self.download_thread.start()\n        self.snapshot_thread.start()\n\n    def indicate_stop(self):\n        \"\"\"Passes the stop request to all SDK related threads.\n\n        * command handler\n        * loop\n        * inotify\n        \"\"\"\n        self.__inotify_running = False\n        self.download_mgr.stop_loop()\n        self.stop_loop()\n        self.queue.put(None)  # Trick the SDK into quitting fast\n        self.command_handler.stop()\n        self.camera_controller.stop()\n\n    def wait_stopped(self):\n        \"\"\"Waits for the SDK threads to join\n\n        * command handler\n        * loop\n        * inotify\n        \"\"\"\n        self.inotify_thread.join()\n        self.loop_thread.join()\n        self.download_thread.join()\n        self.snapshot_thread.join()\n\n    def loop(self):\n        \"\"\"SDKPrinter.loop with thread name.\"\"\"\n        prctl_name()\n        super().loop()\n\n    def inotify_loop(self):\n        \"\"\"Inotify_handler in loop.\"\"\"\n        prctl_name()\n        while self.__inotify_running:\n            try:\n                self.inotify_handler()\n                sleep(0.2)\n            except Exception:  # pylint: disable=broad-except\n                log.exception('Unhandled exception')\n\n    def download_loop(self):\n        \"\"\"Handler for download loop\"\"\"\n        prctl_name()\n        self.download_mgr.loop()\n\n    def snapshot_loop(self):\n        \"\"\"Gives snapshot loop a consistent name with the rest of the app\"\"\"\n        prctl_name()\n        self.camera_controller.snapshot_loop()\n\n    @property\n    def type_string(self):\n        \"\"\"Gets the string version of the printer type\"\"\"\n        if self.type is not None:\n            return PRINTER_CONF_TYPES.inverse[self.type]\n        return None\n"
  },
  {
    "path": "prusa/link/serial/__init__.py",
    "content": ""
  },
  {
    "path": "prusa/link/serial/helpers.py",
    "content": "\"\"\"Contains helper functions, for instruction enqueuing\"\"\"\nimport re\nfrom threading import Event\nfrom typing import Callable, List, Union\n\nfrom ..const import QUIT_INTERVAL\nfrom ..serial.instruction import (\n    Instruction,\n    MandatoryMatchableInstruction,\n    MatchableInstruction,\n)\nfrom .serial_queue import SerialQueue\n\n\ndef wait_for_instruction(instruction,\n                         should_wait: Callable[[], bool] = lambda: True,\n                         should_wait_evt: Event = Event(),\n                         check_every=QUIT_INTERVAL):\n    \"\"\"\n    Wait until the instruction is done, or we shouldn't wait anymore\n\n    :param instruction: The instruction to wait for\n    :param should_wait: a lambda returning true if we should continue waiting\n    :param should_wait_evt: an event, if set, means this should quit\n    :param check_every: how fast to consult the should_wait lambda\n    \"\"\"\n    while should_wait() and not should_wait_evt.is_set():\n        if instruction.wait_for_confirmation(timeout=check_every):\n            return True\n    return False\n\n\ndef enqueue_instruction(queue: SerialQueue,\n                        message: str,\n                        to_front=False,\n                        to_checksum=False) -> Instruction:\n    \"\"\"\n    Creates an instruction, which it enqueues right away\n    :param queue: the queue to enqueue into\n    :param message: the gcode you wish to send to the printer\n    :param to_front: Whether the instruction has a higher priority\n    :param to_checksum: Whether to number and checksum the instruction (use\n    only for print instructions!)\n    :return the enqueued instruction\n    \"\"\"\n    instruction = Instruction(message, to_checksum=to_checksum)\n    queue.enqueue_one(instruction, to_front=to_front)\n    return instruction\n\n\n# pylint: disable=too-many-arguments\ndef enqueue_matchable(queue: SerialQueue,\n                      message: str,\n                      regexp: re.Pattern,\n                      to_front=False,\n                      to_checksum=False,\n                      has_to_match=True) -> Union[\n                                                MandatoryMatchableInstruction,\n                                                MatchableInstruction]:\n    \"\"\"\n    Creates a matchable instruction, which it enqueues right away\n    :param queue: the queue to enqueue into\n    :param message: the gcode you wish to send to the printer\n    :param regexp: the regular expression which the instruction needs to\n    match, otherwise it will refuse confirmation\n    :param to_front: Whether the instruction has a higher priority\n    :param to_checksum: Whether to number and checksum the instruction (use\n    only for print instructions!)\n    :return the enqueued instruction\n    \"\"\"\n    instruction: Union[MandatoryMatchableInstruction, MatchableInstruction]\n    if has_to_match:\n        instruction = MandatoryMatchableInstruction(message,\n                                                    capture_matching=regexp,\n                                                    to_checksum=to_checksum)\n    else:\n        instruction = MatchableInstruction(message,\n                                           capture_matching=regexp,\n                                           to_checksum=to_checksum)\n    queue.enqueue_one(instruction, to_front=to_front)\n    return instruction\n\n\ndef enqueue_list_from_str(queue: SerialQueue,\n                          message_list: List[str],\n                          regexp: re.Pattern,\n                          to_front=False,\n                          to_checksum=False) -> List[MatchableInstruction]:\n    \"\"\"\n    Creates a list of instructions, which it enqueues right away\n    :param queue: Queue to enqueue into\n    :param message_list: List of gcodes you wish to send to the printer\n    :param regexp: a regexp to match each instruction output to (this is used\n    by the execute gcode command, so it enqueues with ok / unknown gcode\n    regexp. Keep in mind, that instruction which won't match will refuse to be\n    confirmed)\n    :param to_front: Whether the instruction has a higher priority\n    :param to_checksum: Whether to number and checksum the instruction (use\n    only for print instructions!)\n    :return List of enqueued instructions\n    \"\"\"\n    instruction_list: List[MatchableInstruction] = []\n    for message in message_list:\n        instruction = MatchableInstruction(message,\n                                           capture_matching=regexp,\n                                           to_checksum=to_checksum)\n        instruction_list.append(instruction)\n    queue.enqueue_list(instruction_list, to_front=to_front)\n    return instruction_list\n"
  },
  {
    "path": "prusa/link/serial/instruction.py",
    "content": "\"\"\"\nContains implementation for all the types of instructions enqueueable to the\nserial queue\n\"\"\"\nimport logging\nimport re\nfrom threading import Event\nfrom time import time\nfrom typing import List, Optional\n\nlog = logging.getLogger(__name__)\n\n\nclass Instruction:\n    \"\"\"Basic instruction which can be enqueued into SerialQueue\"\"\"\n    def __init__(self,\n                 message: str,\n                 to_checksum: bool = False,\n                 data: Optional[bytes] = None,\n                 number: Optional[int] = None,\n                 ):\n        if message.count(\"\\n\") != 0:\n            raise RuntimeError(\"Instructions cannot contain newlines.\")\n\n        # Some messages need to be sent with numbered lines and with checksums\n        # This shall be exclusive for printing from files\n        self.to_checksum = to_checksum\n\n        # Can be changed before the instruction is sent.\n        self.message = message\n\n        # If already sent, this will contain the sent bytes\n        self.data = data\n\n        # If we know our number, it is saved here (used by message history)\n        self.number = number\n\n        # Event set when the write has been _confirmed by the printer\n        self.confirmed_event = Event()\n\n        # Event set when the write has been sent to the printer\n        self.sent_event = Event()\n\n        # Api for registering instruction regexps\n        self.capturing_regexps: List[re.Pattern] = []\n\n        # Measuring the time between sending and confirmation will hopefully\n        # enable me to determine if the motion planner buffer is full\n        self.sent_at: Optional[float] = None\n        self.time_to_confirm: Optional[float] = None\n\n    def __str__(self):\n        return f\"Instruction '{self.message.strip()}'\"\n\n    def __repr__(self):\n        return self.__str__()\n\n    def confirm(self, force=False) -> bool:\n        \"\"\"\n        Return False, if getting confirmed but not wanting to\n        (not used in the base implementation anymore)\n        \"\"\"\n        assert force is not None\n        assert self.sent_at is not None\n        self.time_to_confirm = time() - self.sent_at\n        self.confirmed_event.set()\n        return True\n\n    def sent(self):\n        \"\"\"\n        Sets the instruction sent Event and writes the timestamp,\n        when the instruction got sent\n        \"\"\"\n        self.sent_event.set()\n        self.sent_at = time()\n\n    def output_captured(self, sender, match):\n        \"\"\"\n        Output _captured event handler, this type does not capture anything\n        though\n        \"\"\"\n        assert sender is not None\n        assert match is not None\n\n    def wait_for_send(self, timeout=None):\n        \"\"\"Proxy call to wait method of the sent Event\"\"\"\n        return self.sent_event.wait(timeout)\n\n    def wait_for_confirmation(self, timeout=None):\n        \"\"\"Proxy call to wait method of the confirmed Event\"\"\"\n        return self.confirmed_event.wait(timeout)\n\n    def is_sent(self):\n        \"\"\"Returns whether this instruction has been sent yet\"\"\"\n        return self.sent_event.is_set()\n\n    def is_confirmed(self):\n        \"\"\"Returns whether this instruction has been confirmed yet\"\"\"\n        return self.confirmed_event.is_set()\n\n    def reset(self):\n        \"\"\"Resets the send status of an instruction\"\"\"\n        self.sent_at = None\n        self.sent_event.clear()\n\n    def fill_data(self, message_number: int):\n        \"\"\"\n        Puts together binary data to send as for the given instruction.\n        The specific data might contain a message number and a checksum.\n        Also a newline gets appended at the end\n        :param instruction: Instruction to get data for\n        :return: binary data to send\n        \"\"\"\n        data = self.message.encode(\"ASCII\")\n        if self.to_checksum:\n            number_part = f\"N{message_number} \".encode(\"ASCII\")\n            to_checksum = number_part + data + b\" \"\n            checksum = self.get_checksum(to_checksum)\n            checksum_data = f\"*{checksum}\".encode(\"ASCII\")\n            data = to_checksum + checksum_data\n            self.number = message_number\n        data += b\"\\n\"\n        self.data = data\n\n    @staticmethod\n    def get_checksum(data: bytes):\n        \"\"\"\n        Goes over the given bytes and returns a checksum, which is\n        constructed by XORing each byte of data to a zero\n        :param data: data to make a checksum out of\n        :return: the checksum which is a number\n        \"\"\"\n        checksum = 0\n        for byte in data:\n            checksum ^= byte\n        return checksum\n\n\nclass MatchableInstruction(Instruction):\n    \"\"\"\n    Matches using captures_matching.\n    \"\"\"\n    def __init__(self,\n                 *args,\n                 capture_matching: re.Pattern = re.compile(r\".*\"),\n                 **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # Output _captured between command submission and confirmation\n        self.capture_matching = capture_matching\n        self._captured: List[re.Match] = []\n\n        self.capturing_regexps = [capture_matching]\n\n    def output_captured(self, sender, match):\n        \"\"\"Appends _captured output to the instructions _captured list\"\"\"\n        assert sender is not None\n        self._captured.append(match)\n\n    def match(self, index=0):\n        \"\"\"If match with an index exists, return it, otherwise return None\"\"\"\n        if self._captured and len(self._captured) > index:\n            return self._captured[index]\n        return None\n\n    def get_matches(self):\n        \"\"\"Returns the list of all _captured matches\"\"\"\n        return self._captured\n\n\nclass MandatoryMatchableInstruction(MatchableInstruction):\n    \"\"\"\n    HAS TO MATCH, otherwise refuses confirmation!\n    This should fix a communication error we're having.\n    \"\"\"\n    def confirm(self, force=False) -> bool:\n        # Yes, matchables HAVE TO match now!\n        if not self._captured and not force:\n            log.warning(\n                \"Instruction %s did not capture its expected output, \"\n                \"so it REFUSES to be confirmed!\", self.message)\n            return False\n        return super().confirm()\n"
  },
  {
    "path": "prusa/link/serial/is_planner_fed.py",
    "content": "\"\"\"\nContains implementation of the IsPlannerFed class, with HeapName and TimeValue\nclasses. Tries to guess, whether the printer planner is full\n\"\"\"\nimport logging\nimport os\nfrom collections import deque\nfrom enum import Enum\nfrom typing import Deque, Optional\n\nfrom ..const import (\n    DEFAULT_THRESHOLD,\n    HEAP_RATIO,\n    IGNORE_ABOVE,\n    QUEUE_SIZE,\n    USE_DYNAMIC_THRESHOLD,\n)\nfrom ..printer_adapter.structures.heap import HeapItem, MaxHeap, MinHeap\nfrom ..util import ensure_directory, get_clean_path\n\nlog = logging.getLogger(__name__)\n\n\nclass HeapName(Enum):\n    \"\"\"Heap name enum\"\"\"\n    SHORT_TIMES = \"SHORT_TIMES\"\n    LONG_TIMES = \"LONG_TIMES\"\n\n\nclass TimeValue(HeapItem):\n    \"\"\"Time value with info in which queue it currently resides\"\"\"\n\n    def __init__(self, value: float) -> None:\n        super().__init__(value)\n        self.heap_name: Optional[HeapName] = None\n\n\nclass IsPlannerFed:\n    \"\"\"\n    If the planner queue is full, I expect the printer to take longer when\n    confirming print instructions, if the time surpasses a threshold,\n    I assume full buffer. To stay future-proof, let's compute this threshold on\n    the go.\n\n    Let's measure the times for all instructions, disqualifying the ones that\n    took too long. Now the threshold computation mimics the way one would\n    compute a moving median. I use the two heaps approach.\n\n    left heap is a max_heap, the right one is a min_heap, when a number comes,\n    I compare it with the threshold and depending on the result I put it\n    into one of the heaps. If that throws the ratio of element counts off,\n    the heap that is larger than supposed to gives its root to the smaller one.\n\n    The threshold is an average between the two roots.\n\n    After the queue is full, the heaps shed the oldest values, so it can adapt,\n    if for some reason the print commands start taking different amounts of\n    time during the print. Problems can arise in hi-res cylindrical vases\n    and other shapes with homogeneously long segments.\n\n    To get rid of the inaccuracies caused by an initially low number of\n    measured values, let's use a threshold from a previous run, or a default\n    one until the values accumulate.\n    \"\"\"\n\n    def __init__(self, threshold_path):\n        self.times_queue: Deque[TimeValue] = deque(maxlen=QUEUE_SIZE)\n\n        self.threshold_path = get_clean_path(threshold_path)\n        ensure_directory(os.path.dirname(self.threshold_path))\n\n        if not USE_DYNAMIC_THRESHOLD:\n            self.default_threshold = DEFAULT_THRESHOLD\n        else:\n            try:\n                with open(self.threshold_path,\n                          encoding='utf-8') as threshold_file:\n                    self.default_threshold = float(threshold_file.read())\n            except (FileNotFoundError, ValueError):\n                self.default_threshold = DEFAULT_THRESHOLD\n\n        self.is_fed = False\n\n        self.short_times = MaxHeap()\n        self.long_times = MinHeap()\n\n    @property\n    def item_count(self):\n        \"\"\"Return how many time values are contributing to the percentile\"\"\"\n        return len(self.times_queue)\n\n    @property\n    def threshold(self):\n        \"\"\"\n        Depending on the internal state and settings, it returns\n        the percentile threshold or the default\n        \"\"\"\n        if self.item_count < self.times_queue.maxlen or \\\n                not USE_DYNAMIC_THRESHOLD:\n            return self.default_threshold\n        return self.get_dynamic_threshold()\n\n    def get_dynamic_threshold(self):\n        \"\"\"Returns the Nth percentile value. N is fixed in constants\"\"\"\n        if not self.short_times and not self.long_times:\n            return float(\"inf\")\n        if not self.long_times and self.short_times:\n            return self.short_times[0].value\n        return (self.long_times[0].value + self.short_times[0].value) / 2\n\n    def __call__(self):\n        \"\"\"\n        :return: boolean - Did it take long enough?\n        \"\"\"\n        return self.is_fed\n\n    def process_value(self, value):\n        \"\"\"\n        Adds the given value to tracked values and moves the percentile value\n        accordingly\n\n        :param value: how long did it take from send to confirmation\n        \"\"\"\n        if value > IGNORE_ABOVE:\n            return\n\n        if self.item_count >= self.times_queue.maxlen:\n            self._remove_last()\n        self._add(value)\n\n        self.is_fed = value > self.threshold\n\n        if self.is_fed:\n            log.debug(\"Buffer is fed, threshold: %s, value: %s\",\n                      self.threshold, value)\n\n    def _remove_last(self) -> None:\n        \"\"\"\n        For the median to be influenced only by the last N commands\n        And for the RAM and CPU usage to not slowly creep up,\n        the size of the queue and heaps is capped.\n\n        This removes the item from the queue and from its associated heap\n        \"\"\"\n        item: TimeValue = self.times_queue.popleft()\n        if item.heap_name == HeapName.LONG_TIMES:\n            self.long_times.pop(item.heap_index)\n        else:\n            self.short_times.pop(item.heap_index)\n        self.balance()\n\n    def _add(self, value):\n        \"\"\"\n        Adds a new value to the queue and to the one of the heaps\n\n        Complexity should be O(log n)\n        \"\"\"\n        item = TimeValue(value)\n\n        if not self.short_times:\n            self._short_push(item)\n        elif not self.long_times:\n            if self.short_times[0].value > value:\n                larger_item = self.short_times.pop()\n                self._short_push(item)\n                self._long_push(larger_item)\n            else:\n                self._long_push(item)\n        else:\n            if value < self.get_dynamic_threshold():\n                self._short_push(item)\n            else:\n                self._long_push(item)\n            self.balance()\n\n        self.times_queue.append(item)\n\n    def balance(self):\n        \"\"\"Balances heaps to maintain the percentile\"\"\"\n        num_long = len(self.long_times)\n        num_short = len(self.short_times)\n        total = num_long + num_short\n        ideal_short_count = round(total * HEAP_RATIO)\n        if num_short < ideal_short_count - 1:\n            self._short_push(self.long_times.pop())\n        elif num_short > ideal_short_count + 1:\n            self._long_push(self.short_times.pop())\n\n        if self.short_times[0].value > self.long_times[0].value:\n            raise RuntimeError(\"Smaller value heap has a higher value than \"\n                               \"the higher value heap, that's not right...\")\n\n    def _short_push(self, item: TimeValue):\n        \"\"\"\n        Pushes a value into the heap containing times shorter than percentile\n        \"\"\"\n        item.heap_name = HeapName.SHORT_TIMES\n        self.short_times.push(item)\n\n    def _long_push(self, item: TimeValue):\n        \"\"\"\n        Pushes a value into the heap containing times longer than percentile\n        \"\"\"\n        item.heap_name = HeapName.LONG_TIMES\n        self.long_times.push(item)\n\n    def save(self):\n        \"\"\"\n        Saves the threshold, so when the prusa-link starts up again,\n        it doesn't rely on the default threshold anymore\n        \"\"\"\n        if self.item_count >= self.times_queue.maxlen:\n            with open(self.threshold_path, \"w\",\n                      encoding='utf-8') as threshold_file:\n                threshold_file.write(str(self.get_dynamic_threshold()))\n                os.fsync(threshold_file.fileno())\n"
  },
  {
    "path": "prusa/link/serial/serial.py",
    "content": "\"\"\"Own Serail class \"\"\"\nimport errno\nimport fcntl\nimport logging\nimport os\nimport struct\nimport termios\nfrom select import select\nfrom time import time\nfrom types import MappingProxyType\n\nTIOCM_DTR_str = struct.pack('I', termios.TIOCM_DTR)\nTIOCM_RTS_str = struct.pack('I', termios.TIOCM_RTS)\n\n\nclass SerialException(RuntimeError):\n    \"\"\"Own exception type.\"\"\"\n\n\nclass Serial:\n    \"\"\"PySerial compatible class.\"\"\"\n    baudrates = MappingProxyType({115200: termios.B115200})\n\n    def __init__(self, port: str, baudrate: int, timeout: int):\n        \"\"\"\n        baudrate - must be valid baudrates from Serial.baudrates\n        timeout - read operation timeout\n        \"\"\"\n        if baudrate not in Serial.baudrates:\n            raise SerialException(f\"Baudrate `{baudrate}` is not supported\")\n\n        self.timeout = timeout\n\n        # pylint: disable=invalid-name\n        self.fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)\n        tty = termios.tcgetattr(self.fd)\n\n        # cflag\n        tty[2] &= ~termios.PARENB\n        tty[2] &= ~termios.CSTOPB\n        tty[2] |= termios.CS8\n        tty[2] &= ~termios.CRTSCTS\n        tty[2] |= termios.CREAD | termios.CLOCAL\n        tty[2] &= ~termios.HUPCL  # disable hangup\n\n        # lflag\n        tty[3] &= ~termios.ICANON\n        tty[3] &= ~termios.ECHO\n        tty[3] &= ~termios.ECHOE\n        tty[3] &= ~termios.ECHONL\n        tty[3] &= ~termios.ECHOK\n        tty[3] &= ~termios.ISIG\n        tty[3] &= ~termios.IEXTEN\n\n        # iflag\n        tty[0] &= ~(termios.IXON | termios.IXOFF | termios.IXANY)\n        tty[0] &= ~(termios.IGNBRK | termios.ISTRIP\n                    | termios.INLCR | termios.IGNCR | termios.ICRNL)\n        tty[0] &= ~(termios.IUCLC | termios.PARMRK)\n\n        # oflag\n        tty[1] &= ~termios.OPOST\n        tty[1] &= ~termios.ONLCR\n        tty[1] &= ~termios.OCRNL\n\n        # cc\n        tty[6][termios.VTIME] = 0  # can be timout*10\n        tty[6][termios.VMIN] = 0\n\n        # ispeed\n        tty[4] = termios.B115200\n        # ospeed\n        tty[5] = termios.B115200\n\n        termios.tcsetattr(self.fd, termios.TCSANOW, tty)\n        # TCSAFLUSH set after everything is done\n        termios.tcsetattr(self.fd, termios.TCSAFLUSH, tty)\n        # clear input buffer\n        termios.tcflush(self.fd, termios.TCIFLUSH)\n        try:\n            # Data Terminal Ready\n            fcntl.ioctl(self.fd, termios.TIOCMBIS, TIOCM_DTR_str)\n            # Request To Send\n            fcntl.ioctl(self.fd, termios.TIOCMBIS, TIOCM_RTS_str)\n        except OSError as e:\n            if e.errno == errno.ENOTTY:\n                logging.getLogger(__name__).warning(\n                    \"The file does not support ioctl() ignoring\")\n            else:\n                raise\n\n        self.__buffer = b''\n\n        self.__dtr = False\n\n    def close(self):\n        \"\"\"Close the port.\"\"\"\n        if self.fd is None:\n            return\n        try:\n            os.close(self.fd)\n        except OSError:\n            pass\n        finally:\n            self.fd = None\n\n    def __read(self, timeout):\n        \"\"\"Fill internal buffer by read from file descriptor.\"\"\"\n        try:\n            ready = select([self.fd], [], [], timeout)\n            if ready[0] and self.fd:\n                read_bytes = os.read(self.fd, 1024)\n                if not read_bytes:\n                    raise SerialException(\"The serial became disconnected.\")\n                self.__buffer += read_bytes\n        except (BlockingIOError, InterruptedError, TypeError) as err:\n            self.close()\n            raise SerialException(f\"read failed: {err}\") from err\n\n    def readline(self):\n        \"\"\"Return next line from local buffer or from serial port.\"\"\"\n        times_out_at = time() + self.timeout\n        start_at = 0\n\n        while True:\n            current_time = time()\n            pos = self.__buffer.find(b'\\n', start_at)\n            if pos >= 0:\n                line = self.__buffer[:pos + 1]\n                self.__buffer = self.__buffer[pos + 1:]\n                return line\n\n            start_at = max(0, len(self.__buffer) - 1)\n            if current_time >= times_out_at:\n                break\n\n            self.__read(times_out_at - current_time)\n\n        return b''\n\n    def write(self, data: bytes):\n        \"\"\"Write data to serial port.\"\"\"\n        return os.write(self.fd, data)\n\n    @property\n    def is_open(self):\n        \"\"\"Return true if port is open.\"\"\"\n        return self.fd is not None\n\n    @property\n    def dtr(self):\n        \"\"\"Data Terminal Ready State\"\"\"\n        return self.__dtr\n\n    @dtr.setter\n    def dtr(self, value: bool):\n        self.__dtr = value\n        if value:\n            fcntl.ioctl(self.fd, termios.TIOCMBIS, TIOCM_DTR_str)\n        else:\n            fcntl.ioctl(self.fd, termios.TIOCMBIC, TIOCM_DTR_str)\n"
  },
  {
    "path": "prusa/link/serial/serial_adapter.py",
    "content": "\"\"\"Contains implementation of the Serial class\"\"\"\n\nimport glob\nimport logging\nimport os\nimport re\nfrom importlib import util\nfrom pathlib import Path\nfrom threading import Event, RLock\nfrom time import sleep, time\nfrom typing import List, Optional\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.conditions import CondState\n\nfrom ..conditions import SERIAL\nfrom ..const import (\n    PRINTER_BOOT_WAIT,\n    PRINTER_TYPES,\n    QUIT_INTERVAL,\n    RESET_PIN,\n    SERIAL_REOPEN_TIMEOUT,\n)\nfrom ..printer_adapter.model import Model\nfrom ..printer_adapter.structures.mc_singleton import MCSingleton\nfrom ..printer_adapter.structures.module_data_classes import (\n    Port,\n    SerialAdapterData,\n)\nfrom ..printer_adapter.structures.regular_expressions import (\n    ATTENTION_REGEX,\n    BUSY_REGEX,\n    FW_REGEX,\n    PRINTER_TYPE_REGEX,\n)\nfrom ..printer_adapter.updatable import Thread\nfrom ..util import decode_line, get_usb_printers, prctl_name\nfrom .serial import Serial, SerialException\nfrom .serial_parser import ThreadedSerialParser\n\nlog = logging.getLogger(__name__)\n\n\nclass PortAdapter:\n    \"\"\"Use the Port class, but allow to pass a Serial instance with it\"\"\"\n    def __init__(self, port: Port) -> None:\n        self.port: Port = port\n        self.serial: Optional[Serial] = None\n\n\nclass SerialAdapter(metaclass=MCSingleton):\n    \"\"\"\n    Class handling the basic serial management, opening, re-opening,\n    writing and reading.\n\n    It also can reset the connected device using DTR - works only with USB\n    \"\"\"\n\n    @staticmethod\n    def is_rpi_port(port_path):\n        \"\"\"Figure out, whether we're running through the Einsy RPi port\"\"\"\n        try:\n            port_name = Path(port_path).name\n            if not port_name.startswith(\"ttyAMA\"):\n                return False\n            sys_path = Path(f\"/sys/class/tty/{port_name}\")\n            link_path = os.readlink(str(sys_path))\n            device_path = sys_path.parent.joinpath(link_path).resolve()\n            path_regexp = re.compile(r\"^/sys/devices/platform/soc/\"\n                                     r\"[^.]*\\.serial/tty/ttyAMA\\d+$\")\n            match = path_regexp.match(str(device_path))\n            if match is None:\n                return False\n        except Exception:  # pylint: disable=broad-except\n            log.exception(\"Exception when checking if we're connected through \"\n                          \"the Einsy pins. Assuming we're not.\")\n            return False\n        return True\n\n    def __init__(self,\n                 serial_parser: ThreadedSerialParser,\n                 model: Model,\n                 configured_port=\"auto\",\n                 baudrate: int = 115200,\n                 timeout: int = 2,\n                 reset_disabling: bool = True) -> None:\n\n        # pylint: disable=too-many-arguments\n        self.model: Model = model\n        self.model.serial_adapter = SerialAdapterData(\n            using_port=None, reset_disabling=reset_disabling)\n        self.data: SerialAdapterData = model.serial_adapter\n        self.configured_port = configured_port\n        self.baudrate = baudrate\n        self.timeout = timeout\n\n        self.write_lock = RLock()\n\n        self.serial: Optional[Serial] = None\n        self.serial_parser = serial_parser\n\n        self.failed_signal = Signal()\n        self.renewed_signal = Signal()\n\n        self.running = True\n        self._work_around_power_panic = Event()\n        self._work_around_power_panic.set()\n\n        self.read_thread = Thread(target=self._read_continually,\n                                  name=\"serial_read_thread\",\n                                  daemon=True)\n        self.read_thread.start()\n\n    @staticmethod\n    def is_open(serial) -> bool:\n        \"\"\"Returns bool indicating whether there's a serial connection\"\"\"\n        return serial is not None and serial.is_open\n\n    @staticmethod\n    def _get_info(port_adapter: PortAdapter):\n        \"\"\"Gets info about the supplied port\n        returns whether it figured something out or not\"\"\"\n        serial = port_adapter.serial\n        port = port_adapter.port\n\n        if serial is None:\n            raise SerialException(\"Tried getting info without a serial port \"\n                                  \"(mostly for mypy to stop crying)\")\n\n        name = version = error_text = None\n        serial.write(b\"PRUSA Fir\\nM862.2 Q\\n\")\n        timeout_at = time() + 5\n        while (raw_line := serial.readline()) and time() < timeout_at:\n            line = decode_line(raw_line)\n            log.debug(\"Printer detection for '%s' returned: %s\",\n                      port.path, line)\n            if match := PRINTER_TYPE_REGEX.match(line):\n                if (code := int(match.group(\"code\"))) in PRINTER_TYPES:\n                    name = \"Prusa \" + PRINTER_TYPES[code].name\n                else:\n                    error_text = \"The printer is not supported\"\n            elif match := FW_REGEX.match(line):\n                version = match.group(\"version\")\n            elif BUSY_REGEX.match(line):\n                error_text = \"The printer is busy\"\n            elif ATTENTION_REGEX.match(line):\n                error_text = \"The printer wants user attention\"\n\n            if name and version:\n                port.usable = True\n                port.description = f\"{name} - FW: {version}\"\n                return\n            if error_text:\n                port.description = error_text\n                return\n        port.description = \"A printer did not answer in time\"\n\n    @staticmethod\n    def _detect(port_adapter: PortAdapter):\n        \"\"\"\n        Detects the usability of given port\n        Split into two for pylint, this one is responsible for opening serial\n        \"\"\"\n        prctl_name()\n        port = port_adapter.port\n        serial = None\n        try:\n            if not SerialAdapter.is_open(serial):\n                serial = Serial(port=port.path,\n                                baudrate=port.baudrate,\n                                timeout=port.timeout)\n                port_adapter.serial = serial\n                if not port.is_rpi_port:\n                    port.description = \"Waiting for printer to boot\"\n                    sleep(8)\n\n            SerialAdapter._get_info(port_adapter)\n\n        except (SerialException, FileNotFoundError, OSError) as error:\n            port.description = \"Failed to open. Is a printer connected \" \\\n                               f\"to this port? Error: {error}\"\n            if SerialAdapter.is_open(serial):\n                serial.close()  # type: ignore\n        port.checked = True\n        log.debug(\"Port: '%s' description: '%s'\",\n                  port.path, port.description)\n\n    def _reopen(self) -> bool:\n        \"\"\"Re-open the configured serial port. Do a full re-scan if\n        auto is configured\"\"\"\n        self.data.using_port = None\n        self.data.ports = []\n        port_adapters: List[PortAdapter] = []\n        threads = []\n        with self.write_lock:\n            self.close()\n\n            if self.configured_port == \"auto\":\n                paths = glob.glob(\"/dev/ttyAMA*\")\n                paths.extend(glob.glob(\"/dev/ttyACM*\"))\n                paths.extend(glob.glob(\"/dev/ttyUSB*\"))\n            else:\n                # Follow symlinks to the real device file\n                device_path = os.path.realpath(self.configured_port)\n                paths = [device_path]\n\n            # Pair the usb printer paths with their serial numbers\n            usb_printers = {\n                printer.path: printer.serial_number\n                for printer in get_usb_printers()\n            }\n\n            for path in paths:\n                port = Port(path=path,\n                            baudrate=115200,\n                            timeout=2,\n                            is_rpi_port=self.is_rpi_port(path))\n                if path in usb_printers:\n                    port.sn = usb_printers[path]\n                port_adapter = PortAdapter(port)\n                self.data.ports.append(port)\n                port_adapters.append(port_adapter)\n                thread = Thread(target=self._detect,\n                                args=(port_adapter,),\n                                name=\"port_detector\",\n                                daemon=True)\n                threads.append(thread)\n                thread.start()\n\n            for thread in threads:\n                thread.join()\n\n            found = False\n            for port_adapter in port_adapters:\n                if port_adapter.port.usable and not found:\n                    found = True\n                    port_adapter.port.selected = True\n                    self.data.using_port = port_adapter.port\n                    self.serial = port_adapter.serial\n                    log.info(\"Using the serial port %s\",\n                             self.data.using_port.path)\n                elif self.is_open(port_adapter.serial):\n                    # The above if guarantees there's not a None\n                    # in port.serial. Mypy is being dramatic again\n                    port_adapter.serial.close()  # type: ignore\n                    log.debug(\"Other port - %s\", port)\n            return found\n\n    def close(self):\n        \"\"\"Close the serial. If the read thread is running,\n        it should renew the connection.\n        \"\"\"\n        with self.write_lock:\n            if self.is_open(self.serial):\n                self.serial.close()\n\n    def _renew_serial_connection(self, starting: bool = False):\n        \"\"\"\n        Informs the rest of the app about failed serial connection,\n        After which it keeps trying to re-open the serial port\n\n        If it succeeds, generates a signal to inform the rest of the app\n        \"\"\"\n        # Wait for power panic timeout\n        if not self._work_around_power_panic.is_set():\n            self.failed_signal.send(self)\n            SERIAL.state = CondState.NOK\n\n        while self.running:\n            if self._work_around_power_panic.wait(QUIT_INTERVAL):\n                break\n\n        if self.is_open(self.serial):\n            raise RuntimeError(\"Don't reconnect what is not disconnected\")\n\n        while self.running:\n            if starting:\n                starting = False\n            else:\n                self.failed_signal.send(self)\n                SERIAL.state = CondState.NOK\n\n            if not self._reopen():\n                SERIAL.state = CondState.NOK\n                log.warning(\"Error when connecting to serial according to \"\n                            \"user config:  %s\",\n                            self.configured_port)\n                sleep(SERIAL_REOPEN_TIMEOUT)\n            else:\n                break\n\n        if self.running and not SERIAL:\n            SERIAL.state = CondState.OK\n            self.data.resets_enabled = None\n            self.renewed_signal.send(self)\n\n    def _read_continually(self):\n        \"\"\"Ran in a thread, reads stuff over an over\"\"\"\n        prctl_name()\n        self._renew_serial_connection(starting=True)\n\n        while self.running:\n            raw_line = \"[No data] - This is a fallback value, \" \\\n                       \"so stuff doesn't break\"\n            try:\n                if not self._work_around_power_panic.is_set():\n                    raise SerialException(\n                        \"Need to re-connect serial after power panic\")\n                raw_line = self.serial.readline()\n                line = decode_line(raw_line)\n            except (SerialException, OSError):\n                log.exception(\"Failed when reading from the printer. \"\n                              \"Trying to re-open\")\n                self.close()\n                self._renew_serial_connection()\n            except UnicodeDecodeError:\n                log.error(\"Failed decoding a message %s\", raw_line)\n            else:\n                # with self.write_read_lock:\n                # Why would I not want to write and handle reads\n                # at the same time? IDK, but if something weird starts\n                # happening, i'll re-enable this\n                if line == \"\":\n                    log.debug(\"Printer has most likely sent something, \"\n                              \"which is not human readable\")\n                else:\n                    log.debug(\"Recv: %s\", line)\n                self.serial_parser.decide(line)\n\n    def write(self, message: bytes):\n        \"\"\"\n        Writes a message to serial, if it for some reason fails,\n        calls _renew_serial_connection\n\n        :param message: the message to be sent\n\n        Raises SerialException when the communication fails\n        \"\"\"\n\n        sent = False\n\n        with self.write_lock:\n            if not self.is_open(self.serial):\n                log.warning(\"No serial to send '%s' to\", message)\n                return\n            while not sent and self.running:\n                try:\n                    # Mypy does not work with functions that check for None\n                    self.serial.write(message)  # type: ignore\n                except OSError as error:\n                    log.error(\"Serial error when sending '%s' to the printer\",\n                              message)\n                    self.close()\n                    raise SerialException(\n                        \"Serial error when sending\") from error\n                sent = True\n                log.debug(\"Send: %s\", message)\n\n    def disable_dtr_resets(self):\n        \"\"\"Disables DTR resets - should be used by a command handler\"\"\"\n        if not self.data.reset_disabling:\n            return\n        if self.data.resets_enabled is False:\n            return\n        self.write(b\"\\n;C32u2_RMD\\n\")\n\n    def enable_dtr_resets(self):\n        \"\"\"Enables DTR resets - should be used by a command handler\"\"\"\n        if not self.data.reset_disabling:\n            return\n        if self.data.resets_enabled is True:\n            return\n        self.write(b\"\\n;C32u2_RME\\n\")\n\n    def _reset_pi(self):\n        \"\"\"Resets the connected raspberry pi\"\"\"\n        spam_loader = util.find_spec('wiringpi')\n        if spam_loader is None:\n            log.warning(\"WiringPi missing, cannot reset using pins\")\n            return\n\n        # pylint: disable=import-outside-toplevel\n        # pylint: disable=import-error\n        # ruff: noqa: PLC0415\n        import wiringpi  # type: ignore\n        wiringpi.wiringPiSetupGpio()\n        wiringpi.pinMode(RESET_PIN, wiringpi.OUTPUT)\n        wiringpi.digitalWrite(RESET_PIN, wiringpi.HIGH)\n        wiringpi.digitalWrite(RESET_PIN, wiringpi.LOW)\n        sleep(0.1)\n        wiringpi.digitalWrite(RESET_PIN, wiringpi.LOW)\n\n    def _blip_dtr(self):\n        \"\"\"Pulses the DTR to reset the connected device.\n        Works only over USB\"\"\"\n        with self.write_lock:\n            self.serial.dtr = False\n            self.serial.dtr = True\n            sleep(PRINTER_BOOT_WAIT)\n\n    def reset_client(self):\n        \"\"\"Resets the connected device, over USB or using the reset pin\"\"\"\n        if not self.is_open(self.serial):\n            log.warning(\"No serial connected, will not reset anything.\")\n            return\n\n        if self.data.using_port.is_rpi_port:\n            self._reset_pi()\n        else:\n            self._blip_dtr()\n\n    def stop(self):\n        \"\"\"Stops the component\"\"\"\n        self.running = False\n        self.close()\n\n    def wait_stopped(self):\n        \"\"\"Waits for the serial to be stopped\"\"\"\n        self.read_thread.join()\n\n    def power_panic_observed(self):\n        \"\"\"Called when a power panic is observed\"\"\"\n        self._work_around_power_panic.clear()\n\n    def power_panic_unblock(self):\n        \"\"\"Re-sets the power panic flag that holds the serial disconnected\"\"\"\n        self._work_around_power_panic.set()\n"
  },
  {
    "path": "prusa/link/serial/serial_parser.py",
    "content": "\"\"\"\nContains implementation of the SerialParser and Regex pairing classes\nThe latter is used by the former for tracking which regular expressions\nhave which handlers\n\nAs of writing this doc, the \"ok\" has infinite priority, then every instruction\nhandler has the current time as the priority, meaning later added handlers are\nevaluated first.\n\"\"\"\nimport logging\nimport re\nfrom functools import partial\nfrom queue import Queue\nfrom threading import Lock, Thread\nfrom typing import Any, Callable, Dict, Match, Optional, Union\n\nfrom blinker import Signal  # type: ignore\nfrom sortedcontainers import SortedKeyList  # type: ignore\n\nfrom ..printer_adapter.structures.mc_singleton import MCSingleton\n\nlog = logging.getLogger(__name__)\n\n\nclass RegexPairing:\n    \"\"\"\n    An object representing a bound regexp to its handler, with priority,\n    for us to be able to sort which regexps to try first\n    \"\"\"\n\n    def __init__(self, regexp, priority=0) -> None:\n        self.regexp: re.Pattern = regexp\n        self.signal: Signal = Signal()\n        self.priority: Union[float, int] = priority\n\n    def __str__(self) -> str:\n        receiver_count = len(self.signal.receivers)\n        return f\"RegexPairing for {self.regexp.pattern} \" \\\n               f\"with priority {self.priority} \" \\\n               f\"having {receiver_count} handler\" \\\n               f\"{'s' if receiver_count > 1 else ''}\"\n\n    def __repr__(self) -> str:\n        return self.__str__()\n\n    def fire(self, match: Optional[Match] = None) -> None:\n        \"\"\"\n        Fire the associated signal, catch and log errors, don't want to\n        kill the serial reading component\n        \"\"\"\n        # pylint: disable=broad-except\n        log.debug(\"Matched %s calling %s\", self, self.signal.receivers)\n        try:\n            self.signal.send(self, match=match)\n        except Exception:\n            log.exception(\"Exception during handling of the printer output. \"\n                          \"Caught to stay alive.\")\n\n\nclass SerialParser(metaclass=MCSingleton):\n    \"\"\"\n    Its job is to try and find an appropriate handler for every line that\n    we receive from the printer\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.lock = Lock()\n        self.pattern_list = SortedKeyList(key=lambda item: -item.priority)\n        self.pairing_dict: Dict[re.Pattern, RegexPairing] = {}\n\n    def decide(self, line: str) -> None:\n        \"\"\"\n        The meat of the class, trying different RegexPairings ordered\n        by their priorities, to find the matching one\n        \"\"\"\n        chosen_pairing = None\n\n        with self.lock:\n            for pairing in self.pattern_list:\n                match = pairing.regexp.match(line)\n                if match:\n                    chosen_pairing = pairing\n                    break\n\n        if chosen_pairing is not None:\n            chosen_pairing.fire(match=match)\n        else:\n            log.debug(\"Match not found for %s\", line)\n\n    def add_handler(self,\n                    regexp: re.Pattern,\n                    handler: Callable[[Any, re.Match], None],\n                    priority: float = 0) -> None:\n        \"\"\"\n        Add an entry to output handlers.\n        :param regexp: if this matches, your handler will get called\n        Warning, should be unique, or the exact same as another one,\n        after the first match, the matching is stopped! and all the handlers\n        for the regexp are called\n        :param handler: Callable that will parse the matched output\n        :param priority: Higher priority means the regexp will be attempted\n        sooner in the list. For items with the same priority, the newest gets\n        used first\n        \"\"\"\n        with self.lock:\n            if regexp in self.pairing_dict:\n                existing_pairing: RegexPairing = self.pairing_dict[regexp]\n                if existing_pairing not in self.pattern_list:\n                    log.debug(\"%s is not in %s. What?!\", existing_pairing,\n                              self.pattern_list)\n                if priority > existing_pairing.priority:\n                    self.pattern_list.remove(existing_pairing)\n                    existing_pairing.priority = priority\n                    self.pattern_list.add(existing_pairing)\n                    log.debug(\"Priority updated from %s to %s\",\n                              existing_pairing.priority, priority)\n                existing_pairing.signal.connect(handler, weak=False)\n            else:\n                new_pairing: RegexPairing = RegexPairing(regexp,\n                                                         priority=priority)\n                new_pairing.signal.connect(handler, weak=False)\n\n                self.pairing_dict[regexp] = new_pairing\n                self.pattern_list.add(new_pairing)\n\n    def remove_handler(self, regexp, handler) -> None:\n        \"\"\"\n        Removes the regexp and handler from the list of serial output handlers\n        :param regexp: which regexp to remove a handler from\n        :param handler: Which handler to remove\n        \"\"\"\n        with self.lock:\n            if regexp in self.pairing_dict:\n                pairing: RegexPairing = self.pairing_dict[regexp]\n                pairing.signal.disconnect(handler)\n                if not pairing.signal.receivers:\n                    del self.pairing_dict[regexp]\n                    self.pattern_list.remove(pairing)\n            else:\n                raise RuntimeError(f\"There is no handler registered for \"\n                                   f\"{regexp.pattern}\")\n\n\nclass ThreadedSerialParser(SerialParser):\n    \"\"\"Implements a way to de-couple serial reader from the rest\n    of the app while allowing serial queue to remain coupled\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.handler_queue = Queue()\n        self.running = False\n        self.thread = Thread(target=self.process,\n                             name=\"serial_decoupler\",\n                             daemon=True)\n        self.running = True\n        self.thread.start()\n\n    def decoupled(self, handler):\n        \"\"\"A function generator decoupling the caller thread by enqueuing\n        instead of calling the provided handler with its call arguments\"\"\"\n        def inner(sender, match):\n            self.handler_queue.put(partial(handler, sender, match=match))\n        return inner\n\n    def process(self):\n        \"\"\"Processes the handler as a new thread\"\"\"\n        while self.running:\n            handler = self.handler_queue.get(block=True)\n            if handler is not None:\n                handler()\n\n    def add_decoupled_handler(self,\n                              regexp: re.Pattern,\n                              handler: Callable[[Any, re.Match], None],\n                              priority: float = 0) -> None:\n        \"\"\"Converts given handler, so it does not block the caller\"\"\"\n        self.add_handler(regexp, self.decoupled(handler), priority)\n\n    def stop(self):\n        \"\"\"Signals a stop to the decoupler\"\"\"\n        self.running = False\n        self.handler_queue.put(lambda: None)\n\n    def wait_stopped(self):\n        \"\"\"Waits until the decoupler is fully stopped\"\"\"\n        if self.thread:\n            self.thread.join()\n"
  },
  {
    "path": "prusa/link/serial/serial_queue.py",
    "content": "\"\"\"\nContains implementation of the SerialQueue and the MonitoredSerialQueue\n\nThe idea was to separate the monitoring functionality to not clutter the queue\nand instruction management\n\"\"\"\n\nimport logging\nimport re\nfrom collections import deque\nfrom threading import Event, Lock\nfrom time import time\nfrom typing import Deque, List, Optional\n\nfrom blinker import Signal  # type: ignore\nfrom prusa.connect.printer.conditions import CondState\n\nfrom ..conditions import RPI_ENABLED, SERIAL\nfrom ..const import (\n    HISTORY_LENGTH,\n    MAX_INT,\n    QUIT_INTERVAL,\n    RX_SIZE,\n    SERIAL_QUEUE_MONITOR_INTERVAL,\n    SERIAL_QUEUE_TIMEOUT,\n)\nfrom ..interesting_logger import InterestingLogRotator\nfrom ..printer_adapter.structures.mc_singleton import MCSingleton\nfrom ..printer_adapter.structures.regular_expressions import (\n    ATTENTION_REGEX,\n    BUSY_REGEX,\n    CONFIRMATION_REGEX,\n    HEATING_HOTEND_REGEX,\n    HEATING_REGEX,\n    M110_REGEX,\n    RESEND_REGEX,\n)\nfrom ..printer_adapter.updatable import Thread\nfrom ..util import loop_until, prctl_name\nfrom .instruction import Instruction, MatchableInstruction\nfrom .is_planner_fed import IsPlannerFed\nfrom .serial import SerialException\nfrom .serial_adapter import SerialAdapter\nfrom .serial_parser import ThreadedSerialParser\n\nlog = logging.getLogger(__name__)\n\n\nclass SerialQueue(metaclass=MCSingleton):\n    \"\"\"\n    Class responsible for sending commands to the printer\n\n    Messages need to be sent one by one and need to be confirmed afterwards\n    There are many edge cases like resend requests, message number resets\n    RX buffer dumping and so on, which this class works around to provide\n    as deterministic of a serial connection to a Prusa printer as possible\n    \"\"\"\n\n    def __init__(self,\n                 serial_adapter: SerialAdapter,\n                 serial_parser: ThreadedSerialParser,\n                 threshold_path: str,\n                 rx_size=RX_SIZE):\n        self.serial_adapter = serial_adapter\n        self.serial_parser = serial_parser\n\n        # When the serial_queue cannot re-establish communication with the\n        # printer, let's signal this to other modules\n        self.serial_queue_failed = Signal()\n        self.instruction_confirmed_signal = Signal()\n        self.message_number_changed = Signal()\n\n        # A queue of instructions for the printer\n        self.queue: Deque[Instruction] = deque()\n\n        # This one shall contain time critical instructions\n        self.priority_queue: Deque[Instruction] = deque()\n\n        # Instruction that is currently being handled\n        self.current_instruction: Optional[Instruction] = None\n\n        # Maximum bytes we'll write\n        self.rx_max = rx_size\n\n        # Make it possible to enqueue multiple consecutive instructions\n        self.write_lock = Lock()\n\n        # For numbered messages with checksums\n        self.message_number = 0\n\n        # When filament runs out or other buffer flushing calamity occurs\n        # We need to re-send some commands that we already had dismissed as\n        # confirmed\n        self.send_history: Deque[Instruction] = deque(maxlen=HISTORY_LENGTH)\n\n        # A list which will contain all messages needed to recover\n        self.recovery_list: List[Instruction] = []\n        self.rx_yeet_slot = None\n\n        # For stopping fast (power panic)\n        self.closed = False\n\n        # Flag to be set when serial communication fails\n        self.has_failed = False\n\n        # Workaround around M110 involves syncing the FW buffers using a G4\n        # Whenever an M110 comes, a G4 needs to be prepended.\n        # To avoid getting stuck in an endless loop, let's flip a flag\n        self.m110_workaround_slot = None\n        self.worked_around_m110 = False\n\n        # Allows to temporarily block sending to the serial queue\n        self._block_sending = False\n\n        self.serial_parser.add_handler(CONFIRMATION_REGEX,\n                                       self._confirmation_handler,\n                                       priority=float(\"inf\"))\n        self.serial_parser.add_handler(RESEND_REGEX, self._resend_handler)\n\n        self.is_planner_fed = IsPlannerFed(threshold_path)\n\n        self.quit_evt = Event()\n        self.send_event = Event()\n        self.sender_thread = Thread(name=\"sq_sender\",\n                                    target=self._keep_sending,\n                                    daemon=True)\n\n        self.sender_thread.start()\n\n    def _keep_sending(self):\n        \"\"\"Send the most important instruction when asked nicely\"\"\"\n        prctl_name()\n        while True:\n            self.send_event.wait()\n            if self.quit_evt.is_set():\n                break\n            self.send_event.clear()\n            if self._block_sending:\n                continue\n            with self.write_lock:\n                if not self.can_write():\n                    continue\n                try:\n                    self._send()\n                except (SerialException, OSError):\n                    log.info(\"A serial write has failed, expecting serial \"\n                             \"reader to fix the problem. In the meantime \"\n                             \"waiting for a nudge to send again.\")\n\n    def block_sending(self):\n        \"\"\"Block sending of instructions until we unblock again\"\"\"\n        self._block_sending = True\n\n    def unblock_sending(self):\n        \"\"\"Unblock sending of instructions\"\"\"\n        if self._block_sending:\n            self._block_sending = False\n            self._try_writing()\n\n    def _try_writing(self):\n        \"\"\"\n        Nudge the sender thread to send an instruction\n        \"\"\"\n        self.send_event.set()\n\n    def stop(self):\n        \"\"\"\n        Stops the serial queue sender\n        \"\"\"\n        self.quit_evt.set()\n        self.send_event.set()\n\n    def wait_stopped(self):\n        \"\"\"Waits for the serial queue to stop\"\"\"\n        self.sender_thread.join()\n\n    def peek_next(self):\n        \"\"\"Look, what the next instruction is going to be\"\"\"\n        # pylint: disable=too-many-return-statements\n        if self.m110_workaround_slot is not None:\n            return self.m110_workaround_slot\n        if self.rx_yeet_slot is not None:\n            return self.rx_yeet_slot\n        if self.recovery_list:\n            return self.recovery_list[-1]\n        if self.priority_queue:\n            if self.is_planner_fed() and self.queue:\n                return self.queue[-1]\n            return self.priority_queue[-1]\n        if self.queue:\n            return self.queue[-1]\n        return None\n\n    def _next_instruction(self):\n        \"\"\"\n        Get a fresh instruction into the self.current_instruction handling\n        slot\n        \"\"\"\n\n        if self.current_instruction is not None:\n            raise RuntimeError(\"Cannot send a new instruction. \"\n                               \"When the last one didn't finish processing.\")\n        if self.m110_workaround_slot is not None:\n            self.current_instruction = self.m110_workaround_slot\n            self.m110_workaround_slot = None\n        elif self.rx_yeet_slot is not None:\n            self.current_instruction = self.rx_yeet_slot\n            self.rx_yeet_slot = None\n        elif self.recovery_list:\n            self.current_instruction = self.recovery_list.pop()\n        elif self.priority_queue:\n            if self.is_planner_fed() and self.queue:\n                # Invalidate, so the unimportant queue doesn't go all at once\n                self.is_planner_fed.is_fed = False\n                log.debug(\"Allowing a non-important instruction through\")\n                self.current_instruction = self.queue.pop()\n            else:\n                self.current_instruction = self.priority_queue.pop()\n        elif self.queue:\n            self.current_instruction = self.queue.pop()\n\n    # --- If statements in methods ---\n    def can_write(self):\n        \"\"\"Determines whether we're in a state suitable for writing\"\"\"\n        return self.current_instruction is None and not self.is_empty() and \\\n            not self.closed\n\n    def is_empty(self):\n        \"\"\"Determines whether all queues and slots for writing are empty\"\"\"\n        return not self.queue and not self.priority_queue and \\\n            not self.recovery_list and self.rx_yeet_slot is None\\\n            and self.m110_workaround_slot is None\n\n    # --- Actual methods ---\n\n    def _hookup_output_capture(self):\n        \"\"\"\n        Instructions can capture output, this will register the\n        handlers necessary\n        \"\"\"\n        for regexp in self.current_instruction.capturing_regexps:\n            self.serial_parser.add_handler(\n                regexp,\n                self.current_instruction.output_captured,\n                priority=time())\n\n    def _teardown_output_capture(self):\n        \"\"\"\n        Tears down the capturing handlers, so they're not slowing us down\n        and not preventing garbage collection\n        \"\"\"\n        for regexp in self.current_instruction.capturing_regexps:\n            self.serial_parser.remove_handler(\n                regexp, self.current_instruction.output_captured)\n\n    def _send(self):\n        \"\"\"\n        Gets a new instruction and depending on what appears\n        in the handling slot. Tries its best to send it\n        \"\"\"\n        next_instruction = self.peek_next()\n\n        if M110_REGEX.match(next_instruction.message) and \\\n                not self.worked_around_m110:\n            self.m110_workaround_slot = Instruction(\"M400\")\n            self.worked_around_m110 = True\n\n        self._next_instruction()\n        instruction = self.current_instruction\n\n        if instruction.data is None:\n            if instruction.to_checksum:\n                self.send_history.append(instruction)\n                self.message_number += 1\n                if self.message_number == MAX_INT:\n                    self._reset_message_number()\n\n            instruction.fill_data(self.message_number)\n\n        # If the instruction is M110 read the value it'll set and save it\n        m110_match = M110_REGEX.match(instruction.message)\n        if m110_match:\n            self.worked_around_m110 = False\n            self.send_history.clear()\n            log.debug(\"The message number is getting reset\")\n            number = m110_match.group(\"cmd_number\")\n            if number is not None:\n                try:\n                    self.message_number = int(number)\n                except ValueError:\n                    self.message_number = 0\n\n        size = len(instruction.data)\n        if size > self.rx_max:\n            log.warning(\n                \"The data %s we're trying to write is %sB. \"\n                \"But we can only send %sB at most.\",\n                instruction.data.decode('ASCII'), size, self.rx_max)\n\n        self._hookup_output_capture()\n        self.current_instruction.sent()\n\n        # Send the message number only after the instruction is sent\n        if m110_match:\n            self.message_number_changed.send(self.message_number)\n\n        self.serial_adapter.write(self.current_instruction.data)\n\n    def set_message_number(self, number):\n        \"\"\"Sets the message number to the given value\n        Only for power panic recovery\"\"\"\n        with self.write_lock:\n            self.message_number = number\n\n    def replenish_history(self, messages: List[str]):\n        \"\"\"Expects that the message number is set to the current instruction\n        ought to be sent next\"\"\"\n        from_number = self.message_number - (len(messages) - 1)\n        self.send_history.clear()\n        for i, message in enumerate(messages):\n            instruction = Instruction(message, to_checksum=True)\n            instruction.fill_data(from_number + i)\n            self.send_history.append(instruction)\n\n    def _enqueue(self, instruction: Instruction, to_front=False):\n        \"\"\"Internal method for enqueuing when already locked\"\"\"\n        if to_front:\n            self.priority_queue.appendleft(instruction)\n        else:\n            self.queue.appendleft(instruction)\n\n    def enqueue_one(self, instruction: Instruction, to_front=False):\n        \"\"\"\n        Enqueue one instruction\n        Don't interrupt, if anyone else is enqueueing instructions\n        :param instruction: the thing to be enqueued\n        :param to_front: whether to enqueue to front of the queue\n        \"\"\"\n\n        with self.write_lock:\n            log.debug(\"%s enqueued %s\", instruction,\n                      'to the front' if to_front else '')\n\n            self._enqueue(instruction, to_front)\n\n        self._try_writing()\n\n    def enqueue_list(self,\n                     instruction_list: List[MatchableInstruction],\n                     to_front=False):\n        \"\"\"\n        Enqueue list of instructions\n        Don't interrupt, if anyone else is enqueueing instructions\n        :param instruction_list: the list to enqueue\n        :param to_front: whether to enqueue to front of the queue\n        \"\"\"\n\n        with self.write_lock:\n            log.debug(\"Instructions %s enqueued %s\", instruction_list,\n                      'to the front' if to_front else '')\n\n            for instruction in instruction_list:\n                self._enqueue(instruction, to_front)\n\n        self._try_writing()\n\n    # --- Static capture handlers ---\n\n    def _confirmation_handler(self, sender, match: re.Match):\n        \"\"\"Used to do M105 parsing, but that is not supported anymore.\"\"\"\n        assert sender is not None\n        assert match is not None\n        self._confirmed()\n\n    def _resend_handler(self, sender, match: re.Match):\n        \"\"\"\n        The printer can ask for re-sends of past numbered instructions.\n        This method just parses the received match, does a bunch of checks and\n        calls the actual handler resend()\n        \"\"\"\n        assert sender is not None\n        number = int(match.group(\"cmd_number\"))\n        log.info(\"Resend of %s requested. Current is %s\", number,\n                 self.message_number)\n        if self.message_number >= number:\n            if (self.current_instruction is None\n                    or not self.current_instruction.to_checksum):\n                log.warning(\"Re-send requested for a non-numbered message\")\n                # If that happened, the non-numbered message got yeeted from\n                # the buffer, so let's solve that first\n                self._rx_got_yeeted()\n            self._resend((self.message_number - number) + 1)\n        else:\n            log.warning(\"We haven't sent anything with that number yet. \"\n                        \"The communication shouldn't fail after this.\")\n\n    # ---\n\n    def _resend(self, count):\n        \"\"\"If possible, enqueue already sent instruction starting from the one\n        requested back into the recovery list/queue, to be re-sent\"\"\"\n        if not 0 < count < len(self.send_history):\n            log.error(\"Impossible re-send request! Aborting...\")\n            self._worst_case_scenario()\n        else:\n            with self.write_lock:\n                # get the instructions newest first, they are going to reverse\n                # in the list\n                history = list(reversed(self.send_history))\n\n                self.recovery_list.clear()\n                for instruction_from_history in history[:count]:\n                    instruction = Instruction(\n                        instruction_from_history.message,\n                        to_checksum=True,\n                        data=instruction_from_history.data,\n                        number=instruction_from_history.number)\n                    self.recovery_list.append(instruction)\n\n    def _confirmed(self, force=False):\n        \"\"\"\n        Printer confirmed an instruction. Tears down the instruction\n        and prepares the module for processing of a new one\n        \"\"\"\n        if self.current_instruction is None or \\\n                not self.current_instruction.is_sent():\n            log.error(\"Unexpected message confirmation. Ignoring\")\n        elif self.current_instruction.confirm(force=force):\n            if not force:\n                # If a message was successfully confirmed, the rpi port\n                # had to be ok imo\n                RPI_ENABLED.state = CondState.OK\n            self.instruction_confirmed_signal.send(self)\n            with self.write_lock:\n                instruction = self.current_instruction\n\n                # If the instruction did not refuse to be confirmed\n                # Yes, that needs to happen\n                log.debug(\"%s confirmed\", instruction)\n\n                self._teardown_output_capture()\n\n                if instruction.to_checksum:\n                    # Only check those times for check-summed instructions\n                    self.is_planner_fed.process_value(\n                        instruction.time_to_confirm)\n\n                self.current_instruction = None\n        else:\n            InterestingLogRotator.trigger(\"instruction refusing confirmation.\")\n            log.debug(\n                \"%s refused confirmation. Hopefully it has a reason \"\n                \"for that\", self.current_instruction)\n\n        self._try_writing()\n\n    def _rx_got_yeeted(self):\n        \"\"\"\n        Something caused the RX buffer to get thrown out, let's re-send\n        everything supposed to be in it.\n        \"\"\"\n        log.debug(\"Think that RX Buffer got yeeted, sending instruction again\")\n        # Let's bypass the check and write if we can.\n        if self.current_instruction is not None:\n            instruction = self.current_instruction\n            # These two types have to be recovered in their own ways\n            with self.write_lock:\n                self.rx_yeet_slot = instruction\n                self._teardown_output_capture()\n                instruction.reset()\n                self.current_instruction = None\n                self._send()\n\n    def reset_message_number(self):\n        \"\"\"\n        Does not wait for the result, everything that gets enqueued after this\n        will be executed after this. If this is no longer true, stuff will\n        break\n        \"\"\"\n        with self.write_lock:\n            self._reset_message_number()\n\n    def _reset_message_number(self):\n        \"\"\"Sends a massage number reset gcode to the printer\"\"\"\n        instruction = Instruction(\"M110 N0\")\n        self._enqueue(instruction, to_front=True)\n\n    def flush_print_queue(self):\n        \"\"\"\n        Only printing instructions are checksummed, so let's get rid of\n        those. We don't need to confirm them, they shouldn't be waited on.\n        The only component able to wait on them is file printer and that\n        should be stopping when this is called.\n        \"\"\"\n        with self.write_lock:\n            InterestingLogRotator.trigger(\"flushing of the serial queue.\")\n            new_queue = deque()\n            for instruction in self.priority_queue:\n                if not instruction.to_checksum:\n                    new_queue.append(instruction)\n            self.priority_queue = new_queue\n            self.recovery_list.clear()\n            self._throw_out_current_instruction()\n\n    def _flush_queues(self):\n        \"\"\"\n        Tries to get rid of every queue by fake force confirming all\n        instructions, to keep the serial queue consistent for example after\n        a reboot.\n        \"\"\"\n        if self.current_instruction is not None:\n            # To flush the one instruction, that has not yet been confirmed\n            # but has been sent, use the usual way\n            self._throw_out_current_instruction()\n            self._next_instruction()\n        while self.current_instruction is not None:\n            # obviously don't send the other ones,\n            # so they can be handled faster\n            self.current_instruction.sent()\n            self.current_instruction.confirm(force=True)\n            self.current_instruction = None\n            self._next_instruction()\n\n    def _throw_out_current_instruction(self):\n        \"\"\"Throws out the currently executed instruction\"\"\"\n        if self.current_instruction is not None:\n            self.current_instruction.confirm(force=True)\n            self._teardown_output_capture()\n            self.current_instruction = None\n\n    def _worst_case_scenario(self):\n        \"\"\"\n        Everything has failed, let's abandon whatever we were doing and save\n        the printer/user\n        \"\"\"\n        self.has_failed = True\n        log.error(\"Communication failed. Aborting...\")\n        RPI_ENABLED.state = CondState.NOK\n        self.serial_queue_failed.send(self)\n\n    def printer_reconnected(self, was_printing, was_power_panic):\n        \"\"\"The printer reset, starts a thread to recover the serial queue\n        from such a state\"\"\"\n        Thread(target=self._printer_reconnected,\n               args=(was_printing, was_power_panic),\n               name=\"serial_queue_reset_thread\").start()\n\n    def _printer_reconnected(self, was_printing, was_power_panic):\n        \"\"\"\n        Printer resets for two reasons, it has been stopped by the user,\n        or the serial communication failed.\n\n        Either way, the old instructions inside the serial queue are now\n        useless. This method flushes the queues and depending on what caused\n        the error, moves the printer head up, or demands user attention.\n        \"\"\"\n        prctl_name()\n        with self.write_lock:\n            self._flush_queues()\n            self._block_sending = False\n\n            final_instruction = None\n\n            if self.has_failed:\n                beep_instruction = Instruction(\"M300 S880 P200\")\n                self._enqueue(beep_instruction, to_front=True)\n                stop_instruction = Instruction(\"M603\")\n                self._enqueue(stop_instruction, to_front=True)\n                message_instruction = Instruction(\"M1 FW COMM ERR. Aborted\")\n                self._enqueue(message_instruction, to_front=True)\n                final_instruction = message_instruction\n                self.has_failed = False\n            elif was_printing and not was_power_panic:\n                stop_instruction = Instruction(\"M603\")\n                self._enqueue(stop_instruction, to_front=True)\n                final_instruction = stop_instruction\n\n        if final_instruction is not None:\n            self._try_writing()\n            while not self.closed:\n                if final_instruction.wait_for_confirmation(\n                        timeout=QUIT_INTERVAL):\n                    break\n\n\nclass MonitoredSerialQueue(SerialQueue):\n    \"\"\"Separates the queue monitoring into a different class.\"\"\"\n\n    def __init__(self,\n                 serial_adapter: SerialAdapter,\n                 serial_parser: ThreadedSerialParser,\n                 threshold_path: str,\n                 rx_size=128):\n        super().__init__(serial_adapter, serial_parser,\n                         threshold_path, rx_size)\n\n        self.stuck_counter = 0\n\n        self.serial_parser.add_handler(\n            BUSY_REGEX, lambda sender, match: self._renew_timeout())\n        self.serial_parser.add_handler(\n            ATTENTION_REGEX, lambda sender, match: self._renew_timeout())\n        self.serial_parser.add_handler(\n            HEATING_REGEX, lambda sender, match: self._renew_timeout())\n        self.serial_parser.add_handler(\n            HEATING_HOTEND_REGEX, lambda sender, match: self._renew_timeout())\n\n        # Remember when the last write or confirmation happened\n        # If we want to time out, the communication has to be dead for some\n        # time\n        # Useful only with unbuffered messages\n        self.last_event_on = time()\n        self.monitoring_thread = Thread(target=self.keep_monitoring,\n                                        name=\"sq_stall_recovery\",\n                                        daemon=True)\n        self.monitoring_thread.start()\n\n    def get_current_delay(self):\n        \"\"\"\n        If we are waiting on an instruction to be confirmed, returns the\n        time we've been waiting\n        \"\"\"\n        if self.is_empty() and self.current_instruction is None:\n            return 0\n        return time() - self.last_event_on\n\n    def keep_monitoring(self):\n        \"\"\"Runs the loop of monitoring the queue\"\"\"\n        prctl_name()\n        loop_until(self.quit_evt, lambda: SERIAL_QUEUE_MONITOR_INTERVAL,\n                   self.check_status)\n\n    def check_status(self):\n        \"\"\"\n        Called periodically. If the confirmation wait times out, calls\n        the appropriate handler\n        \"\"\"\n        if self.get_current_delay() > SERIAL_QUEUE_TIMEOUT and SERIAL:\n            # The printer did not respond in time, lets assume it forgot\n            # what it was supposed to do\n            log.info(\"Timed out waiting for confirmation of %s after %ssec.\",\n                     self.current_instruction, SERIAL_QUEUE_TIMEOUT)\n            log.debug(\"Assuming the printer yeeted our RX buffer\")\n            self.stuck_counter += 1\n            if self.stuck_counter > 2:\n                log.warning(\"Closing the serial, because it's stuck\")\n                self.serial_adapter.close()\n            InterestingLogRotator.trigger(\"a stuck instruction\")\n            self._rx_got_yeeted()\n            self._renew_timeout(unstuck=False)\n\n    def stop(self):\n        \"\"\"\n        Stops the monitoring thread\n        If not required to go fast, saves the planner fed threshold\n        \"\"\"\n        super().stop()\n        self.is_planner_fed.save()\n\n    def wait_stopped(self):\n        \"\"\"Waits for the serial queue to stop\"\"\"\n        super().stop()\n        self.monitoring_thread.join()\n\n    def _confirmed(self, force=False):\n        \"\"\"Adds a timeout renewal onto an instruction confirmation\"\"\"\n        self._renew_timeout()\n        super()._confirmed(force=force)\n\n    def _renew_timeout(self, unstuck=True):\n        \"\"\"Renews the instruction confirmation \"\"\"\n        self.last_event_on = time()\n        if unstuck:\n            self.stuck_counter = 0\n"
  },
  {
    "path": "prusa/link/service_discovery.py",
    "content": "\"\"\"\nImplements the things for service discovery\nAs of now only DNS-SD is supported\n\"\"\"\nimport logging\nimport socket\nfrom time import sleep\nfrom urllib.error import HTTPError, URLError\nfrom urllib.request import Request, urlopen\n\nimport zeroconf\nfrom zeroconf import NonUniqueNameException, ServiceInfo, Zeroconf\n\nfrom .const import SELF_PING_RETRY_INTERVAL, SELF_PING_TIMEOUT, instance_id\nfrom .interesting_logger import InterestingLogRotator\nfrom .printer_adapter.updatable import Thread\nfrom .util import prctl_name\n\nlog = logging.getLogger(__name__)\n\n\nclass ServiceDiscovery:\n    \"\"\"\n    A class implementing methods for easy registration of PrusaLink as\n    a network service to be discoverable by prusa-slicer and alike\n    \"\"\"\n\n    def __init__(self, port):\n        \"\"\"Loads configuration and inits Zeroconf\"\"\"\n        # Leave out service discovery logs from the interesting log\n        # was sending too many messages\n        InterestingLogRotator.get_instance().skip_logger(zeroconf._logger.log)\n        self.zeroconf = Zeroconf()\n        self.port = port\n        self.hostname = socket.gethostname()\n        self.number = 0\n\n        self.thread = Thread(target=self._register,\n                             daemon=True,\n                             name=\"zeroconf\")\n        self.thread.start()\n\n    @staticmethod\n    def _get_port_part(port):\n        \"\"\"Return the port part of an url\"\"\"\n        return \"\" if int(port) == 80 else f\":{port}\"\n\n    def is_on_port(self, port):\n        \"\"\"Check, if the same instance is presented on the specified port\"\"\"\n        port_part = self._get_port_part(port)\n        url = f\"http://127.0.0.1{port_part}\"\n        request = Request(url, method=\"HEAD\")\n        try:\n            with urlopen(request, timeout=SELF_PING_TIMEOUT) as response:\n                return response.headers[\"Instance-ID\"] == str(instance_id)\n        except (HTTPError, URLError, socket.timeout):\n            return False\n\n    def _register(self):\n        \"\"\"\n        Registers services provided by us to be discoverable\n\n        one _octoprint for \"legacy\" prusa-slicer support\n        one _http, because we have a web server\n        and one _prusa-link because why not\n        \"\"\"\n        prctl_name()\n        # Wait for our own instance to be reachable on the configured port\n        # if not, just try again\n        while not self.is_on_port(self.port):\n            log.warning(\n                \"Can't reach our own instance at the configured \"\n                \"port: %s. If just initialising, this is normal\", self.port)\n            sleep(SELF_PING_RETRY_INTERVAL)\n\n        # Try to connect using the default http port\n        register_port = self.port\n        if self.is_on_port(80):\n            # if successful, register the 80 we are being forwarded to\n            register_port = 80\n            log.debug(\"Reached our own instance at the port 80, \"\n                      \"running as root or being forwarded, awesome!\")\n\n        self._register_service(\"PrusaLink\", \"prusalink\", register_port)\n        self._register_service(\"PrusaLink\", \"http\", register_port)\n\n        # legacy slicer support\n        self._register_service(\"PrusaLink\", \"octoprint\", register_port)\n\n    def unregister(self):\n        \"\"\"Unregisters all services\"\"\"\n        self.zeroconf.unregister_all_services()\n\n    def _register_service(self, name, service_type, port):\n        \"\"\"\n        Registers one service given its name and type\n\n        param name: name of the service, can contain fairly fancy characters\n        param service_type: The DNS-SD service type. A list can be found here\n            http://www.dns-sd.org/ServiceTypes.html\n            https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml\n        \"\"\"\n        number = self.number\n        while True:\n            port_part = self._get_port_part(port)\n            name_to_use = f\"{name} at {self.hostname}{port_part}\"\n            if number > 0:\n                name_to_use += f\" ({number})\"\n            try:\n                info = ServiceInfo(type_=f\"_{service_type}._tcp.local.\",\n                                   name=f\"{name_to_use}._{service_type}\"\n                                   f\"._tcp.local.\",\n                                   port=port,\n                                   server=f\"{self.hostname}.local\",\n                                   properties={\"path\": \"/\"})\n                self.zeroconf.register_service(info)\n            except NonUniqueNameException:\n                number += 1\n            else:\n                break\n        self.number = number\n        if number > 0:\n            log.warning(\"Registered service named identically to others #%s\",\n                        number)\n        log.debug(\n            \"Registered service name: %s, type: %s, port: %s, \"\n            \"server: %s\", info.name, info.type, info.port, info.server)\n"
  },
  {
    "path": "prusa/link/static/css/bootstrap.connect.css",
    "content": "a {\r\n    color: #fa6831 !important;\r\n}\r\n\r\n.flex-even {\r\n    flex: 1;\r\n}\r\n\r\nsection {\r\n    margin-top: 10px;\r\n}\r\n\r\n#mainmenu {\r\n    border-bottom: 1px solid #e0e0e0;\r\n    padding-top: 0 !important;\r\n    padding-bottom: 0 !important;\r\n}\r\n\r\n.nav-link {\r\n    font-family: Helvetica, sans-serif;\r\n    font-size: 15px;\r\n    color: #2a2a2a !important;\r\n}\r\n\r\n.nav-link.disabled {\r\n    color: #a0a0a0 !important;\r\n}\r\n\r\n.nav-link:hover {\r\n    color: #fa6831 !important;\r\n}\r\n\r\n.nav-tabs {\r\n    border-bottom: 1px solid #707070;\r\n}\r\n\r\n.nav-tabs .nav-link {\r\n    padding: 0.8em 1.3em 0.7em 1.2em;\r\n    text-transform: uppercase;\r\n    color: #585858 !important;\r\n}\r\n\r\n.nav-tabs .nav-link:hover {\r\n    color: #fa6831 !important;\r\n}\r\n\r\n.nav-tabs .nav-link.active:hover {\r\n    color: #585858 !important;\r\n}\r\n\r\n.nav-tabs .nav-link:first-child {\r\n    margin-left: 1em;\r\n}\r\n\r\n.nav-tabs .nav-link:last-child {\r\n    margin-right: 1em;\r\n}\r\n\r\n.nav-tabs .nav-link {\r\n    border-top-left-radius: 0;\r\n    border-top-right-radius: 0\r\n}\r\n\r\n.nav-tabs .nav-link:focus,\r\n.nav-tabs .nav-link:hover {\r\n    border-color: #e9ecef #e9ecef #707070;\r\n}\r\n\r\n.nav-tabs .nav-item.show .nav-link,\r\n.nav-tabs .nav-link.active {\r\n    color: #495057;\r\n    background-color: #fff;\r\n    border-color: #707070 #707070 #fff\r\n}\r\n\r\n.breadcrumb {\r\n    padding: 0 1rem;\r\n    margin-bottom: 0.5rem;\r\n    background-color: transparent;\r\n    border-radius: 0;\r\n    font-size: 0.9em;\r\n    color: black;\r\n}\r\n\r\n.breadcrumb-item .active {\r\n    color: black;\r\n    font-family: Helvetica, sans-serif;\r\n    font-size: 0.9em;\r\n}\r\n\r\n.breadcrumb-item a {\r\n    font-size: 0.9em;\r\n    color: #797979 !important;\r\n    font-family: Helvetica, sans-serif;\r\n}\r\n.btn {\r\n    font-family: Helvetica, sans-serif;\r\n    border: 1px solid transparent;\r\n    padding: 0.3em 0.75em;\r\n    white-space: pre-line;\r\n    word-break: break-word;\r\n}\r\n\r\n.btn img {\r\n    position: relative;\r\n    top: -2px;\r\n    padding: 0;\r\n}\r\n\r\n.btn svg.append {\r\n    margin-right: 0;\r\n    margin-left: 0.25em;\r\n}\r\n\r\n.btn-group .btn {\r\n    border-right-width: 0;\r\n}\r\n\r\n.btn-group .btn:last-child {\r\n    border-right-width: 1px;\r\n}\r\n\r\n.btn-outline-primary,\r\n.btn-outline-secondary,\r\n.btn-outline-success,\r\n.btn-outline-warning,\r\n.btn-outline-danger,\r\n.btn-outline-info,\r\n.btn-outline-light,\r\n.btn-outline-dark {\r\n    border-color: white;\r\n    color: white;\r\n}\r\n\r\n.btn-outline-primary:hover,\r\n.btn-outline-secondary:hover,\r\n.btn-outline-success:hover,\r\n.btn-outline-warning:hover,\r\n.btn-outline-danger:hover,\r\n.btn-outline-info:hover,\r\n.btn-outline-light:hover,\r\n.btn-outline-dark:hover {\r\n    color: black !important;\r\n}\r\n\r\n.btn-back {\r\n    color: #585858 !important;\r\n}\r\n"
  },
  {
    "path": "prusa/link/static/css/bootstrap.prusa-link.css",
    "content": "body,\nhtml {\n    font-size: 18px;\n    color: #7a7a7a;\n    background-color: hsl(0, 0%, 4%);\n    height: 100%;\n    font-family: Helvetica, sans-serif;\n}\n\na {\n    color: #fff !important;\n}\n\na.active {\n    text-decoration: underline !important;\n}\n\nh1,\nh2,\nh3,\nh4 {\n    color: #f5f5f5;\n    margin-top: 2em;\n    margin-bottom: 2em;\n}\n\nh1 {\n    font-size: 28px;\n}\n\nh2 {\n    font-size: 22px;\n}\n\nh3 {\n    font-size: 18px;\n}\n\npre {\n    color: #7a7a7a;\n}\n\np>b {\n    color: #f5f5f5;\n}\n\n.jumbotron {\n    background-color: hsl(0, 0%, 10%);\n}\n\n.nav-link {\n    color: #f5f5f5 !important;\n    font-size: 18px;\n    border-bottom: .1rem solid transparent;\n}\n\n.navbar-logo {\n    background: white url(\"/img/prusa-link-logo.svg\") no-repeat 50%;\n    background-size: cover;\n    width: 288px;\n    height: 69px;\n    bottom: 3px;\n    display: flex;\n}\n\n.white {\n    color: #f5f5f5;\n}\n\n.orange {\n    color: #f9c129\n}\n\n.progress {\n    margin-bottom: 3em;\n}\n\n.progress,\n.progress-bar {\n    overflow: visible;\n}\n\n.progress-bar a {\n    position: relative;\n    top: 2em;\n    text-decoration: underline;\n    text-decoration-color: #6c757d;\n}\n\n.progress-bar a:hover {\n    text-decoration: none;\n}\n\n.align-center {\n    text-align: center;\n}\n\n.text-muted {\n    font-size: 0.75em;\n}\n\n.img-center {\n    text-align: center;\n}\n.navigation {\n    margin-top: 2em;\n}\n\ncode {\n    color: white;\n}\n\n.api-key {\n    font-size: 22px;\n    cursor: copy;\n}\n\n.api-key img {\n    margin-left: 0.5em;\n}\n\n.condensed {\n     margin-bottom: 1em;\n}\n\n#ports_section {display: none;}\n\n#ports_section_show{\n        color: #ffffff;\n        cursor:pointer;\n}\n#ports_section_show:hover{\n       color: #f2f2f2;\n}\n\n@media(max-width: 768px) {\n    input.full-width,\n    button.full-width,\n    a.full-width {\n        width: 100%;\n        margin-bottom: 0.5em;\n    }\n}\n"
  },
  {
    "path": "prusa/link/static/index.html",
    "content": "<!doctype html><html><head><meta charset=\"utf-8\"/><meta name=\"theme-color\" content=\"#fff\"/><link rel=\"icon\" href=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; xml:space=&#39;preserve&#39; width=&#39;25.553&#39; height=&#39;25.555&#39; style=&#39;shape-rendering:geometricPrecision%3btext-rendering:geometricPrecision%3bimage-rendering:optimizeQuality%3bfill-rule:evenodd%3bclip-rule:evenodd&#39; viewBox=&#39;0 0 42.47 42.47&#39;%3e%3cpath d=&#39;M0 0h42.47v42.47H0z&#39; style=&#39;fill:%23fa6831&#39;/%3e%3cpath d=&#39;M13.48 36.05h19.71v-6.08H20.55V8.93h-7.07z&#39; style=&#39;fill:%232b2a29%3bfill-rule:nonzero&#39;/%3e%3cpath d=&#39;M12.23 34.79h19.71v-6.07H19.29V7.68h-7.06z&#39; style=&#39;fill:%23fefefe%3bfill-rule:nonzero&#39;/%3e%3c/svg%3e\" type=\"image/svg+xml\"><title>PrusaLink</title><script defer=\"defer\" src=\"main.9b8dc0068f6e6508dfd4.js\"></script><link href=\"main.b3e029296dd89863b3f2.css\" rel=\"stylesheet\"></head><body><div class=\"header txt-bold\"><div class=\"header__line\"></div><div class=\"content\"><nav><a class=\"navbar-burger\" id=\"menu\"><div></div><div></div><div></div></a><div class=\"logo-wrapper\"><a href=\"/#dashboard\" class=\"brand-logo\"><img src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;174.199&#39; height=&#39;41.946&#39;%3e%3cpath fill=&#39;%23fefefe&#39; fill-rule=&#39;evenodd&#39; d=&#39;M0 12.254h97.344v29.692H0Zm172.083 0h-70.745v29.692h70.745Z&#39; data-name=&#39;Path 309&#39;/%3e%3cpath fill=&#39;%232b2a29&#39; d=&#39;M107.415 36.348h13.537v-4.173h-8.686v-14.45h-4.852ZM9.78 21.611h3.26c1.487 0 2.608.678 2.608 2.452 0 1.7-1.069 2.321-2.608 2.321H9.78ZM4.928 36.348H9.78V30.14h3.991c4.1 0 6.73-1.8 6.73-6.416 0-4.408-3.26-6-6.73-6H4.928v18.623Zm21.9-14.737h3.86c1.356 0 2.452.626 2.452 2.008a1.985 1.985 0 0 1-2.243 2.191h-4.069v-4.2Zm-4.852 14.737h4.852v-6.782h3.678c2.3 0 2.582 1.93 2.712 3.808a9.724 9.724 0 0 0 .548 2.974h4.8c-.652-.73-.7-3.391-.782-4.2-.131-1.774-.756-3.964-2.713-4.46v-.052a4.752 4.752 0 0 0 2.921-4.669c0-3.756-3.26-5.243-6.521-5.243h-9.494v18.623Zm34.1-18.623h-4.851v11.216c0 2.4-.861 3.625-3.26 3.625s-3.26-1.226-3.26-3.625V17.724h-4.852v10.929c0 5.4 2.478 8.086 8.112 8.086s8.112-2.686 8.112-8.086Zm1.343 12.364c0 4.773 3.834 6.651 8.086 6.651 4.095 0 8.112-1.513 8.112-6.234 0-3.365-2.712-4.617-5.4-5.373-2.712-.756-5.4-.991-5.4-2.452 0-1.226 1.3-1.591 2.348-1.591 1.461 0 3.078.574 3 2.243h4.851c0-4.173-3.782-6-7.46-6-3.5 0-7.59 1.591-7.59 5.66 0 3.443 2.817 4.617 5.451 5.373 2.686.756 5.347 1.044 5.347 2.712 0 1.382-1.513 1.9-2.974 1.9-2.086 0-3.417-.7-3.521-2.9h-4.851Zm15.741 6.26h4.982l1.174-3.339h6.495l1.148 3.339h5.06l-6.964-18.623h-4.929Zm9.39-13.459h.052l2.06 6.521h-4.224l2.113-6.521Zm39.979 13.459h4.852V17.724h-4.852Zm7.342 0h4.564V24.741h.052l6.468 11.607h4.982V17.724h-4.564v11.479h-.052l-6.469-11.476h-4.982Zm18.558 0h4.852v-5.738l1.982-2.086 5.008 7.825h6.025l-7.747-11.294 6.886-7.33h-6.025l-6.13 7.147v-7.147h-4.852v18.623Z&#39; data-name=&#39;Path 310&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; fill-rule=&#39;evenodd&#39; d=&#39;M174.199 0h-41.724v15.461h41.724Z&#39; data-name=&#39;Path 311&#39;/%3e%3cpath fill=&#39;%23fefefe&#39; d=&#39;M139.06 4.917h1.991c.539 0 1.051.235 1.051.954a.959.959 0 0 1-1.051.94h-1.991Zm-2.572 7.881h4.742c2.116 0 3.844-.719 3.844-3.07a2.354 2.354 0 0 0-1.7-2.337 2.22 2.22 0 0 0 1.3-1.991c0-2.046-1.867-2.516-3.567-2.475h-4.619v9.872Zm2.572-4.328h2.06c.774 0 1.383.29 1.383 1.161 0 .816-.566 1.106-1.383 1.106h-2.06Zm6.975 4.328h8.005v-2.212h-5.434v-1.88h4.839V6.714h-4.839V4.991h5.3v-2.06h-7.867Zm16.861-9.872h-8.461v2.212h2.945v7.66h2.571v-7.66h2.945Zm-1.583 9.872h2.641l.622-1.77h3.442l.609 1.77h2.682l-3.691-9.872h-2.613Zm4.978-7.135h.028l1.092 3.457h-2.24l1.121-3.457Z&#39; data-name=&#39;Path 312&#39;/%3e%3c/svg%3e\"/></a></div><select id=\"lang-dropdown\" data-type=\"dropdown\"></select><ul class=\"burger-menu\" id=\"navbar\"><li><a href=\"#dashboard\" data-label=\"home.link\">Dashboard</a></li><li><a href=\"#files\" data-label=\"proj.link\">Storage</a></li><li><a href=\"#control\" data-label=\"control.link\">Control</a></li><li><a href=\"#cameras\" data-label=\"cameras.link\">Cameras</a></li><div class=\"separator\"></div><li><a href=\"#settings\" data-label=\"settings.title\">Settings</a></li></ul></nav></div></div><div class=\"content\"><div class=\"content-wrapper\"><div id=\"telemetry-wrapper\"><div class=\"telemetry\"><div class=\"tel-prop\" id=\"conn-status-connect\" hidden=\"true\"><img class=\"icon icon-success\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;24&#39; height=&#39;24&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 50.68 50.68&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M0 0h50.68v50.68H0z&#39;/%3e%3cpath fill=&#39;%232b2a29&#39; fill-rule=&#39;nonzero&#39; d=&#39;M42.3 22.22c-.23-7.8-7.75-12.24-14.78-12.24-9.84 0-16.14 7.48-16.14 16.86 0 9.38 6.3 16.86 16.14 16.86 8.2 0 14.28-4.85 14.78-13.24h-8.43c-.32 3.76-2.54 5.98-6.53 5.98-5.44 0-7.52-4.76-7.52-9.61s2.09-9.61 7.52-9.61c3.58 0 6.03 1.72 6.53 4.99h8.43z&#39;/%3e%3cpath fill=&#39;white&#39; fill-rule=&#39;nonzero&#39; d=&#39;M40.8 20.72c-.23-7.8-7.75-12.24-14.78-12.24-9.84 0-16.14 7.48-16.14 16.86 0 9.38 6.3 16.86 16.14 16.86 8.2 0 14.28-4.85 14.78-13.24h-8.43c-.32 3.76-2.54 5.98-6.53 5.98-5.44 0-7.52-4.76-7.52-9.61s2.09-9.61 7.52-9.61c3.58 0 6.03 1.72 6.53 4.99h8.43z&#39;/%3e%3c/svg%3e\"/> <img class=\"icon icon-warning\" src=\"dfb811cc28f8e3d7c14bcedbe7ef35b8.svg\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"conn.prusa-connect-status\">PRUSA CONNECT connection status</p><div class=\"flex-row justify-between\"><p class=\"txt-bold txt-md\" id=\"conn-status-connect-msg\">NA</p><button class=\"info-message-tooltip tooltip-handle\"><img class=\"icon icon-warning icon-small\" src=\"755aa24665769159d2c1134183bae174.svg\"/> <span id=\"conn-status-connect-tooltip\" class=\"txt-sm\"></span></button></div></div></div><div class=\"tel-prop\" id=\"conn-status-printer\" hidden=\"true\"><img class=\"icon icon-success\" src=\"b891ace1622f34bac5a2b4edb7adc733.svg\"/> <img class=\"icon icon-warning\" src=\"dfb811cc28f8e3d7c14bcedbe7ef35b8.svg\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"conn.printer-status\">PRINTER connection status</p><div class=\"flex-row justify-between\"><p class=\"txt-bold txt-md\" id=\"conn-status-printer-msg\">NA</p><button class=\"info-message-tooltip tooltip-handle\"><img class=\"icon icon-warning icon-small\" src=\"755aa24665769159d2c1134183bae174.svg\"/> <span id=\"conn-status-printer-tooltip\" class=\"txt-sm\"></span></button></div></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"997c391425907810b4a6e42663d11fd4.svg\"><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"home.title\">Printer status</p><p id=\"printer-status\" class=\"txt-bold txt-md\">NA</p></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;12.389&#39; height=&#39;15.439&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 7.2 8.97&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M4.16 4.4v1.1a.56.56 0 0 1-.56.56H1.12v.34h5.53a.56.56 0 0 1 .56.56v1.45a.56.56 0 0 1-.56.56H.57a.56.56 0 1 1 0-1.12H6.1v-.34H.57a.56.56 0 0 1-.56-.56V5.5a.56.56 0 0 1 .56-.56h2.48V4.4h1.12z&#39;/%3e%3cpath fill=&#39;white&#39; d=&#39;M6.13 0v2.06L4.34 4.4H2.86l-1.8-2.33V0H2.2v1.68l1.2 1.6h.4L5 1.68V0z&#39;/%3e%3c/svg%3e\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"prop.temp-nozzle\">Nozzle Temperature</p><p class=\"txt-bold txt-md tel-value\"><span data-type=\"telemetry\" data-format=\"temp\" data-where=\"telemetry.temperature.nozzle.current\" data-zeroes=\"hide\">NA</span> <span data-type=\"telemetry\" data-format=\"temp\" data-where=\"telemetry.temperature.nozzle.target\" data-zeroes=\"hide\">NA</span></p></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;13.875&#39; height=&#39;11.267&#39; fill-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 10.08 8.18&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M3.12 5.02c-.8-.78-.9-1.24-.2-2.13.24-.3.35-.62.35-.92C3.27 1.34 2.8.72 2.2 0l-.95.9c.9 1.05.82 1.16.17 2.08-.2.3-.3.6-.3.9 0 .76.57 1.48 1.1 2.02l.9-.87zm5.7 0c-.8-.78-.9-1.24-.2-2.13.24-.3.35-.62.35-.92C8.96 1.34 8.5.72 7.9 0l-.95.9c.9 1.05.82 1.16.17 2.08-.2.3-.3.6-.3.9 0 .76.57 1.48 1.1 2.02l.9-.87zm-2.88 0c-.8-.78-.9-1.24-.2-2.13.24-.3.35-.62.35-.92 0-.63-.46-1.25-1.06-1.97l-.95.9c.9 1.05.82 1.16.17 2.08-.2.3-.3.6-.3.9 0 .76.57 1.48 1.1 2.02l.9-.87z&#39;/%3e%3cpath fill=&#39;white&#39; fill-rule=&#39;nonzero&#39; d=&#39;M0 6.8h10.08v1.4H0z&#39;/%3e%3c/svg%3e\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"prop.temp-bed\">Heatbed</p><p class=\"txt-bold txt-md tel-value\"><span data-type=\"telemetry\" data-format=\"temp\" data-where=\"telemetry.temperature.bed.current\" data-zeroes=\"hide\">NA</span> <span data-type=\"telemetry\" data-format=\"temp\" data-where=\"telemetry.temperature.bed.target\" data-zeroes=\"hide\">NA</span></p></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;49.999&#39; height=&#39;37.5&#39; fill-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 1028.03 771.02&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M1028.03 514c0 93.56-25.1 181.33-68.97 256.96L884.7 728c36.46-63 57.65-135.96 57.65-214C942.35 277.42 750.6 85.66 514 85.66 277.44 85.66 85.68 277.42 85.68 514c0 78.05 21.2 151 57.65 214l-74.36 42.97C25.1 695.32 0 607.55 0 514 0 230.6 230.6 0 514 0c283.43 0 514.02 230.6 514.02 514zM514 771.02c-55.43 0-100.35-44.92-100.35-100.35 0-37.82 20.95-70.76 51.82-87.86L514 342.67l48.54 240.08c30.92 17.1 51.82 50.03 51.82 87.86 0 55.47-44.92 100.4-100.36 100.4z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M541.77 156.87c-9.37-.78-18.7-1.1-27.8-1.1-9.08 0-18.46.4-27.84 1.1v119.25c9.26-1.06 18.6-1.63 27.84-1.63 9.26 0 18.6.5 27.8 1.63V156.87zM789.83 287.9a377.26 377.26 0 0 0-39.32-40.99l-80.97 84.35c14.52 12.08 27.72 25.87 39.32 41l80.97-84.35zm-111.4-91.23c-16.58-8.77-33.84-16.23-51.4-22.22l-43.86 110.22a240.68 240.68 0 0 1 51.4 22.22l43.86-110.22zM274.3 462.24c5.18-18.63 12.33-36.6 21.34-53.54l-105.77-45.65a384.14 384.14 0 0 0-21.34 53.53l105.77 45.65zm118.2-154.9a240.824 240.824 0 0 1 51.32-22.4l-44.3-110.04a357.044 357.044 0 0 0-51.32 22.44l44.3 110zm-73.47 64.9a254.052 254.052 0 0 1 39.38-41l-80.97-84.35c-14.05 12.6-27.28 26.34-39.32 41l80.9 84.35zm538.57 38.68c-6.04-18.3-13.45-36.15-22.1-53.2L730.37 405.1c9.3 16.87 16.7 34.7 22.15 53.2l105.07-47.37z&#39;/%3e%3c/svg%3e\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"prop.speed\">Printing Speed</p><p class=\"txt-bold txt-md\" data-type=\"telemetry\" data-format=\"print\" data-where=\"telemetry.speed\">NA</p></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;36.544&#39; height=&#39;36.51&#39; fill-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 205.88 205.69&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M39.04 71.14 54.1 56.07l48.84 49.72 48.8-49.72 15.06 15.07-63.86 64.78z&#39;/%3e%3cpath fill=&#39;white&#39; fill-rule=&#39;nonzero&#39; d=&#39;M76.86 54.62 113 10.9H77.85V0h49.5v9.37l-35 43.73h35.25V64H76.86zM0 177.2h205.88v28.5H0z&#39;/%3e%3c/svg%3e\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"prop.z-height\">Z - Height</p><p class=\"txt-bold txt-md\" data-type=\"telemetry\" data-format=\"pos\" data-where=\"telemetry.axis.z\">NA</p></div></div><div class=\"tel-prop\"><img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;64&#39; height=&#39;64&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 800.06 800.06&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M139.99 43.48c.01-14.73 11.96-26.68 26.69-26.68s26.68 11.95 26.68 26.68l-.01 221.04h413.37V43.48c0-14.73 11.95-26.68 26.68-26.68s26.68 11.95 26.68 26.68v221.04h113.3c14.73.03 26.65 11.96 26.68 26.68l.01 264.94c-.03 14.71-11.96 26.64-26.69 26.67l-125.46.01-161.77 189.46a26.822 26.822 0 0 1-21.45 10.96l-129.17.01a27.07 27.07 0 0 1-20.26-9.47L152.15 582.82H26.69C11.97 582.78.04 570.85.02 556.13L0 291.2c.04-14.72 11.97-26.65 26.69-26.68h113.3V43.48zm409.99 486.15V318.04h-299.9v211.59h299.9zm53.36-211.59v211.59H746.7V318.04H603.34zM196.56 529.63V318.04H53.2v211.59h143.36zm25.5 53.36L347.53 729.9h104.86l125.47-146.91-355.62.01h-.17z&#39;/%3e%3c/svg%3e\"/><div class=\"value\"><p class=\"txt-sm txt-grey\" data-label=\"prop.nozzle-diameter\">Nozzle Diameter</p><p class=\"txt-bold txt-md\" data-type=\"telemetry\" data-format=\"diameter\" data-where=\"printer.nozzleDiameter\">NA</p></div></div></div></div><div class=\"main\" id=\"root\"></div></div></div><div class=\"modal\"><div class=\"modal-box\"></div></div><template id=\"dropdown-template\"><div class=\"dropdown\"><div class=\"dropdown-btn\"><span class=\"dropdown-label\">Dropdown</span> <img src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;36.544&#39; height=&#39;36.51&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;m.58 11.406 4.172-4.152 13.526 13.7 13.515-13.7 4.17 4.152-17.685 17.85L.58 11.406z&#39;/%3e%3c/svg%3e\"/></div><div class=\"dropdown-content\"><ul></ul></div></div></template><template id=\"modal-welcome\"><div class=\"modal-welcome\"><img src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; xml:space=&#39;preserve&#39; width=&#39;284.434&#39; height=&#39;49.078&#39; style=&#39;shape-rendering:geometricPrecision%3btext-rendering:geometricPrecision%3bimage-rendering:optimizeQuality%3bfill-rule:evenodd%3bclip-rule:evenodd&#39; viewBox=&#39;0 0 5351.38 923.36&#39;%3e%3cdefs%3e%3cstyle%3e.fil0%7bfill:%23fefefe%7d.fil1%7bfill:%232b2a29%3bfill-rule:nonzero%7d%3c/style%3e%3c/defs%3e%3cg id=&#39;Layer_x0020_1&#39;%3e%3cg id=&#39;_1905460437312&#39;%3e%3cpath d=&#39;M0 0h3027.19v923.36H0V0zm304.12 290.95h101.39c46.23 0 81.11 21.08 81.11 76.24 0 52.72-33.25 72.19-81.11 72.19H304.12V290.95zM153.25 749.23h150.87V556.18h124.1c127.35 0 209.27-55.97 209.27-199.54 0-137.08-101.39-186.56-209.27-186.56H153.25v579.14zm680.94-458.28h120.05c42.18 0 76.24 19.46 76.24 62.45 0 44.61-24.33 68.14-69.75 68.14H834.19V290.95zM683.32 749.23h150.87V538.34h114.37c71.38 0 80.3 60.02 84.36 118.42 2.43 17.04 4.05 72.19 17.03 92.47h149.25c-20.28-22.71-21.9-105.45-24.33-130.59-4.06-55.16-23.52-123.29-84.36-138.7v-1.62c61.65-23.52 90.85-82.73 90.85-145.19 0-116.8-101.39-163.04-202.78-163.04H683.33v579.14zm1060.54-579.14H1593v348.79c0 74.62-26.77 112.74-101.39 112.74s-101.39-38.12-101.39-112.74V170.09h-150.87v339.86c0 167.9 77.05 251.45 252.26 251.45 175.2 0 252.26-83.55 252.26-251.45V170.09zm41.78 384.48c0 148.43 119.23 206.83 251.45 206.83 127.34 0 252.26-47.05 252.26-193.86 0-104.64-84.36-143.57-167.9-167.09-84.36-23.52-167.9-30.83-167.9-76.25 0-38.12 40.55-49.48 73-49.48 45.42 0 95.71 17.85 93.28 69.76h150.86c0-129.78-117.61-186.56-231.98-186.56-108.69 0-236.04 49.48-236.04 176.02 0 107.07 87.6 143.57 169.52 167.09 83.55 23.52 166.28 32.45 166.28 84.36 0 42.99-47.05 59.21-92.47 59.21-64.89 0-106.26-21.9-109.5-90.03h-150.86zm489.5 194.67h154.92l36.5-103.83h201.97l35.69 103.83h157.36L2645 170.1h-153.3l-216.58 579.14zm292.01-418.54h1.62l64.08 202.78h-131.4l65.7-202.78z&#39; class=&#39;fil0&#39;/%3e%3cpath d=&#39;M304.12 290.95h101.39c46.23 0 81.11 21.08 81.11 76.24 0 52.72-33.25 72.19-81.11 72.19H304.12V290.95zM153.25 749.23h150.87V556.18h124.1c127.35 0 209.27-55.97 209.27-199.54 0-137.08-101.39-186.56-209.27-186.56H153.25v579.14zm680.94-458.28h120.05c42.18 0 76.24 19.46 76.24 62.45 0 44.61-24.33 68.14-69.75 68.14H834.19V290.95zM683.32 749.23h150.87V538.34h114.37c71.38 0 80.3 60.02 84.36 118.42 2.43 17.04 4.05 72.19 17.03 92.47h149.25c-20.28-22.71-21.9-105.45-24.33-130.59-4.06-55.16-23.52-123.29-84.36-138.7v-1.62c61.65-23.52 90.85-82.73 90.85-145.19 0-116.8-101.39-163.04-202.78-163.04H683.33v579.14zm1060.54-579.14H1593v348.79c0 74.62-26.77 112.74-101.39 112.74s-101.39-38.12-101.39-112.74V170.09h-150.87v339.86c0 167.9 77.05 251.45 252.26 251.45 175.2 0 252.26-83.55 252.26-251.45V170.09zm41.78 384.48c0 148.43 119.23 206.83 251.45 206.83 127.34 0 252.26-47.05 252.26-193.86 0-104.64-84.36-143.57-167.9-167.09-84.36-23.52-167.9-30.83-167.9-76.25 0-38.12 40.55-49.48 73-49.48 45.42 0 95.71 17.85 93.28 69.76h150.86c0-129.78-117.61-186.56-231.98-186.56-108.69 0-236.04 49.48-236.04 176.02 0 107.07 87.6 143.57 169.52 167.09 83.55 23.52 166.28 32.45 166.28 84.36 0 42.99-47.05 59.21-92.47 59.21-64.89 0-106.26-21.9-109.5-90.03h-150.86zm489.5 194.67h154.92l36.5-103.83h201.97l35.69 103.83h157.36L2645 170.1h-153.3l-216.58 579.14zm292.01-418.54h1.62l64.08 202.78h-131.4l65.7-202.78z&#39; class=&#39;fil1&#39;/%3e%3cpath d=&#39;M5351.38 0H3151.37v923.36h2200.01V0zM3340.37 749.23h420.98V619.45h-270.11V170.09h-150.87v579.14zm470.05 0h150.87V170.09h-150.87v579.14zm228.33 0h141.95V388.28h1.62l201.16 360.95h154.92V170.09h-141.95v356.89h-1.62l-201.16-356.89h-154.92v579.14zm577.11 0h150.87V570.78l61.64-64.89 155.73 243.33h187.37l-240.9-351.21 214.14-227.93h-187.37l-190.61 222.25V170.08h-150.87v579.14z&#39; class=&#39;fil0&#39;/%3e%3cpath d=&#39;M3340.38 749.23h420.98V619.45h-270.11V170.09h-150.87v579.14zm470.05 0h150.87V170.09h-150.87v579.14zm228.33 0h141.95V388.28h1.62l201.16 360.95h154.92V170.09h-141.95v356.89h-1.62l-201.16-356.89h-154.92v579.14zm577.11 0h150.87V570.78l61.64-64.89 155.73 243.33h187.37l-240.9-351.21 214.14-227.93h-187.37l-190.61 222.25V170.08h-150.87v579.14z&#39; class=&#39;fil1&#39;/%3e%3c/g%3e%3c/g%3e%3c/svg%3e\"/> <span class=\"close-button\"></span><div class=\"txt-md\"><p><span data-label=\"msg.modal-p1\">Welcome to the web interface of your</span><br/><span class=\"txt-bold\">Original Prusa i3</span></p><p data-label=\"msg.modal-p2\">Please note that values are shown only when the printer is printing.</p></div></div></template><template id=\"modal-apiKey\"><div class=\"modal-apiKey txt-md\"><p data-label=\"msg.api-key-1\" class=\"txt-bold\">Welcome to the PrusaLink web interface.</p><p><span data-label=\"msg.api-key-2\" class=\"txt-bold\">Please insert the API key.</span><br/><span data-label=\"msg.api-key-3\" class=\"txt-bold\">You can find it in Settings -> Network -> Login credentials.</span></p><input id=\"apiKey\" class=\"txt-md txt-grey\" autofocus/> <button class=\"yes\" id=\"login\"><img src=\"b891ace1622f34bac5a2b4edb7adc733.svg\"/><p data-label=\"btn.login\">Login</p></button></div></template><template id=\"modal-camera-settings\"><div class=\"modal-camera-settings\"><div class=\"txt-bold\"><p id=\"modal-camera-title\" data-label=\"camera.settings\"></p></div><div class=\"camera-settings__form\"><div class=\"flex-row\"><label for=\"camera-settings__name\" data-label=\"camera.name\"></label> <input class=\"grow\" id=\"camera-settings__name\" value=\"\"/></div><div class=\"flex-row\"><label for=\"camera-settings__resolution\" data-label=\"camera.resolution\"></label> <select class=\"grow\" id=\"camera-settings__resolution\" data-type=\"dropdown\"></select></div><div class=\"flex-row\"><label for=\"camera-settings__trigger-scheme\" data-label=\"camera.trigger-scheme\"></label> <select class=\"grow\" id=\"camera-settings__trigger-scheme\" data-type=\"dropdown\"></select></div><div class=\"flex-row\"><label for=\"camera-settings__focus\" data-label=\"camera.focus\"></label> <input class=\"grow slider\" type=\"range\" id=\"camera-settings__focus\" min=\"0\" max=\"100\"/></div></div><div class=\"flex-row\"><button class=\"yes\" id=\"yes\"><img src=\"b891ace1622f34bac5a2b4edb7adc733.svg\"/><p data-label=\"btn.confirm\">Confirm</p></button><div class=\"grow\"></div><button class=\"no\" id=\"no\"><img src=\"557f3616d5a1b407b59795b8328bb51f.svg\"/><p data-label=\"btn.cancel\">Cancel</p></button></div></div></template><div id=\"prusa-toast\"></div><template id=\"toast\"><article><div class=\"toast-header\"><p></p><span class=\"close-button\"></span></div><div class=\"toast-body\"></div></article></template><template id=\"offline-screen\"><div class=\"offline-screen\"><div class=\"txt-md\"><p><span id=\"offline-screen.not-responsing\" data-label=\"msg.offline.not-responsing\"></span></p><p><span id=\"offline-screen.please-wait\" data-label=\"msg.offline.please-wait\"></span></p></div></div></template><template id=\"graph-template\"><svg role=\"img\" viewBox=\"0 40 520 300\" class=\"temp-svg\"><path vector-effect=\"non-scaling-stroke\" fill=\"none\" d=\"M355 280h170.855v20.75H355z\"/><path d=\"M404 290a6 6 0 1012 0 6 6 0 10-12 0\" class=\"temp-legend-orange\"/><path d=\"M477.903 290a6 6 0 1012 0 6 6 0 10-12 0\" class=\"temp-legend-blue\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"434.952\" y=\"290\"><tspan x=\"418\" dx=\"0\" class=\"temp-text\">nozzle</tspan></text><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"486.903\" y=\"290\"><tspan x=\"496.903\" dx=\"0\" class=\"temp-text\">bed</tspan></text><g clip-path=\"url(#a)\" class=\"temp-g-label\"><defs><clipPath id=\"a\"><path vector-effect=\"non-scaling-stroke\" d=\"M50 50h470v200H50z\"/></clipPath></defs><path id=\"temp-line-orange\"/></g><g clip-path=\"url(#b)\" class=\"temp-g-label\"><defs><clipPath id=\"b\"><path vector-effect=\"non-scaling-stroke\" d=\"M50 50h470v200H50z\"/></clipPath></defs><path id=\"temp-line-blue\"/></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-label-line\" d=\"M50 250h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"50\" y=\"260\"><tspan x=\"50\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-180s</tspan></text><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M129.412 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"129.412\" y=\"260\"><tspan x=\"129.412\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-150s</tspan></text><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M208.824 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"208.824\" y=\"260\"><tspan x=\"208.824\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-120s</tspan></text><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M288.235 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"288.235\" y=\"260\"><tspan x=\"288.235\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-90s</tspan></text><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M367.647 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"367.647\" y=\"260\"><tspan x=\"367.647\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-60s</tspan></text></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M447.059 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"447.059\" y=\"260\"><tspan x=\"447.059\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-30s</tspan></text></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M505 250v5\"/><text direction=\"inherit\" dx=\"0\" dy=\"12.825\" x=\"505\" y=\"260\"><tspan x=\"505\" dx=\"0\" text-anchor=\"middle\" class=\"temp-line-text\">-10s</tspan></text></g></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-label-line\" d=\"M50 50v200\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-h-line\" d=\"M50 250h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 250h-5\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"40\" y=\"250\"><tspan x=\"40\" dx=\"0\" text-anchor=\"end\" class=\"temp-line-text\">0°C</tspan></text><path vector-effect=\"non-scaling-stroke\" class=\"temp-h-line\" d=\"M50 200h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 200h-5\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"40\" y=\"200\"><tspan x=\"40\" dx=\"0\" text-anchor=\"end\" class=\"temp-line-text\">75°C</tspan></text><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-h-line\" d=\"M50 150h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 150h-5\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"40\" y=\"150\"><tspan x=\"40\" dx=\"0\" text-anchor=\"end\" class=\"temp-line-text\">150°C</tspan></text></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-h-line\" d=\"M50 100h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 100h-5\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"40\" y=\"100\"><tspan x=\"40\" dx=\"0\" text-anchor=\"end\" class=\"temp-line-text\">225°C</tspan></text></g><g><path vector-effect=\"non-scaling-stroke\" class=\"temp-h-line\" d=\"M50 50h470\"/><path vector-effect=\"non-scaling-stroke\" class=\"temp-connect-text\" d=\"M50 50h-5\"/><text direction=\"inherit\" dx=\"0\" dy=\"5.325\" x=\"40\" y=\"50\"><tspan x=\"40\" dx=\"0\" text-anchor=\"end\" class=\"temp-line-text\">300°C</tspan></text></g></g></svg></template></body></html>\n"
  },
  {
    "path": "prusa/link/static/main.9b8dc0068f6e6508dfd4.js",
    "content": "(()=>{var e={5862:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"f844a0a85dde310826fce450c3e149d3.svg\"},340:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"d0ce7d704590cb0b868ae92249ee86e8.svg\"},3246:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"e6e0734bc2dad609a3e92f1873412f11.svg\"},6730:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"557f3616d5a1b407b59795b8328bb51f.svg\"},8065:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"2e842fcb7b83af8e1e0d26afd80f8fd9.svg\"},8920:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"24c2502c54c43441d0bb4bbc17e10a83.svg\"},2456:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"f6a7f34a841f532dd1e3e4d610d164f6.svg\"},9819:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"53df9c11e268c2390b147239f9ee8796.svg\"},7038:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"9332c323b291ba7226ddcdabb0c8e7c4.svg\"},1656:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"79e90794ba3b36a64a12414decda0932.svg\"},9594:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"a443dadfc7e114c74dae6cd64a74db9d.svg\"},931:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"926f52aea63cfd597b30096bfe4077c6.svg\"},7336:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"44b654f4ca724e154a9bc60335e98847.svg\"},9387:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"8657650f85be23655543d28f58941616.svg\"},5300:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"ea93bcffb8771234f8c641f3b7e9f848.svg\"},3482:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"ccd215a92d0427a21339a8c38e2a40f3.svg\"},5515:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"997c391425907810b4a6e42663d11fd4.svg\"},4578:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"24e2e49c87df140cf16aa18b4e261f12.svg\"},2e3:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"58151992d28dfb34ee417e0ebf6a66b6.svg\"},2290:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"c8a0ca767f73685bbe06863f81e9d3cb.svg\"},3174:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"39e93870e6b8fd4ef2d9ed668f8c1545.svg\"},8796:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"1218694688ee15ee97bcded9e1a75cc8.svg\"},1373:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"dfb811cc28f8e3d7c14bcedbe7ef35b8.svg\"},4622:(e,t,a)=>{\"use strict\";a.r(t),a.d(t,{default:()=>i});const i=a.p+\"b891ace1622f34bac5a2b4edb7adc733.svg\"},5464:(e,t,a)=>{var i=a(7091),s=a(8065),n=a(4622),r=a(6730),o=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div class=\"main-wrapper\" id=\"cameras\"> <div class=\"camera-snapshot\"> <ul class=\"camera-snapshot-meta\"> <li> <p class=\"txt-sm txt-grey\" data-label=\"camera.name\"> Camera Name </p> <p id=\"camera-snapshot-name\" class=\"txt-bold txt-md\"> - </p> </li> <li> <p class=\"txt-sm txt-grey\" data-label=\"camera.time\"> Snapshot Time </p> <p id=\"camera-snapshot-time\" class=\"txt-bold txt-md\"> - </p> </li> </ul> <div id=\"camera-snapshot-container\"> <img id=\"camera-snapshot-picture\"/> </div> </div> <div class=\"cameras-list\"> <div class=\"line\"></div> <ul id=\"cameras-list\"></ul> </div> </div> <template id=\"camera-list-item\"> <li class=\"node line\"> <div class=\"flex-row grow\"> <div class=\"camera__preview mr-md\"> <img class=\"camera__no-snapshot\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; xml:space=&#39;preserve&#39; width=&#39;102.831&#39; height=&#39;102.831&#39; style=&#39;shape-rendering:geometricPrecision%3btext-rendering:geometricPrecision%3bimage-rendering:optimizeQuality%3bfill-rule:evenodd%3bclip-rule:evenodd&#39; viewBox=&#39;0 0 756.91 756.91&#39;%3e%3cdefs%3e%3cstyle%3e.str0%7bstroke:%23898989%3bstroke-width:2.12%3bstroke-miterlimit:22.9256%7d.fil0%7bfill:%23898989%7d%3c/style%3e%3c/defs%3e%3cg id=&#39;Layer_x0020_1&#39;%3e%3cg id=&#39;Layer_x0020_1.pdf&#39;%3e%3cpath d=&#39;M614.07 316.59c0 129.72-105.47 234.94-235.5 234.94s-235.5-105.23-235.5-234.94c0-129.72 105.47-234.95 235.5-234.95 130.02 0 235.5 105.23 235.5 234.95zm-418.66 0c0 100.74 82.19 182.73 183.17 182.73 100.97 0 183.16-82 183.16-182.73 0-100.74-82.19-182.74-183.16-182.74-100.98 0-183.17 82-183.17 182.74zM470.04 587.4l46.08 69.65H240.77l46.08-69.65c-16.59-6.27-32.49-14.16-47.35-23.57l-76.04 114.92c-4.15 6.27-4.49 13.82-1.04 20.43 3.57 6.62 9.91 10.56 17.4 10.56h396.89c7.49 0 13.94-3.83 17.4-10.56 3.57-6.62 3.22-14.16-.92-20.43l-76.04-114.92c-14.86 9.4-30.64 17.3-47.35 23.57h.23z&#39; class=&#39;fil0 str0&#39;/%3e%3cpath d=&#39;M431.15 317.25c0-29.02-23.68-52.7-52.7-52.7-29.14 0-52.7 23.68-52.7 52.7 0 29.02 23.68 52.7 52.7 52.7 29.02 0 52.7-23.68 52.7-52.7z&#39; class=&#39;fil0 str0&#39;/%3e%3c/g%3e%3cpath d=&#39;M133.55 111.68c-10.86-10.86-10.86-28.48 0-39.34 10.86-10.86 28.48-10.86 39.34 0l450.47 450.47c10.86 10.86 10.86 28.48 0 39.34-10.86 10.86-28.48 10.86-39.34 0L133.55 111.68z&#39; style=&#39;fill:%23898989%3bfill-rule:nonzero&#39;/%3e%3cpath d=&#39;M0 0h756.91v756.91H0z&#39; style=&#39;fill:none&#39;/%3e%3c/g%3e%3c/svg%3e\" alt=\"\"/> <img class=\"camera__snapshot\" alt=\"\"/> </div> <div class=\"flex-col grow justify-between\"> <div class=\"flex-row\"> <div class=\"camera__name grow\"></div> <div class=\"camera__controls flex-row\"> <button class=\"camera__register ml-md rounded\"><img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;32&#39; height=&#39;32&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 307.69 307.69&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M77.12 230.71h153.72c42.44 0 76.86-34.42 76.86-76.87 0-42.44-34.42-76.86-76.86-76.86H76.63C34.28 77.11.02 111.49.02 153.84c0 42.44 34.42 76.86 76.86 76.86h.25zm0-26.77c27.67 0 50.09-22.43 50.09-50.09 0-27.67-22.43-50.09-50.09-50.09-27.66 0-50.09 22.43-50.09 50.09 0 27.66 22.43 50.09 50.09 50.09z&#39;/%3e%3c/svg%3e\"/></button> <button class=\"camera__unregister ml-md rounded\"><img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;32&#39; height=&#39;32&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 571.36 571.36&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M428.16 428.41H142.72C63.91 428.4-.01 364.49-.01 285.67c0-78.81 63.92-142.73 142.73-142.73h286.36c78.65.24 142.27 64.08 142.27 142.73 0 78.81-63.92 142.73-142.73 142.73h-.47v.01zm0-49.72c-51.37 0-93.02-41.65-93.02-93.02s41.65-93.02 93.02-93.02 93.02 41.65 93.02 93.02-41.65 93.02-93.02 93.02z&#39;/%3e%3c/svg%3e\"/></button> <button class=\"camera__settings ml-md rounded\"><img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;32&#39; height=&#39;32&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 363.65 363.65&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M181.82 120.79c-33.69 0-61.04 27.35-61.04 61.04 0 33.71 27.35 61.04 61.04 61.04s61.04-27.35 61.04-61.04-27.35-61.04-61.04-61.04zm0 24.45c20.17 0 36.58 16.44 36.58 36.59 0 20.17-16.44 36.59-36.58 36.59-20.17 0-36.59-16.44-36.59-36.59 0-20.17 16.44-36.59 36.59-36.59z&#39;/%3e%3cpath fill=&#39;white&#39; d=&#39;M334.56 209.66V154c-27.4-9.82-34.75-10.24-38.5-19.48-3.89-9.23 1.01-14.85 13.57-41.11l-39.38-39.38c-25.81 12.27-31.74 17.32-41.11 13.57-9.23-3.89-9.82-11.26-19.48-38.5h-55.67c-9.66 27.27-10.24 34.75-19.48 38.5-9.52 4.04-15.3-1.3-41.11-13.57L54.02 93.41c12.4 26.1 17.32 31.75 13.57 41.11-3.89 9.23-11.25 9.66-38.5 19.48v55.66c27.27 9.66 34.75 10.24 38.5 19.48 3.89 9.37-1.01 14.85-13.57 41.11l39.38 39.38c25.54-12.11 31.45-17.46 41.11-13.57 9.23 3.89 9.66 11.26 19.48 38.5h55.67c9.66-27.12 10.24-34.62 19.61-38.66 9.53-3.88 15.14 1.3 40.98 13.57l39.38-39.38c-12.4-26.1-17.32-31.75-13.57-41.11 3.89-9.23 11.25-9.66 38.5-19.48v.16zm-62.16 9.79c-7.35 17.75-.88 31.29 6.2 45.58l-13.84 13.86c-14-6.92-27.54-13.7-45.58-6.2-17.75 7.34-22.8 21.5-27.83 36.64h-19.48c-5.06-15.14-10.09-29.27-27.83-36.64-18.04-7.5-31.88-.59-45.58 6.2L84.6 265.03c7.08-14.29 13.57-27.83 6.2-45.58-7.34-17.75-21.5-22.8-36.64-27.83v-19.48c15.14-5.05 29.27-10.08 36.64-27.83 7.35-17.75.88-31.29-6.2-45.58l13.86-13.84c13.84 6.78 27.54 13.7 45.58 6.2 17.75-7.34 22.78-21.5 27.83-36.64h19.48c5.05 15.14 10.08 29.27 27.83 36.64 18.04 7.5 31.88.59 45.58-6.2l13.84 13.84c-7.08 14.29-13.57 27.83-6.2 45.58 7.34 17.75 21.5 22.8 36.64 27.83v19.48c-15.14 5.06-29.27 10.09-36.64 27.83z&#39;/%3e%3c/svg%3e\"/></button> <button class=\"camera__add ml-md rounded\"><img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;64&#39; height=&#39;64&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 4000 4000&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M2717.66 2118.9H2118.9v598.76h-238.88V2118.9h-598.76v-238.88h598.76v-598.76h238.88v598.76h598.76z&#39;/%3e%3cpath fill=&#39;white&#39; d=&#39;M2000 562.5c-793.48 0-1437.5 642.98-1437.5 1437.5 0 793.48 642.98 1437.5 1437.5 1437.5 793.48 0 1437.5-642.98 1437.5-1437.5 0-793.48-642.98-1437.5-1437.5-1437.5zm0 239.92c659.82 0 1197.54 537.76 1197.54 1197.54 0 659.82-537.76 1197.54-1197.54 1197.54-659.82 0-1197.54-537.76-1197.54-1197.54 0-659.82 537.76-1197.54 1197.54-1197.54z&#39;/%3e%3c/svg%3e\"/></button> <button class=\"camera__remove ml-md rounded\"><img class=\"icon icon-small\" src=\"'+i(s)+'\"/></button> </div> </div> <div class=\"flex-row\"> <div class=\"mr-md\"> <p class=\"camera__path__label txt-sm txt-grey\"></p> <p class=\"camera__path txt-bold txt-md\"></p> </div> <div class=\"mr-md\"> <p class=\"camera__driver__label txt-sm txt-grey\"></p> <p class=\"camera__driver txt-bold txt-md\"></p> </div> <div class=\"mr-md\"> <p class=\"camera__cloud__label txt-sm txt-grey\"></p> <p class=\"camera__cloud txt-bold txt-md\"></p> </div> </div> </div> </div> </li> </template> <template id=\"modal-question\"> <div class=\"modal-confirm\"> <div> <p id=\"modal-question-label\"></p> </div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+i(n)+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+i(r)+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </template> ';e.exports=o},3478:(e,t,a)=>{var i=a(7091),s=a(340),n=a(5862),r=a(1656),o=i(s),l=i(n),d=i(r),c=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div class=\"main-wrapper\" id=\"control\"> <div class=\"resp-row\"> <div class=\"col\"> <p data-label=\"control.coordinates\" class=\"txt-bold txt-grey txt-md title\"> printer coordinates </p> <div class=\"grid-container\"> <div class=\"grid-item\"> <p data-label=\"control.axis.x\" class=\"txt-md\"> X axis </p> </div> <div class=\"grid-item\"> <p data-format=\"pos\" data-type=\"control\" data-where=\"telemetry.axis.x\" class=\"txt-md\"> 0 mm </p> </div> <div class=\"grid-item\"> <p data-label=\"control.axis.y\" class=\"txt-md\"> Y axis </p> </div> <div class=\"grid-item\"> <p data-format=\"pos\" data-type=\"control\" data-where=\"telemetry.axis.y\" class=\"txt-md\"> 0 mm </p> </div> <div class=\"grid-item\"> <p data-label=\"control.axis.z\" class=\"txt-md\"> Z axis </p> </div> <div class=\"grid-item\"> <p data-format=\"pos\" data-type=\"control\" data-where=\"telemetry.axis.z\" class=\"txt-md\"> 0 mm </p> </div> </div> </div> <div class=\"separator\"></div> <div class=\"col\"> <p data-label=\"control.stepper-motors\" class=\"txt-bold txt-grey txt-md title\"> stepper motors </p> <div class=\"row\"> <button class=\"action rectangle\" id=\"disable-steppers\"> <p data-label=\"btn.disable-steppers\">disable steppers</p> </button> </div> </div> </div> <div class=\"resp-row\"> <div class=\"resp-group\"> <div class=\"col\" id=\"heated-bed-xy-move\"> <p data-label=\"control.heated-bed-move\" class=\"txt-bold txt-grey txt-md txt-underline title\"> heated bed X and Y move </p> <div class=\"row\"> <div class=\"square\"></div> <button class=\"action square\" data-action=\"move\" data-value=\"y+\"> <img src=\"'+o+'\"/> </button> <div class=\"square\"></div> </div> <div class=\"row\"> <button class=\"action square\" data-action=\"move\" data-value=\"x-\"> <img src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;64&#39; height=&#39;64&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 363.64 363.64&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;m252.6 0 42.84 42.84-141.47 139.03 141.47 138.94-42.84 42.83L68.2 181.87z&#39;/%3e%3c/svg%3e\"/> </button> <div class=\"square\"></div> <button class=\"action square\" data-action=\"move\" data-value=\"x+\"> <img src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;64&#39; height=&#39;64&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 235.25 235.25&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M71.85 0 44.13 27.72l91.54 89.96-91.54 89.9 27.72 27.71 119.31-117.61z&#39;/%3e%3c/svg%3e\"/> </button> </div> <div class=\"row\"> <div class=\"square\"></div> <button class=\"action square\" data-action=\"move\" data-value=\"y-\"> <img src=\"'+l+'\"/> </button> <button id=\"home\" class=\"action square\" data-action=\"home\" data-value=\"x,y\"> <img src=\"'+d+'\"/> </button> </div> <p data-label=\"control.move-step\" class=\"txt-bold txt-grey txt-md title\"> move step [mm] </p> <div class=\"row\" id=\"move-step\"> <button class=\"action square\" data-step=\"0.1\"> <p>0.1</p> </button> <button class=\"action square\" data-step=\"1\"> <p>1</p> </button> <button class=\"action square\" data-step=\"10\"> <p>10</p> </button> <button class=\"action square\" data-step=\"100\"> <p>100</p> </button> </div> </div> <div class=\"col\" id=\"nozzle-z-move\"> <p data-label=\"control.nozzle-z-move\" class=\"txt-bold txt-grey txt-md txt-underline title\"> nozzle Z move </p> <div class=\"row\"> <button class=\"action square\" data-action=\"move\" data-value=\"z+\"> <img src=\"'+o+'\"/> </button> </div> <div class=\"row\"> <button class=\"action square\" data-action=\"move\" data-value=\"z-\"> <img src=\"'+l+'\"/> </button> </div> <div class=\"row\"> <button class=\"action square\" data-action=\"home\" data-value=\"z\"> <img src=\"'+d+'\"/> </button> </div> </div> </div> <div class=\"col\" id=\"extrude-retract\"> <p data-label=\"control.extruder\" class=\"txt-bold txt-grey txt-md txt-underline title\"> extruder </p> <div class=\"row\"> <button class=\"action rectangle\" id=\"extrude\"> <p data-label=\"btn.extrude\">extrude</p> </button> </div> <div class=\"row\"> <button class=\"action rectangle\" id=\"retract\"> <p data-label=\"btn.retract\">retract</p> </button> </div> <div class=\"separator\"></div> <p data-label=\"control.extrude-retract-step\" class=\"txt-bold txt-grey txt-md title\"> extrude/retract step [mm] </p> <div class=\"row\" id=\"extrude-retract-step\"> <button class=\"action square\" data-step=\"0.1\"> <p>0.1</p> </button> <button class=\"action square\" data-step=\"1\"> <p>1</p> </button> <button class=\"action square\" data-step=\"10\"> <p>10</p> </button> <button class=\"action square\" data-step=\"50\"> <p>50</p> </button>  </div> </div> </div> <div class=\"resp-row\"> <div class=\"col\"> <p data-label=\"control.nozzle-temp\" class=\"txt-bold txt-grey txt-md txt-underline title\"> nozzle temperature </p> <div class=\"input-wrapper\" id=\"nozzle\"> <div class=\"square\"> <p data-format=\"temp_int\" data-type=\"control\" data-where=\"telemetry.temperature.nozzle.target\" class=\"txt-bold txt-md\"> 0°C </p> </div> <input type=\"text\" class=\"txt-bold txt-md\" data-action=\"nozzle\"/> <button class=\"action square\"> <p data-label=\"btn.set\">SET</p> </button> </div> </div> <div class=\"col\"> <p data-label=\"control.speed\" class=\"txt-bold txt-grey txt-md txt-underline title\"> speed </p> <div class=\"input-wrapper\" id=\"speed\"> <div class=\"square\"> <p data-format=\"percent\" data-type=\"control\" data-where=\"telemetry.speed\" class=\"txt-bold txt-md\"> 100% </p> </div> <input type=\"text\" class=\"txt-bold txt-md\" data-action=\"speed\"/> <button class=\"action square\"> <p data-label=\"btn.set\">SET</p> </button> </div> </div> </div> <div class=\"resp-row\"> <div class=\"col\"> <p data-label=\"control.heated-bed-temp\" class=\"txt-bold txt-grey txt-md txt-underline title\"> heated bed temperature </p> <div class=\"input-wrapper\" id=\"bed\"> <div class=\"square\"> <p data-format=\"temp_int\" data-type=\"control\" data-where=\"telemetry.temperature.bed.target\" class=\"txt-bold txt-md\"> 0°C </p> </div> <input type=\"text\" class=\"txt-bold txt-md\" data-action=\"bed\"/> <button class=\"action square\"> <p data-label=\"btn.set\">SET</p> </button> </div> </div> <div class=\"col\"> <p data-label=\"control.flow\" class=\"txt-bold txt-grey txt-md txt-underline title\"> flow </p> <div class=\"input-wrapper\" id=\"flowrate\"> <div class=\"square\"> <p data-format=\"percent\" data-type=\"control\" data-where=\"telemetry.flow\" class=\"txt-bold txt-md\"> 100% </p> </div> <input type=\"text\" class=\"txt-bold txt-md\" data-action=\"flowrate\"/> <button class=\"action square\"> <p data-label=\"btn.set\">SET</p> </button> </div> </div> </div> </div>';e.exports=c},2373:(e,t,a)=>{var i=a(7091),s=a(2e3),n=a(2290),r=a(3246),o=a(4578),l=a(7038),d=a(2456),c=a(9819),u=a(9387),p=a(5300),m=a(3482),v=a(4622),g=a(6730),h=a(3174),b=a(8796),f=i(s),y=i(n),k=i(r),w=i(o),x=i(l),z=i(d),S=i(c),E=i(u),P=i(p),L=i(m),I=i(v),N=i(g),_=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div id=\"job\" hidden> <div class=\"job-title\"> <p data-type=\"job\" data-where=\"file.displayName\" class=\"txt-bold txt-md file-name\"></p> <button id=\"job-close\" class=\"close-button\"></button> </div> <div id=\"preview-wrapper\"> <div class=\"progress progress-with-img\"> <div> <div class=\"preview-img-wrapper\"></div> <div class=\"progress-bar\" role=\"status\"> <div class=\"fill\"></div> </div> <div class=\"progress-pct\"> <p class=\"txt-lg\" data-format=\"percent\" data-type=\"job\" data-where=\"progress\"> 0% </p> </div> </div> <img class=\"thumbnail-fallback\" src=\"'+f+'\" hidden/> </div> <div class=\"job-details\"> <div class=\"job-prop\"> <div class=\"icon\"> <img src=\"'+y+'\"/> </div> <div class=\"job-prop-grid\"> <div id=\"rem-time\"> <p data-label=\"prop.rem-time\" class=\"txt-sm txt-grey\"> Remaining Time </p> <p data-format=\"time\" data-type=\"job\" data-where=\"timeRemaining\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"pnt-time-est\"> <p data-label=\"prop.time-est\" class=\"txt-sm txt-grey\"> Print Time Estimate </p> <p data-format=\"time\" data-type=\"file\" data-where=\"meta.estimatedPrintTime\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"est-end\"> <p data-label=\"prop.est-end\" class=\"txt-sm txt-grey\"> Estimated End </p> <p data-format=\"timeEst\" data-type=\"job\" data-where=\"timeRemaining\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"pnt-time\"> <p data-label=\"prop.pnt-time\" class=\"txt-sm txt-grey\"> Printing Time </p> <p data-format=\"time\" data-type=\"job\" data-where=\"timePrinting\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\" id=\"file-last-mod\"> <div class=\"icon\"> <img src=\"'+k+'\"/> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.last-mod\" class=\"txt-sm txt-grey\"> Last Modified </p> <p data-format=\"date\" data-type=\"file\" data-where=\"lastModified\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\"> <div class=\"icon\"> <img src=\"'+w+'\"/> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.material\" class=\"txt-sm txt-grey\"> Material </p> <p data-format=\"material\" data-type=\"file\" data-where=\"meta.filamentType\" class=\"txt-bold txt-md\"> NA </p> </div> <div> <p data-label=\"prop.layer-ht\" class=\"txt-sm txt-grey\"> Layer Height </p> <p data-format=\"layer\" data-type=\"file\" data-where=\"meta.layerHeight\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\"> <div class=\"icon\"> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.size\" class=\"txt-sm txt-grey\"> File Size </p> <p data-format=\"size\" data-type=\"file\" data-where=\"size\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> </div> <div class=\"loading-overlay\"> <img class=\"icon abs-center\" src=\"'+x+'\"/> </div> </div> <div class=\"job-buttons\"> <button id=\"delete\" class=\"no\" hidden> <img src=\"'+z+'\"/> <p data-label=\"btn.del\"> delete </p> </button> <button id=\"download\" class=\"btn action\" hidden> <img src=\"'+S+'\"/> <p data-label=\"btn.download\"> download </p> </button> <div class=\"job-buttons-separator\"></div> <button id=\"pause\" class=\"action\" hidden> <img src=\"'+E+'\"/> <p data-label=\"btn.pause-pt\"> pause print </p> </button> <button id=\"resume\" class=\"yes\" hidden> <img src=\"'+P+'\"/> <p data-label=\"btn.resume-pt\"> resume </p> </button> <button id=\"start\" class=\"yes\" hidden> <img src=\"'+P+'\"/> <p data-label=\"btn.start-pt\"> start print </p> </button> <button id=\"stop\" class=\"no\" hidden> <img src=\"'+L+'\"/> <p data-label=\"btn.stop-print\"> stop print </p> </button> </div> </div> <template id=\"modal-confirm\"> <div class=\"modal-confirm\"> <div> <span data-label=\"print.fdm.1\">Is the printer ready?</span> <div data-label=\"print.fdm.2\">Is printing sheet is empty and clean?</div> </div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+I+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+N+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </template> <template id=\"modal-question\"> <div class=\"modal-confirm\"> <div> <p id=\"modal-question-label\"></p> </div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+I+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+N+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </template> <div class=\"main-wrapper home-row\"> <div class=\"component\"> <p data-label=\"upld.title\" class=\"txt-grey txt-md title\">Upload file</p> <div id=\"upld\" class=\"txt-sm\"> <div class=\"upld-source\"> <img class=\"icon icon-small\" src=\"'+i(h)+'\"/> <p class=\"txt-bold\" data-label=\"proj.add-from.title\">Add file from</p> <span>&nbsp;</span> <ul class=\"node-upload-source\"> <li class=\"txt-bold\"> <a data-tab-btn=\"direct\" data-label=\"proj.add-from.local\">Local storage</a> </li> <li class=\"txt-bold\"> <a data-tab-btn=\"remote\" data-label=\"proj.add-from.remote\">Remote URL</a> </li> </ul> </div> <div> <div data-tab=\"direct\"> <div id=\"upld-direct\" data-state=\"choose\"> <div id=\"upld-direct-frame\" class=\"form-row\"> <input type=\"file\"/> <div class=\"state-choose\"> <img class=\"icon icon-small\" src=\"'+i(b)+'\"/> <p>Click to choose a *.sl1 file or drag it here</p> </div> <div class=\"abs-center state-uploading\"> <img class=\"icon abs-center\" src=\"'+x+'\"/> <p id=\"upld-progress\" class=\"abs-center\"></p> </div> </div> <div class=\"state-choose state-choose-checkbox\"> <input type=\"checkbox\" id=\"upld-direct-start-pt\"/> <label data-label=\"upld.start-pt\" for=\"upld-direct-start-pt\"> Start print after transfer </label> </div> </div> </div> <div data-tab=\"remote\"> <div id=\"upld-remote\" data-state=\"choose\"> <div class=\"state-choose\"> <div class=\"form-row\"> <p data-label=\"upld.remote.source\" class=\"txt-grey\">Source URL</p> <input id=\"remote-url\" class=\"txt-md\" type=\"text\" placeholder=\"http://\"/> <p class=\"txt-grey txt-italic txt-sm\" data-label=\"upld.remote.hint-fdm\"> Type URL of G-CODE file </p> </div> <div class=\"form-row\"> <p data-label=\"upld.remote.file\" class=\"txt-grey\" style=\"margin-top:10px\">File name</p> <input id=\"remote-file-name\" class=\"txt-md\" type=\"text\"/> <p class=\"txt-grey txt-italic\" data-label=\"upld.remote.file-hint\"> Type or edit file name </p> </div> <div class=\"form-submit\"> <div class=\"state-choose-checkbox\"> <input type=\"checkbox\" id=\"upld-remote-start-pt\"/> <label data-label=\"upld.start-pt\" for=\"upld-remote-start-pt\"> Start print after transfer </label> </div> <button id=\"upld-file\" class=\"outlined\"> <p class=\"uppercase\" data-label=\"btn.upld-file\"> UPLOAD FILE </p> </button> </div> </div> <div class=\"state-uploading\"> <div class=\"progress-bar\"> <div class=\"fill\"></div> <p class=\"txt-black hide-scrollbar\" data-where=\"file.displayName\" data-type=\"download\"> </p> </div> <div class=\"upld-details\"> <div class=\"progress\"> <p data-label=\"prop.progress\" class=\"txt-grey\"> Progress </p> <p data-format=\"percent\" data-type=\"download\" data-where=\"progress\" class=\"txt-bold\"> NA </p> </div> <div class=\"file.size\"> <p data-label=\"prop.size\" class=\"txt-grey\"> Size </p> <p data-format=\"size\" data-type=\"download\" data-where=\"file.size\" class=\"txt-bold\"> NA </p> </div> <div class=\"timeStarted\"> <p data-label=\"download.dl-started\" class=\"txt-grey\"> Download Started </p> <p data-format=\"date\" data-type=\"download\" data-where=\"timeStarted\" class=\"txt-bold\"> NA </p> </div> <div class=\"timeRemaining\"> <p data-label=\"prop.rem-time\" class=\"txt-grey\"> Remaining Time </p> <p data-format=\"time\" data-type=\"download\" data-where=\"timeRemaining\" class=\"txt-bold\"> NA </p> </div> <div class=\"file.toPrint\"> <p data-label=\"download.start-pt\" class=\"txt-grey\"> Autostart </p> <p data-format=\"boolean\" data-type=\"download\" data-where=\"file.toPrint\" class=\"txt-bold\"> NA </p> </div> </div> </div> </div> </div> </div> <div style=\"margin-bottom:20px\"></div> </div> </div> <div class=\"component component-fixed\"> <p data-label=\"temps.title\" class=\"txt-grey txt-md title\">Temperatures</p> <div id=\"graph\"></div> </div> </div> <div class=\"main-wrapper component component-inline\" id=\"cameras\"> <p data-label=\"camera.title\" class=\"txt-grey txt-md title\">Cameras</p> <div class=\"camera-snapshot\"> <ul class=\"camera-snapshot-meta\"> <li> <p class=\"txt-sm txt-grey\" data-label=\"camera.name\"></p> <p id=\"camera-snapshot-name\" class=\"txt-bold txt-md\"> - </p> </li> <li> <p class=\"txt-sm txt-grey\" data-label=\"camera.time\"></p> <p id=\"camera-snapshot-time\" class=\"txt-bold txt-md\"> - </p> </li> </ul> <div id=\"camera-snapshot-container\"> <img id=\"camera-snapshot-picture\" class=\"camera__snapshot\"/> </div> </div> </div> <template id=\"cameras-snapshots__item\"> <li> <img class=\"camera__snapshot\" src=\"'+f+'\" alt=\"\"/> </li> </template> ';e.exports=_},7189:(e,t,a)=>{var i=a(7091),s=a(2e3),n=a(2290),r=a(3246),o=a(4578),l=a(7038),d=a(2456),c=a(9819),u=a(9387),p=a(5300),m=a(3482),v=a(4622),g=a(6730),h=a(931),b=a(5515),f=a(3174),y=a(8796),k=a(9594),w=a(8065),x=a(8920),z=i(s),S=i(n),E=i(r),P=i(o),L=i(l),I=i(d),N=i(c),_=i(u),T=i(p),j=i(m),C=i(v),A=i(g),D=i(h),B=i(b),O=i(f),R=i(y),q=i(k),M=i(w),H=i(x),U=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div class=\"main-wrapper\"> <div id=\"job\" hidden> <div class=\"job-title\"> <p data-type=\"job\" data-where=\"file.displayName\" class=\"txt-bold txt-md file-name\"></p> <button id=\"job-close\" class=\"close-button\"></button> </div> <div id=\"preview-wrapper\"> <div class=\"progress progress-with-img\"> <div> <div class=\"preview-img-wrapper\"></div> <div class=\"progress-bar\" role=\"status\"> <div class=\"fill\"></div> </div> <div class=\"progress-pct\"> <p class=\"txt-lg\" data-format=\"percent\" data-type=\"job\" data-where=\"progress\"> 0% </p> </div> </div> <img class=\"thumbnail-fallback\" src=\"'+z+'\" hidden/> </div> <div class=\"job-details\"> <div class=\"job-prop\"> <div class=\"icon\"> <img src=\"'+S+'\"/> </div> <div class=\"job-prop-grid\"> <div id=\"rem-time\"> <p data-label=\"prop.rem-time\" class=\"txt-sm txt-grey\"> Remaining Time </p> <p data-format=\"time\" data-type=\"job\" data-where=\"timeRemaining\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"pnt-time-est\"> <p data-label=\"prop.time-est\" class=\"txt-sm txt-grey\"> Print Time Estimate </p> <p data-format=\"time\" data-type=\"file\" data-where=\"meta.estimatedPrintTime\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"est-end\"> <p data-label=\"prop.est-end\" class=\"txt-sm txt-grey\"> Estimated End </p> <p data-format=\"timeEst\" data-type=\"job\" data-where=\"timeRemaining\" class=\"txt-bold txt-md\"> NA </p> </div> <div id=\"pnt-time\"> <p data-label=\"prop.pnt-time\" class=\"txt-sm txt-grey\"> Printing Time </p> <p data-format=\"time\" data-type=\"job\" data-where=\"timePrinting\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\" id=\"file-last-mod\"> <div class=\"icon\"> <img src=\"'+E+'\"/> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.last-mod\" class=\"txt-sm txt-grey\"> Last Modified </p> <p data-format=\"date\" data-type=\"file\" data-where=\"lastModified\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\"> <div class=\"icon\"> <img src=\"'+P+'\"/> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.material\" class=\"txt-sm txt-grey\"> Material </p> <p data-format=\"material\" data-type=\"file\" data-where=\"meta.filamentType\" class=\"txt-bold txt-md\"> NA </p> </div> <div> <p data-label=\"prop.layer-ht\" class=\"txt-sm txt-grey\"> Layer Height </p> <p data-format=\"layer\" data-type=\"file\" data-where=\"meta.layerHeight\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> <div class=\"job-prop\"> <div class=\"icon\"> </div> <div class=\"job-prop-grid\"> <div> <p data-label=\"prop.size\" class=\"txt-sm txt-grey\"> File Size </p> <p data-format=\"size\" data-type=\"file\" data-where=\"size\" class=\"txt-bold txt-md\"> NA </p> </div> </div> </div> </div> <div class=\"loading-overlay\"> <img class=\"icon abs-center\" src=\"'+L+'\"/> </div> </div> <div class=\"job-buttons\"> <button id=\"delete\" class=\"no\" hidden> <img src=\"'+I+'\"/> <p data-label=\"btn.del\"> delete </p> </button> <button id=\"download\" class=\"btn action\" hidden> <img src=\"'+N+'\"/> <p data-label=\"btn.download\"> download </p> </button> <div class=\"job-buttons-separator\"></div> <button id=\"pause\" class=\"action\" hidden> <img src=\"'+_+'\"/> <p data-label=\"btn.pause-pt\"> pause print </p> </button> <button id=\"resume\" class=\"yes\" hidden> <img src=\"'+T+'\"/> <p data-label=\"btn.resume-pt\"> resume </p> </button> <button id=\"start\" class=\"yes\" hidden> <img src=\"'+T+'\"/> <p data-label=\"btn.start-pt\"> start print </p> </button> <button id=\"stop\" class=\"no\" hidden> <img src=\"'+j+'\"/> <p data-label=\"btn.stop-print\"> stop print </p> </button> </div> </div> <template id=\"modal-confirm\"> <div class=\"modal-confirm\"> <div> <span data-label=\"print.fdm.1\">Is the printer ready?</span> <div data-label=\"print.fdm.2\">Is printing sheet is empty and clean?</div> </div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+C+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+A+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </template> <template id=\"modal-question\"> <div class=\"modal-confirm\"> <div> <p id=\"modal-question-label\"></p> </div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+C+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+A+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </template> <div id=\"node-storage\"> <div class=\"storage-select\"> <div class=\"storage-select-left\"></div> <div class=\"storage-select-btn\"> <div class=\"storage-select-btn-inner\"> <img class=\"icon\" src=\"'+D+'\"/> <p class=\"txt-md\">Local</p> </div> <img class=\"icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;36.544&#39; height=&#39;36.51&#39;%3e%3cpath fill=&#39;%23d5d5d5&#39; d=&#39;m.58 11.406 4.172-4.152 13.526 13.7 13.515-13.7 4.17 4.152-17.685 17.85L.58 11.406z&#39;/%3e%3c/svg%3e\"/> </div> <ul class=\"storage-select-content\"> <li data-storage=\"LOCAL\" hidden=\"true\"> <img class=\"icon pc-only\" src=\"'+B+'\"/> <img class=\"icon mobile-only\" src=\"'+B+'\"/> <p class=\"txt-md\">Local</p> </li> <li data-storage=\"SDCARD\" hidden=\"true\"> <img class=\"icon pc-only\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; fill=&#39;white&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 24 24&#39;%3e%3cpath d=&#39;M19.2 5.422v1.639h-3.47V5.422zm1.735 11.33v1.639h-3.47v-1.639zm0-2.891V15.5h-3.47v-1.639zm0-2.747v1.639h-3.47v-1.639zm0-2.891v1.639h-3.47V8.223zM18.67 3.548h-5.156v.867h-1.542v-.867H1.996v16.92h9.109v-.82h5.639v.82h5.253V6.878c0-.167-.642-.715-.783-.856-.285-.285-2.37-2.47-2.542-2.47z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M13.925 20.456v-.705h2.716v.705z&#39;/%3e%3c/svg%3e\"/> <img class=\"icon mobile-only\" src=\"'+D+'\"/> <p class=\"txt-md\">SD Card</p> </li> <li data-storage=\"USB\" hidden=\"true\"> <img class=\"icon pc-only\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;24&#39; height=&#39;24&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 235.29 235.29&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M36.9 63.75h135.19v8.32h53.86v91.08h-53.86v8.32h-7.98c-1.04 0-111.35.01-112.36.02-13.91.1-25.56.63-34.31-8.13-4.98-4.98-8.09-11.87-8.09-19.45V91.29c0-15.16 12.39-27.55 27.55-27.55v-.01zm151.61 60.86h15.71v18.85h-15.71v-18.85zm0-32.83h15.71v18.85h-15.71V91.78zm-16.4-11.71v75.12h45.88V80.07h-45.88zm-7.98-8.55H51.77c-11.48 0-21.58-1.11-28.67 5.98-3.55 3.55-5.75 8.45-5.75 13.82v52.62c0 10.77 8.8 19.58 19.58 19.58h127.21v-92z&#39;/%3e%3c/svg%3e\"/> <img class=\"icon mobile-only\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;24&#39; height=&#39;24&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 235.29 235.29&#39;%3e%3cpath fill=&#39;%23313131&#39; d=&#39;M36.9 63.75h135.19v8.32h53.86v91.08h-53.86v8.32h-7.98c-1.04 0-111.35.01-112.36.02-13.91.1-25.56.63-34.31-8.13-4.98-4.98-8.09-11.87-8.09-19.45V91.29c0-15.16 12.39-27.55 27.55-27.55v-.01zm151.61 60.86h15.71v18.85h-15.71v-18.85zm0-32.83h15.71v18.85h-15.71V91.78zm-16.4-11.71v75.12h45.88V80.07h-45.88zm-7.98-8.55H51.77c-11.48 0-21.58-1.11-28.67 5.98-3.55 3.55-5.75 8.45-5.75 13.82v52.62c0 10.77 8.8 19.58 19.58 19.58h127.21v-92z&#39;/%3e%3c/svg%3e\"/> <p class=\"txt-md\">USB</p> </li> </ul> <div class=\"storage-select-right\"></div> </div> <div style=\"padding-bottom:30px\"> <div class=\"node-storage-space txt-sm\"> <div class=\"progress-bar\" role=\"status\"> <div class=\"fill\"></div> <p id=\"storage-pct\" class=\"txt-black txt-bold\"> 0% </p> </div> <p id=\"storage-space\">0 GB of 0 GB free</p> </div> </div> </div> <div id=\"upld\" class=\"txt-sm\"> <div class=\"upld-source\"> <img class=\"icon icon-small\" src=\"'+O+'\"/> <p class=\"txt-bold\" data-label=\"proj.add-from.title\">Add file from</p> <span>&nbsp;</span> <ul class=\"node-upload-source\"> <li class=\"txt-bold\"> <a data-tab-btn=\"direct\" data-label=\"proj.add-from.local\">Local storage</a> </li> <li class=\"txt-bold\"> <a data-tab-btn=\"remote\" data-label=\"proj.add-from.remote\">Remote URL</a> </li> </ul> </div> <div> <div data-tab=\"direct\"> <div id=\"upld-direct\" data-state=\"choose\"> <div id=\"upld-direct-frame\" class=\"form-row\"> <input type=\"file\"/> <div class=\"state-choose\"> <img class=\"icon icon-small\" src=\"'+R+'\"/> <p>Click to choose a *.sl1 file or drag it here</p> </div> <div class=\"abs-center state-uploading\"> <img class=\"icon abs-center\" src=\"'+L+'\"/> <p id=\"upld-progress\" class=\"abs-center\"></p> </div> </div> <div class=\"state-choose state-choose-checkbox\"> <input type=\"checkbox\" id=\"upld-direct-start-pt\"/> <label data-label=\"upld.start-pt\" for=\"upld-direct-start-pt\"> Start print after transfer </label> </div> </div> </div> <div data-tab=\"remote\"> <div id=\"upld-remote\" data-state=\"choose\"> <div class=\"state-choose\"> <div class=\"form-row\"> <p data-label=\"upld.remote.source\" class=\"txt-grey\">Source URL</p> <input id=\"remote-url\" class=\"txt-md\" type=\"text\" placeholder=\"http://\"/> <p class=\"txt-grey txt-italic txt-sm\" data-label=\"upld.remote.hint-fdm\"> Type URL of G-CODE file </p> </div> <div class=\"form-row\"> <p data-label=\"upld.remote.file\" class=\"txt-grey\" style=\"margin-top:10px\">File name</p> <input id=\"remote-file-name\" class=\"txt-md\" type=\"text\"/> <p class=\"txt-grey txt-italic\" data-label=\"upld.remote.file-hint\"> Type or edit file name </p> </div> <div class=\"form-submit\"> <div class=\"state-choose-checkbox\"> <input type=\"checkbox\" id=\"upld-remote-start-pt\"/> <label data-label=\"upld.start-pt\" for=\"upld-remote-start-pt\"> Start print after transfer </label> </div> <button id=\"upld-file\" class=\"outlined\"> <p class=\"uppercase\" data-label=\"btn.upld-file\"> UPLOAD FILE </p> </button> </div> </div> <div class=\"state-uploading\"> <div class=\"progress-bar\"> <div class=\"fill\"></div> <p class=\"txt-black hide-scrollbar\" data-where=\"file.displayName\" data-type=\"download\"> </p> </div> <div class=\"upld-details\"> <div class=\"progress\"> <p data-label=\"prop.progress\" class=\"txt-grey\"> Progress </p> <p data-format=\"percent\" data-type=\"download\" data-where=\"progress\" class=\"txt-bold\"> NA </p> </div> <div class=\"file.size\"> <p data-label=\"prop.size\" class=\"txt-grey\"> Size </p> <p data-format=\"size\" data-type=\"download\" data-where=\"file.size\" class=\"txt-bold\"> NA </p> </div> <div class=\"timeStarted\"> <p data-label=\"download.dl-started\" class=\"txt-grey\"> Download Started </p> <p data-format=\"date\" data-type=\"download\" data-where=\"timeStarted\" class=\"txt-bold\"> NA </p> </div> <div class=\"timeRemaining\"> <p data-label=\"prop.rem-time\" class=\"txt-grey\"> Remaining Time </p> <p data-format=\"time\" data-type=\"download\" data-where=\"timeRemaining\" class=\"txt-bold\"> NA </p> </div> <div class=\"file.toPrint\"> <p data-label=\"download.start-pt\" class=\"txt-grey\"> Autostart </p> <p data-format=\"boolean\" data-type=\"download\" data-where=\"file.toPrint\" class=\"txt-bold\"> NA </p> </div> </div> </div> </div> </div> </div> <div style=\"margin-bottom:20px\"></div> </div> <div id=\"files\"></div> </div> <template id=\"node-folder\"> <div class=\"node line\"> <div class=\"node-icon-box\"> <img class=\"node-img node-icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;19.252&#39; height=&#39;14.955&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 15.062 11.696&#39;%3e%3cpath fill=&#39;%23707070&#39; d=&#39;M.001 5.847v-5.85h3.077l.214.247c.277.323.822.867 1.066 1.069.413.339.784.547 1.246.696.576.185.197.172 5.093.18l4.365.005v9.503H0z&#39;/%3e%3c/svg%3e\"/> </div> <div> <p id=\"name\" class=\"txt-md txt-bold\"></p> <p id=\"details\" class=\"txt-sm txt-grey\">x folders | x files</p> </div> <div class=\"separator\"></div> <div class=\"kebab kebab-responsive\"> <img class=\"kebab-menu icon\" src=\"'+q+'\"/> <ul class=\"txt-black txt-md\"> <li id=\"delete\" data-label=\"\" data-label-target=\"title\" title=\"Delete\"> <img class=\"pc-only icon icon-small\" src=\"'+M+'\"/> <img class=\"mobile-only icon\" src=\"'+H+'\"/> <span class=\"mobile-only\" data-label=\"\"> Delete </span> </li> </ul> </div> </div> </template> <template id=\"node-file\"> <div class=\"node line\"> <div class=\"node-img-box\"> <img class=\"node-img\" src=\"'+z+'\" data-firmware=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;48&#39; height=&#39;48&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 571.43 571.43&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M454.66 196.28h59.62v19.88h-59.62v-19.88zm-237.32 97.56h32.99v-11.45h-32.99v-25.56h35.32v-11.67h-50.17v81.14h14.85v-32.46zm81.03 32.46 13.9-51.12c2.23-8.06 2.65-12.73 2.65-12.73h.21s.42 4.67 2.65 12.83l14.11 51.02h14.96l22.06-81.14h-13.79l-12.73 50.27c-2.02 8.06-2.55 13.26-2.55 13.26h-.21s-.85-5.2-3.07-13.37l-13.79-50.17h-14.32l-13.68 50.27c-2.23 8.06-2.76 13.26-2.76 13.26h-.21s-.43-5.2-2.55-13.37l-12.94-50.17h-15.17l22.06 81.14h15.17zm76.78 128.36v59.63l-19.87-.01v-59.63l19.87.01zm-159 0v59.63l-19.88-.01v-59.63l19.88.01zm79.5 0v59.63l-19.87-.01v-59.63l19.87.01zm39.76 0v59.63l-19.88-.01v-59.63l19.88.01zm-79.51 0v59.63l-19.88-.01v-59.63l19.88.01zM57.15 196.28h59.62v19.88H57.15v-19.88zm0 159h59.62v19.88l-59.62-.01v-19.88.01zm0-79.5h59.62v19.87H57.15v-19.87zm0-39.75h59.62v19.88H57.15v-19.88zm0 79.51h59.62v19.88H57.15v-19.88zM375.16 57.16v59.63l-19.87-.01V57.15l19.87.01zm-159 0v59.63l-19.88-.01V57.15l19.88.01zm79.5 0v59.63l-19.87-.01V57.15l19.87.01zm39.76 0v59.63l-19.88-.01V57.15l19.88.01zm-79.51 0v59.63l-19.88-.01V57.15l19.88.01zm198.77 298.13h59.62v19.88l-59.62-.01v-19.88.01zm0-79.5h59.62v19.87h-59.62v-19.87zm0-39.75h59.62v19.88h-59.62v-19.88zm0 79.51h59.62v19.88h-59.62v-19.88z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M434.79 176.4c0-21.94-17.81-39.75-39.75-39.75H176.41c-21.94 0-39.75 17.81-39.75 39.75v218.63c0 21.94 17.81 39.75 39.75 39.75h218.63c21.94 0 39.75-17.81 39.75-39.75V176.4zm-54.66 0c8.23 0 14.91 6.68 14.91 14.91v188.81c0 8.23-6.68 14.9-14.91 14.9l-188.82.01c-8.23 0-14.91-6.68-14.91-14.91V191.31c0-8.23 6.68-14.9 14.91-14.9l188.82-.01z&#39;/%3e%3c/svg%3e\" data-file=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;48&#39; height=&#39;48&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 363.64 363.64&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M252.054 199.141V95.604H108.113v172.731h73.331c22.767 0 70.61-51.984 70.61-69.194zm-129.549 54.797V109.997l115.156.005v83.074c0 29.554-43.183 17.684-43.183 17.684s10.925 43.183-18.986 43.183z&#39; style=&#39;stroke-width:.475005&#39;/%3e%3c/svg%3e\"/> </div> <div class=\"node-wrapper txt-sm\"> <div class=\"node-header\"> <p id=\"name\" class=\"txt-md txt-bold\"></p> <div class=\"kebab kebab-responsive\"> <img class=\"kebab-menu icon\" src=\"'+q+'\"/> <ul class=\"txt-black txt-md\"> <li id=\"details\" data-label=\"proj.details\" data-label-target=\"title\" title=\"File details\"> <img class=\"pc-only icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 44.87 50.47&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M22.44 50.47c7.48-4.14 14.96-8.28 22.44-12.43V10.27L22.44 0 0 10.27v27.77c7.48 4.14 14.96 8.28 22.44 12.43zm0-29.68-16.62-9.2 16.62-7.6 16.62 7.6-16.62 9.2zm1.81 24.54V23.92l17.01-9.42v21.41l-17.01 9.42zm-3.62-21.41v21.41L3.62 35.91V14.5l17.01 9.42z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;m9.49 11.72 12.95 7.17 12.94-7.17L22.44 5.8z&#39;/%3e%3c/svg%3e\"/> <img class=\"mobile-only icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 44.87 50.47&#39;%3e%3cpath fill=&#39;%23585858&#39; d=&#39;M22.44 50.47c7.48-4.14 14.96-8.28 22.44-12.43V10.27L22.44 0 0 10.27v27.77c7.48 4.14 14.96 8.28 22.44 12.43zm0-29.68-16.62-9.2 16.62-7.6 16.62 7.6-16.62 9.2zm1.81 24.54V23.92l17.01-9.42v21.41l-17.01 9.42zm-3.62-21.41v21.41L3.62 35.91V14.5l17.01 9.42z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;m9.49 11.72 12.95 7.17 12.94-7.17L22.44 5.8z&#39;/%3e%3c/svg%3e\"/> <span class=\"mobile-only\" data-label=\"proj.details\"> File details </span> </li> <li id=\"delete\" data-label=\"proj.del\" data-label-target=\"title\" title=\"Delete\"> <img class=\"pc-only icon icon-small\" src=\"'+M+'\"/> <img class=\"mobile-only icon\" src=\"'+H+'\"/> <span class=\"mobile-only\" data-label=\"proj.del\"> Delete </span> </li> <li id=\"download\" data-label=\"proj.download\" data-label-target=\"title\" title=\"Download\"> <img class=\"pc-only icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 24 24&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M8.667 12h2.5V8.667h1.667v3.332l2.5.001-3.333 3.333z&#39;/%3e%3cpath fill=&#39;white&#39; d=&#39;M12 4.5a6.243 6.243 0 0 0-6.232 5.91 4.582 4.582 0 0 0 .815 9.09h10.83a4.583 4.583 0 0 0 .815-9.09 6.243 6.243 0 0 0-6.232-5.91zm0 1.667c2.878 0 4.909 2.331 4.639 5.65 1.454-.038 3.694.626 3.694 3.1a2.92 2.92 0 0 1-2.916 2.916H6.587a2.92 2.92 0 0 1-2.916-2.916c0-2.331 2.066-3.194 3.694-3.1-.14-3.515 1.84-5.65 4.639-5.65z&#39;/%3e%3c/svg%3e\"/> <img class=\"mobile-only icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 24 24&#39;%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M8.667 12h2.5V8.667h1.667v3.332l2.5.001-3.333 3.333z&#39;/%3e%3cpath fill=&#39;%23313131&#39; d=&#39;M12 4.5a6.243 6.243 0 0 0-6.232 5.91 4.582 4.582 0 0 0 .815 9.09h10.83a4.583 4.583 0 0 0 .815-9.09 6.243 6.243 0 0 0-6.232-5.91zm0 1.667c2.878 0 4.909 2.331 4.639 5.65 1.454-.038 3.694.626 3.694 3.1a2.92 2.92 0 0 1-2.916 2.916H6.587a2.92 2.92 0 0 1-2.916-2.916c0-2.331 2.066-3.194 3.694-3.1-.14-3.515 1.84-5.65 4.639-5.65z&#39;/%3e%3c/svg%3e\"/> <span class=\"mobile-only\" data-label=\"proj.download\"> Download </span> </li> </ul> </div> </div> <div class=\"node-details\"> <div class=\"details\" data-format=\"time\" data-where=\"meta.estimatedPrintTime\"> <p data-label=\"prop.pnt-time\" class=\"txt-grey\"> Printing time </p> <p data-value=\"data-value\"></p> </div> <div class=\"details\" data-format=\"material\" data-where=\"meta.filament_type\"> <p data-label=\"prop.material\" class=\"txt-grey\"> Material </p> <p data-value=\"data-value\"></p> </div> <div class=\"separator pc-only\"></div> <div class=\"details\" data-format=\"date\" data-where=\"m_timestamp\"> <p class=\"pc-only\" data-value=\"data-value\"></p> </div> <div class=\"details\" data-format=\"size\" data-where=\"size\"> <p class=\"pc-only\" data-value=\"data-value\"></p> </div> </div> </div> </div> </template> <template id=\"node-current\"> <div class=\"line-y node-current\"> <div class=\"flex-row\"> <img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;64&#39; height=&#39;64&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 363.64 363.64&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M156.08 87.05c-23.07 0-32.74-15-56.79-42.6H0v255.21L29.64 144.1h310.94V87.04h-184.5zm207.56 71.52H41.52L10.91 319.18h322.12l30.61-160.61z&#39;/%3e%3c/svg%3e\"/> <p id=\"path\" class=\"txt-sm txt-grey\"></p> <p id=\"name\" class=\"txt-sm\"></p> </div> <div class=\"separator\"></div> <div class=\"flex-row\"> <ul class=\"node-btn-list\"> <li id=\"create\"> <img class=\"icon icon-small\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; viewBox=&#39;0 0 24 24&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;m6 6 4 2h8v3h1V7h-9L7 5H2v14h17-3v-1H3V6z&#39;/%3e%3cpath fill=&#39;%23FA6831&#39; d=&#39;M15 17v-2h3v-3h1v3h3v2h-3v2h-1v-2z&#39;/%3e%3c/svg%3e\"/> </li> </ul> <ul class=\"sort-bar flex-row\"> <li id=\"sort-by-name\" class=\"flex-row sort-by\"> <p class=\"txt-sm\">name</p> <div class=\"sort-direction flex-col\"> <svg class=\"sort-asc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 3997.6 3997.6\"> <path d=\"M1999 499.7L3997.6 3497.9 0 3497.9z\"></path> </svg> <svg class=\"sort-desc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 571.36 571.36\"> <path d=\"M285.71 499.94L571.36 71.42 0 71.42z\"></path> </svg> </div> </li> <li id=\"sort-by-date\" class=\"flex-row sort-by\"> <p class=\"txt-sm\">date</p> <div class=\"sort-direction flex-col\"> <svg class=\"sort-asc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 3997.6 3997.6\"> <path d=\"M1999 499.7L3997.6 3497.9 0 3497.9z\"></path> </svg> <svg class=\"sort-desc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 571.36 571.36\"> <path d=\"M285.71 499.94L571.36 71.42 0 71.42z\"></path> </svg> </div> </li> <li id=\"sort-by-size\" class=\"flex-row sort-by\"> <p class=\"txt-sm\">size</p> <div class=\"sort-direction flex-col\"> <svg class=\"sort-asc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 3997.6 3997.6\"> <path d=\"M1999 499.7L3997.6 3497.9 0 3497.9z\"></path> </svg> <svg class=\"sort-desc\" xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 571.36 571.36\"> <path d=\"M285.71 499.94L571.36 71.42 0 71.42z\"></path> </svg> </div> </li> </ul> </div> </div> </template> <template id=\"node-up\"> <div class=\"node line\"> <div class=\"node-icon-box\"> <img class=\"node-img node-icon\" src=\"data:image/svg+xml,%3csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;21&#39; height=&#39;22.275&#39; fill-rule=&#39;evenodd&#39; clip-rule=&#39;evenodd&#39; image-rendering=&#39;optimizeQuality&#39; shape-rendering=&#39;geometricPrecision&#39; text-rendering=&#39;geometricPrecision&#39; viewBox=&#39;0 0 82.32 87.32&#39;%3e%3cpath fill=&#39;white&#39; d=&#39;M82.32 70.56H50.96V39.2h19.6L35.28 0 0 39.2h19.6v48.12h62.72z&#39;/%3e%3c/svg%3e\"/> </div> <div> <p id=\"description\" data-label=\"proj.up-folder\" class=\"txt-sm txt-grey\"> parent folder </p> </div> </div> </template> <div id=\"drop-zone\" hidden> <input type=\"file\"/> <div class=\"txt-md\"> <p><span id=\"msg.drop-zone.label\" data-label=\"msg.drop-zone.label\"></span></p> </div> </div> <template id=\"modal-file-name\"> <div class=\"modal-file-name\"> <div class=\"txt-bold\"> <p id=\"modal-file-name-label\" data-label=\"msg.create-folder\"></p> </div> <div class=\"file-name__form\"> <div class=\"flex-row\"> <div class=\"flex-col\"> <label class=\"grow\" for=\"modal-file-name__input\" data-label=\"msg.create-folder-name\"></label> <input id=\"modal-file-name__input\" value=\"\"/> </div> </div> <div> <div class=\"file-name__submit flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+C+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+A+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> </div> </div> </div></div></template> ';e.exports=U},5198:(e,t,a)=>{var i=a(7091),s=a(4622),n=a(6730),r=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div class=\"main-wrapper\"> <div id=\"question\" class=\"txt-md\"></div> <div class=\"flex-row\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+i(s)+'\"/> <p>save changes</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+i(n)+'\"/> <p>cancel</p> </button> </div> </div> ';e.exports=r},2936:(e,t,a)=>{var i=a(7091),s=a(7038),n=a(7336),r=a(1373),o=a(4622),l=a(6730),d=i(s),c=i(n),u=i(r),p=' <div id=\"title\" class=\"txt-md\"> <p id=\"title-status-label\" class=\"txt-grey\"></p> <p class=\"title-printer\"> <span id=\"title-printer-label\" class=\"txt-grey\" data-label=\"printer.title\"></span> <span id=\"title-printer\" class=\"txt-orange\"></span> </p> </div> <div class=\"main-wrapper\" id=\"settings\"> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"version.title\">version</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.api\">api </p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.api\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.hostname\">hostname</p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.hostname\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.firmware\">firmware</p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.firmware\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.server\">server</p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.server\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.text\">text</p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.text\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.sdk\">sdk</p> </div> <div class=\"col txt-md\"> <p data-type=\"settings\" data-where=\"version.sdk\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"version.fe\">frontend</p> </div> <div class=\"col txt-md\"> <p> 3.12.0 </p> </div> </div> </div> </div> <div class=\"component\" id=\"sys-version\"> <p class=\"txt-grey txt-md\" data-label=\"sys-version.title\">system version</p> <div class=\"table\"> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"updates.title\">updates</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col\"> <button class=\"action\" id=\"updates-check\"> <p data-label=\"btn.check-updates\"> Check Updates </p> </button> <img id=\"updates-check__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"conn.title\">connection</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"conn.prusa-connect-url\">PRUSA CONNECT</p> </div> <div class=\"col txt-md\"> <input id=\"conn-prusa-connect-url\" type=\"text\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"> <button class=\"action\" id=\"edit-connect-set\"> <p data-label=\"btn.connect.link\"> set </p> </button> <img id=\"edit-connect-set__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"conn.prusa-connect-status\">connection status</p> </div> <div class=\"col txt-md\"> <div class=\"connection-status\" ok=\"false\" id=\"conn-prusa-connect-status\"> <p ok=\"true\"> <span class=\"conn-status-msg\" id=\"conn-prusa-connect-status-ok\">Successfully connected</span> <img class=\"icon\" src=\"'+c+'\"/> </p> <p ok=\"false\"> <span class=\"conn-status-msg\" id=\"conn-prusa-connect-status-not-ok\"></span> <img class=\"icon\" src=\"'+u+'\"/> </p> </div> </div> <div class=\"col\"> <button class=\"action\" id=\"edit-connect-del\"> <p data-label=\"btn.connect.unlink\"> delete </p> </button> <img id=\"edit-connect-del__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"conn.printer-status\">3D printer connection status</p> </div> <div class=\"col txt-md\"> <div class=\"connection-status\" ok=\"false\" id=\"conn-printer-status\"> <p ok=\"true\"> <span class=\"conn-status-msg\" id=\"conn-printer-status-ok\">Successfully connected</span> <img class=\"icon\" src=\"'+c+'\"/> </p> <p ok=\"false\"> <span class=\"conn-status-msg\" id=\"conn-printer-status-not-ok\"></span> <img class=\"icon\" src=\"'+u+'\"/> </p> </div> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"printer.title\">printer</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"printer.name\">printer name</p> </div> <div class=\"col txt-md\"> <input id=\"printer-name\" type=\"text\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"printer.location\">printer location</p> </div> <div class=\"col txt-md\"> <input id=\"printer-location\" type=\"text\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"printer.network_error_chime\">network error chime</p> </div> <div class=\"col txt-md\"> <input id=\"printer-network_error_chime\" type=\"checkbox\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col\"> <button class=\"action\" id=\"edit-printer\" disabled=\"disabled\"> <p data-label=\"btn.chg\"> change </p> </button> <img id=\"edit-printer__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"user.title\">user</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"user.username\">username</p> </div> <div class=\"col txt-md\"> <input id=\"username\" type=\"text\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p data-label=\"user.format.name\" class=\"txt-sm\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"user.new-password\">new password</p> </div> <div class=\"col txt-md\"> <input id=\"new-password\" type=\"password\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"user.re-password\">repeat password</p> </div> <div class=\"col txt-md\"> <input id=\"re-password\" type=\"password\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p data-label=\"user.format.password-1\" class=\"txt-sm\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p data-label=\"user.format.password-2\" class=\"txt-sm\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p data-label=\"user.format.password-3\" class=\"txt-sm\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p data-label=\"user.format.password-4\" class=\"txt-sm\"> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col txt-md\"> <p> </p> </div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"user.password\">current password</p> </div> <div class=\"col txt-md\"> <input id=\"password\" type=\"password\" value=\"\" placeholder=\"\"/> </div> <div class=\"col\"></div> </div> <div class=\"row\"> <div class=\"col txt-sm\"> </div> <div class=\"col\"> <button class=\"action\" id=\"edit-user\" disabled=\"disabled\"> <p data-label=\"btn.chg\"> change </p> </button> <img id=\"edit-user__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"serial.label\">serial number</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"serial.label\">serial number</p> </div> <div class=\"col txt-md\"> <input id=\"serial\" type=\"text\" value=\"\" placeholder=\"CZPX4242X042XC42042\"/> </div> <div class=\"col\"> <button class=\"action\" id=\"edit-serial\" disabled=\"disabled\"> <p data-label=\"btn.chg\"> set </p> </button> <img id=\"edit-serial__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"api_key.label\">api key</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"api_key.label\">api key</p> </div> <div class=\"col txt-md\"> <p id=\"api_key\" data-type=\"text\"> </p> </div> <div class=\"col\"> <button class=\"action\" id=\"api_key-reset\"> <p data-label=\"btn.reset\"> reset </p> </button> <img id=\"api_key-reset__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <p class=\"txt-grey txt-md\" data-label=\"logs.title\">logs</p> <div class=\"table\"> <div class=\"row\"> <div class=\"col txt-sm\"> <p class=\"txt-bold txt-grey\" data-label=\"logs.select-file\">select file</p> </div> <div class=\"col txt-md\"> <select data-type=\"dropdown\"></select> </div> <div class=\"col\"> <button class=\"action\" id=\"download-log\"> <p data-label=\"btn.download\"> Download </p> </button> <img id=\"download-log__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> </div> </div> </div> </div> <div class=\"component\"> <ul class=\"logs\"> <li data-label=\"logs.not-selected\" class=\"txt-md\">No log file is selected!</li> </ul> </div> </div> <template id=\"modal-sysupgrade\"> <div class=\"modal-sysupgrade\"> <div class=\"txt-bold txt-center\"> <p id=\"modal-sysupgrade__title\" data-label=\"msg.sysupgrade.title\"></p> </div> <div class=\"modal-sysupgrade__message\"> <p class=\"txt-sm txt-center\"> <span id=\"modal-sysupgrade__target\"></span> <span id=\"modal-sysupgrade__current\"></span> &rarr; <span id=\"modal-sysupgrade__version\"></span> </p> <p class=\"txt-sm txt-center\" data-label=\"msg.sysupgrade.remark\"></p> </div> <div class=\"modal-sysupgrade__submit flex-row txt-center justify-center\"> <button class=\"yes mr-md\" id=\"yes\"> <img src=\"'+i(o)+'\"/> <p data-label=\"btn.confirm\">Confirm</p> </button> <button class=\"no\" id=\"no\"> <img src=\"'+i(l)+'\"/> <p data-label=\"btn.cancel\">Cancel</p> </button> <img id=\"modal-sysupgrade__spinner\" class=\"icon\" hidden src=\"'+d+'\"/> <span id=\"modal-sysupgrade__status\"></span> </div> </div> </template> ';e.exports=p},7091:e=>{\"use strict\";e.exports=function(e,t){return t||(t={}),\"string\"!=typeof(e=e&&e.__esModule?e.default:e)?e:(t.hash&&(e+=t.hash),t.maybeNeedQuotes&&/[\\t\\n\\f\\r \"'=<>`]/.test(e)?'\"'.concat(e,'\"'):e)}},6648:(e,t,a)=>{\"use strict\";a.d(t,{LK:()=>d,O2:()=>n,Z5:()=>g,gJ:()=>m,iT:()=>c,wU:()=>o});var i=a(8236),s=a(1351);const n=window.location.pathname.endsWith(\"/\")?window.location.pathname.slice(0,-1):window.location.pathname,r=()=>new Promise(((e,t)=>{(0,i.o)((e=>t=>{const a=document.getElementById(\"modal-apiKey\"),i=document.importNode(a.content,!0);return i.getElementById(\"apiKey\").addEventListener(\"keydown\",(a=>{\"Enter\"==a.key&&(t(),e(a.target.value))})),i.getElementById(\"login\").addEventListener(\"click\",(a=>{a.preventDefault();let i=document.getElementById(\"apiKey\").value;t(),e(i)})),i})(e),{timeout:0,closeOutside:!1})})).then((e=>sessionStorage.setItem(\"apiKey\",e))),o=(e=\"application/json\")=>\"ApiKey\"==sessionStorage.getItem(\"authType\")?{\"X-Api-Key\":sessionStorage.getItem(\"apiKey\"),Accept:e}:{Accept:e},l=()=>new Promise(((e,t)=>(sessionStorage.setItem(\"auth\",\"pending\"),fetch(`${n}/api/v1/info`,{headers:o()}).then((t=>{if(401==t.status){const a=t.headers.get(\"WWW-Authenticate\").split(\" \")[0];return sessionStorage.setItem(\"authType\",a),sessionStorage.removeItem(\"apiKey\"),\"ApiKey\"==a?r().then((()=>l().then((t=>e(t))))):l().then((t=>e(t)))}{const e=t.json();return 200!=t.status&&e.then((e=>(0,s.S)({data:e}))),e}})).then((t=>{sessionStorage.setItem(\"auth\",\"true\"),e(t)}))))),d=(e,t={})=>u(e,t,\"application/json\",\"json\"),c=(e,t={})=>u(e,t,\"text/plain\",\"text\");async function u(e,t={},a,i){if(\"true\"!=sessionStorage.getItem(\"auth\"))throw{code:401};{t.headers={...t.headers,...o(a)};const s=await fetch(`${n}${e}`,t),r=s.status,l={code:r,eTag:s.headers.get(\"etag\")};switch(r){case 401:throw sessionStorage.setItem(\"auth\",\"false\"),l;case 204:case 304:return l;default:const e=await s.text();if(!s.ok){if(e.length>0)try{l.data=JSON.parse(e)}catch{}throw l.data=l.data||{title:`Error ${r}`,message:s.statusText},l}return l.data=\"json\"===i?0===e.length?{}:JSON.parse(e):e,l}}}const p=(e,t,a)=>new Promise(((i,s)=>{\"true\"==sessionStorage.getItem(\"auth\")?(t.headers={...o(),...t.headers},fetch(a?`${n}${e}?ct=${a}`:e,t).then((e=>{401==e.status&&(sessionStorage.setItem(\"auth\",\"false\"),s(e)),e.ok?e.blob().then((t=>i({url:URL.createObjectURL(t),headers:e.headers}))):s(e)})).catch((e=>s(e)))):s()})),m=(e,t,a={})=>p(`${n}${e}`,{...a,headers:{...a.headers,Accept:\"image/*\"}},t),v=e=>{const t=document.getElementById(\"modal-welcome\"),a=document.importNode(t.content,!0);return a.querySelector(\".close-button\").addEventListener(\"click\",e),a},g=()=>null==localStorage.getItem(\"showWelcome\")?new Promise(((e,t)=>{(0,i.o)(v,{closeCallback:()=>{localStorage.setItem(\"showWelcome\",!0),e()}})})).then((()=>l())):l()},1972:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>s});var i=a(6648);const s=function(e,t){let a=document.createElement(\"a\");a.href=`${i.O2}${e}`,a.download=t||\"\",a.click(),a.remove()}},646:(e,t,a)=>{\"use strict\";function i(e,t=!0){e&&(t&&!e.hasAttribute(\"hidden\")&&e.setAttribute(\"hidden\",!0),!t&&e.hasAttribute(\"hidden\")&&e.removeAttribute(\"hidden\"))}function s(e,t=!0){i(e,!t)}function n(e,t=!0){e&&(t&&!e.hasAttribute(\"disabled\")&&e.setAttribute(\"disabled\",!0),!t&&e.hasAttribute(\"disabled\")&&e.removeAttribute(\"disabled\"))}function r(e,t=!0){return n(e,!t)}function o(){s(document.querySelector(\"#job .loading-overlay\"))}function l(){i(document.querySelector(\"#job .loading-overlay\"))}function d(e,...t){const a=i=>{if(t)for(const e of t)if(e&&e.contains(i.target))return;e&&e(),window.removeEventListener(\"pointerup\",a)};window.addEventListener(\"pointerup\",a)}a.d(t,{H:()=>n,QH:()=>i,QP:()=>o,Ti:()=>d,Zk:()=>l,gL:()=>r,yx:()=>s})},5537:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>i});const i=function(e){return\"string\"==typeof e?document.getElementById(e):e||document.body}},9741:e=>{e.exports=function(e,t){let a=t.split(\".\"),i=e;for(const e of a)if(i=i[e],!i)break;return i}},7780:(e,t,a)=>{\"use strict\";a.d(t,{G3:()=>v,Iu:()=>h,Vb:()=>g,m0:()=>m,ot:()=>f});var i=a(5537),s=a(9741),n=a.n(s);const r=a(4977),o=r.langs,l=r.texts,d=o.indexOf(\"en\");let c,u;function p(){return navigator.language||navigator.userLanguage||\"\"}function m(e){const t=o.indexOf(e);return-1!==t&&(u=t,c=e,localStorage.setItem(\"lang\",c),!0)}function v(){return c}function g(){return[...o]}function h(e,t){let a=n()(l,`${e}.${u}`);if(!a)return a=n()(l,`${e}.${d}`),a||(a=e),b(a,t),a;let i=null;if(t&&(i=Object.assign({},t),delete i.query,delete i.ref),i&&Object.keys(i).length>0){let e,i=a,s=/{{(.*?)}}/g;for(;e=s.exec(a);){let s=a.substr(e.index+2,e[0].length-4);if(\"query\"!==s&&\"ref\"!==s)if(s in t){let a=t[s];i=i.replace(e[0],a)}else 0}a=i}return b(a,t),a}function b(e,t){if(t)if(t.ref){t.ref.innerHTML=e}else if(t.query){let a=document.querySelector(t.query);a?a.innerHTML=e:console.warn(`cannot find element with \"${t.query}\" query`)}}function f(e){(0,i.Z)(e).querySelectorAll('[data-label]:not([data-label=\"\"])').forEach((e=>{const t=h(e.getAttribute(\"data-label\")),a=e.getAttribute(\"data-label-target\");a?e.setAttribute(a,t):e.innerHTML=t}))}m(localStorage.getItem(\"lang\"))||m(p().toLowerCase())||m(p().toLowerCase().split(\"-\")[0])||m(\"en\")},2451:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>L});var i=a(6648),s=a(646),n=a(7780),r=a(3707),o=a(8236),l=a(5412),d=a(1351);let c,u=!1,p=[];const m={TEN_SEC:\"TEN_SEC\",THIRTY_SEC:\"THIRTY_SEC\",SIXTY_SEC:\"SIXTY_SEC\",EACH_LAYER:\"EACH_LAYER\",FIFTH_LAYER:\"FIFTH_LAYER\",MANUAL:\"MANUAL\"},v=e=>{switch(e){case m.TEN_SEC:return(0,n.Iu)(\"cameras.trigger-scheme.ten-sec\");case m.THIRTY_SEC:return(0,n.Iu)(\"cameras.trigger-scheme.thirty-sec\");case m.SIXTY_SEC:return(0,n.Iu)(\"cameras.trigger-scheme.sixty-sec\");case m.EACH_LAYER:return(0,n.Iu)(\"cameras.trigger-scheme.each-layer\");case m.FIFTH_LAYER:return(0,n.Iu)(\"cameras.trigger-scheme.fifth-layer\");case m.MANUAL:return(0,n.Iu)(\"cameras.trigger-scheme.manual\");default:return`${e}`}},g=()=>(0,r.Vp)((0,n.Iu)(\"ntf.success\"),(0,n.Iu)(\"ntf.camera-suc\")),h=(e,t=E)=>{void 0===c&&(c=e.camera.id),u=e.link.connect.ok,(0,i.LK)(\"/api/v1/cameras\").then((e=>{const a=(e?.data?.camera_list||[]).map((e=>{let t=p.find((t=>t.id===e.camera_id))||{};return e.camera_id===c&&(e.connected||(c=null)),{id:e.camera_id,config:e.config,connected:e.connected,detected:e.detected,stored:e.stored,registered:e.registered,nextSnapshotAt:t?.nextSnapshotAt,lastSnapshotAt:t?.lastSnapshotAt,lastSnapshotUrl:t?.lastSnapshotUrl}})),i=p.filter((e=>!a.find((t=>e.id===t.id))));t&&t(a,i),p=a})).catch(d.S),p.filter((e=>e.connected)).forEach((e=>y(e.id)))},b=e=>`camera_${e}`,f=e=>document.getElementById(b(e)),y=e=>{const t=p.find((t=>t.id===e));if(!t)return;const a=new Date;t.lastSnapshotAt&&t.nextSnapshotAt&&a<t.nextSnapshotAt||(0,i.gJ)(`/api/v1/cameras/${e}/snap`,0,{headers:t.lastSnapshotAt?{\"If-Modified-Since\":t.lastSnapshotAt.toUTCString()}:{}}).then((({url:t,headers:i})=>{const n=p.find((t=>t.id===e)),r=b(e),o=document.querySelector(`#${r} .camera__no-snapshot`),l=document.querySelector(`#${r} .camera__snapshot`);if(o&&(0,s.yx)(o,!1),l&&((0,s.yx)(l,!0),l.src=t),n){const e=`${i.get(\"cache-control\")}`.match(/max-age=(\\d+)/);let s;e&&(s=parseInt(e[1],10)),s||(s=11);const r=i.get(\"expires\"),o=i.get(\"last-modified\"),l=e=>{const t=new Date;return new Date(t.getTime()+1e3*e)};n.nextSnapshotAt=r?new Date(r):l(s),n.nextSnapshotAt<a&&(n.nextSnapshotAt=l(s)),n.lastSnapshotAt=o?new Date(o):new Date,n.lastSnapshotUrl=t,c&&n.id!==c||k(n.id)}}))},k=e=>{e||(e=c);const t=e?p.find((t=>t.id===e)):null,a=document.getElementById(\"camera-snapshot-picture\"),i=document.getElementById(\"camera-snapshot-time\"),s=document.getElementById(\"camera-snapshot-name\"),[n,r,o,l]=t?.lastSnapshotAt?[t.id,t.config.name,t.lastSnapshotAt.toLocaleString(),t.lastSnapshotUrl]:[null,\"-\",\"-\",\"\"];c=n,a&&(a.src=l),i&&(i.innerText=o),s&&(s.innerText=r)},w=(e,t,a=!1)=>{e.querySelector(\".camera__name\").innerText=t.config.name,e.querySelector(\".camera__path\").innerText=t.config.path||\"-\",e.querySelector(\".camera__driver\").innerText=t.config.driver||\"-\",e.querySelector(\".camera__cloud\").innerText=t.registered?(0,n.Iu)(\"camera.cloud.linked\"):(0,n.Iu)(\"camera.cloud.not-linked\");const i=e.querySelector(\".camera__register\"),r=e.querySelector(\".camera__unregister\"),o=e.querySelector(\".camera__add\"),l=e.querySelector(\".camera__remove\"),d=e.querySelector(\".camera__settings\");(0,s.yx)(i,u&&t.connected&&!t.registered),(0,s.yx)(r,u&&t.connected&&t.registered),(0,s.yx)(o,!1),(0,s.yx)(l,!t.connected),(0,s.yx)(d,t.connected),a&&(o.title=(0,n.Iu)(\"camera.btn.connect\"),d.title=(0,n.Iu)(\"camera.btn.settings\"),i.title=(0,n.Iu)(\"camera.btn.link\"),r.title=(0,n.Iu)(\"camera.btn.unlink\"),o.addEventListener(\"click\",(e=>{e.stopPropagation(),z(t.id)}),!1),l.addEventListener(\"click\",(e=>{e.stopPropagation(),S(t.id)}),!1),d.addEventListener(\"click\",(e=>{e.stopPropagation(),P(t.id)}),!1),i.addEventListener(\"click\",(e=>{e.stopPropagation(),x(t.id,\"POST\")}),!1),r.addEventListener(\"click\",(e=>{e.stopPropagation(),x(t.id,\"DELETE\")}),!1))},x=(e,t)=>{(0,i.LK)(`/api/v1/cameras/${e}/connection`,{method:t}).then((()=>g())).catch(d.S)},z=e=>{const t=p.find((t=>t.id===e));t&&(0,i.LK)(`/api/v1/cameras/${t.id}`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({config:t.config})}).then((()=>g())).catch(d.S)},S=e=>{const t=p.find((t=>t.id===e));t&&(0,i.LK)(`/api/v1/cameras/${t.id}`,{method:\"DELETE\"}).then((()=>g())).catch(d.S)},E=(e,t)=>{const a=document.getElementById(\"cameras-list\");t.forEach((e=>{const t=f(e.id);t&&a.removeChild(t)})),e.sort(((e,t)=>t.connected-e.connected)).forEach((e=>{const t=f(e.id);if(t)w(t,e);else{const t=(e=>{const t=document.getElementById(\"camera-list-item\")?.content;if(!t)return null;const a=document.importNode(t,!0),i=a.querySelector(\"li\"),r=e.id;return i.addEventListener(\"click\",(e=>{const t=p.find((e=>e.id===r));t&&t.connected&&k(t.id),e.preventDefault()}),!1),i.id=b(e.id),a.querySelector(\".camera__path__label\").innerText=(0,n.Iu)(\"camera.path\"),a.querySelector(\".camera__driver__label\").innerText=(0,n.Iu)(\"camera.driver\"),a.querySelector(\".camera__cloud__label\").innerText=(0,n.Iu)(\"camera.cloud.label\"),(0,s.yx)(a.querySelector(\".camera__snapshot\"),!1),w(a,e,!0),a})(e);t&&a.appendChild(t)}}))},P=e=>new Promise(((t,a)=>{(0,o.o)(((e,t)=>t=>{const a=`/api/v1/cameras/${e}`,o=document.getElementById(\"modal-camera-settings\"),c=document.importNode(o.content,!0),u=c.getElementById(\"camera-settings__name\"),p=c.getElementById(\"camera-settings__focus\"),g=l.L.init(c.getElementById(\"camera-settings__resolution\"),\"camera-settings__resolution\"),h=l.L.init(c.getElementById(\"camera-settings__trigger-scheme\"),\"camera-settings__trigger-scheme\"),b=c.getElementById(\"yes\");return(0,i.LK)(a).then((e=>{const o=e.data,l=o.available_resolutions.sort(((e,t)=>e.width===t.width?t.height-e.height:t.width-e.width)).map(((e,t)=>`${e.width}x${e.height}`)),c=Object.keys(m),f=c.map((e=>v(e))),y=o.capabilities.includes(\"FOCUS\");u.value=o.name,g.setOptions(l),g.value=`${o.resolution.width}x${o.resolution.height}`,h.setOptions(f),h.value=v(o.trigger_scheme),(0,s.yx)(p.parentNode,y),y&&(p.value=Math.round(100*o.focus)),b.addEventListener(\"click\",(()=>{const[e,s]=g.value.split(\"x\").map((e=>parseInt(e))),o=c[f.indexOf(h.value)],l=y?{focus:p.value/100}:{};(0,i.LK)(`${a}/config`,{method:\"PATCH\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({name:u.value,resolution:{width:e,height:s},trigger_scheme:o,...l})}).then((()=>(0,r.Vp)((0,n.Iu)(\"ntf.success\"),(0,n.Iu)(\"ntf.camera-config-success\")))).catch(d.S).finally(t)}))})).catch(d.S),c.getElementById(\"no\").addEventListener(\"click\",(()=>t())),c})(e),{timeout:0,closeOutside:!1})})).then((()=>{lastUpdated=null,h()})),L={load:e=>{p=[],(0,n.Iu)(\"cameras.link\",{query:\"#title-status-label\"}),h(e)},update:h,getCameraNode:f,getCameraNodeId:b,updateCurrentCamera:k}},732:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>v});var i=a(7780),s=a(646),n=a(5489),r=a(6648);var o=a(1351),l=a(4800);let d=1,c=1;const u=e=>{(0,n.ZP)(\"control\",e),function(e){const t=e.state,a=document.querySelectorAll(\"#control button\"),i=[\"extrude\",\"retract\"],n=[\"flowrate\",\"bed\",\"nozzle\",\"speed\"],r=[...n,...i,\"move-step\",\"extrude-retract-step\",\"heated-bed-xy-move\"];if([l.PT.PRINTING||l.PT.BUSY||l.PT.PAUSED].includes(t)){const e=t===l.PT.PAUSED?r:n;a.forEach((t=>{const a=t.id||t.parentNode.id||t.parentNode.parentNode.id;(0,s.H)(t,!e.includes(a))}))}const o=e.telemetry.temperature.nozzle.current||0,d=e.printer.minExtrusionTemp||0,c=o&&d&&o>=d;i.forEach((e=>(0,s.H)(document.getElementById(e),!c)))}(e)};function p(e,t){if(\"move\"===e){(e=>(0,r.LK)(\"/api/printer/printhead\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"jog\",...e})}))(function(e){const t=e.includes(\"-\"),a=e.replace(RegExp(\"[+-]\"),\"\");return a?{[a]:t?-d:d}:(console.error(`\"${e}\" is not valid direction`),{})}(t)).catch((e=>(0,o.S)(e)))}else if(\"home\"===e){(e=>(0,r.LK)(\"/api/printer/printhead\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"home\",axes:e})}))(t.split(\",\")).catch((e=>(0,o.S)(e)))}}function m(e,t,a){function i(e){return Number.parseFloat(e.getAttribute(\"data-step\"))}if(e){const n=e.querySelectorAll(\"button[data-step]\"),r=e=>{n.forEach((t=>{i(t)===e?((0,s.H)(t),t.setAttribute(\"selected\",!0)):((0,s.gL)(t),t.hasAttribute(\"selected\")&&t.removeAttribute(\"selected\"))}))};n.forEach((e=>{const t=i(e);isNaN(t)||(e.onclick=()=>{r(t),a(t)})})),r(t)}}const v={load:e=>{(0,i.Iu)(\"control.title\",{query:\"#title-status-label\"}),function(e){function t(e,t){switch(e){case\"bed\":return i=t,(0,r.LK)(\"/api/printer/bed\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"target\",target:i})});case\"flowrate\":return a=t,(0,r.LK)(\"/api/printer/tool\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"flowrate\",factor:a})});case\"nozzle\":return(e=>(0,r.LK)(\"/api/printer/tool\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"target\",targets:{tool0:e}})}))(t);case\"speed\":return(e=>(0,r.LK)(\"/api/printer/printhead\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"speed\",factor:e})}))(t);default:throw Error(\"Unknown property!\")}var a,i}e&&e.querySelectorAll(\".input-wrapper\").forEach((e=>{const a=e.querySelector(\"input\"),i=e.querySelector(\"button\");if(a){const e=()=>{const e=a.getAttribute(\"data-action\"),i=Number.parseFloat(a.value);isNaN(i)||t(e,i).then((e=>{a.value=\"\"})).catch((e=>(0,o.S)(e)))};a.onkeyup=t=>{\"Enter\"===t.key&&(e(),a.blur())},i&&(i.onclick=e)}}))}(document.querySelector(\"#control\")),m(document.querySelector(\"#control #move-step\"),d,(e=>d=e)),m(document.querySelector(\"#control #extrude-retract-step\"),c,(e=>c=e)),function(){const e=document.querySelector(\"#control #disable-steppers\");e&&(e.onclick=()=>{(0,r.LK)(\"/api/printer/printhead\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"disable_steppers\"})}).catch((e=>(0,o.S)(e)))})}(),function(){const e=document.querySelector(\"#control #extrude\");e&&(e.onclick=()=>{var e;(e=c,(0,r.LK)(\"/api/printer/tool\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"extrude\",amount:e})})).catch((e=>(0,o.S)(e)))})}(),function(){const e=document.querySelector(\"#control #retract\");e&&(e.onclick=()=>{var e;(e=c,(0,r.LK)(\"/api/printer/tool\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"extrude\",amount:-e})})).catch((e=>(0,o.S)(e)))})}(),function(){const e=document.querySelector(\"#control #heated-bed-xy-move\");e&&e.querySelectorAll(\"button[data-action]\").forEach((e=>{e.onclick=()=>{p(e.getAttribute(\"data-action\"),e.getAttribute(\"data-value\"))}}))}(),function(){const e=document.querySelector(\"#control #nozzle-z-move\");e&&e.querySelectorAll(\"button[data-action]\").forEach((e=>{e.onclick=()=>{p(e.getAttribute(\"data-action\"),e.getAttribute(\"data-value\"))}}))}(),u(e)},update:u}},5502:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>y});var i=a(7780);const s=(0,i.Iu)(\"prop.at\"),n=(0,i.Iu)(\"unit.h\"),r=(0,i.Iu)(\"prop.less-than\"),o=(0,i.Iu)(\"unit.min\"),l=(0,i.Iu)(\"unit.ml\"),d=(0,i.Iu)(\"unit.rpm\"),c=(0,i.Iu)(\"prop.today-at\"),u=(0,i.Iu)(\"prop.tmw-at\"),p=(0,i.Iu)(\"unit.b\"),m=(0,i.Iu)(\"unit.kb\"),v=(0,i.Iu)(\"unit.mb\"),g=(0,i.Iu)(\"unit.gb\"),h=(0,i.Iu)(\"prop.true\"),b=(0,i.Iu)(\"prop.false\");function f(e,t=!0,a=1){return e>0?t?e.toFixed(a):e:0}const y=(e,t)=>{if(void 0===t||null===t&&\"progress\"!==e)return(0,i.Iu)(\"prop.na\");switch(e){case\"int\":return parseInt(t);case\"number\":return f(t);case\"layer\":return f(t,!1)+\" mm\";case\"totalLayer\":return function(e){const t=e?.progress?.currentLayer,a=e?.job?.file?.layers;return null==t||null==a?(0,i.Iu)(\"prop.na\"):`${t}/${a}`}(t);case\"material\":case\"material\":return t||(0,i.Iu)(\"prop.na\");case\"temp\":return f(t)+\" °C\";case\"temp_int\":return f(t,0)+\"°C\";case\"fan\":return f(t)+` ${d}`;case\"resin\":return f(t)+` ${l}`;case\"cover\":return t?(0,i.Iu)(\"prop.cover-closed\"):(0,i.Iu)(\"prop.cover-opened\");case\"print\":case\"progressPct\":return f(t||0,!0,0)+\"%\";case\"pos\":return f(t)+\" mm\";case\"date\":return function(e){const t=new Date(1e3*e);var a=localStorage.getItem(\"lang\");return(t.toLocaleDateString(a,{year:\"numeric\",month:\"numeric\",day:\"numeric\"})+\" \"+t.toLocaleTimeString(a,{hour:\"numeric\",minute:\"numeric\"})).substring(0,25)}(t);case\"time\":return function(e){if(e<60)return r;const t=Math.floor(e/60%60),a=Math.floor(e/3600);return(a>0?`${a} ${n}`:\"\")+(t>0?` ${t} ${o}`:\"\")}(t);case\"timeEst\":return function(e){let t=\"00:00\";if(e){let a=new Date,i=new Date(a.getTime()+1e3*e),n=new Date(a);n.setDate(n.getDate()+1);let r=\"\";if(i.getDate()==a.getDate()&&i.getMonth()==a.getMonth())r=`${c} `;else if(i.getDate()==n.getDate()&&i.getMonth()==n.getMonth())r=`${u} `;else{let e={month:\"numeric\",day:\"numeric\"};r=`${i.toLocaleString(window.navigator.language,e)} ${s} `}t=r+(\"0\"+i.getHours()).substr(-2)+\":\"+(\"0\"+i.getMinutes()).substr(-2)}return t}(t);case\"progress\":return f(100*(t||0),!0,0)+\"%\";case\"percent\":return`${f(t||0,!0,0)}%`;case\"size\":return function(e){let t=e;const a=[p,m,v,g];for(let e=0;e<a.length;e++){if(t<1e3||e==a.length-1)return t.toLocaleString(localStorage.getItem(\"lang\"),{maximumFractionDigits:2})+\" \"+a[e];t/=1024}}(t);case\"boolean\":return function(e){switch(e){case!0:case\"true\":case 1:return h;case!1:case\"false\":case 0:return b;default:return e}}(t);case\"diameter\":return f(t,!0,2)+\" mm\";default:return t}}},5412:(e,t,a)=>{\"use strict\";a.d(t,{L:()=>s});var i=a(5537);class s{set value(e){this._value=e,this.updateLabel()}get value(){return this._value}constructor(e,t,a){const i=this;this._label=t,this._ul=a,this._value=t.innerHTML,this.is_open=!1,this._options=[],this._onKeyDown=e=>{if(i.is_open){switch(e.key.toLowerCase()){case\"escape\":return void i.close();case\"arrowdown\":let e=!1;for(const t of this._ul.childNodes){if(e){this._highlight(t);break}t.classList?.contains(\"select\")&&(e=!0)}break;case\"arrowup\":let t=null;for(const e of this._ul.childNodes){if(e.classList?.contains(\"select\")){t&&this._highlight(t);break}t=e}break;case\"enter\":for(const e of this._ul.childNodes)if(e.classList.contains(\"select\")){const t=e.innerText;this.value=t,this.onselect&&this.onselect(t),this.close();break}}e.preventDefault()}},this._onClick=t=>{const a=t.target.parentNode;if(!i.is_open)return!0;a!==e&&i.close()},window.addEventListener(\"keydown\",this._onKeyDown,!1),window.addEventListener(\"click\",this._onClick,!1),this.onselect=void 0,e.onclick=e=>{e.preventDefault(),this.is_open?this.close():this.open()}}_highlight(e){this._ul.childNodes.forEach((t=>{t===e?t.classList.add(\"select\"):t.classList.remove(\"select\")}))}destructor(){window.removeEventListener(\"keypress\",this._onKeyPress),window.removeEventListener(\"click\",this._onClick)}static init(e,t){let a=(0,i.Z)(e);const n=document.getElementById(\"dropdown-template\"),r=\"dropdown\"===a.getAttribute(\"data-type\")?a:a.querySelector('select[data-type=\"dropdown\"]');if(!r)return;r.after(document.importNode(n.content,!0));const o=r.nextElementSibling;o.id=t,r.remove();const l=o.querySelector(\".dropdown-btn\"),d=l.querySelector(\".dropdown-label\"),c=o.querySelector(\".dropdown-content ul\");return l&&d&&c?new s(l,d,c):void 0}setOptions(e){this._options=e}updateLabel(){this._label.innerHTML=this._value}select(e){this._label.innerHTML=e}open(){this._ul.classList.contains(\"open\")||(this._options.forEach((e=>{const t=document.createElement(\"li\");e===this._value&&(t.className=\"select selected\"),t.innerText=e,t.onclick=()=>{this.select(e),this.value=e,this.onselect&&this.onselect(e),this.close()},t.onmouseover=()=>this._highlight(t),this._ul.appendChild(t)})),this._ul.classList.add(\"open\")),this.is_open=!0}close(){for(this._ul.classList.remove([\"open\"]);this._ul.firstChild;)this._ul.removeChild(this._ul.firstChild);this.is_open=!1}}},1351:(e,t,a)=>{\"use strict\";a.d(t,{S:()=>n});var i=a(3707);let s={};function n(e,t){let a=e?.data?.title||t?.fallbackMessage?.title||\"Error\",n=e?.data?.message||t?.fallbackMessage?.message||\"Action can not be performed\",r=t?.isWarning??!1;if(e?.data){const t=e.data;t.code&&(a+=` - ${t.code}`,\"7\"==`${t.code}`[3]&&(r=!0)),t.url&&(n+=`<br/><a href=\"${t.url}\" target=\"_blank\">more info</a>`)}const o=e?.data?.code||`${a}\\n${n}`;if(s[o])return;s[o]=!0;const l=()=>s[o]=!1;r?(0,i.Kp)(a,n,l):(0,i.vU)(a,n,l)}},8236:(e,t,a)=>{\"use strict\";a.d(t,{o:()=>n});var i=a(7780);const s={count:0,current:0},n=(e,t={})=>{const a=Object.assign({timeout:5500,closeOutside:!0},t),n=s.count;s.count=s.count+1;const r=document.querySelector(\".modal-box\");t.className&&r.classList.add(t.className);const o=r.parentElement;for(;r.firstChild;)r.removeChild(r.firstChild);const l=()=>{n==s.current&&o.classList.contains(\"show-modal\")&&(o.classList.remove(\"show-modal\"),a.closeCallback&&a.closeCallback())},d=e=>{e.target===o&&l()};a.closeOutside&&window.addEventListener(\"click\",d);const c=e(l);s.current=n,r.appendChild(c),(0,i.ot)(r),o.classList.add(\"show-modal\"),a.timeout>0&&setTimeout(l,a.timeout)}},5951:(e,t,a)=>{\"use strict\";function i(e,t,a=\"right\"){if(!e)return;const i=e.querySelector(\".fill\");if(i){const e=`${[\"top\",\"right\",\"bottom\",\"left\"].map((e=>(e=>e===a?100-(t||0)+\"%\":0)(e))).join(\" \")}`;i.style.inset=e}}a.d(t,{g:()=>i})},2957:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>x});var i=a(7780),s=a(5489),n=a(6648),r=a(646),o=a(1351);var l=a(3707),d=a(8236);let c=null,u=null,p=null,m=!1;const v=a(5493).Z,g=()=>(0,l.Vp)((0,i.Iu)(\"ntf.success\"),(0,i.Iu)(\"ntf.settings-suc\"));function h(e){const t=document.getElementById(\"api_key\");t&&(t.innerText=e)}function b(e,t){(0,s.ZP)(\"con-settings\",e.link),function(e,t){const a=document.getElementById(\"conn-prusa-connect-status\"),i=document.getElementById(\"conn-prusa-connect-url\"),s=document.getElementById(\"edit-connect-del\"),n=\"FINISHED\"===e.connect.registration,{hostname:o,tls:l,port:d}=e.connect.settings,{ok:c,message:u}=e.connect,m=c&&n,v=document.getElementById(\"conn-prusa-connect-status-\"+(m?\"ok\":\"not-ok\")),g=`${l?\"https\":\"http\"}://${o}${d?`:${d}`:\"\"}`,h=`(${g})`;(t||p!==g)&&(i.value=g,p=g);(0,r.QH)(i.parentNode.parentNode,n),s&&(0,r.yx)(s,n||!c);f(a,v,m,u,h)}(e.link,t),function(e,t){const a=document.getElementById(\"conn-printer-status\"),{port:i,baudrate:s}=t.printer.settings,{ok:n,message:r}=t.printer,o=document.getElementById(\"conn-printer-status-\"+(n?\"ok\":\"not-ok\"));f(a,o,n,r,`(${i||\"/dev/ttyACM0\"} @ ${s||0}bps)`)}(e.state,e.link)}function f(e,t,a,s,n){e&&e.setAttribute(\"ok\",Boolean(a)),t&&(t.innerHTML=(a?(0,i.Iu)(\"conn.suc\"):s)+\"</br>\"+n)}function y(e){switch(e.toLowerCase()){case\"python\":return(0,i.Iu)(\"sys-version.python\");case\"description\":return(0,i.Iu)(\"sys-version.description\");case\"id\":return(0,i.Iu)(\"sys-version.id\");case\"os\":return(0,i.Iu)(\"sys-version.os\");default:return e}}const k=(e,t,a)=>{const s=document.getElementById(\"modal-sysupgrade\"),l=document.importNode(s.content,!0),d=l.getElementById(\"modal-sysupgrade__target\"),c=l.getElementById(\"modal-sysupgrade__current\"),p=l.getElementById(\"modal-sysupgrade__version\"),m=l.getElementById(\"modal-sysupgrade__status\"),v=l.getElementById(\"modal-sysupgrade__spinner\"),g=l.getElementById(\"yes\"),h=l.getElementById(\"no\");return d.innerText=t,c.innerText=u,p.innerText=a,g.addEventListener(\"click\",(t=>{t.preventDefault(),(0,r.H)(g,!0),(0,r.H)(h,!0),(0,r.yx)(g,!1),(0,r.yx)(h,!1),(0,r.yx)(v,!0),m.innerText=(0,i.Iu)(\"msg.sysupgrade.pending\"),(0,n.LK)(\"/api/v1/update/prusalink\",{method:\"POST\"}).then((()=>(m.innerText=(0,i.Iu)(\"msg.sysupgrade.wait-for-printer\"),w(a)))).catch((e=>(0,o.S)(e))).finally((()=>{e(),window.location.href=\"/\"}))})),h.addEventListener(\"click\",(()=>e())),l},w=e=>new Promise(((e,t)=>{const a=()=>(0,n.LK)(\"/api/version\").then((t=>{e()})).catch((e=>setTimeout(a,2500)));setTimeout(a,5e3)})),x={load:e=>{(0,i.Iu)(\"settings.title\",{query:\"#title-status-label\"}),(0,n.LK)(\"/api/version?system=1\").then((e=>{const t={version:e.data};u=t.version.server,function(e){if(m||void 0===e.version.system||void 0===e.version.system.DESCRIPTION||\"Raspbian GNU/Linux 11 (bullseye)\"!==e.version.system.DESCRIPTION)return;const t=(0,i.Iu)(\"msg.outdated-os.message\"),a=(0,i.Iu)(\"msg.outdated-os.title\");(0,l.Kp)(a,t,(()=>{}),!1),m=!0}(t),(0,s.ZP)(\"settings\",t),function(e){const t=document.querySelector(\"#sys-version .table\");if(t)for(const[a,i]of Object.entries(e.version.system)){const e=document.createElement(\"div\");e.className=\"row\";const s=document.createElement(\"div\");s.className=\"col txt-sm\",s.innerHTML=`<p class=\"txt-bold txt-grey\">${y(a)}</p>`,e.appendChild(s);const n=document.createElement(\"div\");n.className=\"col txt-md\",n.innerHTML=`<p>${i}</p>`,e.appendChild(n),t.appendChild(e)}}(t)})).catch((e=>(0,o.S)(e))),function(){const e=document.getElementById(\"updates-check\"),t=document.getElementById(\"updates-check__spinner\"),a=a=>{a&&document.querySelectorAll(\".update-pkg\").forEach((e=>{e.parentNode.removeChild(e)})),(0,r.yx)(t,a),(0,r.gL)(e,!a)};e&&t&&(e.onclick=()=>{a(!0),(0,n.LK)(\"/api/v1/update/prusalink\").then((t=>{const a=t.data?.new_version,s=[{name:\"PrusaLink\",new_version:a}],n=e.parentNode.parentNode.parentNode;s.forEach((e=>{const t=document.createElement(\"div\"),a=document.createElement(\"div\"),s=document.createElement(\"p\"),r=document.createElement(\"div\"),o=document.createElement(\"p\"),l=document.createElement(\"div\"),c=document.createElement(\"span\"),p=document.createElement(\"span\"),m=document.createElement(\"span\"),v=\"PrusaLink\";if(t.className=\"row update-pkg\",a.className=\"col\",r.className=\"col\",l.className=\"col\",s.className=\"txt-bold txt-grey txt-sm\",o.className=\"txt-md\",c.className=\"txt-grey txt-sm\",m.className=\"txt-grey txt-sm\",s.innerText=v,p.innerHTML=\" &rarr; \",e.new_version){c.innerText=u,m.innerText=e.new_version;const t=document.createElement(\"button\"),a=document.createElement(\"p\");a.innerText=(0,i.Iu)(\"btn.upgrade\"),t.className=\"action\",t.appendChild(a),l.appendChild(t),t.onclick=()=>(0,d.o)((t=>k(t,v,e.new_version)),{timeout:0,closeOutside:!1})}else m.innerText=\"The package is up to date\";o.appendChild(c),o.appendChild(p),o.appendChild(m),a.appendChild(s),r.appendChild(o),t.appendChild(a),t.appendChild(r),t.appendChild(l),n.appendChild(t)}))})).catch((e=>(0,o.S)(e))).finally((()=>a(!1)))})}(),function(e){b(e,!0),document.getElementById(\"edit-connect-del\").addEventListener(\"click\",(t=>{(0,n.LK)(\"/api/connection\",{method:\"DELETE\"}).then(g).catch((e=>(0,o.S)(e))).finally((()=>e.updateConnection()))}));const t=document.getElementById(\"edit-connect-set\"),a=document.getElementById(\"edit-connect-set__spinner\"),i=e=>{e&&document.querySelectorAll(\".update-pkg\").forEach((e=>{e.parentNode.removeChild(e)})),(0,r.yx)(a,e),(0,r.gL)(t,!e)};t.addEventListener(\"click\",(t=>{i(!0);const a=document.getElementById(\"conn-prusa-connect-url\")?.value;if(!a)return;const s=new URL(a);(0,n.LK)(\"/api/connection\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({connect:{hostname:s.hostname,port:s.port?parseInt(s.port):0,tls:s.protocol.startsWith(\"https\")?1:0}})}).then((t=>{const a=t?.data?.url;a&&window.open(a,\"_blank\"),setTimeout((()=>{e.updateConnection().finally((()=>i(!1)))}),5e3)})).catch((e=>{i(!1),(0,o.S)(e)}))}))}(e),(0,n.LK)(\"/api/settings\").then((e=>{const t=e.data;(function(e){const t=document.querySelector(\"#settings #printer-name\"),a=document.querySelector(\"#settings #printer-location\"),i=document.querySelector(\"#settings #printer-network_error_chime\"),s=document.querySelector(\"#settings #edit-printer\"),l=()=>{(0,r.gL)(s,t.value.length>0&&a.value.length>0)};t.oninput=l,a.oninput=l,i.oninput=l,t.value=e.printer?.name||\"\",a.value=e.printer?.location||\"\",i.checked=!!e.printer?.network_error_chime,\"api-key\"in e&&h(e[\"api-key\"]),s.onclick=()=>{(({name:e,location:t,network_error_chime:a})=>(0,n.LK)(\"/api/settings\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({printer:{name:e,location:t},network_error_chime:a})}))({name:t.value,location:a.value,network_error_chime:i.checked}).then((()=>g())).catch((e=>(0,o.S)(e)))},l()})(t),function(e){const t=document.querySelector(\"#settings #username\"),a=document.querySelector(\"#settings #password\"),i=document.querySelector(\"#settings #new-password\"),s=document.querySelector(\"#settings #re-password\"),l=document.querySelector(\"#settings #edit-user\"),d=()=>{(0,r.gL)(l,a.value.length>0&&(t.value.length>0||i.value.length>0&&s.value.length>0))};t.oninput=d,a.oninput=d,i.oninput=d,s.oninput=d,t.value=e.username||\"\",d(),l.onclick=()=>{((e,{username:t,newPassword:a,rePassword:i})=>(0,n.LK)(\"/api/settings\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({user:{password:e,username:t,new_password:a,new_repassword:i}})}))(a.value,{username:t.value||void 0,newPassword:i.value||void 0,rePassword:s.value||void 0}).then((()=>g())).catch((e=>(0,o.S)(e)))}}(t)})).catch((e=>(0,o.S)(e))),function(){const e=document.querySelector(\"#settings #serial\"),t=document.querySelector(\"#settings #edit-serial\"),a=()=>{if(e&&t){c&&(e.value=`${c}`);const i=()=>{(0,r.gL)(t,!c&&e.value.length>0),(0,r.yx)(t,!c)};i(),e.oninput=i,(0,r.gL)(e,!c);const s=()=>{var t;(t=e.value,(0,n.LK)(\"/api/settings/sn\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({serial:t})})).then((e=>{c=e.data.serial,g()})).catch((e=>(0,o.S)(e))).finally((()=>a()))};e.onkeyup=t=>{\"Enter\"===t.key&&(s(),e.blur())},t.onclick=s}};(0,n.LK)(\"/api/settings/sn\",{headers:{\"Content-Type\":\"application/json\"}}).then((e=>{c=e.data.serial})).catch((e=>(0,o.S)(e))).finally((()=>a()))}(),function(){const e=document.getElementById(\"api_key-reset\");e&&e.addEventListener(\"click\",(()=>{(0,n.LK)(\"/api/settings/apikey\",{method:\"POST\"}).then((e=>{h(e.data[\"api-key\"]),g()})).catch((e=>(0,o.S)(e)))}),!1)}(),v?.load(),e.updateConnection()},update:e=>{v?.update(),b(e,!1)}}},5493:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>g});var i=a(7780),s=a(6648),n=a(1972),r=a(5412),o=a(1351),l=a(5502);let d=null,c=null;const u=67108864;function p(e){const t=document.querySelector(\"ul.logs\");t&&(t.innerHTML=m(e))}function m(e){return`<li class=\"txt-md\">${e}</li>`}const v=()=>{(0,s.LK)(\"/api/logs\").then((e=>{const t=e.data.files;if(d){const e=t.find((e=>e.name===d));e&&(!c||e.date>c)&&(c=e.date,null===e.size?p((0,i.Iu)(\"logs.file-size-unknown\")):e.size>u?p((0,i.Iu)(\"logs.file-too-large\",{size:(0,l.Z)(\"size\",u)})):(a=d,(0,s.iT)(`/api/logs/${a}`).then((e=>{const t=document.querySelector(\"ul.logs\");t&&(e.data?t.innerHTML=e.data.split(\"\\n\").map((e=>m(e))).join(\"\"):t.innerHTML=m((0,i.Iu)(\"logs.empty-file\")))}))))}var a})).catch((e=>(0,o.S)(e)))},g={load:()=>{(0,s.LK)(\"/api/logs\").then((e=>{const t=e.data.files,a=r.L.init(\"settings\",\"log-list\"),s=t.map((e=>e.name)),o=(0,i.Iu)(\"logs.select-file-placeholder\");a.setOptions(s),a.select(o),a.onselect=e=>{!function(e){d=e,c=null,document.getElementById(\"download-log\").onclick=()=>{(0,n.Z)(`/api/logs/${e}`,e)},v()}(e)}})).catch((e=>(0,o.S)(e)))},update:v}},3707:(e,t,a)=>{\"use strict\";a.d(t,{Kp:()=>r,Vp:()=>o,vU:()=>l});const i=document.getElementById(\"prusa-toast\"),s={info:10500,success:10500,warning:10500,error:10500};function n({title:e,message:t,type:a,onClose:n,autoClose:r=!0}){const o=((e,t,a)=>{const i=document.getElementById(\"toast\"),s=document.importNode(i.content,!0),n=s.querySelector(\"article\");return n.className=a,s.querySelector(\"p\").innerHTML=e,s.querySelector(\".toast-body\").innerHTML=t,n})(e,t,a),l=()=>{i.contains(o)&&i.removeChild(o),n?.()};o.querySelector(\"span\").addEventListener(\"click\",(e=>{e.preventDefault(),l()}));const d=s[a];d&&r&&setTimeout(l,d),i.appendChild(o)}function r(e,t,a,i=!0){n({type:\"warning\",title:e,message:t,onClose:a,autoClose:i})}function o(e,t,a,i=!0){n({type:\"success\",title:e,message:t,onClose:a,autoClose:i})}function l(e,t,a,i=!0){n({type:\"error\",title:e,message:t,onClose:a,autoClose:i})}},5489:(e,t,a)=>{\"use strict\";a.d(t,{NA:()=>n,ZP:()=>o,zR:()=>r});var i=a(646),s=a(5502);const n=(e,t)=>{try{const a=e.split(\".\");let i=t;for(;a.length;)i=i[a.shift()];return i}catch(e){return}},r=(e,t)=>{document.querySelectorAll(`[data-type=\"${e}\"]`).forEach((e=>{const a=e.dataset.where,r=e.dataset.zeroes,o=a?n(a,t):t;e.innerHTML=(0,s.Z)(e.dataset.format,o),(0,i.QH)(e,\"hide\"===r&&!o)}))},o=r},2038:(e,t,a)=>{\"use strict\";a.d(t,{$:()=>o,i:()=>l});var i=a(8236),s=a(646),n=a(4800),r=a(7780);const o=(e,t,a)=>{const i=document.getElementById(\"modal-question\"),n=document.importNode(i.content,!0),o=n.getElementById(\"modal-question-label\"),l=document.createElement(\"p\"),d=document.createElement(\"p\");l.className=\"txt-sm my-md txt-bold\",l.innerText=t,o.innerText=(0,r.Iu)(\"msg.file-exists.title\"),d.innerText=(0,r.Iu)(\"msg.file-exists.overwrite-it\"),o.parentNode.append(l),o.parentNode.append(d);const c=n.getElementById(\"yes\"),u=n.getElementById(\"no\");return u.addEventListener(\"click\",e),c.addEventListener(\"click\",(t=>{t.preventDefault(),(0,s.H)(c,!0),(0,s.H)(u,!0),a(),e()})),n},l=e=>{e.addEventListener(\"change\",(t=>{e.checked&&e.getAttribute(\"data-link-state\")!==n.PT.READY&&(0,i.o)((t=>((e,t)=>{const a=document.getElementById(\"modal-confirm\"),i=document.importNode(a.content,!0),n=i.getElementById(\"yes\"),r=i.getElementById(\"no\");return n.addEventListener(\"click\",(a=>{a.preventDefault(),(0,s.H)(n,!0),(0,s.H)(r,!0),t.checked=!0,e()})),r.addEventListener(\"click\",(()=>{t.checked=!1,e()})),i})(t,e)),{timeout:0,closeOutside:!1})}))}},7049:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>k});var i=a(6648),s=a(1351),n=a(646),r=a(3707),o=a(7780),l=a(5951),d=a(2038),c=a(5489),u=a(4800),p=a(3532);let m=!1,v=null;function g(e){const t=p.Z.getContext(),a=u.cG.includes(e),i=document.querySelector(\"#upld-remote-start-pt\");if(i&&(i.setAttribute(\"data-link-state\",e),a||(i.checked=!1),(0,n.H)(i,!a)),t.transfer?.id){y(\"uploading\"),(0,c.ZP)(\"download\",t.transfer);const e=document.querySelector(\"#upld-remote .progress-bar\");(0,l.g)(e,t.transfer.progress||0)}else y(\"choose\")}function h(e){const t=e.data;if(![200,201,204].includes(e.code))return(0,s.S)(e),void f();if(t){if(\"FROM_WEB\"===t.type){y(\"uploading\"),(0,c.ZP)(\"download\",{...t,time_start:t.time_transferring?Math.round((new Date).getTime()/1e3)-t.time_transferring:null});const e=document.querySelector(\"#upld-remote .progress-bar\");(0,l.g)(e,t.progress||0)}}else{if(m)return function(){const e=(0,o.Iu)(\"ntf.success\"),t=(0,o.Iu)(\"ntf.upld-suc\",{file_name:\"\"});(0,r.Vp)(e,t)}(),void f();y(\"choose\")}}const b=(e,t,a,s)=>{if(\"local\"===t)return e=e.split(\"?\")[0].split(\"&\")[0].split(\"#\")[0],(0,i.LK)(`/api/download/${t}`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({url:e,destination:a,...s})}).then((e=>function(e){y(\"uploading\");const t=document.querySelector(\"#upld-remote .progress-bar\");(0,l.g)(t,0);const a=(0,o.Iu)(\"ntf.success\"),i=(0,o.Iu)(\"ntf.upld-start\");(0,r.Vp)(a,i)}())).catch((e=>h(e)));(0,r.vU)(\"Can't upload to \"+t,\"You can upload only to local storage via remote upload!\")};function f(){y(\"choose\"),function(e){const t=document.querySelector(\"#upld-remote .progress-bar\");t&&(0,l.g)(t,e||0)}(0),(0,c.ZP)(\"download\",{}),v=null}function y(e){m=\"uploading\"==e;const t=document.getElementById(\"upld-remote\");t&&t.setAttribute(\"data-state\",e)}const k={init:function(e,t){const a=document.getElementById(\"upld-remote\");if(a){const i=a.querySelector(\"#remote-url\"),s=a.querySelector(\"#remote-file-name\"),r=a.querySelector(\"#upld-file\"),o=a.querySelector(\"#upld-remote-start-pt\");o&&(0,d.i)(o),r.onclick=()=>b(i.value,e,t,{to_print:o.checked,rename:s.value});const l=()=>{(0,n.H)(r,\"\"===i.value)};l(),i.oninput=l}g()},update:g,get isUploading(){return m}}},3532:(e,t,a)=>{\"use strict\";a.d(t,{Z:()=>Je});var i=a(7780);const s=500;let n=100,r=new Map;const o=(e,t)=>{r=e,n=t};function l(){const e=document.getElementById(\"graph\");e&&(0==e.childElementCount&&function(){const e=document.getElementById(\"graph-template\"),t=document.importNode(e.content,!0);document.getElementById(\"graph\").appendChild(t),(0,i.ot)(\"graph\")}(),r.forEach(((e,t)=>{!function(e,t){const a=(new Date).getTime();let i=[];if(e.length>1){let t=e[0],r=s-2.66*(a-t[0]-1e4)/1e3;for(let e=1;e<t.length;e++)i.push(`M${r},${250-2*t[e]/(n/100)}`);for(let o=1;o<e.length;o++){t=e[o],r=s-2.66*(a-t[0]-1e4)/1e3;for(let e=0;e<i.length;e++)i[e]=i[e]+`L${r},${250-2*t[e+1]/(n/100)}`}}t.setAttribute(\"d\",i)}(e,document.getElementById(t))})))}function d(e,t){!function(e,t){e.push(t);const a=(new Date).getTime();for(;e.length>1&&e[1][0]<a-18e4;)e.shift()}(r.get(e),t)}var c=a(3707),u=a(1351),p=a(646),m=a(6648);const v=function(e,t,a){return new Promise(((i,s)=>{var n=new XMLHttpRequest;const r=()=>{s(void 0)};!function(e,t){if(t){const a=e=>{if(!e.lengthComputable)return;let a=Math.round(e.loaded/e.total*100);t({loaded:e.loaded,total:e.total,percentage:a})};e.upload.addEventListener(\"progress\",a,!1)}}(n,a.onProgress),n.addEventListener(\"load\",(e=>{const t=function(e){function t(e){try{return JSON.parse(e)}catch{return}}return{status:e.status,statusText:e.statusText,ok:e.status>=200&&e.status<=299,data:t(e.response)}}(e.target);t.ok?i(t):s(t)}),!1),n.addEventListener(\"error\",r,!1),n.addEventListener(\"abort\",r,!1),n.open(\"PUT\",`${m.O2}${e}`),a.print&&n.setRequestHeader(\"Print-After-Upload\",\"?1\"),n.setRequestHeader(\"Overwrite\",\"?1\"),n.setRequestHeader(\"Content-Type\",e.endsWith(\".bgcode\")?\"application/gcode+binary\":\"text/x.gcode\");for(const[e,t]of Object.entries((0,m.wU)()))n.setRequestHeader(e,t);n.send(t)}))};var g=a(2038),h=a(4800),b=a(8236);let f=!1,y=0;function k(e,t,a,i,s){const n=document.querySelector(e);n&&(n.setAttribute(\"accept\",i.join(\", \")),n.onchange=()=>{if(n.files.length>0&&!f){let e=n.files[0];S(e,t,a,s?.checked||!1)}})}function w(){[document.querySelector('#upld-direct input[type=\"file\"]'),document.querySelector('#drop-zone input[type=\"file\"]')].filter((e=>!!e)).forEach((e=>e.value=\"\")),z(0),x(\"choose\")}function x(e){f=\"uploading\"===e;const t=document.getElementById(\"upld-direct\");t&&t.setAttribute(\"data-state\",e)}function z(e){y=e;const t=document.getElementById(\"upld-progress\");t&&(t.innerHTML=`${e} %`)}const S=(e,t,a,s)=>{const n=[\"/api/v1/files\",t,a,e.name].filter((e=>!!e)).join(\"/\"),r=e.display_name||e.name,o=()=>e.arrayBuffer().then((e=>{x(\"uploading\"),z(0),v(n,e,{onProgress:e=>{return t=e.percentage,x(\"uploading\"),void z(t);var t},print:s}).then((()=>function(e){const t=(0,i.Iu)(\"ntf.success\"),a=(0,i.Iu)(\"ntf.upld-suc\",{file_name:e});(0,c.Vp)(t,a)}(r))).catch((e=>function(e,t){if(t)(0,u.S)(t);else{const t=(0,i.Iu)(\"ntf.error\"),a=(0,i.Iu)(\"ntf.upld-unsuc\",{file_name:e});(0,c.vU)(t,a)}}(r,e))).finally((()=>w()))}));(0,m.LK)(n,{method:\"HEAD\"}).then((()=>{try{(0,b.o)((e=>(0,g.$)((()=>{e(),w()}),r,o)),{timeout:0,closeOutside:!1})}catch(e){console.error(\"Modal error\",e)}})).catch((()=>o()))};const E={init:function(e,t,a){(0,i.Iu)(\"upld.direct.choose\",{query:\"#upld-direct p\",file:a.join(\", \")}),function(e,t,a){const i=document.getElementById(\"upld-direct-start-pt\");i&&(0,g.i)(i);const s=document.getElementById(\"drop-zone\");s&&(document.ondragenter=e=>(0,p.yx)(s,!0),s.ondragleave=e=>(0,p.yx)(s,!1),document.ondrop=e=>(0,p.yx)(s,!1),k('#drop-zone input[type=\"file\"]',e,t,a,i));k('#upld-direct input[type=\"file\"]',e,t,a,i)}(e,t,a),f&&(x(\"uploading\"),z(y))},update:function(e){const t=h.cG.includes(e),a=document.querySelector(\"#upld-direct-start-pt\");a&&(a.setAttribute(\"data-link-state\",e),t||(a.checked=!1),(0,p.H)(a,!t))},get isUploading(){return f},initInputByQuery:k};const P=class{get selected(){return this._selected}get isLocked(){return this._isLocked}lock(){this._isLocked=!0,this._root&&this._root.querySelectorAll(\"[data-tab-btn]\").forEach((e=>{e.getAttribute(\"data-tab-btn\")!==this.selected&&e.setAttribute(\"locked\",!0)}))}unlock(){this._isLocked=!1,this._root&&this._root.querySelectorAll(\"[data-tab-btn]\").forEach((e=>{e.hasAttribute(\"locked\")&&e.removeAttribute(\"locked\")}))}constructor(){this._root=null,this._selected=null,this._isLocked=!1}init(e){this._root=e,this._root&&(e.querySelectorAll(\"[data-tab-btn]\").forEach((e=>{e.onclick=()=>{if(!this._isLocked){const t=e.getAttribute(\"data-tab-btn\");t===this._selected||(this.closeTab(),this.openTab(t))}}})),this.openTab(this._selected,!0))}openTab(e){if(this._root&&e){const t=this._root.querySelector(`[data-tab=\"${e}\"]`);t&&t.setAttribute(\"opened\",!0);const a=this._root.querySelector(`[data-tab-btn=\"${e}\"]`);a&&a.setAttribute(\"selected\",!0),this._selected=e}}closeTab(){if(this._root&&this._selected){const e=this._root.querySelector(`[data-tab=\"${this._selected}\"]`);e&&e.setAttribute(\"opened\",!1);const t=this._root.querySelector(`[data-tab-btn=\"${this._selected}\"]`);t&&t.setAttribute(\"selected\",!1),this._selected=null}}},L=a(7049).Z,I=new P;function N(){E.isUploading?(I.openTab(\"direct\"),I.lock()):L?.isUploading?(I.openTab(\"remote\"),I.lock()):(I.unlock(),I.selected||I.openTab(\"direct\"))}const _={init:function(e,t=\"\",a){E.init(e,t,a),L?.init(e,t),I.init(document.getElementById(\"upld\")),N()},update:function(e){E?.update(e),L?.update(e),N()},hide:function(e){(0,p.QH)(document.getElementById(\"upld\"),e)}};var T=a(2451),j=a(5489),C=a(8751);const A=e=>(0,m.LK)(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"}}),D=e=>(0,m.LK)(`/api/v1/job/${e}/resume`,{method:\"PUT\"}).catch((e=>(0,u.S)(e))),B=(e,t)=>{(0,b.o)((a=>((e,t,a)=>{const s=document.getElementById(\"modal-question\"),n=document.importNode(s.content,!0);n.getElementById(\"modal-question-label\").innerText=(0,i.Iu)(\"msg.cancel\");const r=n.getElementById(\"yes\"),o=n.getElementById(\"no\");return r.addEventListener(\"click\",(i=>{i.preventDefault(),t.onConfirm(),(0,p.H)(r,!0),(0,p.H)(o,!0),(0,m.LK)(`/api/v1/job/${a}`,{method:\"DELETE\",headers:{\"Content-Type\":\"application/json\"}}).catch((e=>{t.onError(),(0,u.S)(e)})),e()})),o.addEventListener(\"click\",e),n})(a,t,e)),{timeout:0,closeOutside:!1})},O=(e,t)=>{e?(0,b.o)((e=>((e,t)=>{const a=document.getElementById(\"modal-confirm\"),i=document.importNode(a.content,!0),s=i.getElementById(\"yes\"),n=i.getElementById(\"no\");return s.addEventListener(\"click\",(a=>{a.preventDefault(),(0,p.H)(s,!0),(0,p.H)(n,!0),A(t).then((()=>(0,C.c4)(\"#dashboard\"))).catch((e=>(0,u.S)(e))).finally((()=>e()))})),n.addEventListener(\"click\",e),i})(e,t)),{timeout:0,closeOutside:!1}):A(t).then((()=>(0,C.c4)(\"#dashboard\"))).catch((e=>(0,u.S)(e)))},R=()=>(0,m.LK)(\"/api/job\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({command:\"cancel\"})}).catch((e=>(0,u.S)(e)));var q=a(1972);const M=(e,t,a)=>{(0,q.Z)(e,t),a()},H=(e,t,a,s,n=!1)=>{const r=document.getElementById(\"modal-question\"),o=document.importNode(r.content,!0);o.getElementById(\"modal-question-label\").innerText=n?(0,i.Iu)(\"msg.del-folder\",{folder_name:a}):(0,i.Iu)(\"msg.del-proj\",{file_name:a});const l=o.getElementById(\"yes\"),d=o.getElementById(\"no\");return d.addEventListener(\"click\",e),l.addEventListener(\"click\",(a=>{a.preventDefault(),(0,p.H)(l,!0),(0,p.H)(d,!0),(0,m.LK)(t,{method:\"DELETE\",headers:{force:\"?1\"}}).then((()=>s&&s())).catch((e=>(0,u.S)(e))).finally((t=>e()))})),o},U=(e,t,a)=>{(0,b.o)((i=>H(i,e,t,a)),{timeout:0,closeOutside:!1})},K=(e,t)=>{(0,b.o)((a=>((e,t,a)=>{const i=document.getElementById(\"modal-file-name\"),s=document.importNode(i.content,!0),n=s.getElementById(\"modal-file-name__input\"),r=s.getElementById(\"yes\"),o=s.getElementById(\"no\");return o.addEventListener(\"click\",e),r.addEventListener(\"click\",(i=>{i.preventDefault();const s=n.value;if(s){const i=t(s);(0,p.H)(r,!0),(0,p.H)(o,!0),(0,m.LK)(i,{method:\"PUT\",headers:{\"create-folder\":\"?1\"}}).then((()=>a&&a())).catch((e=>(0,u.S)(e))).finally((t=>e()))}})),s})(a,e,t)),{timeout:0,closeOutside:!1})};function V(e){return`inset(${100-e}% 0% 0% 0%)`}var Z=a(5951);const F={},$=()=>{F.title=null,F.questionChildren=null,F.yes=null,F.no=null,(0,C.g9)(F.next)},G={load:()=>{F.title||(0,C.c4)(\"#dashboard\"),document.getElementById(\"title-status-label\").innerHTML=F.title;const e=document.getElementById(\"question\"),t=F.questionChildren;Array.isArray(t)?t.forEach((t=>e.appendChild(t))):e.innerHTML=t;for(let e of[\"yes\",\"no\"]){const t=document.getElementById(e),a=F[e];t.querySelector(\"p\").innerHTML=F[e+\"Text\"],t.addEventListener(\"click\",(e=>{e.stopPropagation(),a($)}))}}};(0,i.Iu)(\"exp-times.exp-time\"),(0,i.Iu)(\"exp-times.inc\"),(0,i.Iu)(\"exp-times.layer-1st\"),(0,i.Iu)(\"exp-times.profile\");const W=e=>{const t=e.querySelector(\"img\");(0,p.gL)(e,!1),t&&(t.setAttribute(\"data-src\",t.src),t.src=document.querySelector(\".loading-overlay img\").src)},Y=e=>{const t=e.querySelector(\"img\");if((0,p.gL)(e,!0),t){const e=t.getAttribute(\"data-src\");e&&(t.src=e,t.setAttribute(\"data-src\",\"\"))}};var J=a(5502);const Q=function(e,t=\"smooth\"){if(!e)return;const a=e.getBoundingClientRect(),i=document.body.getBoundingClientRect(),s=a.top-(i.top+function(){const e=document.querySelector(\".header\");return e&&\"sticky\"===getComputedStyle(e).position?e.getBoundingClientRect().height:0}());(a.bottom>window.innerHeight||a.top<0)&&window.scroll({top:s,behavior:t})};var X=a(5537);const ee=e=>{const t=(0,X.Z)(e).querySelector(\".kebab\"),a=t.querySelector(\".kebab-menu\"),i=t.querySelector(\"ul\");a.addEventListener(\"click\",(e=>{e.stopPropagation(),function(e){return e&&e.classList.contains(\"open\")}(i)?te(i):function(e,t){e.classList.add(\"open\"),(0,p.Ti)((()=>te(e)),e,t)}(i,a)})),i.querySelectorAll(\"li\").forEach((e=>{e.addEventListener(\"click\",(()=>te(i)))}))};function te(e){e.classList.remove(\"open\")}const ae=e=>{const t=document.querySelectorAll(\".storage-select-content li\");t&&t.forEach((t=>t.setAttribute(\"selected\",t.getAttribute(\"data-storage\")===e)))},ie=e=>{const t=document.querySelector(\"#node-storage .storage-select-btn-inner\");t&&(t.innerHTML=e)},se=(e,t)=>{e&&t&&(t.classList.toggle(\"open\"),(0,p.Ti)((()=>{t?.classList.remove(\"open\")}),e,t))},ne=e=>{const t=document.querySelector(\".node-storage-space\");if(!t)return;const a=e.available&&!!e.totalSpace;if((0,p.yx)(t,a),a){const a=e.freeSpace,s=e.totalSpace,n=s-a,r=Math.round(100*(s?n/s:0)),o=(0,i.Iu)(\"prop.storage-used-space\",{used:(0,J.Z)(\"size\",n),free:(0,J.Z)(\"size\",a),total:(0,J.Z)(\"size\",s)});(0,Z.g)(t,r),document.getElementById(\"storage-pct\").innerHTML=(0,J.Z)(\"percent\",r),document.getElementById(\"storage-space\").innerHTML=o}},re=()=>{const e=document.querySelector(\"#node-storage .storage-select-btn\");e&&(e.onclick=t=>{t.stopPropagation(),se(e,dropdownContent)})},oe=(e,t,a,i=!1)=>{const s=document.querySelector(\".storage-select-content\");document.querySelectorAll(\".storage-select-content li\").forEach((i=>{const n=i.getAttribute(\"data-storage\");let r=!1;if(n in e){const o=e[n],l=i.querySelector(\"p\");r=e[n].available,o.name&&(l.innerText=o.name),i.setAttribute(\"selected\",n===t),i.onclick=e=>{e.stopPropagation(),ie(i.innerHTML),ae(n),s.classList.remove(\"open\"),ne(o),a(n)}}(0,p.yx)(i,r)})),i&&ne(e[t])},le=\"FOLDER\",de=\"PRINT_FILE\",ce=\"FIRMWARE\",ue=[\"name\",\"date\",\"size\"];let pe=null;const me={origin:null,current_path:[],storages:{},files:[],eTag:null,sort:{field:\"date\",order:\"desc\"}};function ve(){const e=me.origin;return me.storages[e]}function ge(){return me.current_path.map((e=>e.path)).join(\"/\")}function he(e){const t=ve(),a=ge();return be(t.path,a,e)}function be(e,t,a){const i=[\"/api/v1/files\",e,t,a].filter((e=>!!e)).join(\"/\");return a?i:`${i}/`}const fe=(e={})=>{(0,m.LK)(\"/api/v1/storage\",{}).then((t=>{const a=t.data?.storage_list;let i=!!e.redraw,s=!!e.redraw;if(a&&a.forEach((e=>{const t=e.type,a={name:e.name,path:e.path.replaceAll(\"/\",\"\"),available:e.available,readOnly:e.read_only,freeSpace:e.free_space,totalSpace:e.total_space};if(t in me.storages){const e=me.storages[t];e.available!==a.available&&(i=!0),e.freeSpace!==a.freeSpace&&me.origin===t&&(s=!0)}else i=!0,s=!0;me.storages[t]=a})),!me.origin){let e=Object.keys(me.storages).find((e=>me.storages[e].available));e||(e=Object.keys(me.storages).find((()=>!0))),e&&Le(e)}i&&oe(me.storages,me.origin,Le,s)}))},ye=(e={})=>{if(!ve())return;const t=he();let a=me.eTag;e.force&&(me.eTag=null,a=null,function(e){const t=ve(),a=ge();_.init(t.path,a,e?.fileExtensions),_.hide(!1!==t?.readOnly)}(Je.getContext()));const i=e=>{me.files=e,ke(),we()};(0,m.LK)(t,{headers:{\"If-None-Match\":a}}).then((s=>{if(304===s.code)return;if(t!==he())return;const n=s.eTag;if(!n||n!==a){me.eTag=n;const t=s.data.children||[];if(!n&&!e.force){const e=(e,t)=>e.display_name.localeCompare(t.display_name);if(JSON.stringify([...me.files].sort(e))===JSON.stringify(t.sort(e)))return}i(t)}})).catch((()=>{me.eTag=null,i([]),Je.getContext().selectFile(null)}))},ke=()=>{const e=document.getElementById(\"files\");if(e){for(;e?.firstChild;)e.removeChild(e.firstChild);e.appendChild(function(){const e=ve(),t=[e.name,...me.current_path.map((e=>e.name))],a=xe(\"node-current\",t.pop()||\"Root\");a.getElementById(\"path\").innerHTML=`${t.join(\"/\")}/`;const s=a.getElementById(\"create\");s&&(s.onclick=e=>{e.stopPropagation(),K(he)});(0,p.H)(s,!1!==e.readOnly),a.querySelector(\"#sort-by-name p\").innerText=(0,i.Iu)(\"sort.by-name\"),a.querySelector(\"#sort-by-date p\").innerText=(0,i.Iu)(\"sort.by-date\"),a.querySelector(\"#sort-by-size p\").innerText=(0,i.Iu)(\"sort.by-size\");return a.querySelector(`#sort-by-${me.sort.field}`).classList.add(me.sort.order),ue.forEach((e=>{a.getElementById(`sort-by-${e}`).addEventListener(\"click\",(t=>{const a=document.getElementById(`sort-by-${e}`);document.getElementById(`sort-by-${me.sort.field}`).classList.remove(me.sort.order);const i=me.sort.field===e&&\"asc\"===me.sort.order?\"desc\":\"asc\";a.classList.add(i),me.sort.field=e,me.sort.order=i,ke(),we()}),!1)})),a}()),me.current_path.length&&e.appendChild(function(){const e=xe(\"node-up\",\"\",(()=>{me.current_path.pop(),ke(),ye({force:!0})}));return(0,i.ot)(e),e}())}},we=()=>{const e=document.getElementById(\"files\");let t;if(e)for(let a of(e=>(e.sort(((e,t)=>{if(e.type===le&&t.type!==le)return-1;if(e.type!==le&&t.type===le)return 1;const a=\"desc\"===me.sort.order?-1:1;switch(me.sort.field){case\"date\":return a*((e.m_timestamp||0)-(t.m_timestamp||0));case\"size\":return a*((e.size||0)-(t.size||0));default:return a*e.display_name.localeCompare(t.display_name)}})),e))(me.files)){switch(a.type.toUpperCase()){case le:t=ze(a,{files:void 0,folders:void 0});break;case de:t=Ee(a);break;case ce:t=Pe(a,\"firmware\");break;default:t=Pe(a,\"file\")}e.appendChild(t)}};function xe(e,t,a){const i=document.getElementById(e).content,s=document.importNode(i,!0);return a&&s.querySelector(\".node\").addEventListener(\"click\",(e=>{a(e),e.preventDefault()})),s.querySelector(\"#name\")?.appendChild(document.createTextNode(t)),s}function ze(e,t){const a=e.display_name||e.name,i=e.name,s=he(i),n=xe(\"node-folder\",a,(()=>{me.current_path.push({path:i.replace(\"/\",\"\"),name:a}),ke(),ye({force:!0})})),r=[t?.files?`${t.files} files`:null,t?.folders?`${t.folders} folders`:null].filter((e=>null!=e)).join(\" | \");n.getElementById(\"details\").innerHTML=r;const o=n.getElementById(\"delete\");(0,p.gL)(o,!(e.read_only||e.ro)),o&&(o.onclick=e=>{var t,i,n;e.stopPropagation(),t=s,i=a,n=()=>{},(0,b.o)((e=>H(e,t,i,n,!0)),{timeout:0,closeOutside:!1})});const l=n.getElementById(\"rename\");return l&&(l.onclick=e=>{e.stopPropagation(),console.log(\"renameFolder\")}),n}const Se=e=>{!function(e){const t=Je.getContext(),a=t.files.selected,i=ge(),s=he(e.name);if(a===i)return;t.selectFile({...e,path:i,resource:s});const n=document.getElementById(\"job\");n&&Q(n)}(e)};function Ee(e){const t=xe(\"node-file\",e.display_name||e.name,(t=>Se(e))),a=t.querySelector(\".node-details\");a.querySelectorAll(\".details\").forEach((t=>{(0,i.ot)(t);const s=(0,j.NA)(t.dataset.where,e);if(s){const e=(0,J.Z)(t.dataset.format,s);t.querySelector(\"p[data-value]\").innerHTML=e}else a.removeChild(t)}));const s=e?.refs?.thumbnail;if(s){const a=t.querySelector(\"img.node-img\");a.setAttribute(\"data-src\",e.date?`${s}?ct=${e.date}`:s),pe.observe(a)}return ee(t),function(e,t){const a=he(e.name),i=e.display_name||e.name,s=t.getElementById(\"details\");s&&(s.onclick=t=>{Se(e)});const n=t.getElementById(\"start\");n&&(n.onclick=e=>{e.stopPropagation(),console.log(\"startPrint\")});const r=t.getElementById(\"rename\");r&&(r.onclick=e=>{e.stopPropagation(),console.log(\"renameFile\")});const o=t.getElementById(\"delete\");o&&((0,p.gL)(o,!(e.read_only||e.ro)),o.onclick=e=>{U(a,i),e.stopPropagation()});const l=t.getElementById(\"download\");l&&((0,p.gL)(l,!!e.refs?.download),l.onclick=t=>{W(l),M(e.refs?.download,i,(()=>Y(l))),t.stopPropagation()})}(e,t),(0,i.ot)(t),t}function Pe(e,t){const a=xe(\"node-file\",e.display_name||e.name,(e=>{})),s=a.querySelector(\".node-details\");s.querySelectorAll(\".details\").forEach((t=>{(0,i.ot)(t);const a=(0,j.NA)(t.dataset.where,e);if(a){const e=(0,J.Z)(t.dataset.format,a);t.querySelector(\"p[data-value]\").innerHTML=e}else s.removeChild(t)}));const n=a.querySelector(\"img.node-img\");if(t){const e=n.getAttribute(`data-${t}`);e&&(n.src=e),pe.observe(n)}else(0,p.yx)(n,!1);return function(e){[\"details\",\"start\",\"rename\",\"delete\",\"download\"].forEach((t=>{const a=e.getElementById(t);a&&(0,p.yx)(a,!1)}))}(a),a}function Le(e){if(e in me.storages){const t=me.storages[e];me.origin=e,me.current_path=[],ke(),t.available&&ye({force:!0}),_.hide(!1!==t.readOnly)}}const Ie={load:function(e){if(me.eTag=null,(0,i.Iu)(\"proj.link\",{query:\"#title-status-label\"}),!pe){pe=new IntersectionObserver(((e,t)=>{e.forEach((e=>{if(e.isIntersecting){const t=e.target.getAttribute(\"data-src\");(0,m.gJ)(t).then((({url:t})=>{e.target.src=t})).catch((()=>{}))}}))}),{rootMargin:\"0px 0px 50px 0px\",threshold:0})}e||(e=Je.getContext());const t=e.files.selected;if(t){(function(e,t){if(!e||!t)return null;let a=me.files.find((t=>t.origin===e));const i=t.split(\"/\").filter((e=>e)).slice(1);for(const e of i){if(!a)break;a=a.children.find((t=>t.name===e))}return\"machinecode\"===a?.type?a:null})(t.origin,t.path)||e.selectFile(null)}Te(e,!0),re(),fe({redraw:!0}),ye({force:!0})},update:e=>{const t=e.state;fe(),ye(),Te(e,!0),_.update(t)},getApiPath:be};let Ne=null,_e=null;function Te(e,t=!1){const a=je(e,t);Ne&&Ne.state!==e.state&&(Ne=null),a&&Ce(e,t)}function je(e,t){const a=document.getElementById(\"job\");if(!a)return!1;const i=t?!!e.files.selected:!!e.job?.id;return(0,p.yx)(a,i),i}function Ce(e,t){const a=t?e.files.selected:e.job,s=a?.file?.resource;e.state;var n;if(a.file?(0,p.Zk)():(0,p.QP)(),function(e,t){const a=document.querySelector(\".progress-bar\"),i=document.querySelector(\".progress-pct\"),s=document.querySelector(\".preview-img-wrapper\"),n=void 0!==t;(function(e,t,a){if(!e)return;const i=t?.url&&t.ready?t.url:document.querySelector(\".thumbnail-fallback\")?.src;if(e.getAttribute(\"data-src\")!==i){for(;e.firstChild;)e.removeChild(e.firstChild);e.setAttribute(\"data-src\",i);const t=document.createElement(\"div\");t.className=\"progress-img\";const s=document.createElement(\"img\");s.src=i,s.className=\"background\",t.appendChild(s);const n=document.createElement(\"img\");n.src=i,n.className=\"foreground\",n.style.clipPath=V(a??100),t.appendChild(n),e.appendChild(t)}else{const t=e.querySelector(\".foreground\");t&&(t.style.clipPath=V(a))}})(s,e,t),(0,Z.g)(a,t,\"top\"),(0,p.yx)(a,n),(0,p.yx)(i,n)}(a.thumbnail,a.progress),n=t,(0,p.QH)(document.querySelector(\"#job #pnt-time\"),n),(0,p.QH)(document.querySelector(\"#job #rem-time\"),n),(0,j.ZP)(\"job\",a),s&&a.file){const s=t?null:a.id;!function(e,t,a){const i=t.file;(function(e,t){const a=document.querySelector(\"#job #stop\"),i=document.querySelector(\"#job-close\"),s=h.PT.fromApi(e),n=[h.PT.IDLE,h.PT.READY,h.PT.FINISHED].includes(s),r=!Ne&&h.Dt.includes(e),o=Je.getContext();if((0,p.gL)(a,r),a&&t){const i=t&&![h.PT.REFILL].includes(e);(0,p.yx)(a,i),a.onclick=()=>{B(t,{onConfirm:()=>{Ne={code:\"stop\",state:e},(0,p.gL)(a,!1)},onError:()=>Ne=null})}}i&&((0,p.yx)(i,n||!t),i.onclick=t?R:()=>o.selectFile(null))})(e,a),function(e,t,a){const i=document.querySelector(\"#job #start\"),s=h.cG.includes(e);i&&((0,p.yx)(i,s),(0,p.gL)(i,s),i.onclick=()=>O(e!==h.PT.READY,t.resource))}(e,i),function(e,t,a){const i=document.querySelector(\"#job #delete\");if(i){const s=t.display_name||t.name;(0,p.gL)(i,!t.readOnly&&t.resource),(0,p.yx)(i,!a||e===h.cG.includes(e)),i.onclick=()=>{U(t.resource,s,(()=>{if(!a){Je.getContext().selectFile(null)}}))}}}(e,i,a),function(e,t,a){const i=document.querySelector(\"#job #download\");if(i){const e=!a&&t.refs?.download&&(!_e||_e===t.refs.download),s=t.display_name||t.name;(0,p.yx)(i,e),e&&(i.onclick=()=>{_e=t.refs.download,W(i),M(t.refs.download,s,(()=>{_e=null,Y(i)}))})}}(0,i,a),!a||(function(e,t,a){const i=document.querySelector(a),s=e===h.PT.PRINTING;(0,p.yx)(i,s),(0,p.gL)(i,!Ne&&s),i&&(i.onclick=()=>{(0,p.gL)(i,!1),Ne={code:\"pause\",state:e},(e=>(0,m.LK)(`/api/v1/job/${e}/pause`,{method:\"PUT\"}).catch((e=>(0,u.S)(e))))(t).catch((()=>Ne=null))})}(e,a,\"#job #pause\"),function(e,t){const a=document.querySelector(\"#job #resume\"),i=[h.PT.PAUSED].includes(e);(0,p.yx)(a,i),(0,p.gL)(a,!Ne&&i),a&&(a.onclick=()=>{(0,p.gL)(a,!1),Ne={code:\"resume\",state:e},D(t).catch((()=>Ne=null))})}(e,a))}(e.state,a,s),(0,j.ZP)(\"file\",a.file),function(e,t){const a=(0,i.Iu)(\"prop.na\");document.getElementById(\"job\").querySelectorAll(\".job-details .job-prop\").forEach((e=>{const t=e.querySelector(\".job-prop-grid\").children;let i=!0;for(const e of t){var s=e.querySelector(\"[data-type]\")?.innerHTML.trim()===a;(0,p.QH)(e,s),s||(i=!1)}(0,p.QH)(e,i)}))}()}}p.QP,p.Zk;const Ae=e=>{if(!e.printer)return;const t=e.state;Te(e),_.update(t),T.Z.update(e,null)},De={load:e=>{(0,i.Iu)(\"home.link\",{query:\"#title-status-label\"}),l(),Ae(e),T.Z.update(e,null),T.Z.updateCurrentCamera(),(0,m.LK)(\"/api/v1/storage\").then((t=>{const a=t.data.storage_list.find((e=>e.available&&!e.read_only));if(a){const t=a.path.replace(\"/\",\"\");_.init(t,\"\",e.fileExtensions)}}))},update:Ae},Be=e=>[...e].filter((e=>!!e)).map((e=>e.trim())).join(\" - \"),Oe=e=>{e.getAttribute(\"tooltip\")||(e.addEventListener(\"click\",(t=>{e.classList.toggle(\"tooltip-handle--active\");const a=e.querySelector(\"span\");(0,p.Ti)((()=>e.classList.remove(\"tooltip-handle--active\")),a,e),t.preventDefault()}),!1),e.setAttribute(\"tooltip\",!0))};let Re=0,qe=null;const Me=()=>{const e=e=>{const t=document.getElementById(\"offline-screen\"),a=document.importNode(t.content,!0);return[\"not-responsing\",\"please-wait\"].forEach((e=>{const t=a.getElementById(`offline-screen.${e}`);t&&(t.innerHTML=(e=>{switch(e){case\"not-responsing\":return(0,i.Iu)(\"msg.offline.not-responsing\");case\"please-wait\":return(0,i.Iu)(\"msg.offline.please-wait\");default:return\"\"}})(e))})),qe={node:a,close:()=>{e(),qe=null}},a};(0,b.o)((t=>e(t)),{timeout:0,closeOutside:!1,className:\"offline-screen\"})},He=({link:e,isConnected:t})=>{const a=[\"connect\"];for(const t in e){const{ok:s,message:n}=e[t],r=document.getElementById(`conn-status-${t}-msg`),o=\"ok\"===n.toLowerCase();r&&(r.innerHTML=\"connect\"===t?o?(0,i.Iu)(\"conn.connect.linked\"):(0,i.Iu)(\"conn.connect.not-linked\"):(0,i.Iu)(\"conn.error_status\"));const l=document.getElementById(`conn-status-${t}`),d=l.querySelector(\".icon-success\"),c=l.querySelector(\".icon-warning\"),u=l.querySelector(\".info-message-tooltip\"),m=u.querySelector(\"span\"),v=void 0===s||s&&!a.includes(t);u&&m&&(o||(m.innerText=n,Oe(u)),(0,p.QH)(u,o)),(0,p.QH)(l,v),v||((0,p.QH)(d,!s),(0,p.QH)(c,s))}const s=e=>{Re<3?Re+=1:(e?Me():qe.close(),Re=0)};t?qe&&s(!1):!qe&&s(!0)};const Ue=e=>{return{resource:e.resource??(t=e.path,a=e.name,`/api/v1/files${t}${t.endsWith(\"/\")?\"\":\"/\"}${a}`),name:e.name,displayName:e.display_name??e.name,path:e.path,displayPath:e.display_path??e.path,size:e.size,refs:{download:e.refs?.download,icon:e.refs?.icon,thumbnail:e.refs?.thumbnail},lastModified:e.m_timestamp||0,meta:{filamentType:e.meta?.filament_type,layerHeight:e.meta?.layer_height,estimatedPrintTime:e.meta?.estimated_print_time,exposureTime:e.meta?.exposure_time,exposureTimeCalibration:e.meta?.exposure_time_calibration,exposureTimeFirst:e.meta?.exposure_time_first,exposureUserProfile:e.meta?.exposure_user_profile},readOnly:e.read_only||e.ro};var t,a},Ke=new class{constructor(){this.state=void 0,this.printer=void 0,this.job=void 0,this.transfer=void 0,this.version=void 0,this.storage=[],this.currentStorage=void 0,this.telemetry={temperature:{nozzle:{current:0,target:0},bed:{current:0,target:0}}},this.flow=0,this.speed=0,this.fan={hotend:0,print:0},this.link={connect:{ok:!0,message:\"OK\",settings:{hostname:\"connect.prusa3d.com\",tls:!0,port:0}},printer:{ok:!0,message:\"OK\",settings:{port:\"\",baudrate:115200}}},this.files={location:\"/\",selected:{file:void 0,thumbnail:void 0}},this.fileExtensions=[\".gcode\"],this.updateConnection()}updateConnection(){return(0,m.LK)(\"/api/connection\",{method:\"GET\"}).then((e=>{this.link.connect.settings={hostname:e.data.connect?.hostname,port:e.data.connect?.port,tls:e.data.connect?.tls},this.link.connect.registration=e.data.connect?.registration,this.link.printer.settings={port:e.data.current?.port,baudrate:e.data.current?.baudrate}}))}update({status:e,printer:t}){e?.ok&&e.payload&&this.updateStatus(e.payload.data),t&&this.updatePrinter(t)}updateStatus(e){this.updateTelemetry(e.printer),this.updateJob(e.job),this.updateStorage(e.storage),this.updateTransfer(e.transfer),this.updateCamera(e.camera)}updatePrinter(e){this.printer={name:e.name,location:e.location,farmMode:e.farm_mode,nozzleDiameter:e.nozzle_diameter,minExtrusionTemp:e.min_extrusion_temp,serial:e.serial,hostname:e.hostname,port:e.port},this.fileExtensions=e.project_extensions??[\".gcode\"]}updateTelemetry(e){this.state=h.PT.fromApi(e.state.toUpperCase()),this.telemetry={temperature:{nozzle:{current:e.temp_nozzle,target:e.target_nozzle},bed:{current:e.temp_bed,target:e.target_bed},ambient:{current:e.temp_ambient},cpu:{current:e.temp_cpu},uvLED:{current:e.temp_uv_led}},axis:{x:e.axis_x,y:e.axis_y,z:e.axis_z},flow:e.flow,speed:e.speed,fan:{hotend:e.fan_hotend,print:e.fan_print,blower:e.fan_blower,rear:e.fan_rear,uvLED:e.fan_uv_led},coverClosed:e.cover_closed,isCalibrated:e.is_calibrated},this.link.connect.message=e.status_connect?.message??\"\",this.link.connect.ok=e.status_connect?.ok,this.link.printer.message=e.status_printer?.message??\"ok\",this.link.printer.ok=e.status_printer?.ok??!0}updateJob(e){const t=this.job?.id||null,a=e?.id||null;if(t!==a||this.job?.dirty){if(!a)return void(this.job=void 0);this.updateJobDetails()}if(e&&a){const t=this.job?.timeRemaining,s=e.time_remaining,n=t!=s?(i=s,Math.round(Date.now()/1e3)+i):this.job?.estimatedEnd;0,this.job={dirty:!1,file:void 0,...this.job,timePrinting:e.time_printing,id:a,progress:e.progress,timeRemaining:e.time_remaining,estimatedEnd:n}}var i}updateJobDetails(){return(0,m.LK)(\"/api/v1/job\").then((e=>{const t=e.data;if(t.id===this.job.id&&(this.job={...this.job,dirty:!1,file:Ue(t.file),thumbnail:{source:!t.file.refs?.thumbnail,ready:!t.file.refs?.thumbnail,url:void 0}},!this.job.thumbnail.ready)){const e=this.job.id;(0,m.gJ)(this.job.file.refs.thumbnail).then((({url:t})=>{this.job.id===e&&(this.job.thumbnail.url=t)})).catch((e=>console.error(\"Failed to fetch thumbnail\",e))).finally((()=>this.job.thumbnail.ready=!0))}})).catch((e=>{this.job.dirty=!0,(0,u.S)(e)}))}updateStorage(e){Object.keys(e).forEach((e=>{const t={path:e.path,name:e.name,readOnly:e.readOnly,freeSpace:e.freeSpace},a=this.storage.findIndex((e=>e.path===t.path));-1!==a?this.storage[a]=t:this.storage.push(t)}))}updateTransfer(e){const t=this.transfer?.id||null,a=e?.id||null;if(t!==a){if(!a)return void(this.transfer=void 0);(0,m.LK)(\"/api/v1/transfer\").then((e=>{const t=e.data;this.transfer={...this.transfer,file:{displayName:t.display_name??t.name,path:t.path,size:t.size,toPrint:t.to_print}}})).catch((e=>(0,u.S)(e)))}if(a){const t=Math.round(Date.now()/1e3),i=this.transfer?.file?.size||0,s=e.time_transferring,n=void 0!==s?t-s:void 0,r=e.data_transferred,o=i-r,l=s>0&&o>=0?o/(r/s):void 0;this.transfer={...this.transfer,timeTransferring:s,timeStarted:n,timeRemaining:l,id:a,progress:e.progress,dataTransferred:r}}}updateCamera(e){this.camera={id:e?.id}}selectFile(e){if(!e)return void(this.files.selected=null);const t=e.refs?.thumbnail;if(this.files.selected={file:Ue(e),thumbnail:{source:t,ready:!t,url:void 0},timeRemaining:e.meta?.estimated_print_time},!e.meta){const e=this.files.selected.file.resource;(0,m.LK)(e).then((t=>{const a=Ue({...t.data,resource:e});this.files.selected.file.resource===e&&(this.files.selected.file.meta=a.meta,this.files.selected.timeRemaining=a.meta?.estimatedPrintTime)}))}t&&(0,m.gJ)(t).then((({url:e})=>{t===this.files.selected.thumbnail.source&&(this.files.selected.thumbnail.url=e)})).catch((()=>{})).finally((()=>{t===this.files.selected.thumbnail.source&&(this.files.selected.thumbnail.ready=!0)}))}},Ve=e=>{const t=e.load;return e.load=()=>{(()=>{const e=document.getElementById(\"title-printer\");e&&(e.innerHTML=Ze())})(),t(Ke)},e},Ze=()=>(e=>Be([e.printer?.location||e.printer?.hostname,e.printer?.name]))(Ke);let Fe=De;const $e={routes:[{path:\"dashboard\",html:a(2373),module:Ve(De),getTitle:()=>(0,i.Iu)(\"home.link\")},{path:\"question\",html:a(5198),module:Ve(G)},null,{path:\"files\",html:a(7189),module:Ve(Ie),getTitle:()=>(0,i.Iu)(\"proj.storage\")},{path:\"settings\",html:a(2936),module:Ve(a(2957).Z),getTitle:()=>(0,i.Iu)(\"settings.title\")},{path:\"control\",html:a(3478),module:Ve(a(732).Z),getTitle:()=>(0,i.Iu)(\"control.link\")},{path:\"cameras\",html:a(5464),module:Ve(a(2451).Z),getTitle:()=>(0,i.Iu)(\"cameras.link\")}].filter((e=>null!=e)),init:e=>{Ke.update(e),Ge()},update:e=>{Ke.update(e);const t=(0,C.nC)(),a=(e=>{const t=e.state;let a=(0,h.ny)(t);switch(t){case\"IDLE\":return\"\";case\"PRINTING\":return`${a} ${Math.round(e?.job?.progress||0)}%`;default:return a}})(Ke);var i;document.title=(i=[a,$e.routes.find((e=>e.path===t)).getTitle()],Be([...i,Ze(),\"PrusaLink\"])),(0,j.zR)(\"telemetry\",Ke),(e=>{const t=e,a=document.getElementById(\"printer-status\");a&&(a.innerHTML=(0,h.ny)(t))})(Ke.state),We(Ke.telemetry),Ye()},setConnected:e=>{He({link:Ke.link,isConnected:e})},setModule:e=>{Fe=e},getContext:()=>Ke},Ge=()=>{const e=new Map([[\"temp-line-blue\",[]],[\"temp-line-orange\",[]]]);o(e,300),l()},We=e=>{const t=(new Date).getTime();d(\"temp-line-blue\",[t,e.temperature.bed.current]),d(\"temp-line-orange\",[t,e.temperature.nozzle.current]),l()},Ye=()=>{Fe&&Fe.update&&Fe.update(Ke)},Je=$e},8751:(e,t,a)=>{\"use strict\";a.d(t,{c4:()=>l,g9:()=>d,nC:()=>r});var i=a(7780),s=a(3532);const n=e=>{const[t,a]=e.split(\"#\");return a||\"dashboard\"},r=()=>n(window.location.hash);function o(e,t){const a=n(e),r=s.Z.routes.find((e=>e.path===a));if(!r)return!1;t&&function(e){window.location.hash!=e&&history.pushState(null,\"\",e)}(\"#\"+a);const o=document.getElementById(\"root\");var l;return function(e,t){t.innerHTML=\"\",(new DOMParser).parseFromString(e,\"text/html\").body.childNodes.forEach((e=>t.appendChild(e)))}(r.html,o),(0,i.ot)(o),function(e){const t=document.querySelector(`a[href=\"#${e}\"]`);t&&(document.getElementById(\"navbar\").childNodes.forEach((e=>{\"li\"===e.nodeName.toLowerCase()&&e.classList.remove([\"active\"])})),t.parentNode.className=\"active\")}(a),(l=r.getTitle)&&(document.title=l()),window.scrollTo({top:0}),r.module.load(),s.Z.setModule(r.module),!0}const l=e=>o(e,!0),d=e=>o(e,!1)},4800:(e,t,a)=>{\"use strict\";a.d(t,{Dt:()=>r,PT:()=>s,cG:()=>n,ny:()=>o});var i=a(7780);const s={UNKNOWN:\"UNKNOWN\",IDLE:\"IDLE\",READY:\"READY\",BUSY:\"BUSY\",POUR_IN_RESIN:\"POUR IN RESIN\",REFILL:\"FEED ME\",PRINTING:\"PRINTING\",PAUSED:\"PAUSED\",FINISHED:\"FINISHED\",STOPPED:\"STOPPED\",SELECTED:\"SELECTED\",ERROR:\"ERROR\",ATTENTION:\"ATTENTION\",fromApi:e=>{switch(e.toUpperCase()){case\"IDLE\":return s.IDLE;case\"READY\":return s.READY;case\"BUSY\":return s.BUSY;case\"PRINTING\":return s.PRINTING;case\"PAUSED\":return s.PAUSED;case\"FINISHED\":return s.FINISHED;case\"STOPPED\":return s.STOPPED;case\"ERROR\":return s.ERROR;case\"ATTENTION\":return s.ATTENTION;case\"POUR IN RESIN\":return s.POUR_IN_RESIN;case\"FEED ME\":return s.REFILL;case\"SELECTED\":return s.SELECTED;case\"UNKNOWN\":return s.UNKNOWN;default:return console.error(`Unsupported state: ${e}`),s.UNKNOWN}}},n=[s.IDLE,s.READY,s.FINISHED,s.SELECTED],r=[s.PRINTING,s.PAUSED,s.POUR_IN_RESIN,s.SELECTED],o=e=>{switch(e){case s.IDLE:return(0,i.Iu)(\"prop.st-idle\");case s.READY:return(0,i.Iu)(\"prop.st-ready\");case s.BUSY:return(0,i.Iu)(\"prop.st-busy\");case s.PRINTING:return(0,i.Iu)(\"prop.st-printing\");case s.PAUSED:return(0,i.Iu)(\"prop.st-paused\");case s.FINISHED:return(0,i.Iu)(\"prop.st-finished\");case s.STOPPED:return(0,i.Iu)(\"prop.st-stopped\");case s.ERROR:return(0,i.Iu)(\"prop.st-error\");case s.ATTENTION:return(0,i.Iu)(\"prop.st-attention\");case s.POUR_IN_RESIN:return(0,i.Iu)(\"prop.st-pour-resin\");case s.SELECTED:return(0,i.Iu)(\"prop.st-ready\");case s.REFILL:return(0,i.Iu)(\"prop.st-feedme\");default:return console.error(`Unsupported state: ${e}`),(0,i.Iu)(\"prop.st-unknown\")}}},4977:e=>{\"use strict\";e.exports=JSON.parse('{\"langs\":[\"cs\",\"de\",\"en\",\"es\",\"fr\",\"it\",\"kr\",\"lt\",\"nl\",\"pl\",\"sk\"],\"texts\":{\"home\":{\"link\":[\"Přehled\",\"Dashboard\",\"Dashboard\",\"Panel principal\",\"Tableau de bord\",\"Dashboard\",\"대쉬보드\",\"Darbastalis\",\"Dashboard\",\"Panel kontrolny\",\"Prehľad\"],\"title\":[\"Stav tiskárny\",\"Druckerstatus\",\"Printer Status\",\"Estado de la impresora\",\"État de l\\'imprimante\",\"Stato della stampante\",\"프린터 상태\",\"Spausdintojo būsena\",\"Printerstatus\",\"Stan drukarki\",\"Stav tlačiarne\"]},\"proj\":{\"link\":[\"Úložiště\",\"Projekte\",\"Storage\",\"Proyectos\",\"Projets\",\"Progetti\",\"저장장치\",\"Saugykla\",\"Opslag\",\"Projekty\",\"Úložisko\"],\"storage\":[\"Úložiště\",\"Speicher\",\"Storage\",\"Almacenamiento\",\"Stockage\",\"Archivio\",\"저장장치\",\"Saugykla\",\"Opslag\",\"Projekty\",\"Úložisko\"],\"add-from\":{\"title\":[\"Nahrát soubor z\",\"Datei hinzufügen von\",\"Add file from\",\"Añadir archivo desde\",\"Ajouter un fichier à partir de\",\"Aggiungi file da\",\"에서 파일 추가하기\",\"Pridėti failą iš\",\"Bestanden toevoegen van\",\"Dodaj plik z\",\"Nahrať súbor z\"],\"local\":[\"Lokálního úložiště\",\"Lokaler Speicher\",\"Local storage\",\"Almacenamiento local\",\"Stockage local\",\"Archivio locale\",\"로컬 저장장치\",\"Vietinis saugykla\",\"Lokale opslag\",\"Magazyn lokalny\",\"Lokálneho úložiska\"],\"remote\":[\"Vzdálené url\",\"Remote-URL\",\"Remote url\",\"Eliminar url\",\"URL distante\",\"Url remoto\",\"원격 URL\",\"Nuotolinis URL\",\"Externe URL\",\"Zdalny URL\",\"Vzdialenej URL\"]},\"details\":[\"Detaily souboru\",\"Datei-Details\",\"File details\",\"Detalles del archivo\",\"Détails du fichier\",\"Dettagli file\",\"파일 상세정보\",\"Failo informacija\",\"Bestandseigenschappen\",\"Szczegóły pliku\",\"Detaily súboru\"],\"del\":[\"Smazat\",\"Datei löschen\",\"Delete\",\"Borrar archivo\",\"Supprimer le Fichier\",\"Elimina File\",\"삭제\",\"Ištrinti\",\"Verwijderen\",\"Usuń plik\",\"Zmazať\"],\"download\":[\"Stáhnout\",\"Herunterladen\",\"Download\",\"Descargar\",\"Télécharger\",\"Download\",\"다운로드\",\"Atsisiųsti\",\"Downloaden\",\"Pobierz\",\"Stiahnuť\"],\"up-folder\":[\"nadřazená složka\",\"übergeordneter Ordner\",\"parent folder\",\"carpeta contenedora\",\"dossier parent\",\"cartella superiore\",\"상위폴더\",\"tėvinis aplankas\",\"Bovenliggende map\",\"folder nadrzędny\",\"nadradený priečinok\"]},\"control\":{\"link\":[\"Ovládání\",\"Kontrolle\",\"Control\",\"Control\",\"Contrôle\",\"Controllo\",\"제어\",\"Valdymas\",\"Bediening\",\"Sterowanie\",\"Ovládanie\"],\"coordinates\":[\"Souřadnice tiskárny\",\"Drucker-Koordinaten\",\"Printer Coordinates\",\"Coordenadas de la Impresora\",\"Coordonnées de l\\'imprimante\",\"Coordinate Stampante\",\"프린터 좌표\",\"Spausdintojo koordinatės\",\"Printercoördinaten\",\"Współrzędne drukarki\",\"Súradnice tlačiarne\"],\"axis\":{\"x\":[\"Osa X\",\"X-Achse\",\"X axis\",\"Eje X\",\"axe X\",\"Asse X\",\"X축\",\"X ašis\",\"X-as\",\"Oś X\",\"Os X\"],\"y\":[\"Osa Y\",\"Y-Achse\",\"Y axis\",\"Eje Y\",\"axe Y\",\"Asse Y\",\"Y축\",\"Y ašis\",\"Y-as\",\"Oś Y\",\"Os Y\"],\"z\":[\"Osa Z\",\"Z-Achse\",\"Z axis\",\"Eje Z\",\"axe Z\",\"Asse Z\",\"Z축\",\"Z ašis\",\"Z-as\",\"Oś Z\",\"Os Z\"]},\"stepper-motors\":[\"Krokové motory\",\"Schrittmotoren\",\"Stepper Motors\",\"Motores Paso-a-paso\",\"moteurs\",\"Motori Passo-Passo\",\"스텝 모터\",\"Sustojimo varikliai\",\"Stappenmotors\",\"Silniki krokowe\",\"Krokové motory\"],\"heated-bed-move\":[\"Pohyb podložky v ose X a Y\",\"Heizbett X und Y bewegen\",\"Heated Bed X and Y Move\",\"Base Calefactable Mover X e Y\",\"Mouvement en x/y\",\"Spostamento X e Y Piano Riscaldato\",\"히트베드 X 및 Y 이동\",\"Šildomojo patalo X ir Y poslinkis\",\"Beweeg verwarmd bed in X en Y\",\"Ruch stołu grzewczego X i Y\",\"Pohyb podložky v osi X a Y\"],\"move-step\":[\"Krok pohybu [mm]\",\"Schrittweite [mm]\",\"Move Step [mm]\",\"Mover Paso [mm]\",\"déplacement [mm]\",\"Sposta Passo [mm]\",\"[mm]mm 스텝 이동\",\"Poslinkis [mm]\",\"Beweegafstand [mm]\",\"Krok ruchu [mm]\",\"Krok pohybu [mm]\"],\"nozzle-z-move\":[\"Pohyb extrudéru v ose Z\",\"Düse Z Bewegung\",\"Nozzle Z Move\",\"Boquilla Mover Z\",\"déplacement en z\",\"Spostamento Z Ugello\",\"노즐 Z 이동\",\"Antgalio Z poslinkis\",\"Beweeg omhoog/omlaag\",\"Ruch dyszy w osi Z\",\"Pohyb extruderu v osi Z\"],\"extruder\":[\"Extrudér\",\"Extruder\",\"Extruder\",\"Extrusor\",\"extrudeur\",\"Estrusore\",\"익스트루더\",\"Ištraukiklis\",\"Extruder\",\"Ekstruder\",\"Extruder\"],\"extrude-retract-step\":[\"Krok vytlačení/vtažení [mm]\",\"Extrudieren/Zurückziehen Schritt [mm]\",\"Extrude/Retract Step [mm]\",\"Distancia Extruir/Retraer [mm]\",\"longueur d\\'extrusion ou de rétractation [mm]\",\"Passo Estrusione/Retrazione [mm]\",\"[mm]mm 스텝 압출/철회\",\"Ištrauka/atgalinimas [mm]\",\"Extrude/retract-afstand [mm]\",\"Krok ekstruzji/retrakcji [mm]\",\"Krok vysunutia/zasunutia [mm]\"],\"nozzle-temp\":[\"Teplota trysky\",\"Düsentemperatur\",\"Nozzle Temperature\",\"Temperatura Boquilla\",\"température de la buse\",\"Temperatura Ugello\",\"노즐 온도\",\"Antgalio temperatūra\",\"Temperatuur nozzle\",\"Temperatura dyszy\",\"Teplota trysky\"],\"speed\":[\"Rychlost\",\"Geschwindigkeit\",\"Speed\",\"Velocidad\",\"vitesse\",\"Velocità\",\"속도\",\"Greitis\",\"Snelheid\",\"Prędkość\",\"Rýchlosť\"],\"heated-bed-temp\":[\"Teplota vyhřívané podložky\",\"Temperatur des Heizbettes\",\"Heated Bed Temperature\",\"Temperatura de la Base Calefactable\",\"Température du lit\",\"Temperatura piano riscaldato\",\"히트베드 온도\",\"Šildomojo patalo temperatūra\",\"Temperatuur verwarmd bed\",\"Temperatura stołu\",\"Teplota vyhrievanej podložky\"],\"flow\":[\"Průtok\",\"Flow\",\"Flow\",\"Flujo\",\"flux\",\"Flusso\",\"흐름\",\"Srautas\",\"Extrusiefactor\",\"Przepływ\",\"Prietok\"],\"title\":[\"Ovládání tiskárny\",\"Druckersteuerung\",\"Printer Control\",\"Control de la Impresora\",\"Contrôle de l\\'imprimante\",\"Controllo Stampante\",\"프린터 제어\",\"Spausdintojo valdymas\",\"Printerbediening\",\"Sterowanie drukarką\",\"Ovládanie tlačiarne\"]},\"cameras\":{\"link\":[\"Kamery\",\"Kameras\",\"Cameras\",\"Cámaras\",\"Caméras\",\"Fotocamere\",\"카메라\",\"Kameros\",\"Camera\\'s\",\"Kamery\",\"\"],\"trigger-scheme\":{\"ten-sec\":[\"Každých 10 sekund\",\"Alle 10 Sekunden\",\"Every 10 seconds\",\"Cada 10 segundos\",\"Toutes les 10 secondes\",\"Ogni 10 secondi\",\"매 10초\",\"Kas 10 sekundžių\",\"Elke 10 seconden\",\"Co 10 sekund\",\"\"],\"thirty-sec\":[\"Každých 30 sekund\",\"Alle 30 Sekunden\",\"Every 30 seconds\",\"Cada 30 segundos\",\"Toutes les 30 secondes\",\"Ogni 30 secondi\",\"매 30초\",\"Kas 30 sekundžių\",\"Elke 30 seconden\",\"Co 30 sekund\",\"\"],\"sixty-sec\":[\"Každých 60 sekund\",\"Alle 60 Sekunden\",\"Every 60 seconds\",\"Cada 60 segundos\",\"Toutes les 60 secondes\",\"Ogni 60 secondi\",\"매 60초\",\"Kas 60 sekundžių\",\"Elke minuut\",\"Co 60 sekund\",\"\"],\"each-layer\":[\"Při změně vrstvy\",\"Beim Schichtwechsel\",\"On layer change\",\"Al cambiar de capa\",\"Au changement de couche\",\"Al cambio layer\",\"매 레이어\",\"Pakeitus sluoksnį\",\"Bij laagwissel\",\"Przy zmianie warstwy\",\"\"],\"fifth-layer\":[\"Každou pátou vrstvu\",\"Alle 5 Schichten\",\"Every fifth layer\",\"Cada cinco capas\",\"Toutes les cinq couches\",\"Ogni cinque layer\",\"매 5레이어\",\"Kas penktas sluoksnis\",\"Elke 5 lagen\",\"Co piąta warstwa\",\"\"],\"manual\":[\"Ručně\",\"Manuell\",\"Manual\",\"Manual\",\"Manuel\",\"Manuale\",\"수동\",\"Rankinis\",\"Handmatig\",\"Ręcznie\",\"\"]}},\"settings\":{\"title\":[\"Nastavení\",\"Einstellungen\",\"Settings\",\"Ajustes\",\"Réglages\",\"Impostazioni\",\"세팅\",\"Nustatymai\",\"Instellingen\",\"Ustawienia\",\"Nastavenie\"]},\"conn\":{\"prusa-connect-status\":[\"Stav připojení PrusaConnect\",\"PrusaConnect Status\",\"PrusaConnect Status\",\"Estado de PrusaConnect\",\"Informations de connexion à PrusaConnect\",\"Stato PrusaConnect\",\"프루사커넥트 상태\",\"PrusaConnect būsena\",\"Status van PrusaConnect\",\"Stan PrusaConnect\",\"Stav pripojenia PrusaConnect\"],\"printer-status\":[\"Stav připojení 3D tiskárny\",\"3D-Drucker Verbindungsstatus\",\"3D Printer Connection Status\",\"Estado Conexión Impresora 3D\",\"Informations de connexion à l\\'imprimante 3D\",\"Stato Connessione Stampante 3D\",\"3D 프린터 연결 상태\",\"3D spausdintojo prisijungimo būsena\",\"Verbindingsstatus van 3D-printer\",\"Status połączenia z drukarką 3D\",\"Stav pripojenia 3D tlačiarne\"],\"title\":[\"připojení\",\"Verbindung\",\"Connection\",null,\"connexion\",null,\"연결\",\"Prisijungimas\",\"Verbinding\",null,\"pripojenie\"],\"prusa-connect-url\":[\"Adresa PRUSA CONNECT\",\"PRUSA CONNECT Adresse\",\"PRUSA CONNECT Address\",null,null,null,\"프루사커넥트 주소\",\"PRUSA CONNECT adresas\",\"Adres van PRUSA CONNECT\",null,\"Adresa PrusaConnect\"],\"suc\":[\"Úspěšně připojeno\",\"Erfolgreich verbunden\",\"Successfully connected\",\"Correctamente conectado\",\"Connecté avec succès\",\"Connessione riuscita\",\"연결 성공\",\"Prisijungta sėkmingai\",\"Succesvol verbonden\",\"Połączono pomyślnie\",\"Úspešne pripojené\"],\"connect\":{\"linked\":[\"\",\"Verbunden\",\"Linked\",\"Enlazado\",\"Associée\",\"Collegato\",\"연결됨\",\"Susieta\",\"Verbonden\",\"Połączono\",\"\"],\"not-linked\":[null,\"Nicht verbunden\",\"Not Linked\",\"No enlazado\",\"Non Associée\",\"Non collegato\",\"연결안됨\",\"Nesusieta\",\"Niet verbonden\",\"Nie połączono\",\"\"]},\"error_status\":[\"Chyba\",\"Fehler\",\"Error\",\"Error\",\"Erreur\",\"Errore\",\"에러\",\"\",\"\",\"Błąd\",\"\"]},\"prop\":{\"temp-nozzle\":[\"Teplota trysky\",\"Düsentemperatur\",\"Nozzle Temperature\",\"Temperatura del nozzle\",\"Température de la buse\",\"Temperatura ugello\",\"노즐 온도\",\"Nozzle Temperature\",\"Nozzletemperatuur\",\"Temperatura dyszy\",\"Teplota trysky\"],\"temp-bed\":[\"Vyhřívaná podložka\",\"Heizbett\",\"Heatbed\",\"Base calefactable\",\"Plateau chauffant\",\"Piano riscaldato\",\"히트베드\",\"Dėklo temperatūra\",\"Verwarmd bed\",\"Stół\",\"Vyhrievaná podložka\"],\"speed\":[\"Rychlost tisku\",\"Druckgeschwindigkeit\",\"Printing Speed\",\"Velocidad de impresión\",\"Vitesse d\\'impression\",\"Velocità di stampa\",\"출력속도\",\"Spausdinimo greitis\",\"Printsnelheid\",\"Prędkość drukowania\",\"Rýchlosť tlače\"],\"z-height\":[\"Výška Z\",\"Z-Höhe\",\"Z-Height\",\"Altura-Z\",\"Hauteur en z\",\"Altezza-Z\",\"Z높이\",\"Z-Height\",\"Z-Hoogte\",\"Wysokość Z\",\"Výška Z\"],\"nozzle-diameter\":[\"Velikost trysky\",\"Düsendurchmesser\",\"Nozzle Diameter\",\"Diámetro de Boquilla\",\"Diamètre de la Buse\",\"Diametro ugello\",\"노즐지름\",\"Galvutės skersmuo\",\"Nozzlediameter\",\"Średnica dyszy\",\"Veľkosť trysky\"],\"rem-time\":[\"Zbývající čas\",\"Restzeit\",\"Remaining time\",\"Tiempo restante\",\"Temps restant\",\"Tempo residuo\",\"남은시간\",\"Likęs laikas\",\"Resterende tijd\",\"Pozostały czas\",\"Zostávajúci čas\"],\"time-est\":[\"Odhad doby tisku\",\"Druckzeit Schätzen\",\"Print Time Estimate\",\"Tiempo Estimado de Impresión\",\"Estimation du temps d\\'impression\",\"Stima del Tempo di Stampa\",\"예상 출력 시간\",\"Print Time Estimate\",\"Geschatte printtijd\",\"Szacowany Czas Druku\",\"Odhad času tlače\"],\"est-end\":[\"Odhadovaný konec\",\"Erwartetes Ende\",\"Estimated end\",\"Fin estimado\",\"Fin d\\'impression estimée\",\"Fine prevista\",\"예상완료\",\"Numatomas pabaiga\",\"geschatte eindtijd\",\"Szacowane zakończenie\",\"Odhadovaný koniec\"],\"pnt-time\":[\"Doba tisku\",\"Druckzeit\",\"Printing time\",\"Tiempo de impresión\",\"Temps d\\'impression\",\"Tempo di stampa\",\"출력시간\",\"Spausdinimo laikas\",\"Printtijd\",\"Czas druku\",\"Čas tlače\"],\"last-mod\":[\"Naposledy upraveno\",\"Zuletzt Geändert\",\"Last Modified\",\"Ultima Modificación\",\"Dernière Modification\",\"Ultima Modifica\",\"최종 수정됨\",\"Paskutinis modifikavimas\",\"Laatst gewijzigd\",\"Ostatnio Zmodyfikowany\",\"Posledná úprava\"],\"material\":[\"Materiál\",\"Material\",\"Material\",\"Material\",\"Matériau\",\"Materiale\",\"재료\",\"Medžiaga\",\"Materiaal\",\"Materiał\",\"Materiál\"],\"layer-ht\":[\"Výška vrstvy\",\"Schichthöhe\",\"Layer Height\",\"Altura de Capa\",\"Hauteur de Couche\",\"Altezza Layer\",\"레이어 높이\",\"Sluoksnio storis\",\"Laagdikte\",\"Wysokość Warstwy\",\"Výška vrstvy\"],\"size\":[\"Velikost souboru\",\"Größe der Datei\",\"File size\",\"Tamaño de archivo\",\"Taille du fichier\",\"Dimensione file\",\"파일크기\",\"Failo dydis\",\"Bestandsgrootte\",\"Rozmiar pliku\",\"Veľkosť súboru\"],\"progress\":[\"Postup\",\"Fortschritt\",\"Progress\",\"Progreso\",\"Progression\",\"Progresso\",\"진행\",\"Eiga\",\"Voortgang\",\"Postęp\",\"Progres\"],\"st-idle\":[\"Nečinná\",\"Leerlauf\",\"Idle\",\"En espera\",\"Repos\",\"Inattivo\",\"대기\",\"Pramoginė veikla\",\"Inactief\",\"Oczekuje\",\"Nečinná\"],\"st-ready\":[\"Připravena\",\"Bereit\",\"Ready\",\"Lista\",\"Prête\",\"Pronto\",\"준비\",\"Pasirengęs\",\"Gereed voor gebruik\",\"Gotowe\",\"Pripravené\"],\"st-busy\":[\"Zaneprázdeněná\",\"Beschäftigt\",\"Busy\",\"Ocupada\",\"Occupée\",\"Occupato\",\"작업중\",\"Užsiėmęs\",\"Bezig\",\"Zajęty\",\"Zaneprázdnená\"],\"st-printing\":[\"Probíhá tisk\",\"Druckt\",\"Printing\",\"Imprimiendo\",\"Impression\",\"Stampa\",\"출력중\",\"Spausdinama\",\"Printen\",\"Drukowanie\",\"Prebieha tlač\"],\"st-paused\":[\"Pozastavena\",\"Pausiert\",\"Paused\",\"Pausado\",\"En pause\",\"In pausa\",\"일시정지\",\"Pristabdyta\",\"Gepauzeeerd\",\"Pauza\",\"Pozastavená\"],\"st-finished\":[\"Dokončeno\",\"Beendet\",\"Finished\",\"Terminado\",\"Terminé\",\"Completato\",\"완료됨\",\"Užbaigta\",\"Klaar\",\"Zakonczono\",\"Dokončené\"],\"st-stopped\":[\"Zastaveno\",\"Gestoppt\",\"Stopped\",\"Parada\",\"Arrêtée\",\"Arrestato\",\"정지됨\",\"Sustabdytas\",\"Gestopt\",\"Zatrzymano\",\"Zastavené\"],\"st-error\":[\"Chyba\",\"Fehler\",\"Error\",\"Error\",\"Erreur\",\"Errore\",\"에러\",\"Klaida\",\"Fout\",\"Błąd\",\"Chyba\"],\"st-attention\":[\"Vyžaduje pozornost\",\"Achtung\",\"Attention\",\"Atención\",\"Attention\",\"Attenzione\",\"주의\",\"Dėmesio\",\"Attentie\",\"Uwaga\",\"Pozor\"],\"st-pour-resin\":[\"Nalití resinu\",\"Gieße Harz ein\",\"Pour in resin\",\"Vierte resina\",\"Faire affluer la résine\",\"Versa la resina\",\"레진 붓기\",\"Įpilkite dervą\",\"Giet hars in vat\",\"Wlej żywicę\",\"Nalejte resin\"],\"st-feedme\":[\"Doplnění resinu\",\"Harz nachfüllen\",\"Refill resin\",\"Añadir resina\",\"remplir le réservoir de résine\",\"Ricarica la resina\",\"레진 리필\",\"Papildykite dervą\",\"Hervul resin\",\"Uzupełnij żywicę\",\"Doplnenie resinu\"],\"st-unknown\":[\"Neznámý\",\"Unbekannt\",\"Unknown\",\"Desconocido\",\"Inconnu\",\"Sconosciuto\",\"알수없음\",\"Nežinoma\",\"Onbekend\",\"Nieznane\",\"Neznáme\"],\"at\":[\"v\",\"um\",\"at\",\"a las\",\"à\",\"alle\",\"에서\",\"iš\",\"op\",\"o\",\"v\"],\"less-than\":[\"Méně než minuta\",\"Weniger als eine Minute\",\"Less than a minute\",\"Menos de un minuto\",\"Moins d\\'une minute\",\"Meno di un minuto\",\"1분 미만\",\"Mažiau nei minutė\",\"Minder dan een minuut\",\"Mniej niż minuta\",\"Menej ako minúta\"],\"today-at\":[\"Dnes v\",\"Heute um\",\"Today at\",\"Hoy a las\",\"Aujourd\\'hui à\",\"Oggi alle\",\"오늘\",\"Šiandien\",\"Vandaag om\",\"Dzisiaj o\",\"Dnes o\"],\"tmw-at\":[\"Zítra v\",\"Morgen um\",\"Tomorrow at\",\"Mañana a las\",\"Demain à\",\"Domani alle\",\"내일\",\"Rytoj\",\"Morgen om\",\"Jutro o\",\"Zajtra o\"],\"true\":[\"Ano\",\"Ja\",\"Yes\",\"Si\",\"Oui\",\"Si\",\"예\",\"Taip\",\"Ja\",\"Tak\",\"Áno\"],\"false\":[\"Ne\",\"Nein\",\"No\",\"No\",\"Non\",\"No\",\"아니오\",\"Ne\",\"Nee\",\"Nie\",\"Nie\"],\"na\":[\"N/A\",\"NV\",\"NA\",\"NA\",\"NA\",\"ND\",\"없음\",\"NA\",\"N.v.t.\",\"N/D\",\"N/A\"],\"cover-closed\":[\"Uzavřené\",\"Geschlossen\",\"Closed\",\"Cerrada\",\"Fermé\",\"Chiuso\",\"닫힘\",\"Uždarytas\",\"Gesloten\",\"Zamknięta\",\"Zavreté\"],\"cover-opened\":[\"Otevřené\",\"Geöffnet\",\"Opened\",\"Abierta\",\"Ouvert\",\"Aperto\",\"열림\",\"Atidarytas\",\"Open\",\"Otwarta\",\"Otvorené\"],\"storage-used-space\":[\"Použito {{used}} z {{total}} ({{free}} volných)\",\"{{used}} benutzt von {{total}} ({{free}} frei)\",\"Used {{used}} of {{total}} ({{free}} free)\",\"Usados {{used}} de {{total}} ({{free}} libres)\",\"{{used}} utilisés sur {{total}} ({{free}} libre)\",\"Usato {{used}} di {{total}} ({{free}} liberi)\",\"{{total}} 중 {{used}} 사용중({{free}} 남음)\",\"\",\"\",\"Zajęte {{used}} z {{total}} ({{free}} wolne)\",\"\"]},\"msg\":{\"modal-p1\":[\"Vítejte na webovém rozhraní vaší\",\"Willkommen zur Weboberfläche Ihres\",\"Welcome to the web interface of your\",\"Bienvenido a la interfaz web de tu\",\"Bienvenue dans l\\'interface web de votre\",\"Benvenuto nell\\'interfaccia web del tuo\",\"당신의 웹 인터페이스에 오신 것을 환영합니다.\",\"Sveiki atvykę į savo\",\"Welkom bij de webinterface van uw\",\"Witaj w interfejsie Twojej\",\"Vitajte vo webovom rozhraní vašej\"],\"modal-p2\":[\"Upozornění: některé hodnoty jsou zobrazeny pouze v průběhu tisku.\",\"Bitte beachten Sie, dass die Werte nur angezeigt werden, wenn der Drucker gerade druckt.\",\"Please note that values are shown only when the printer is printing.\",\"Tenga en cuenta que los valores se muestran solo cuando la impresora está imprimiendo.\",\"Veuillez noter que les valeurs sont affichées uniquement lorsque l\\'imprimante imprime.\",\"Si noti che i valori vengono visualizzati solo durante la stampa.\",\"프린터가 출력중일 경우에만 값이 표시됩니다.\",\"Prašome atkreipti dėmesį, kad reikšmės rodomos tik tada, kai spausdintuvas spausdina.\",\"De waarden worden alleen getoond tijdens het printen.\",\"Wartości będą wyświetlane tylko podczas drukowania.\",\"Upozornenie: niektoré hodnoty sú zobrazené len počas tlače.\"],\"api-key-1\":[\"Vítejte ve webovém rozhraní PrusaLink Web!\",\"Willkommen auf der Web-Oberfläche von PrusaLink Web.\",\"Welcome to the PrusaLink Web web interface.\",\"Bienvenido a la interfaz web de PrusaLink Web.\",\"Bienvenue sur l\\'interface web locale de PrusaLink Web.\",\"Benvenuti nell\\'interfaccia web di PrusaLink Web.\",\"프루사 커넥트 웹 웹인터페이스에 오신 것을 환영합니다.\",\"Sveiki atvykę į PrusaLink Web interneto sąsają.\",\"Welkom bij de PrusaLink Web webinterface.\",\"Witaj w interfejsie sieciowym PrusaLink Web.\",\"Vitajte vo webovom rozhraní PrusaLink Web!\"],\"api-key-2\":[\"Vložte API klíč.\",\"Bitte geben Sie den API-Schlüssel ein.\",\"Please insert the API key.\",\"Por favor introduce la clave API.\",\"Veuillez insérer la clé API.\",\"Si prega di inserire la chiave API.\",\"API키를 입력하십시오.\",\"Prašome įvesti API raktą.\",\"Voer de API-sleutel in\",\"Wpisz klucz API.\",\"Vložte API kľúč.\"],\"api-key-3\":[\"Otevřete menu Nastavení -> Síť -> PrusaLink.\",\"Sie finden ihn in Einstellungen -> Netzwerk -> PrusaLink.\",\"You can find it in Settings -> Network -> PrusaLink.\",\"Puedes encontrarla en Ajustes -> Red -> PrusaLink.\",\"Vous pouvez la trouver dans Réglages -> Réseau -> Informations de connexion.\",\"È possibile trovarla in Impostazioni -> Rete -> PrusaLink\",\"설정 -> 네트워크 -> 프루사링크에서 찾을 수 있습니다.\",\"Jį galite rasti Nustatymai -> Tinklas -> PrusaLink.\",\"Deze kan gevonden worden bij Settings -> -> Network -> PrusaLink.\",\"Możesz znaleźć go w sekcji Ustawienia -> Sieć -> PrusaLink.\",\"Nájdete ho v Nastavenia -> Sieť -> PrusaLink.\"],\"offline\":{\"not-responsing\":[\"Tiskárna nereaguje.\",\"Der Drucker antwortet nicht.\",\"Printer is not responding.\",\"La impresora no responde.\",\"L\\'imprimante ne répond pas.\",\"La stampante non risponde.\",\"프린터가 응답하지 않습니다.\",\"Spausdintuvas neatsako.\",\"Printer reageert niet.\",\"Drukarka nie odpowiada.\",\"Tlačiareň nereaguje.\"],\"please-wait\":[\"Prosím, čekejte...\",\"Bitte warten...\",\"Please wait...\",\"Espera, por favor...\",\"Veuillez patienter...\",\"Attendere...\",\"기다려주십시오...\",\"Prašome palaukti...\",\"Even geduld...\",\"Proszę czekać...\",\"Prosím, čakajte...\"]},\"drop-zone\":{\"label\":[\"Přetažením souboru zahájíte nahrávání\",\"Datei hier ablegen, um den Upload zu starten\",\"Drop file here to start the uploading\",\"Arrastra archivo aquí para subirlo\",\"Déposez le fichier ici pour commencer le téléchargement\",\"Trascina il file qui per iniziare il caricamento\",\"파일을 여기로 끌어놓으면 업로드가 시작됩니다.\",\"Tempti failą čia, kad predėti įkrovimą\",\"Sleep het bestand hierheen om het te uploaden\",\"Upuść plik tutaj, aby rozpocząć przesyłanie.\",\"\"]},\"create-folder\":[\"Nová složka\",\"Neuer Ordner\",\"New Folder\",\"Nueva Carpeta\",\"Nouveau Dossier\",\"Nuova Cartella\",\"새 폴더\",\"\",\"\",\"Nowy folder\",\"\"],\"create-folder-name\":[\"Název složky\",\"Ordnername\",\"Folder Name\",\"Nombre de la Carpeta\",\"Nom de Dossier\",\"Nome cartella\",\"폴더 이름\",\"\",\"\",\"Nazwa folderu\",\"\"],\"sysupgrade\":{\"title\":[\"Aktualizovat systémový balíček?\",\"Systempaket aktualisieren?\",\"Upgrade System Package?\",\"¿Actualizar el Paquete del Sistema?\",\"Mettre à niveau le package système ?\",\"Aggiornare il pacchetto di sistema?\",\"시스템 패키지를 업그레이드하겠습니까?\",\"\",\"\",\"Zaktualizować paczkę systemową?\",\"\"],\"remark\":[\"Aktualizace může trvat několik desítek vteřin.\",\"Das Upgrade kann einige Sekunden dauern.\",\"The upgrade may take several tens of seconds.\",\"La actualización puede tardar varias decenas de segundos.\",\"La mise à jour peut prendre plusieurs dizaines de secondes.\",\"L\\'aggiornamento può richiedere qualche minuto.\",\"업그레이드에는 수십초가 걸릴 수 있습니다.\",\"\",\"\",\"Aktualizacja może trwać kilkadziesiąt sekund.\",\"\"],\"pending\":[\"Probíhá aktualizace... Nevypínejte tiskárnu.\",\"Upgraden... Schalten Sie den Drucker nicht aus.\",\"Upgrading... Do not switch the printer off.\",\"Actualizando... No apagues la impresora.\",\"Mise à niveau... N\\'éteignez pas l\\'imprimante.\",\"Aggiornamento... Non spegnere la stampante.\",\"업그레이드중... 프린터를 끄지 마십시오.\",\"\",\"\",\"Aktualizacja... Nie wyłączaj drukarki.\",\"\"],\"wait-for-printer\":[\"Čeká se na tiskárnu...\",\"Warten auf den Drucker...\",\"Waiting for the printer...\",\"Esperando a la impresora...\",\"Attente de l\\'imprimante...\",\"In attesa della stampante...\",\"프린터를 기다리는 중...\",\"\",\"\",\"Oczekiwanie na drukarkę...\",\"\"]},\"outdated-os\":{\"message\":[\"PrusaLink běží na zastaralém obrazu systému. Nahrajte ho prosím znovu podle <a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>tohoto návodu</a>. Bohužel bude nutné PrusaLink znovu nastavit\",\"Ihr PrusaLink läuft mit einem veralteten Raspberry Pi Betriebssystem. Folgen Sie <a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>dieser Anleitung, um die neueste Version zu flashen</a>. Dies wird PrusaLink zurücksetzen\",\"Your PrusaLink is running on an outdated Raspberry Pi OS. Follow <a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>this guide to flash the latest version</a>. This will reset PrusaLink\",\"Tu PrusaLink está funcionando con un Raspberry Pi OS obsoleto. Sigue <a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>esta guía para flashear la última versión</a>. Esto reiniciará PrusaLink\",\"Votre PrusaLink fonctionne sur un système un Raspberry Pi OS obsolète. Suivez <a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>ce guide pour flasher la dernière version</a>. Cela réinitialisera PrusaLink\",\"Il sistema PrusaLink è in esecuzione su un sistema operativo Raspberry Pi non aggiornato. Seguire <a href=\\'https://help.prusa3d.com/it/guide/impostazione-prusalink-e-prusa-connect-mk3-s-_221744#222176\\'>questa guida per flashare l\\'ultima versione</a>. Questo ripristinerà PrusaLink\",\"PrusaLink가 오래된 Raspberry Pi OS에서 실행되고 있습니다.<a href=\\'https://help.prusa3d.com/guide/_221744#222170\\'>이 가이드를 따라 최신 버전을 업데이트하세요</a>. PrusaLink가 재설정됩니다.\",\"\",\"\",\"Twój PrusaLink działa na nieaktualnej wersji systemu operacyjnego Raspberry Pi. Postępuj zgodnie z <a href=\\'https://help.prusa3d.com/pl/guide/konfiguracja-prusalink-i-prusa-connect-mk3-s-_221744\\'>tym przewodnikiem, aby wgrać najnowszą wersję</a>. Spowoduje to zresetowanie PrusaLink\",\"\"],\"title\":[\"Raspberry Pi OS je zastaralý\",\"Raspberry Pi OS ist veraltet\",\"Raspberry Pi OS is out-of-date\",\"El SO de la Raspberry Pi está desactualizado\",\"Raspberry Pi OS est obsolète\",\"Il sistema operativo di Raspberry Pi è obsoleto\",\"Raspberry Pi OS가 최신 버전이 아닙니다.\",\"\",\"\",\"System operacyjny Raspberry Pi jest nieaktualny\",\"\"]},\"del-folder\":[\"Opravdu chcete smazat {{folder_name}} a její obsah?\",\"Möchten Sie wirklich {{folder_name}} und seinen Inhalt löschen?\",\"Do you really want to delete {{folder_name}} and its contents?\",\"¿Realmente deseas borrar {{folder_name}} y su contenido?\",\"Voulez-vous vraiment supprimer {{folder_name}} et son contenu ?\",\"Vuoi davvero cancellare la cartella {{folder_name}} e il suo contenuto?\",\"정말 {{folder_name}} 폴더 및 내용을 지우시겠습니까?\",\"\",\"\",\"Czy naprawdę chcesz usunąć {{folder_name}} i jego zawartość?\",\"\"],\"del-proj\":[\"Opravdu chcete smazat {{file_name}}?\",\"Wollen Sie wirklich {{file_name}} löschen?\",\"Do you really want to delete {{file_name}}?\",\"¿Realmente deseas borrar {{file_name}}?\",\"Voulez-vous vraiment supprimer le fichier {{file_name}} ?\",\"Vuoi davvero eliminare il file {{file_name}}?\",\"{{file_name}}을 정말로 삭제하겠습니까?\",\"Ar tikrai norite ištrinti {{file_name}}?\",\"Weet u zeker dat u {{file_name}} wilt verwijderen?\",\"Czy na pewno chcesz usunąć plik {{file_name}}?\",\"Naozaj chcete zmazať {{file_name}}?\"],\"sla-pour-resin\":[\"Nalijte do vaničky dostatečné množství resinu pro vybraný soubor a zavřete víko. Minimální objem resinu je zobrazen na dotykovém displeji tiskárny.\",\"Füllen Sie genügend Harz für die ausgewählte Datei in den Tank und schließen Sie den Deckel. Die minimale Menge des Harzes wird auf dem Touchscreen angezeigt.\",\"Pour enough resin for selected file into the tank and close the lid. Minimal amount of the resin is displayed on the touchscreen.\",\"Vierte suficiente resina para el archivo seleccionado y cierra la tapa.\\\\nLa mínima cantidad necesaria se muestra en la pantalla táctil.\",\"Versez suffisamment de résine dans le réservoir pour le projet sélectionné et fermez le capot.\",\"Versare nel serbatoio la quantità di resina sufficiente per il file selezionato e chiudere il coperchio. La quantità minima di resina viene visualizzata sul touchscreen.\",\"선택한 파일을 위한 레진을 탱크에 붓고 뚜껑을 닫습니다. 터치스크린에 최소량의 레진이 표시됩니다.\",\"Įpilkite pakankamai dervos pasirinktam failui ir uždarykite dangtį. Mažiausias dervos kiekis yra rodomas ant ekrano.\",\"Doe genoeg resin voor het geselcteerde bestand in het vat en sluit de kap. De minimale hoeveelheid resin wordt aangegeven op het touchscreen.\",\"Wlej do zbiornika wystarczającą ilość żywicy dla wybranego pliku i zamknij pokrywę. Minimalna ilość żywicy jest wyświetlana na ekranie dotykowym.\",\"Do nádržky nalejte dostatočné množstvo resinu pre vybraný súbor a zatvorte veko. Minimálne množstvo resinu sa zobrazí na dotykovom displeji.\"],\"cancel\":[\"Chcete opravdu zrušit tisk?\",\"Wollen Sie den Druck wirklich abbrechen?\",\"Do you really want to cancel print?\",\"¿Realmente quieres cancelar la impresión?\",\"Voulez-vous vraiment annuler l\\'impression ?\",\"Vuoi davvero annullare la stampa?\",\"출력을 취소하겠습니까?\",\"Ar tikrai norite atšaukti spausdinimą?\",\"Weet u zeker dat u de print wilt stoppen?\",\"Czy na pewno chcesz anulować wydruk?\",\"Chcete naozaj zrušiť tlač?\"],\"file-exists\":{\"title\":[\"Soubor již existuje\",\"Datei existiert bereits\",\"File already exists\",\"El archivo ya existe\",\"Le fichier existe déjà\",\"File già esistente\",\"파일이 이미 존재합니다.\",\"\",\"\",\"Plik już istnieje\",\"\"],\"overwrite-it\":[\"Chcete ho přepsat?\",\"Möchten Sie sie überschreiben?\",\"Do you want to overwrite it?\",\"¿Quieres sobrescribirlo?\",\"Voulez-vous l\\'écraser ?\",\"Vuoi sovrascriverlo?\",\"덮어쓰시겠습니까?\",\"\",\"\",\"Czy chcesz go nadpisać?\",\"\"]}},\"btn\":{\"login\":[\"Přihlášení\",\"Login\",\"Login\",\"Iniciar sesión\",\"Connexion\",\"Accedi\",\"로그인\",\"Prisijungti\",\"Login\",\"Zaloguj\",\"Prihlásenie\"],\"confirm\":[\"Potvrdit\",\"Bestätigen\",\"Confirm\",\"Confirmar\",\"Confirmer\",\"Conferma\",\"결정\",\"Patvirtinti\",\"Bevestigen\",\"Potwierdzam\",\"Potvrdiť\"],\"cancel\":[\"Zrušit\",\"Abbrechen\",\"Cancel\",\"Cancelar\",\"Annuler\",\"Annulla\",\"취소\",\"Atšaukti\",\"Annuleren\",\"Anuluj\",\"Zrušiť\"],\"del\":[\"Smazat\",\"löschen\",\"Delete\",\"borrar\",\"supprimer\",\"elimina\",\"삭제\",\"Ištrinti\",\"Verwijderen\",\"usuń\",\"Zmazať\"],\"download\":[\"Stáhnout\",\"Herunterladen\",\"Download\",\"Descargar\",\"télécharger\",\"Download\",\"다운로드\",\"Atsisiųsti\",\"Download\",\"Pobierz\",\"Stiahnuť\"],\"pause-pt\":[\"Pozastavit tisk\",\"Druck pausieren\",\"Pause Print\",\"Pausar Impresión\",\"Mettre l\\'impression en pause\",\"Pausa Stampa\",\"출력 일시 정지\",\"Pristabdyti spausdinimą\",\"Pauzeer print\",\"Wstrzymaj drukowanie\",\"Pozastaviť tlač\"],\"resume-pt\":[\"Pokračovat v tisku\",\"Druck fortsetzen\",\"Resume Print\",\"Continuar Impresión\",\"Redémarrer l\\'impression\",\"Riprendi stampa\",\"출력 복귀\",\"Tęsti spausdinimą\",\"Hervat print\",\"Wznów drukowanie\",\"Pokračovať v tlači\"],\"start-pt\":[\"Start tisku\",\"Druck starten\",\"Start Print\",\"Empezar impresión\",\"Lancer l\\'impression\",\"Inizia stampa\",\"출력 시작\",\"Pradėti spausdinimą\",\"Start print\",\"Start druku\",\"Štart tlače\"],\"stop-print\":[\"Zrušit tisk\",\"Druck abbrechen\",\"Stop Print\",\"Cancelar impresión\",\"Annuler l\\'impression\",\"Annulla stampa\",\"출력 정지\",\"Sustabdyti spausdinimą\",\"Stop print\",\"Anuluj wydruk\",\"Zrušiť tlač\"],\"upld-file\":[\"Nahrát soubor\",\"Datei hochladen\",\"Upload File\",\"Cargar Archivo\",\"Télécharger un Fichier\",\"Carica file\",\"파일 업로드\",\"Įkelti failą\",\"Upload bestand\",\"Prześlij plik\",\"Nahrať súbor\"],\"disable-steppers\":[\"Vypnout motory\",\"Schrittmotoren deaktivieren\",\"Disable Steppers\",\"Desactivar motores\",\"Desactiver les moteurs\",\"Disabilita motori\",\"모터 정지\",\"Išjungti stoperius\",\"Stappenmotors uitschakelen\",\"Wyłącz silniki krokowe\",\"Vypnúť motory\"],\"extrude\":[\"Vytlačit\",\"Extrudieren\",\"Extrude\",\"Extruye\",\"extruder\",\"Estrudi\",\"압출\",\"Ištraukti\",\"Extrudeer\",\"Ekstruzja\",\"Vysunúť\"],\"retract\":[\"Vtáhnout\",\"Zurückziehen\",\"Retract\",\"Retraer\",\"Retracter\",\"Retrai\",\"리트렉트\",\"Atitraukti\",\"Retract\",\"Retrakcja\",\"Zasunúť\"],\"set\":[\"Nastavit\",\"Setzen\",\"Set\",\"Ajuste\",\"Régler\",\"Imposta\",\"설정\",\"Nustatyti\",\"Instellen\",\"Ustaw\",\"Nastaviť\"],\"check-updates\":[\"Kontrola aktualizací\",\"Updates suchen\",\"Check Updates\",\"Comprobar Actualizaciones\",\"Vérifier les Mises à Jour\",\"Controlla aggiornamenti\",\"업데이트 체크\",\"Tikrinti atnaujinimus\",\"\",\"Sprawdź aktualizacje\",\"\"],\"connect\":{\"link\":[null,\"Verbinden\",\"Link\",\"Enlace\",\"Associer\",\"Collegamento\",\"연결\",\"Susieti\",\"Verbinden\",\"Link\",\"\"],\"unlink\":[\"Odpojit\",\"Trennen\",\"Unlink\",\"Desenlazar\",\"Dissocier\",\"Scollegato\",\"연결끊기\",\"Atsieti\",\"Verbinding verbreken\",\"Odłącz\",\"\"]},\"chg\":[\"Změnit\",\"Ändern\",\"Change\",\"Cambia\",\"Mettre à jour\",\"Cambia\",\"변경\",\"Pakeisti\",\"Wijzig\",\"Zmień\",\"Zmeniť\"],\"reset\":[\"\",\"Reset\",\"Reset\",\"Reset\",\"Réinitialiser\",\"Reset\",\"리셋\",\"Perkrauti\",\"Reset\",\"Reset\",\"\"],\"yes\":[\"Ano\",\"Ja\",\"Yes\",\"Sí\",\"Oui\",\"Sì\",\"예\",\"Taip\",\"Ja\",\"Tak\",\"Áno\"],\"no\":[\"Ne\",\"Nein\",\"No\",\"No\",\"Non\",\"No\",\"아니오\",\"Ne\",\"Nee\",\"Nie\",\"Nie\"],\"upgrade\":[\"Aktualizovat\",\"Aktualisieren\",\"Upgrade\",\"Actualización\",\"Mise à niveau\",\"Aggiorna\",\"업그레이드\",\"Atnaujinti\",\"\",\"Aktualizacja\",\"\"],\"chg-print-set\":[\"Tisková nastavení\",\"Druckeinstellungen\",\"Print Settings\",\"Ajustes de la Impresora\",\"Réglages d\\'Impression\",\"Impostazioni di stampa\",\"프린트 설정\",\"Spausdinimo nustatymai\",\"Printinstellingen\",\"Ustawienia druku\",\"\"],\"save-chgs\":[\"Uložit změny\",\"Änderungen speichern\",\"Save changes\",\"Guardar cambios\",\"Enregistrer les modifications\",\"Salva le modifiche\",\"변경내용 저장\",\"Išsaugoti pakeitimus\",\"Wijzigingen opslaan\",\"Zapisz zmiany\",\"Uložiť zmeny\"]},\"camera\":{\"settings\":[\"Nastavení kamery\",\"Kamera-Einstellungen\",\"Camera Settings\",\"Ajustes de la Cámara\",\"Paramètres de la Caméra\",\"Impostazioni Fotocamera\",\"카메라 세팅\",\"Kameros nustatymai\",\"Camera-instellingen\",\"Ustawienia kamery\",\"\"],\"name\":[\"Název kamery\",\"Kameraname\",\"Camera Name\",\"Nombre de la Cámara\",\"Nom de la Caméra\",\"Nome Fotocamera\",\"카메라 명칭\",\"Kameros pavadinimas\",\"Cameranaam\",\"Nazwa kamery\",\"\"],\"resolution\":[\"Rozlišení\",\"Auflösung\",\"Resolution\",\"Resolución\",\"Résolution\",\"Risoluzione\",\"해상도\",\"Raiška\",\"Resolutie\",\"Rozdzielczość\",\"\"],\"trigger-scheme\":[\"\",\"Auslöse-Schema\",\"Trigger Scheme\",\"Modo de Disparo\",\"Mode de Déclenchement\",\"Schema di attivazione\",\"트리거 계획\",\"Paleisti schemą\",\"Triggerschema\",\"Schemat wyzwalania\",\"\"],\"focus\":[\"Zaostření\",\"Fokus\",\"Focus\",\"Enfocar\",\"Focus\",\"Messa a fuoco\",\"포커스\",\"\",\"\",\"Ostrość\",\"\"],\"title\":[\"Kamery\",\"Kameras\",\"Cameras\",\"Cámaras\",\"Caméras\",\"Fotocamere\",\"카메라\",\"Kameros\",\"Camera\\'s\",\"Kamery\",\"\"],\"time\":[\"Čas snímku\",\"Zeit der Momentaufnahme\",\"Snapshot Time\",\"Hora de la Foto\",\"Heure de la Capture\",\"Tempo Istantanea\",\"스냅샷 시간\",\"Momentinis vaizdas\",\"Snapshot-tijd\",\"Czas migawki\",\"\"],\"cloud\":{\"linked\":[\"Připojeno\",\"Verbunden\",\"Linked\",\"Enlazado\",\"Associée\",\"Collegato\",\"연결됨\",\"Susieta\",\"Verbonden\",\"Połączono\",\"\"],\"not-linked\":[\"Nepřipojeno\",\"Unverbunden\",\"Not Linked\",\"No Enlazado\",\"Non Associée\",\"Non collegato\",\"연결되지 않음\",\"Nesusieta\",\"Niet verbonden\",\"Nie połączono\",\"\"],\"label\":[\"\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"CONNECT\",\"\"]},\"btn\":{\"connect\":[\"Zkusit povolit kameru\",\"Versuche Kamera zu aktivieren\",\"Try to enable camera\",\"Intenta habilitar la cámara\",\"Essayer d\\'activer la caméra\",\"Prova ad abilitare la fotocamera\",\"카메라 활성화 시도중\",\"Pabandykite įjungti kamerą\",\"Probeer de camera in te schakelen\",\"Spróbuj aktywować kamerę\",\"\"],\"settings\":[\"Otevřít nastavení kamery\",\"Öffne Kamera-Einstellungen\",\"Open camera settings\",\"Abrir los ajustes de la cámara\",\"Ouvrir les paramètres de la caméra\",\"Apri impostazioni fotocamera\",\"카메라 설정 열기\",\"Atidaryti kameros nustatymus\",\"Open camera-instellingen\",\"Otwórz ustawienia kamery\",\"\"],\"link\":[\"Připojit kameru do Connectu\",\"Kamera mit CONNECT verbinden\",\"Link camera to CONNECT\",\"Asocia cámara a CONNECT\",\"Associer la caméra à CONNECT\",\"Collega fotocamera a CONNECT\",\"카메라를 CONNECT에 연결하기\",\"Susieti kamerą su CONNECT\",\"Verbind camera met CONNECT\",\"Połącz kamerę z CONNECT\",\"\"],\"unlink\":[\"Odpojit kameru z Connectu\",\"Kamera von CONNECT trennen\",\"Unlink camera from CONNECT\",\"Desvincular la cámara de CONNECT\",\"Dissocier la caméra de CONNECT\",\"Scollega fotocamera da CONNECT\",\"카메라를 CONNECT에서 연결 해제하기\",\"Atsieti kamerą nuo CONNECT\",\"Verbreek verbinding van camera met CONNECT\",\"Odłącz kamerę od CONNECT\",\"\"]},\"path\":[\"Cesta\",\"Pfad\",\"Path\",\"Ruta\",\"Chemin\",\"Percorso\",\"경로\",\"Kelias\",\"Pad\",\"Ścieżka\",\"\"],\"driver\":[\"Ovladač\",\"Treiber\",\"Driver\",\"Controlador\",\"Driver\",\"Driver\",\"드라이버\",\"Draiveris\",\"Driver\",\"Sterownik\",\"\"]},\"printer\":{\"title\":[\"Tiskárna\",\"Drucker\",\"Printer\",\"Impresora\",\"imprimante\",\"Stampante\",\"프린터\",\"Spausdintuvas\",\"Printer\",\"Drukarka\",\"Tlačiareň\"],\"name\":[\"Název tiskárny\",\"Name des Druckers\",\"Printer name\",\"Nombre de la impresora\",\"nom de l\\'imprimante\",\"Nome stampante\",\"프린터 이름\",\"Spausdintuvo pavadinimas\",\"Naam van de printer\",\"Nazwa drukarki\",\"Názov tlačiarne\"],\"location\":[\"Umístění tiskárny\",\"Standort des Druckers\",\"Printer location\",\"Ubicación de la impresora\",\"localisation de l\\'imprimante\",\"Posizione stampante\",\"프린터 위치\",\"Spausdintuvo vieta\",\"Locatie van de printer\",\"Lokalizacja drukarki\",\"Umiestnenie tlačiarne\"],\"network_error_chime\":[\"Zvukové upozornění při síťové chybě\",\"Netzwerk-Fehlerton\",\"Network Error Chime\",\"Tono Error Red\",\"Carillon d\\'erreur réseau\",\"Suono di errore di rete\",\"네트워크 에러 차임벨\",\"\",\"\",\"Dźwięk błędu sieci\",\"\"]},\"print\":{\"fdm\":{\"1\":[\"Je tiskárna připravena?\",\"Ist der Drucker bereit?\",\"Is the printer ready?\",\"¿Está lista la impresora?\",\"L\\'imprimante est-elle prête ?\",\"La stampante è pronta?\",\"프린터가 준비되었습니까?\",\"Ar spausdintuvas pasirengęs?\",\"Is de printer klaar voor gebruik?\",\"Czy drukarka jest gotowa?\",\"Je tlačiareň pripravená?\"],\"2\":[\"Je tisková podložka prázdná a čistá?\",\"Ist der Druckbogen leer und sauber?\",\"Is printing sheet empty and clean?\",\"¿Está vacía y limpia la lámina de impresión?\",\"Le surface d\\'impression est-elle vide et propre ?\",\"La piastra di stampa è vuota e pulita?\",\"프린트 시트가 비어있고 깨끗합니까?\",\"Ar spausdinimo lapas tuščias ir švarus?\",\"Is het printplatform leeg en schoon?\",\"Czy arkusz druku jest pusty i czysty?\",\"Je tlačová podložka prázdna a čistá?\"]}},\"upld\":{\"title\":[\"Nahrát soubor\",\"Projekt hochladen\",\"Upload file\",\"envía proyecto\",\"Téléversement de projet\",\"Carica file\",\"파일업로드\",\"Įkelti failą\",\"Bestand uploaden\",\"prześlij projekt\",\"Nahrať súbor\"],\"start-pt\":[\"po přenosu zahájit tisk\",\"Druck nach der Übertragung starten\",\"Start print after transfer\",\"Iniciar la impresión tras la transferencia\",\"Démarrer l\\'impression après le transfert\",\"Avvia stampa dopo il trasferimento\",\"전송 후 출력 시작\",\"Pradėti spausdinimą po perdavimo\",\"Start print na het omzetten\",\"Rozpocznij drukowanie po przesłaniu\",\"po prenose spustiť tlač\"],\"remote\":{\"source\":[\"URL zdroje\",\"Quell-URL\",\"Source URL\",\"URL Fuente\",\"URL de la source\",\"URL fonte\",\"원본 URL\",\"Šaltinio URL\",\"Bron van URL\",\"Źródłowy URL\",\"URL zdroja\"],\"hint-fdm\":[\"Zadejte URL souboru G-CODE\",\"URL der G-CODE-Datei eingeben\",\"Type URL of G-CODE file\",\"Escribe URL de archivo G-CODE\",\"Entrez l\\'URL du fichier G-CODE du projet\",\"Digitare URL del file G-CODE\",\"G코드 파일의 주소를 입력하십시오\",\"Įveskite G-CODE failo URL\",\"Type de URL van het gcode-bestand\",\"Wpisz adres URL pliku G-Code\",\"Zadajte URL súboru G-code\"],\"file\":[\"Název souboru\",\"Dateiname\",\"File name\",\"Nombre del archivo\",\"Nom de fichier\",\"Nome file\",\"파일명\",\"Failo pavadinimas\",\"Bestandsnaam\",\"Nazwa pliku\",\"Názov súboru\"],\"file-hint\":[\"Zadejte název souboru\",\"Dateiname eingeben\",\"Type file name\",\"Escribe nombre de archivo\",\"Tapez le nom du fichier\",\"Digitare nome file\",\"파일명을 입력하십시오\",\"Įveskite failo pavadinimą\",\"Type de bestandsnaam\",\"Wpisz nazwę pliku\",\"Zadajte názov súboru\"]},\"direct\":{\"choose\":[\"Vyberte {{file}} nebo jej přetáhněte sem.\",\"Klicke um eine {{file}} Datei auszuwählen oder ziehe sie hier hin\",\"Click to choose a {{file}} file or drag it here\",\"Haz clic para elegir un archivo {{file}} o arrástralo hasta aquí\",\"Cliquer pour choisir un fichier {{file}} ou déposez le ici\",\"Fare clic per scegliere un file {{file}} o trascinarlo qui\",\"선택한 {{file}} 파일을 클릭하거나 여기로 드래그하십시오\",\"Pasirinkite {{file}} failą arba vilkite jį čia\",\"Klik om een {{file}}-bestand te kiezen of sleep het hierheen\",\"Kliknij, aby wybrać plik {{file}} lub przeciągnij go tutaj\",\"Kliknutím vyberte súbor {{file}} alebo ho sem potiahnite.\"]}},\"download\":{\"dl-started\":[\"Začátek stahování\",\"Herunterladen Gestartet\",\"Download Started\",\"Se inició la descarga\",\"téléchargement démarré\",\"Scaricamento Avviato\",\"다운로드가 시작됨\",\"Atsisiuntimas pradėtas\",\"Downloaden gestart\",\"Pobieranie rozpoczęte\",\"Sťahovanie začalo\"],\"start-pt\":[\"Spustit tisk\",\"Autostart\",\"Autostart\",\"Inicio automático\",\"Démarrage automatique\",\"Avvio automatico\",\"자동시작\",\"Automatinis paleidimas\",\"Autostart\",\"Autostart\",\"Spustiť tlač\"]},\"temps\":{\"title\":[\"Teploty\",\"Temperaturen\",\"Temperatures\",\"Temperaturas\",\"Températures\",\"Temperature\",\"온도\",\"Temperatūros\",\"Temperatuur\",\"Temperatury\",\"Teploty\"]},\"\":[null,null,null,null,null,null,null,null,null,null,null],\"version\":{\"title\":[\"Verze\",\"Version\",\"Version\",null,\"version\",null,\"버전\",\"Versija\",\"Versie\",null,\"Verzia\"],\"api\":[\"API\",\"API\",\"API\",null,\"api\",null,\"API\",\"API\",\"API\",null,\"API\"],\"hostname\":[\"Hostname\",\"Hostname\",\"Hostname\",null,\"nom d\\'hôte\",null,\"호스트명\",\"Hostname\",\"Hostnaam\",null,\"Hostname\"],\"firmware\":[\"Firmware\",\"Firmware\",\"Firmware\",null,\"micrologiciel\",null,\"펌웨어\",\"Firmware\",\"Firmware\",null,\"Firmware\"],\"server\":[\"Server\",\"Server\",\"Server\",null,\"serveur\",null,\"서버\",\"Serveris\",\"Server\",null,\"Server\"],\"text\":[\"Text\",\"Text\",\"Text\",null,\"texte\",null,\"텍스트\",\"Tekstas\",\"Tekst\",null,\"Text\"],\"sdk\":[\"SDK\",\"SDK\",\"SDK\",null,\"sdk\",null,\"SDK\",\"SDK\",\"SDK\",null,\"SDK\"],\"fe\":[\"Frontend\",\"Frontend\",\"Frontend\",null,\"frontend\",null,\"프론트엔드\",\"Frontendas\",\"Frontend\",null,\"Frontend\"]},\"sys-version\":{\"title\":[\"Verze systému\",\"Systemversion\",\"System Version\",null,\"version du système\",null,\"시스템버전\",\"Sistemos versija\",\"Systeenversie\",null,\"Verzia systému\"],\"python\":[\"Python\",\"Python\",\"Python\",\"Python\",\"python\",\"Python\",\"파이썬\",\"Python\",\"Python\",\"Python\",\"Python\"],\"description\":[\"Popis\",\"Beschreibung\",\"Description\",\"Descripción\",\"description\",\"Descrizione\",\"설명\",\"Aprašymas\",\"Beschrijving\",\"Opis\",\"Popis\"],\"id\":[\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\",\"ID\"],\"os\":[\"OS\",\"OS\",\"OS\",\"OS\",\"Système d\\'exploitation\",\"S.O.\",\"OS\",\"OS\",\"OS\",\"OS\",\"OS\"]},\"updates\":{\"title\":[null,null,\"System Updates\",null,null,null,null,\"Sistemos atnaujinimai\",null,null,null]},\"user\":{\"title\":[\"Uživatel\",\"Benutzer\",\"User\",null,\"utilisateur\",null,\"유저\",\"Vartotojas\",\"Gebruiker\",null,\"Používateľ\"],\"username\":[\"Uživatelské jméno\",\"Benutzername\",\"User Name\",\"Nombre de usuario\",\"nom d\\'utilisateur\",\"Nome Utente\",\"유저명\",\"Vartotojo vardas\",\"Gebruikersnaam\",\"Nazwa użytkownika\",\"Používateľské meno\"],\"format\":{\"name\":[\"Uživatelské jméno musí obsahovat alespoň 3 znaky.\",\"Der Benutzername muss mindestens 3 Zeichen lang sein.\",\"Username length must be at least 3 characters long.\",\"El nombre de usuario debe tener al menos 3 caracteres.\",\"La longueur du nom d\\'utilisateur doit être d\\'au moins 3 caractères.\",\"La lunghezza del nome utente deve essere di almeno 3 caratteri.\",\"유저명은 최소 3자리 이상 입력하십시오.\",\"Vartotojo vardo ilgis turi būti mažiausiai 3 simboliai.\",\"De gebruikersnaam moet minimaal 3 karakters lang zijn.\",\"Długość nazwy użytkownika musi mieć co najmniej 3 znaki.\",\"Používateľské meno musí obsahovať aspoň 3 znaky.\"],\"password-1\":[\"Heslo nesmí začínat či končit mezerami, a zároveň musí splňovat jednu z následujících podmínek:\",\"Das Passwort darf weder am Anfang noch am Ende Leerzeichen enthalten und muss mindestens eine der folgenden Optionen erfüllen:\",\"Password can\\'t contain spaces on the beggining nor the end and must meet at least one of these options:\",\"La Contraseña no puede empezar o terminar con espacios y debe cumplir al menos una de estas opciones:\",\"Le mot de passe ne peut pas contenir d\\'espaces au début ni à la fin et doit répondre à au moins l\\'une de ces options :\",\"La password non può contenere spazi né all\\'inizio né alla fine e deve soddisfare almeno una di queste opzioni:\",\"암호는 시작부분에 공백을 둘 수 없으며 다음옵션 중 하나이상을 충족해야 합니다.\",\"Slaptažodis negali turėti tarpų pradžioje arba pabaigoje ir turi atitikti bent vieną iš šių variantų:\",\"Het wachtwoord mag geen spaties aan het begin of het einde bevatten en moet aan minstens één van deze opties voldoen:\",\"Hasło nie może zawierać spacji na początku ani na końcu i musi spełniać przynajmniej jeden z poniższych warunków:\",\"Heslo nesmie začínať ani končiť medzerami a musí spĺňať aspoň jednu z týchto podmienok:\"],\"password-2\":[\"- Heslo musí obsahovat minimálně 8 znaků, zahrnujících alespoň 1 malé písmeno, 1 velké písmeno a 1 číslo\",\"- Minimale Länge 8 Zeichen, davon ein Kleinbuchstabe, ein Großbuchstabe und eine Zahl\",\"- Minimal length 8 characters, including one lowercase letter, one uppercase letter and one number\",\"- Longitud mínima 8 caracteres, incluyendo una minúscula, una mayúscula y un número\",\"- Longueur minimale de 8 caractères, dont une lettre minuscule, une lettre majuscule et un chiffre\",\"- Lunghezza minima 8 caratteri, di cui una lettera minuscola, una lettera maiuscola e un numero\",\"- 최소 길이 8자(소문자 1개, 대문자 1개, 숫자 1개 포함)\",\"- 8 simbolių ilgio minimalus, įskaitant vieną mažąją raidę, vieną didžiąją raidę ir vieną skaičių\",\"- Minimaal 8 karakters lang, met tenminste een kleine letter, een hoofdletter en een nummber\",\"- Minimalna długość 8 znaków, w tym jedna mała litera, jedna duża litera i jedna cyfra\",\"- Heslo musí obsahovať minimálne 8 znakov, vrátane aspoň 1 malého písmena, 1 veľkého písmena a 1 číslice\"],\"password-3\":[\"- Heslo musí obsahovat minimálně 8 znaků, zahrnujících alespoň 1 speciální symbol (např. @)\",\"- Mindestlänge 8 Zeichen, einschließlich eines nicht alphanumerischen Zeichens (z. B. @)\",\"- Minimal length 8 characters, including one non-alphanumeric character (e.g. @)\",\"- Longitud mínima 8 caracteres, incluyendo los no alfanuméricos (como @)\",\"- Longueur minimale de 8 caractères, dont un caractère non alphanumérique (par exemple @)\",\"- Lunghezza minima 8 caratteri, compreso un carattere non alfanumerico (es. @)\",\"- 영숫자가 아닌 문자(예: @) 1개를 포함한 최소 길이 8자\",\"- 8 simbolių ilgio minimalus, įskaitant vieną neteisingą simbolį (pvz., @)\",\"- Minimaal 8 karakters lang, met tenminste één niet-alfanumeriek karakter (bijv. @)\",\"- Minimalna długość 8 znaków, w tym jeden znak niealfanumeryczny (np. @)\",\"- Heslo musí obsahovať minimálne 8 znakov, vrátane aspoň 1 špeciálneho symbolu (napr. @)\"],\"password-4\":[\"- Heslo musí obsahovat minimálně 15 znaků\",\"- Minimale Länge 15 Zeichen\",\"- Minimal length 15 characters\",\"- Longitud mínima 15 caracteres\",\"- Longueur minimale 15 caractères\",\"- Lunghezza minima 15 caratteri\",\"- 최소 길이 15자\",\"- 15 simbolių ilgio minimalus\",\"- Minimaal 15 karakters lang\",\"- Minimalna długość 15 znaków\",\"- Heslo musí obsahovať minimálne 15 znakov\"]},\"new-password\":[\"Nové heslo\",\"Neues Passwort\",\"New Password\",\"Nueva Contraseña\",\"nouveau mot de passe\",\"Nuova Password\",\"신규 암호\",\"Naujas slaptažodis\",\"Nieuw wachtwoord\",\"Nowe hasło\",\"Nové heslo\"],\"re-password\":[\"Zopakujte heslo\",\"Passwort wiederholen\",\"Repeat Password\",\"Repetir Contraseña\",\"répéter le mot de passe\",\"Ripeti Password\",\"암호 재입력\",\"Pakartokite slaptažodį\",\"Herhaal wachtwoord\",\"Powtórz hasło\",\"Zopakujte heslo\"],\"password\":[\"Heslo\",\"Passwort\",\"Password\",\"Contraseña\",\"mot de passe\",\"Password\",\"암호\",\"Slaptažodis\",\"Wachtwoord\",\"Hasło\",\"Heslo\"]},\"serial\":{\"label\":[\"Sériové číslo\",\"Seriennummer\",\"Serial Number\",\"Número de serie\",\"numéro de série\",\"Numero Seriale\",\"시리얼넘버\",\"Serijinis numeris\",\"Serienummer\",\"Numer seryjny\",\"Sériové číslo\"]},\"api_key\":{\"label\":[\"\",\"API Key\",\"API Key\",\"Clave API\",\"Clé API\",\"Chiave API\",\"API KEY\",\"API raktas\",\"API-sleutel\",\"Klucz API\",\"\"]},\"logs\":{\"title\":[\"Logy\",\"Protokolle\",\"Logs\",null,\"logs\",null,\"로그\",\"Žurnalai\",\"Logbestanden\",null,\"Logy\"],\"select-file\":[\"Vyberte soubor\",\"Datei auswählen\",\"Select File\",null,\"Sélectionnez une fichier\",null,\"파일 선택\",\"Pasirinkti failą\",\"Selecteer bestand\",null,\"Vyberte súbor\"],\"not-selected\":[\"Není vybrán soubor logů!\",\"Es ist keine Protokolldatei ausgewählt!\",\"No log file is selected!\",\"¡No se ha seleccionado archivo de registro!\",\"Pas de fichier de log sélectionné!\",\"Nessun file di registro selezionato!\",\"로그 파일이 선택되지 않았습니다!\",\"Nepasirinktas joks žurnalų failas!\",\"Geen logbestand geselecteerd!\",\"Nie wybrano żadnego pliku logu!\",\"Nie je vybraný žiadny logovací súbor!\"],\"select-file-placeholder\":[\"Vyberte soubor logů\",\"Protokolldatei auswählen\",\"Select log file\",\"Selecciona archivo de registro\",\"selectionnez un fichier de log\",\"Selezionare un file di registro\",\"로그 파일 선택\",\"Pasirinkti žurnalų failą\",\"Selecteer logbestand\",\"Wybierz plik logu\",\"Vyberte logovací súbor\"],\"empty-file\":[\"Soubor je prázdný!\",\"Die Protokolldatei ist leer!\",\"Log file is empty!\",\"¡El archivo de registro está vacío!\",\"Le fichier de log est vide!\",\"Il file di registro è vuoto!\",\"로그파일이 비어있음!\",\"Žurnalų failas yra tuščias!\",\"Logbestand is leeg!\",\"Plik logu jest pusty!\",\"Logovací súbor je prázdny!\"],\"file-size-unknown\":[\"\",\"Die Protokolldatei hat eine unbekannte Größe und ist daher nur zum Herunterladen verfügbar\",\"The log file is of an unknown size and therefore is available only for downloading\",\"El archivo de registro es de tamaño desconocido y, por lo tanto, solo está disponible para su descarga.\",\"Le fichier de journal est d\\'une taille inconnue et n\\'est donc disponible que pour le téléchargement\",\"Il file di log è di dimensioni sconosciute e quindi è disponibile solo per il download.\",\"로그파일의 크기를 알 수 없으므로 다운로드만 가능합니다.\",\"\",\"\",\"Plik logu ma nieznany rozmiar i dlatego jest dostępny tylko do pobrania\",\"\"],\"file-too-large\":[\"Soubory logů o velikosti větší než {{size}} jsou k dispozici pouze ke stažení!\",\"Protokolldateien, die größer als {{size}} sind, stehen nur zum Herunterladen zur Verfügung.\",\"Log files of size larger than {{size}} are available only for downloading.\",\"Los archivos de registro mayores de {{size}} sólo se pueden descargar.\",\"Les fichiers de log d\\'une taille supérieure à {{size}} ne sont disponibles que pour le téléchargement.\",\"I file di registro di dimensioni superiori a {{size}} sono disponibili solo per il download.\",\"로그 파일을 다운로드 하기 위해서는 최소 {{size}} 이상의 용량이 필요합니다.\",\"Žurnalų failai, didesni nei {{size}}, yra prieinami tik atsiunčiant.\",\"Logbestanden groter dan {{size}} kunnen alleen gedownload worden.\",\"Pliki logów o rozmiarze większym niż {{size}} są dostępne tylko do pobrania.\",\"Logovacie súbory s veľkosťou väčšou ako {{size}} sú k dispozícii len na stiahnutie.!\"]},\"ntf\":{\"success\":[\"Požadavek byl úspěšný\",\"Erfolg\",\"The request was successful\",\"Éxito\",\"la demande a été traitée avec succès\",\"Successo\",\"요청이 성공했습니다.\",\"Užklausa buvo sėkminga\",\"De aanvraag was succesvol\",\"Sukces\",\"Žiadosť bola úspešná\"],\"settings-suc\":[\"Nastavení bylo úspěšně změněno.\",\"Die Einstellungen wurden erfolgreich geändert.\",\"Settings was changed successfully.\",\"Los ajustes se han cambiado correctamente.\",\"Paramètres modifiés avec succès.\",\"Impostazioni modificate correttamente\",\"설정이 성공적으로 변경되었습니다.\",\"Nustatymai sėkmingai pakeisti.\",\"Instelling succesvol gewijzigd.\",\"Ustawienia zostały zmienione pomyślnie\",\"Nastavenia boli úspešne zmenené.\"],\"camera-suc\":[\"\",\"Kamera Steuerungsanfrage wurde gesendet\",\"Camera control request has been sent\",\"La petición de control de cámara se ha enviado\",\"La demande de contrôle de la caméra a été envoyée\",\"È stata inviata una richiesta di controllo della fotocamera\",\"카메라 조작 요청이 전송되었습니다.\",\"Kameros valdymo užklausa buvo išsiųsta\",\"Aanvraag voor camerabesturing is verzonden\",\"Wysłano żądanie sterowania kamerą\",\"\"],\"camera-config-success\":[\"Konfigurace kamery byla aktualizovaná\",\"Kamerakonfiguration wurde aktualisiert\",\"Camera configuration has been updated\",\"La configuración de la cámara se ha actualizado\",\"La configuration de la caméra a été mise à jour\",\"La configurazione della fotocamera è stata aggiornata\",\"카메라 구성이 업데이트되었습니다.\",\"Kameros konfigūracija buvo atnaujinta\",\"Cameraconfiguration is geüpdatet\",\"Konfiguracja kamery została zaktualizowana\",\"\"],\"calibration-error\":[\"\",\"Kalibrierungsfehler\",\"Calibration Error\",\"Error de calibración\",\"Erreur de Calibration\",\"Errore di calibrazione\",\"캘리브레이션 에러\",\"\",\"\",\"Błąd kalibracji\",\"\"],\"n-calibrated\":[\"Tiskárna není zkalibrovaná!\",\"Der Drucker ist nicht kalibriert!\",\"Printer is not calibrated!\",\"¡Impresora no calibrada!\",\"L\\'imprimante n\\'est pas calibrée !\",\"La stampante non è calibrata!\",\"프린터가 보정되지 않았습니다!\",\"Spausdintuvas nėra sukalibruotas!\",\"Printer niet gekalibreerd!\",\"Drukarka nie jest skalibrowana!\",\"Tlačiareň nie je kalibrovaná!\"],\"low-resin\":{\"title\":[\"\",\"Harz niedrig\",\"Resin low\",\"Poca resina\",\"Niveau de résine bas\",\"Resina bassa\",\"레진 부족함\",\"\",\"\",\"Niski poziom żywicy\",\"\"],\"message\":[\"\",\"Die gemessene Harzmenge ist zu gering. Der Druck kann fortgesetzt werden, es kann jedoch ein Nachfüllen erforderlich sein.\",\"Measured resin volume is too low. The print can continue, however, a refill might be required.\",\"El volumen de resina medido es demasiado bajo. La impresión puede continuar, sin embargo, es posible que se requiera una recarga.\",\"Le volume de résine mesuré est trop bas. L\\'impression peut continuer, mais un remplissage pourra être nécessaire.\",\"Il volume di resina misurato è troppo basso. La stampa può continuare, ma potrebbe essere necessaria una ricarica.\",\"측정된 레진양이 적습니다. 인쇄는 계속할 수 있지만 리필이 필요할 수는 있습니다.\",\"\",\"\",\"Zmierzona ilość żywicy jest zbyt niska. Drukowanie można kontynuować, jednak konieczne może być uzupełnienie w trakcie.\",\"\"]},\"upld-suc\":[\"Soubor {{file_name}} byl úspěšně nahrán.\",\"Das Hochladen des Projekts {{file_name}} war erfolgreich.\",\"The file {{file_name}} was uploaded successfully.\",\"El envío del proyecto {{file_name}} se completó.\",\"Le projet {{file_name}} a été téléversé correctement.\",\"Caricamento del progetto {{file_name}} riuscito.\",\"{{file_name}}파일이 성공적으로 업로드되었습니다.\",\"Failas {{file_name}} sėkmingai įkeltas.\",\"Het bestand {{file_name}} is succesvol geüpload.\",\"Przesyłanie projektu {{file_name}} powiodło się.\",\"Súbor {{file_name}} bol úspešne nahraný.\"],\"error\":[\"Chyba\",\"Fehler\",\"Error\",\"Error\",\"Erreur\",\"Errore\",\"에러\",\"Klaida\",\"Fout\",\"Błąd\",\"Chyba\"],\"upld-unsuc\":[\"Nahrání souboru {{file_name}} selhalo.\",\"Das Hochladen des Projekts {{file_name}} war nicht erfolgreich.\",\"The file {{file_name}} upload was unsuccessful.\",\"El envío del proyecto {{file_name}} ha fallado.\",\"Le téléversement du projet {{file_name}} a échoué.\",\"Caricamento del progetto {{file_name}} non riuscito.\",\"{{file_name}}파일의 업로드가 실패하였습니다.\",\"Failo {{file_name}} įkėlimas nepavyko.\",\"Het bestand {{file_name}} is niet succesvol geüpload.\",\"Przesyłanie projektu {{file_name}} nie powiodło się.\",\"Nahrávanie súboru {{file_name}} zlyhalo.\"],\"upld-start\":[\"Požadavek na nahrání souboru byl přijat\",\"Upload-Anfrage wurde angenommen\",\"Upload request has been accepted\",\"Se acepto la petición de subida\",\"La demande de téléchargement a été acceptée\",\"Richiesta di caricamento accettata\",\"업로드 요청이 수락되었습니다.\",\"Įkėlimo užklausa priimta\",\"Uploadverzoek is geaccepteerd\",\"Żądanie przesłania zostało zaakceptowane\",\"Žiadosť o odoslanie bola prijatá\"]},\"sort\":{\"by-name\":[\"Název\",\"Name\",\"Name\",\"Nombre\",\"Nom\",\"Nome\",\"이름으로 정렬하기\",\"Pavadinimas\",\"Naam\",\"Nazwa\",\"\"],\"by-date\":[\"Datum\",\"Datum\",\"Date\",\"Fecha\",\"Date\",\"Data\",\"날짜로 정렬하기\",\"Data\",\"Datum\",\"Data\",\"\"],\"by-size\":[\"Velikost\",\"Größe\",\"Size\",\"Tamaño\",\"Taille\",\"Dimensioni\",\"크기로 정렬하기\",\"Dydis\",\"Grootte\",\"Rozmiar\",\"\"]},\"unit\":{\"h\":[\"h\",\"h\",\"h\",\"h\",\"h\",\"h\",\"h\",\"h\",\"u\",\"g\",\"h\"],\"min\":[\"min\",\"min\",\"min\",\"min\",\"min\",\"min\",\"min\",\"min\",\"min\",\"min\",\"min\"],\"ml\":[\"ml\",\"ml\",\"ml\",\"ml\",\"mL\",\"ml\",\"ml\",\"ml\",\"ml\",\"ml\",\"ml\"],\"rpm\":[\"RPM\",\"UPM\",\"RPM\",\"RPM\",\"TPM\",\"RPM\",\"RPM\",\"RPM\",\"RPM\",\"Obr./min\",\"RPM\"],\"b\":[\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\",\"B\"],\"kb\":[\"KB\",\"KB\",\"KB\",\"KB\",\"KB\",\"KB\",\"KB\",\"KB\",\"kB\",\"KB\",\"KB\"],\"mb\":[\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\",\"MB\"],\"gb\":[\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\",\"GB\"]},\"exp-times\":{\"exp-time\":[\"Doba osvitu [s]\",\"Belichtung [s]\",\"Exposure [s]\",\"Exposición [s]\",\"Exposition [s]\",\"Esposizione [s]\",\"[s]초 노출\",\"Ekspozicija [s]\",\"Belichting [s]\",\"Czas naświetlania [s]\",\"Osvit [s]\"],\"inc\":[\"Navýšení expozice (s)\",\"Belichtungszeit inkr. [s]\",\"Exposure time incr. [s]\",\"Incremento tiempo exp. [s]\",\"Incr. du temps d\\'exposition [s]\",\"Incremento tempo di esposizione [s]\",\"[s]초 노출 시간 증가\",\"Ekspozicijos laiko padidėjimas [s]\",\"Belichtingstijd verhogen [s]\",\"Przyrost czasu naświetl. [s]\",\"Prírastok osvitu (s)\"],\"layer-1st\":[\"Osvit první vrstvy [s]\",\"Erste Schicht Bel. [s]\",\"First Layer Expo. [s]\",\"Primera Capa Expo. [s]\",\"Première couche Expo. [s]\",\"Esposizione Primo layer [s]\",\"첫번째 레이어 노출시간 [s]초\",\"Pirmojo sluoksnio ekspozicija [s]\",\"Belichtingstijd eerste laag [s]\",\"Naśw. 1. warstwy [s]\",\"Osvit prvej vrstvy [s]\"],\"profile\":[\"Tiskový profil\",\"Druckprofil\",\"Print profile\",\"Perfil de Impresión\",\"Profil d\\'Impression\",\"Profilo di stampa\",\"출력 프로필\",\"Spausdinimo profilis\",\"Printprofielen\",\"Profil druku\",\"Tlačový profil\"],\"faster\":[\"Rychlejší\",\"Schneller\",\"Faster\",\"Rápido\",\"Plus rapide\",\"Più veloce\",\"빠르게\",\"Greičiau\",\"Sneller\",\"Szybciej\",\"Rýchlejšie\"],\"slower\":[\"Pomalejší\",\"Langsamer\",\"Slower\",\"Lento\",\"Plus lent\",\"Più lento\",\"느리게\",\"Lėčiau\",\"Langzamer\",\"Wolniej\",\"Pomalšie\"],\"high-viscosity\":[\"Velmi Viskózní\",\"Hohe Viskosität\",\"High Viscosity\",\"Alta Viscosidad\",\"Haute viscosité\",\"Alta Viscosità\",\"고점도\",\"Aukštas klampumas\",\"Hoge viscositeit\",\"Wysoka lepkość\",\"Vysoká viskozita\"]}}}')}},t={};function a(i){var s=t[i];if(void 0!==s)return s.exports;var n=t[i]={exports:{}};return e[i](n,n.exports,a),n.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var i in t)a.o(t,i)&&!a.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),a.r=e=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},a.p=\"\",(()=>{\"use strict\";var e=a(8751),t=a(3532),i=a(6648);const s=()=>{document.getElementById(\"menu\").addEventListener(\"click\",(()=>{document.getElementById(\"menu\").classList.contains(\"burger-open\")?n():(document.getElementById(\"menu\").classList.add(\"burger-open\"),document.getElementById(\"navbar\").classList.remove(\"burger-menu\"))})),document.getElementById(\"navbar\").querySelectorAll(\"a[href]\").forEach((e=>{e.addEventListener(\"click\",n)}))};function n(){document.getElementById(\"menu\").classList.remove(\"burger-open\"),document.getElementById(\"navbar\").classList.add(\"burger-menu\")}var r=a(7780),o=a(1351),l=a(5412);const d=(e,t)=>{const a=l.L.init(e,t);a&&(a.setOptions((0,r.Vb)()),a.value=(0,r.G3)(),a.onselect=e=>{(0,r.m0)(e),window.location.reload()})};let c=!1;const u={status:{get:()=>(0,i.LK)(\"/api/v1/status\"),init:!0,update:!0}};async function p(e){const t=(new Date).getTime(),a=Object.fromEntries(Object.entries(u).map((([a,i])=>[a,(()=>{if(!e)return i.init;if(i.update){if(!i.updateInterval)return!0;if(i.timestamp||(i.timestamp=t+i.updateInterval),t>=i.timestamp)return i.timestamp=t+i.updateInterval,!0}})()?i.get():void 0])).filter((([,e])=>void 0!==e))),i=Object.values(a),s=await Promise.all(i.map((e=>e.then((e=>({ok:!0,payload:e}))).catch((e=>({ok:!e.code&&null,error:e}))))));return Object.fromEntries(Object.entries(a).map((([e],t)=>[e,s[t]])))}async function m(e){let a=!1;for(;;){let i=!1;try{const t=await p(a);t.status&&(c=null===t.status.ok),Object.values(t).forEach((({ok:e,error:t})=>{var a;e||(i=!0,null!==e&&(a=t,(0,o.S)(a,{fallbackMessage:{title:\"API error\",message:\"Cannot connect to printer\"}})))})),a?g(t):i||(v({...t,printer:e}),a=!0)}catch(e){h(e)}t.Z.setConnected(!c),await new Promise((e=>setTimeout(e,1e3)))}}function v(a){try{t.Z.init(a),window.onpopstate=t=>t&&(0,e.g9)(t.currentTarget.location.hash||\"#dashboard\"),(0,e.g9)(window.location.hash||\"#dashboard\")}catch(e){h(e)}}function g(e){try{t.Z.update(e)}catch(e){h(e)}}function h(e){(0,o.S)(e,{fallbackMessage:{title:\"Application error\",message:\"Something bad happened on application side\"}}),console.error(e)}window.onload=()=>{console.log(\"PrusaLink v.3.12.0 #b'b2c9fe5'\"),s(),d(\"lang-dropdown\",\"lang-dropdown\"),(0,r.ot)(),document.querySelectorAll(\"a[href]\").forEach((t=>{t.addEventListener(\"click\",(a=>{(0,e.c4)(t.href)&&a.preventDefault()}))})),(0,i.Z5)().then((e=>{e&&m(e)}))}})()})();\n"
  },
  {
    "path": "prusa/link/static/main.b3e029296dd89863b3f2.css",
    "content": "@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;transform:scale3d(.3,.3,.3)}to{opacity:0}}@-webkit-keyframes slideInUp{0%{transform:translate3d(0,100%,0);visibility:visible}to{transform:translate3d(0,0,0)}}@keyframes slideInUp{0%{transform:translate3d(0,100%,0);visibility:visible}to{transform:translate3d(0,0,0)}}html{font-size:32pt;font-family:Helvetica,sans-serif;overflow:-moz-scrollbars-vertical;overflow-y:scroll}@media only screen and (min-width:992px){html{font-size:14pt}}select{font-family:Helvetica,sans-serif}.txt-bold,select{font-weight:700}.txt-italic{font-style:italic}.uppercase{text-transform:uppercase}.txt-orange{color:#fa6831}.txt-grey{color:#707070}.txt-black{color:#000!important}.txt-lg{font-size:1.25rem}.txt-md,button>p,nav{font-size:1rem}.txt-sm{font-size:.75rem}.txt-center{text-align:center}button>p{font-family:Helvetica,sans-serif;font-weight:700;width:100%;margin:6px}.flex-row{display:flex}.flex-col,.tel-prop>div{display:flex;flex-direction:column}.grow{flex-grow:1}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.mr-md{margin-right:12px}.ml-md{margin-left:12px}.my-md{margin-top:12px;margin-bottom:12px}.rounded{border-radius:50%}button.rounded{padding:0;margin:0}button.rounded img{padding:4px;margin:0}button.rounded:hover{background-color:#5b5b5b}button,html,nav>ul a,select{color:#efefef}#upld-direct p,.progress>p,nav>ul a{text-align:left}html{line-height:1.5;-webkit-text-size-adjust:100%}.node-btn-list>li .icon-small,body,p{margin:0}body,body>.header{background-color:#0a0a0a}a,button.rounded{background-color:transparent}a:not([href]){cursor:pointer}a:not([href]):hover{text-decoration:underline}img{border-style:none;height:auto;width:100%;max-width:100%}[hidden]{display:none!important}.icon{width:48px;height:48px;min-width:48px}.icon-small{width:32px;height:32px;min-width:32px;margin-right:4px}button>img{width:36px;height:36px;min-width:36px;margin:2px}button,input{font-family:inherit;font-size:100%;line-height:1.15;margin:0;overflow:visible}button::-moz-focus-inner{border-style:none;padding:0}.header{position:relative}.header__line{position:absolute;bottom:0;width:100%;height:2px;background-color:#707070;z-index:4}#title,.component>p,.line,.line-y{border-bottom:1px solid #bababa;border-radius:0}.component p.title{padding-bottom:.25rem;line-height:1.5;margin-bottom:15px}.line-y{border-top:1px solid #bababa}button:-moz-focusring{outline:ButtonText dotted 1px}progress{vertical-align:baseline}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.header{padding:32px 0}.node:hover,.upld:hover{background-color:#313131;cursor:pointer}.content{display:block;margin:0 auto;max-width:1280px;padding:0 40px}body>.content{margin-top:16px}.component>p{display:flex;margin:0}.tooltip-handle{position:relative;cursor:pointer;border:0;background-color:transparent;margin:0;padding:0}.tooltip-handle span{visibility:hidden;min-width:120px;max-width:200px;background-color:#0a0a0a;color:#fff;opacity:.9;text-align:center;padding:16px;border-radius:6px;position:absolute;z-index:1;right:8px;top:8px;border:1px solid #fa6831}.tooltip-handle--active span,.tooltip-handle:hover span{visibility:visible}nav{display:flex;flex-wrap:wrap;min-width:685px;align-items:stretch}nav .logo-wrapper{min-width:240px;display:flex;align-self:flex-start;padding:4px 0 16px;flex-grow:1}nav .brand-logo,nav>ul a{display:flex;align-items:center}nav .brand-logo{order:1;flex-grow:1;justify-content:center}nav .brand-logo img{max-width:75%}nav .navbar-burger,nav>ul{display:flex;flex-direction:column}nav>ul{order:3;align-items:stretch;flex-grow:1;width:100%;margin:0;padding:32px 0}nav>ul>li{display:flex;list-style-type:none}nav>ul>li.active{border-bottom:2px solid #fa6831}nav>ul a{list-style:none;text-decoration:none;width:100%;padding:32px}nav .navbar-burger{order:0;align-items:flex-end;align-self:flex-end;margin-bottom:auto;margin-top:auto}nav .navbar-burger div{transition-duration:.5s;background-color:#efefef;width:100px;height:12px;margin:11px 0;display:flex;opacity:1}.burger-menu{display:none;white-space:nowrap}nav .navbar-burger.burger-open div:first-child{transform:rotateZ(45deg) scaleX(1.25) translate(19px,31px)}nav .navbar-burger.burger-open div:nth-child(2){transition-property:opacity;opacity:0}nav .navbar-burger.burger-open div:last-child{transform:rotateZ(-45deg) scaleX(1.25) translate(13px,-24px)}#lang-dropdown{order:2;align-items:center;display:flex;text-transform:uppercase}.telemetry{display:block;padding:0;position:sticky;position:-webkit-sticky;top:78px}.tel-value span:not(:first-child)::before{content:' / '}.tel-prop{display:flex;align-items:center;padding:12px 0;width:45%}#upld-remote .upld-details>div,.tel-prop>img{margin-right:1rem}.tel-prop>div>p{margin:0;line-height:1.5}.content-wrapper{display:flex;flex-direction:column}.main{display:block;flex-grow:1;padding:12px 0 0}#title{width:100%;padding-bottom:.25rem}.title-printer{margin-left:inherit;margin-bottom:0}#title,.main-wrapper{display:flex;flex-direction:column;justify-content:space-between}.main-wrapper{flex-grow:1;margin:0 0 .75rem}.progress-bar{background:#fff;position:relative;min-height:5px;overflow-y:visible;display:flex;align-items:center;justify-content:center}.progress-bar>.fill{background:#fa6831;position:absolute;z-index:1}.progress-bar>p{z-index:2}.hide-scrollbar::-webkit-scrollbar{width:0;height:0}.connection-status[ok=false] [ok]:not([ok=false]),.connection-status[ok=true] [ok]:not([ok=true]),[data-tab]:not([opened=true]){display:none}[data-tab-btn][selected=true]{color:#fa6831}.component,.separator{flex-grow:1}.component{flex-wrap:wrap;padding-top:64px;line-height:1}.component-inline{padding-top:0}.component-fixed{flex:0 0 auto}.abs-center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}#upld a{text-decoration:underline}#upld a:hover{text-decoration:none}#upld .form-row{margin-bottom:24px}#upld .form-submit,#upld-direct-frame div{display:flex;align-items:center}#upld .upld-source{display:flex;align-items:end;flex-wrap:wrap;margin-bottom:24px}#upld .state-choose-checkbox{flex:1 1 0}#upld-direct-frame{position:relative;border:2px dashed #fff;padding:24px}#upld-direct-frame div{flex-direction:row;justify-content:center}#upld-direct input[type=file]{position:absolute;opacity:0;width:100%;height:100%;top:-2.4px;left:-2.4px;padding:2.4px}#upld-direct input[type=file]:hover{cursor:pointer}#upld-direct .state-choose{opacity:0}#upld-direct .state-uploading{display:none;width:100%;height:100%}#drop-zone,#drop-zone div,#upld-direct .state-uploading img{width:100%;height:100%}#upld-direct[data-state=choose] .state-choose{opacity:1}#upld-direct[data-state=uploading] .state-uploading,.kebab>ul.open{display:block}#upld-remote input[type=text]{width:100%;box-sizing:border-box;line-height:1.5;margin:4px 0}#upld-remote .upld-details{display:flex;flex-wrap:wrap;margin-top:1rem}#upld-remote .progress-bar{height:36px;overflow:hidden}#upld-remote .progress-bar p{position:absolute;left:0;right:0;top:50%;transform:translate(0,-50%);padding:2px;overflow-x:auto;overflow-y:hidden;white-space:nowrap}#upld-remote[data-state=choose] .state-uploading,#upld-remote[data-state=uploading] .state-choose{display:none}#drop-zone{position:fixed;top:0;left:0;background-color:#000;opacity:.8;z-index:9999;padding:10px}#drop-zone input[type=file]{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0}#drop-zone div{display:flex;align-items:center;justify-content:center}#graph{position:relative;width:520px}.temp-svg{pointer-events:all;top:0;right:2.5%;bottom:0;left:2.5%}.temp-g-label{max-width:100%;height:100%;width:100%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.temp-legend-blue{fill:#1b73f8}.temp-legend-orange{fill:#fa6831}#temp-line-blue,#temp-line-orange,#temp-line-yellow{fill:transparent;opacity:1;stroke-width:1%}#temp-line-blue{stroke:#1b73f8}#temp-line-orange{stroke:#fa6831}#temp-line-yellow{stroke:#f3f12c}.temp-text{fill:#efefef;font-size:10.5pt;letter-spacing:normal;padding:8px;stroke:transparent;stroke-width:0}.temp-label-line{stroke:#efefef;fill:transparent;stroke-width:2px}.temp-connect-text,.temp-h-line,.temp-label-line{stroke-linecap:round;stroke-linejoin:round}.temp-h-line{stroke:gray;stroke-dasharray:none;fill:none;pointer-events:painted}.temp-connect-text{stroke:#efefef;size:5px;fill:transparent;stroke-width:1px}.temp-line-text{font-size:10.5pt;padding:5px;fill:#efefef;letter-spacing:normal;stroke:transparent;stroke-width:0}#files{width:100%;margin-top:16px}.node-icon-box,.node-img-box{margin-right:12px}.node,.node-btn-list>li{display:flex;align-items:center;padding:6px 0}.node-icon,.node-img{position:absolute;width:100%;height:100%;top:0;left:0;right:0;bottom:0;-o-object-fit:contain;object-fit:contain}.node-icon{max-width:80px;max-height:80px;margin:auto}.node-icon-box,.node-img-box{flex-shrink:0;position:relative;width:150px;height:150px;align-self:stretch}.node-icon-box{height:100px}.node-header,.node-wrapper{display:flex;grid-gap:12px;gap:12px}.node-wrapper{width:100%;flex-direction:column}.node-header{justify-content:space-between}.node-header>p{-ms-word-break:break-all;word-break:break-all;margin:0}.node-details{display:flex;flex-wrap:wrap;width:100%;grid-column-gap:15px;-moz-column-gap:15px;column-gap:15px}.node-details .details{display:flex;flex-direction:column;justify-content:flex-end}.node-current{display:flex;flex-direction:column;align-items:stretch;padding:6px 0}.node-btn-list{display:flex;list-style:none;margin:0;padding:0}.node-btn-list>li{padding:5px;cursor:pointer}.node-btn-list>li:not([disabled]):hover{background-color:rgba(255,255,255,.1);border-radius:50%}.node-btn-list>li[disabled]{cursor:default;pointer-events:none;opacity:.3}#node-storage{margin-top:16px}.node-storage-space{display:flex;margin-top:10px;grid-column-gap:15px;-moz-column-gap:15px;column-gap:15px;align-items:center;justify-content:flex-end}.node-storage-space .progress-bar{flex-grow:1;max-width:200px}.node-upload-source{margin:0;padding:0;list-style:none;display:flex}.node-upload-source>li{display:flex}.dropdown li.selected,.node-upload-source>li[selected=true],.storage-select-btn:hover span{color:#fa6831}.node-upload-source>li:not(:first-child)::before{content:\"\\00a0|\\00a0\";color:#fff}#preview-wrapper{display:flex;align-items:start;justify-content:start;flex-wrap:wrap;position:relative}.loading-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background-color:rgba(10,10,10,.5)}.loading-overlay>img{width:20%;height:20%}.job-details{width:100%;margin-left:1rem}.job-prop{display:flex;padding-bottom:12px}.job-prop>.icon{margin:12px 6px 6px}.job-prop p{padding:.2rem}.job-prop-grid{display:grid;grid-template-columns:repeat(2,1fr);flex-grow:1}.job-buttons{margin-top:16px;flex-wrap:wrap;justify-content:flex-start}.job-buttons button{margin:0 12px 12px 0}.job-buttons:first-child button{margin-left:0}button{text-transform:none;display:flex;align-items:center;border-radius:6px;border-color:transparent}button:hover{cursor:pointer}button:disabled{cursor:default;pointer-events:none;box-shadow:none;opacity:.5}.action{background-color:#5b5b5b}.action:hover{background-color:#6b6868}.action:disabled{background-color:#5b5b5b;border-color:#dbdbdb}.yes{background-color:#35a913}.yes:hover{background-color:#6ba959}.yes:disabled{background-color:#35a913;border-color:#dbdbdb}.no{background-color:#be0000}.no:hover{background-color:#b34a4a}.no:disabled{background-color:#be0000;border-color:#dbdbdb}.outlined{border-style:solid;background-color:transparent;border-color:#fff}.outlined:hover{background-color:#fff;color:#000}.message{display:flex;justify-content:center;margin-top:15%;margin-bottom:15%}.progress{margin-bottom:16px}#job{width:100%;margin-top:1rem}#job .file-name{word-wrap:anywhere;margin-bottom:20px}#job .progress{width:100%;font-size:40px}.progress-img{flex:1 1 auto;min-width:320px}.progress-img>.background{filter:grayscale(1);background:#363636;width:100%;height:auto;display:block}.progress-img>.foreground{position:absolute;top:0;left:0;width:100%;height:auto;display:block}.progress-with-img,.progress-with-img>div{display:flex;flex:1 1 auto}.progress-with-img .progress-pct{display:flex;padding:0 0 0 1rem;align-items:center;justify-content:left;flex:0 0 auto;flex-wrap:nowrap}.progress-with-img .progress-bar{height:auto;width:20px;flex-shrink:0;background:#242526}.preview-img-wrapper{display:flex;flex-grow:1;flex-wrap:wrap;position:relative}.job-buttons,.refill{display:flex}.refill{margin:10px 0 20px}.refill div:first-child{padding-right:6px}.refill div:last-child{padding-left:6px}#control .row{display:flex;flex-direction:row;width:100%}#control .col,#control .resp-row{display:flex;flex-direction:column}#control .resp-row{width:100%;flex-wrap:wrap}#control .resp-group{display:flex;justify-content:space-between;flex-grow:1}#control .resp-group .col:last-child{margin:0 auto}#control .txt-underline{border-bottom:1px solid #707070;padding-bottom:.25rem}#control .title{margin:1rem 0}#control img{-webkit-user-drag:none}#control .rectangle,#control .square{min-width:6rem;width:6rem;height:6rem;margin:.5rem;border-radius:8px;padding:0}#control .square{display:flex;align-items:center;justify-content:center}#control .rectangle{width:100%}#control .input-wrapper{display:flex;margin:.25rem;border-radius:12px;background:#fff}#control .input-wrapper>.square{margin:0;width:auto}#control .input-wrapper>.square:first-child{border-radius:8px 0 0 8px;background-color:#5b5b5b}#control .input-wrapper>.square:last-child{border-radius:0 8px 8px 0}#control input[type=text]{margin:0;padding:0;border:0;width:150px;flex-grow:1;text-align:center}#control .grid-container{display:grid;grid-template-columns:auto auto}#control .grid-item:nth-child(even){text-align:right}#control .separator{display:flex;flex-grow:1}#control button[selected],#control button[selected]:disabled{border:0;background:#fa6831;opacity:1}.modal{position:fixed;left:0;top:0;width:100%;height:100%;background-color:rgba(10,10,10,.86);opacity:0;visibility:hidden;z-index:5}.modal-box{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:#4a4a4a;padding:1rem 1.5rem;border-radius:.5rem;z-index:1000}.modal-box.offline-screen{background-color:transparent;text-align:center}.close-button{float:right;text-align:center;border-radius:.25rem;background-color:rgba(10,10,10,.2);border:0;border-radius:50px;cursor:pointer;pointer-events:auto;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:40px;max-height:40px;max-width:40px;min-height:40px;min-width:40px;outline:0;position:relative;vertical-align:top;width:40px}.close-button:after{height:50%;width:2px}.close-button:before{height:2px;width:50%}.close-button:after,.close-button:before{background-color:#efefef;content:\"\";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%) rotate(45deg);transform-origin:center center}.show-modal{opacity:1;visibility:visible;transform:scale(1)}.modal-welcome img{width:90%;padding-bottom:20px}.modal-welcome p{padding:.2rem;text-align:center}.modal-welcome p:last-child{margin-top:2rem}.modal-apiKey>p{padding:.2rem}.modal-apiKey>p:first-child{padding-bottom:1.5rem;padding-top:1rem}.modal-apiKey>input{width:100%;box-sizing:border-box;margin-bottom:5px;border-radius:4px;min-height:2.2rem;margin-top:5px}.modal-apiKey>button{float:right}.modal-confirm{max-width:320px;overflow:hidden;text-overflow:ellipsis}.modal-confirm>div>div{margin:15px 15px 15px 0}.modal-exposure{display:grid;grid-template-columns:7fr 1fr 3fr 1fr;grid-template-rows:auto;margin-bottom:10px;justify-items:center}.modal-exposure>p{width:100%;align-self:center;white-space:nowrap}.modal-exposure>img{border-radius:50%;cursor:pointer;width:48px;height:48px}.modal-exposure #next,.modal-exposure #previous{display:none}.modal-sysupgrade__message{padding-top:16px}.file-name__form flex-row,.modal-sysupgrade__message p{margin-bottom:16px}#question{display:flex;justify-content:center;margin-top:15%;margin-bottom:15%}#prusa-toast{line-height:1.5;display:flex;flex-direction:column;z-index:4;position:fixed;right:0;bottom:0;font-size:1.25rem}#prusa-toast>article{display:block;text-align:center;margin:7px;-webkit-animation:slideInUp .5s,zoomOut .5s 10s;animation:slideInUp .5s,zoomOut .5s 10s}.toast-header{text-align:center;align-items:center;border-radius:10px 10px 0 0;display:flex;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative;color:#efefef;background-color:#4a4a4a}.toast-body{word-break:break-all;padding:1.25em 1.5em;border-radius:0 0 10px 10px;color:defines.#efefef;background-color:#707070}#prusa-toast>article.warning>div.toast-header{color:rgba(0,0,0,.7);background-color:#ffdd57}#prusa-toast>article.warning>div.toast-body{color:#947600;background-color:#fffbeb}#prusa-toast>article.success>div.toast-header{color:#efefef;background-color:#48c774}#prusa-toast>article.success>div.toast-body{color:#257942;background-color:#effaf3}#prusa-toast>article.error>div.toast-header{color:#efefef;background-color:#f14668}#prusa-toast>article.error>div.toast-body{color:#cc0f35;background-color:#feecf0}#prusa-toast>article.error,#prusa-toast>article.warning{-webkit-animation:slideInUp .5s;animation:slideInUp .5s}.dropdown{align-self:center;position:relative;width:-webkit-max-content;width:-moz-max-content;width:max-content}#log-list{text-transform:none}.dropdown .open,.storage-select .open{visibility:visible}.dropdown ul{background-color:#0a0a0a;border:1px solid #707070;list-style:none;padding:0;margin:0;max-height:280px;overflow-y:auto}.dropdown li,.dropdown-btn{cursor:pointer;padding:16px 0}.dropdown li{padding:15px 20px;white-space:nowrap}.dropdown li.select{color:#fff;background:#fa6831}.dropdown-btn img{height:.7em;width:.7em;max-width:none;padding:0}.dropdown-content{top:90%;min-width:100%;position:absolute;visibility:collapse;z-index:5}.storage-select{display:flex;align-items:stretch;text-transform:uppercase;position:relative;width:100%;color:#000}.storage-select-left{width:0;border-bottom:1px solid #d5d5d5}.storage-select-right{flex:0 0 auto;border-bottom:1px solid #d5d5d5}.storage-select .icon{padding:0 8px}.storage-select-content{background:#fff;position:absolute;visibility:collapse;z-index:3;list-style:none;padding:0;margin:0;border-radius:6px;border:1px solid #d5d5d5;width:100%}.storage-select-content>li{display:flex;align-items:center;padding:16px 0;cursor:pointer}.storage-select-content>li:hover{background:#d5d5d5}.storage-select-btn{flex:1 0 0}.storage-select-btn-inner{display:flex;align-items:center;flex:1 0 0}.storage-select-btn{cursor:pointer;padding:16px 0;display:flex;align-items:center;border-radius:6px;background:#fff;border:1px solid #d5d5d5}.storage-select-btn img:last-child{margin-left:auto;border-left:1px solid #d5d5d5;padding:0 10px}.kebab{position:relative}.kebab>ul{position:absolute;right:0;z-index:3;display:none;list-style:none;width:-webkit-max-content;width:-moz-max-content;width:max-content;margin:0;padding:0;background-color:#f1f1f1;border:1px solid rgba(0,0,0,.15);border-radius:4px;cursor:default}.kebab>ul>li{padding:0 24px;display:flex;align-items:center;cursor:pointer}.kebab>ul>li:not([disabled]):hover{background-color:rgba(0,0,0,.1)}.kebab>ul>li>.icon{margin:12px 12px 12px 0}.kebab>ul>li[disabled]{cursor:default;pointer-events:none;opacity:.3}.kebab>ul>li:first-child{padding-top:12px}.kebab>ul>li:last-child{padding-bottom:12px}.loading{display:flex;justify-content:center;width:100%;margin-top:15%;margin-bottom:15%}.loading>img{width:60px;height:auto}input[type=password]:disabled,input[type=text]:disabled{background:0 0;border:0;color:#fff}.edit{cursor:pointer}#settings input[type=password],#settings input[type=text]{line-height:1.5;width:100%}#settings button{min-width:120px}#settings .table{padding:7px 0;max-width:800px}#settings .row{display:flex;flex-wrap:wrap}#settings .col{word-wrap:break-word;box-sizing:border-box;display:flex;align-items:center;padding-bottom:7px}#settings .col:nth-child(1){width:30%;justify-content:flex-end;text-align:right}#settings .col:nth-child(2){width:70%;flex-grow:1}#settings .col:nth-child(3){width:70%;margin-left:30%}#settings .col:nth-child(1),#settings .col:nth-child(2){padding-right:15px}#settings .logs{display:block;background:#fff;color:#000;height:12rem;overflow:auto;line-height:1.5;list-style:none;padding:10px;position:relative}#settings .logs li{width:100%;word-wrap:break-word;word-break:break-all}.connection-status p{display:flex}.connection-status img{margin-left:7px}.conn-status-msg{margin:0}.camera__snapshot,.file-name__form input{display:block}.camera-snapshot-meta{list-style:none;padding:0;display:flex;flex-direction:row;grid-gap:16px;gap:16px;margin:16px 0}.camera-snapshot-meta p{line-height:1.5}.file-name__form,.file-name__submit{margin-top:16px}.camera-settings__form label{flex:1 0 0;padding-right:8px;display:block;white-space:nowrap}.camera-settings__form input,.camera-settings__form select{flex-grow:1 0 0;display:block;max-width:196px}.camera-settings__form .flex-row{margin:16px 0;align-items:center;justify-content:space-evenly}#cameras-list{margin:0;padding:0}#cameras-list li{list-style:none}.camera__snapshot{cursor:pointer}.camera__preview{width:100px;height:100px;display:flex;align-items:center}.camera__preview img{width:100%;height:auto}.camera-settings__form .dropdown-btn{padding:0}.sort-bar{margin:0;padding:0}.sort-by{margin-left:16px;cursor:pointer;align-items:center}.sort-by p{margin-right:8px}.sort-asc,.sort-desc{fill:#efefef}.asc .sort-asc,.desc .sort-desc{fill:#fa6831}.sort-direction{justify-content:center}.slider{-webkit-appearance:none;height:15px;border-radius:5px;background:#707070;outline:0;opacity:.7;transition:opacity .2s}.slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:25px;height:25px;border-radius:50%;background:#fa6831;cursor:pointer}.slider::-moz-range-thumb{width:25px;height:25px;border-radius:50%;background:#fa6831;cursor:pointer}@media only screen and (max-width:991.5px){.pc-only{display:none}.camera-snapshot-meta{flex-direction:column}}@media only screen and (min-width:992px){.mobile-only{display:none}.header{padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:4}nav .logo-wrapper{flex-grow:0}nav{flex-direction:inherit}nav>ul{flex-wrap:nowrap;flex-direction:row;width:inherit;padding:0}nav>ul>li{position:relative;z-index:5;margin-top:4px;border-bottom:2px solid #707070}nav>ul>li a{padding:0 16px}nav>ul>li:hover{border-bottom:2px solid #efefef}nav .dropdown{padding:0;align-self:unset!important;min-width:-webkit-fit-content!important;min-width:-moz-fit-content!important;min-width:fit-content!important}.dropdown li{padding:2px 4px}#lang-dropdown{order:4}.burger-menu,.kebab-responsive>ul.open{display:flex}nav .brand-logo{max-height:100%;max-width:300px;min-height:auto;margin-bottom:auto;flex-grow:0;justify-content:flex-start}nav .brand-logo img{max-width:none;max-height:none;width:auto;height:auto}nav .navbar-burger{display:none}#telemetry-wrapper{flex:0 0 auto;width:256px}.tel-prop{width:100%}#title,.content-wrapper{flex-direction:row}.title-printer{margin-left:auto}.icon,button>img{width:24px;height:24px;min-width:24px}.icon-small{width:20px;height:20px;min-width:20px}button{border-radius:4px}#preview-wrapper{flex-wrap:nowrap}.job-title{display:flex;flex-direction:row}.job-title p{flex:1 0 0}.progress>progress{width:96%}.modal-welcome img{width:75%}#lang{margin-top:0}#settings .col:nth-child(1){width:25%}#settings .col:nth-child(2){width:45%}#settings .col:nth-child(3){width:30%;margin-left:0}#control .rectangle,#control .square{min-width:3.5rem;height:3.5rem;margin:.25rem;border-radius:4px}#control .square{width:3.5rem}#control .resp-row{flex-direction:row;justify-content:space-between}#control .input-wrapper>.square:first-child{border-radius:4px 0 0 4px;background-color:#5b5b5b}#control .input-wrapper>.square:last-child{border-radius:0 4px 4px 0}#job .progress,#prusa-toast{font-size:1rem}.close-button{height:40px;max-height:40px;max-width:40px;min-height:40px;min-width:40px}.node-current{flex-direction:row}.node,.node-btn-list{padding-right:5px}.node .node-btn-list{padding-right:0}.node-icon{max-width:48px;max-height:48px}.node-img-box{width:100px;height:100px}.node-icon-box{width:100px;height:60px}.kebab-responsive>.kebab-menu{display:none}.kebab-responsive>ul{position:static;display:flex;background:0 0;border:0}.kebab-responsive>ul>li,.kebab-responsive>ul>li:first-child,.kebab-responsive>ul>li:last-child{padding:5px}.kebab-responsive>ul>li:last-child{margin-right:-5px}.kebab-responsive>ul>li:not([disabled]):hover{background-color:rgba(255,255,255,.1);border-radius:50%}.kebab-responsive>ul>li .icon{margin:0}.storage-select{color:#fff;border-radius:0}.storage-select-btn{display:none}.storage-select-content{visibility:visible;position:static;border:0;background:0 0;list-style:none;display:flex;width:auto}.storage-select-content>li{display:flex;align-items:center;padding:5px 30px 5px 5px;cursor:pointer;border:1px solid transparent;border-bottom-color:#bababa}.storage-select-content>li:hover{background:0 0}.storage-select-content>li[selected=false]:hover{background:#313131}.storage-select-content>li[selected=true]{color:#fa6831;border:1px solid #bababa;border-bottom:1px solid #0a0a0a}.storage-select-left{width:10px}.storage-select-right{flex:1 0 auto}.job-buttons-separator{flex:1 0 0}.camera-snapshot-meta{flex-direction:row}}@media only screen and (min-width:1200px){.home-row{flex-direction:row}.home-row>div:not(:first-child){padding-left:12px}.home-row>div:first-child{padding-right:12px}}\n"
  },
  {
    "path": "prusa/link/templates/_footer.html",
    "content": "{% if wx is defined %}\n        </td>\n        <td></td>\n    </tr></table>\n    <br>\n\n    <footer class=\"footer mt-auto py-3\">\n      <div class=\"container\">\n{% endif %}\n\n    <div><font color=\"#7a7a7a\"><center><small>Copyright &copy; 2024 Prusa Research a.s. All rights reserved.</small></center></font></div>\n        {%- if template_info is defined %}\n        {%- from 'template_info.html' import render_info %}\n        <pre class=\"template-info\">\n            {{ render_info(template_info.context().get_exported()) }}\n        </pre>\n        {% endif %}\n    </div>\n  </footer>\n\n</body>\n</html>\n"
  },
  {
    "path": "prusa/link/templates/_header.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" lang=\"cs\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <title>Prusa Link{{ ' | ' + title if title is defined else '' }}</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /> {%- if refresh %}\n    <meta http-equiv=\"refresh\" content=\"{{ refresh }}; url=/\" /> {%- endif %}\n\n    <link rel=\"stylesheet\" href=\"/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"/css/bootstrap.connect.css\">\n    <link rel=\"stylesheet\" href=\"/css/bootstrap.prusa-link.css\">\n\n    <script src=\"/js/jquery-3.3.1.slim.min.js\"></script>\n    <script src=\"/js/popper.min.js\"></script>\n    <script src=\"/js/bootstrap.min.js\"></script>\n\n    <link href=\"/favicon.ico\" type=\"image/x-icon\" rel=\"shortcut icon\" />\n</head>\n\n<body class=\"d-flex flex-column h-100\" bgcolor=\"black\" text=\"white\" link=\"white\">\n\n{% if wx is not defined %}\n    <header>\n        <nav class=\"navbar navbar-expand-md navbar-dark\">\n            <div class=\"container-fluid\">\n                <span class=\"flex-even d-flex justify-content-center navbar-brand\">\n                    <img src=\"/img/prusa-link-logo.svg\" alt=\"Prusa Link\"/>\n                </span>\n            </div>\n        </nav>\n    </header>\n\n    <div class=\"flex-shrink-0\">\n{% else %}\n    <table width=\"100%\" cellpadding=\"50\">\n      <tr><td></td>\n          <td><img src=\"/img/prusa-link-logo.gif\" alt=\"Prusa Link\"/></td>\n          <td></td></tr>\n      <tr><td></td>\n          <td bgcolor=\"#1a1a1a\">\n{% endif %}\n"
  },
  {
    "path": "prusa/link/templates/_wizard.html",
    "content": "</div>\n\n<footer class=\"footer mt-auto py-3\">\n    <div class=\"container\">\n\n<div class=\"progress\">\n    {%- if active in ('welcome', 'restore', 'credentials', 'printer', 'finish') %}\n    <div class=\"progress-bar bg-warning {{ 'progress-bar-striped progress-bar-animated' if active == 'welcome' else '' }}\" role=\"progressbar\" style=\"width: 25%\" aria-valuenow=\"25\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n        <a href=\"/wizard\">1. Welcome</a>\n    </div>\n    {%- endif %} {%- if active in ('credentials', 'printer', 'finish') %}\n    <div class=\"progress-bar bg-warning {{ 'progress-bar-striped progress-bar-animated' if active == 'auth' else '' }}\" role=\"progressbar\" style=\"width: 25%\" aria-valuenow=\"25\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n        <a href=\"/wizard/credentials\">2. Setup Credentials</a>\n    </div>\n    {%- endif %} {%- if active in ('printer', 'finish') %}\n    <div class=\"progress-bar bg-warning {{ 'progress-bar-striped progress-bar-animated' if active == 'printer' else '' }}\" role=\"progressbar\" style=\"width: 25%\" aria-valuenow=\"25\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n        <a href=\"/wizard/printer\">3. Printer Info</a>\n    </div>\n    {%- endif %} {%- if active == 'finish' %}\n    <div class=\"progress-bar bg-warning {{ 'progress-bar-striped progress-bar-animated' if active == 'finish' else '' }}\" role=\"progressbar\" style=\"width: 25%\" aria-valuenow=\"25\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n        <a href=\"/wizard/finish\">4. Recap</a>\n    </div>\n    {%- endif %}\n</div>\n"
  },
  {
    "path": "prusa/link/templates/error-gone.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = '410 Gone' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n\n    <div class=\"container\">\n      <div class=\"jumbotron http-error\">\n        <h1>{{ title }}</h1>\n        <p>\n          Target resource <code>{{ this_uri }}</code> is no longer available\n          on this server.\n        </p>\n      </div>\n    </div>\n\n  </div>\n\n  <footer class=\"footer mt-auto py-3\">\n      <div class=\"container\">\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/error-internal-server-error.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set wx = True %}\n{% set title = '500 Internal Server Error' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n\n        <h1>{{ title }}</h1>\n        <p>\n          We're sorry, but there is a error in service.\n          <br /><br />Please try again later.\n        </p>\n\n {% if debug %}\n        </td>\n        <td></td>\n      </tr>\n      <tr><td></td>\n          <td><h2> Exception Traceback</h2>\n              <pre class=\"striped\">\n{%- for line in traceback -%}\n<div>{{ line }}</div>\n{%- endfor -%}\n              </pre>\n    {% endif %}\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/error.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set wx = True %}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n\n            <h1>{{ status_code }} {{ title }}</h1>\n            <p>\n              {{ text }}\n            </p>\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/index.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Home' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n\n    <div class=\"container\">\n        <h1 class=\"align-center\">API key</h1>\n        <p class=\"align-center\">For uploading gcodes from the Prusa Slicer use this Api-Key</p>\n        <p class=\"align-center\">\n          <code class=\"api-key\" id=\"apikey\" onclick=\"navigator.clipboard.writeText(document.getElementById('apikey').innerText)\" title=\"Click to copy to clipboard\">{{ api_key }}<img src=\"img/copy.svg\" height=\"16\" /></code>\n        </p>\n        <h1 class=\"align-center\">Status</h1>\n        <p class=\"align-center\">{{ errors }}</p>\n\n    </div>\n\n  </div>\n\n  <footer class=\"footer mt-auto py-3\">\n      <div class=\"container\">\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/link_info.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Home' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n\n    <div class=\"container\">\n        <h1 class=\"align-center\">Prusa Link Debug Info Page</h1>\n\n        <h2 class=\"align-center\">Ports</h2>\n        <ul>\n            {% for port in prusa_link.model.serial_adapter.ports %}\n                <li>{{ port }}</li>\n            {% endfor %}\n        </ul>\n\n        {% if prusa_link.model.serial_adapter.using_port %}\n            <h3 class=\"align-center\">Using port</h3>\n            {{ prusa_link.model.serial_adapter.using_port }}\n        {% endif %}\n\n        <h2 class=\"align-center\">Printer</h2>\n        <ul>\n            <li>Name: <span class=\"white\">{{ app.settings.printer.name }}</span></li>\n            <li>Location: <span class=\"white\">{{ app.settings.printer.location }}</span></li>\n            <li>Type: <span class=\"white\">{{ printer.type | printer_type }}</span></li>\n            <li>Port: <span class=\"white\">{{ app.cfg.printer.port }}</span></li>\n            <li>Baudrate: <span class=\"white\">{{ app.cfg.printer.baudrate }}</span></li>\n            <li>SN: <span class=\"white\">{{ printer.sn }}</span></li>\n            <li>Firmware: <span class=\"white\">{{ printer.firmware }}</span></li>\n            <li>Nozzle: <span class=\"white\">{{ printer.nozzle_diameter }}</span></li>\n            <li>SD Ready: <span class=\"white\">{{ prusa_link.sd_ready }}</span></li>\n        </ul>\n\n        <h2 class=\"align-center\">Prusa Link</h2>\n        <ul>\n            <li>Version: <span class=\"white\">{{ version }}</span></li>\n            <li>SDK Version: <span class=\"white\">{{ sdk_version }}</span></li>\n            <li>SDK State: <span class=\"white\">{{ printer.state }}</span></li>\n            <li>State history:</li>\n            <ul>\n            {%- for state in prusa_link.model.state_manager.state_history | reverse %}\n            <li><span class=\"white\">{{ state }}</span></li>\n            {%- endfor %}\n            <br>\n            </ul>\n            <li>Errors state:</li>\n            <ul>\n            {%- for key, (state, msg) in errors.items() %}\n            <li>{{ key }}: <span class=\"white\">{{ msg if state == False else state }}</span></li>\n            {%- endfor %}\n            </ul>\n        </ul>\n\n        {% if prusa_link.model.job.job_state.name != \"IDLE\" %}\n        <h2 class=\"align-center\">Job info</h2>\n            <li>Job ID:\n                <span class=\"white\">{{ prusa_link.model.job.job_id }}</span>\n            </li>\n            <li>Job State:\n                <span class=\"white\">{{ prusa_link.model.job.job_state }}</span>\n            </li>\n            <li>Job started by command with ID:\n                <span class=\"white\">{{ prusa_link.model.job.job_start_cmd_id }}</span>\n            </li>\n            <li>Printing file:\n                <span class=\"white\">{{ prusa_link.model.job.selected_file_path }}</span>\n            </li>\n            <li>Is path incomplete:\n                <span class=\"white\">{{ prusa_link.model.job.path_incomplete }}</span>\n            </li>\n            <li>Last modified at:\n                <span class=\"white\">{{ prusa_link.model.job.selected_file_m_timestamp }}</span>\n            </li>\n            <li>Size:\n                <span class=\"white\">{{ prusa_link.model.job.selected_file_size }}</span>\n            </li>\n            <li>From SD card:\n                <span class=\"white\">{{ prusa_link.model.job.from_sd }}</span>\n            </li>\n            <li>File contains inbuilt print stats:\n                <span class=\"white\">{{ prusa_link.model.job.inbuilt_reporting }}</span>\n            </li>\n            <li>Byte position:\n                <span class=\"white\">{{ prusa_link.model.job.printing_file_byte }}/{{ prusa_link.model.job.printing_file_size }}</span>\n            </li>\n        {% endif %}\n\n        <h2 class=\"align-center\">Network</h2>\n        {% if printer %}\n        <ul>\n            {%- for key, val in printer.network_info.items() %}\n            <li>{{ key }}: <span class=\"white\">{{ val }}</span></li>\n            {%- endfor %}\n            <li>Api-Key: <span class=\"white\">{{ printer.api_key }}</span></li>\n        </ul>\n        {% endif %}\n\n\n        <h2 class=\"align-center\">Connect</h2>\n        <ul>\n            <li>Server: <span class=\"white\">{{ printer.server }}</span></li>\n            <li>Fingerprint: <span class=\"white\">{{ printer.fingerprint }}</span></li>\n            <li>Token: <span class=\"white\">{{ printer.token }}</span></li>\n        {% if prusa_link %}\n            <li>Telemetry</li>\n            <ul>\n                {%- for key, val in prusa_link.model.latest_telemetry %}\n                <li>{{ key }}: <span class=\"white\">{{ val }}</span></li>\n                {%- endfor %}\n            </ul>\n        {% endif %}\n        </ul>\n\n\n        {% if transfer %}\n        <h2 class=\"align-center\">Transfer</h2>\n        <ul>\n            <li>Type: <span class=\"white\">{{ transfer.type.value }}</span></li>\n            <li>Path: <span class=\"white\">{{ transfer.path }}</span></li>\n            <li>URL: <span class=\"white\">{{ transfer.url }}</span></li>\n            <li>To select: <span class=\"white\">{{ transfer.to_select }}</span></li>\n            <li>To print: <span class=\"white\">{{ transfer.to_print }}</span></li>\n            <li>Size: <span class=\"white\">{{ transfer.size }}</span></li>\n            <li>Transferred: <span class=\"white\">{{ transfer.transferred }}</span></li>\n            <li>Progress: <span class=\"white\">{{ transfer.progress }}</span></li>\n            <li>Start time: <span class=\"white\">{{ transfer.start_ts|int }}</span></li>\n            <li>Stop time: <span class=\"white\">{{ transfer.start_ts|int }}</span></li>\n            <li>Remaining time: <span class=\"white\">{{ transfer.time_remaining() }}</span></li>\n          </ul>\n        {% endif %}\n\n\n        {% if upload %}\n        <h2 class=\"align-center\">Upload</h2>\n        <ul>\n            <li>In progress: <span class=\"white\">{{ upload }}</span></li>\n        </ul>\n        {% endif %}\n\n\n        <h2 class=\"align-center\">Files</h2>\n        {% if printer %}\n        <pre>\n            {{ printer.get_info()[\"files\"]|pprint }}\n        </pre>\n        {% endif %}\n    </div>\n\n  </div>\n\n  <footer class=\"footer mt-auto py-3\">\n      <div class=\"container\">\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/multi-instance.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Multi-Instance landing page' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"welcome\" %}\n\n<div class=\"container\">\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h1 class=\"align-leftr\">PrusaLink instances</h1>\n\n            <ul>\n                {% for printer in printer_info.values() %}\n                    <li>\n                        <a href=\"{{ printer.number }}/\">\n                            {{ printer.name[0]|upper }}{{ printer.name[1:] }}\n                        </a>\n                    </li>\n                {% endfor %}\n            </ul>\n        </div>{# /col-md-12 so end of content #}\n    </div>{# /row #}\n</div>{# /container #}\n\n<script>\n     function changeIconImage(img) {\n    document.getElementById('restore_icon').style.content = `url(${img})`;\n  }\n</script>\n\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"welcome\" %}\n\n<div class=\"container\">\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h1 class=\"align-center\">Welcome to PrusaLink configuration Wizard</h1>\n            {% if wizard.daemon.prusa_link and not wizard.daemon.prusa_link.model.serial_adapter.using_port %}\n                <div class=\"img-center\">\n                    <img src=\"img/printers/rpi_mk3.svg\" height=\"200px\"/>\n                </div>\n                <h3>Unfortunately, we are unable to detect your printer on any serial port.</h3>\n                <p>PrusaLink requires some time to initialize properly. Please wait a few seconds and refresh the page. If the problem persists, try power cycling your printer/PrusaLink and attempt again.</p>\n                <p>Please ensure that the RPi port is set to the correct state. Set it to OFF for a USB connection or ON for a GPIO connection.\n                    Additionally, make sure your printer is idle and displaying the status screen. If you soldered the pins, please double-check the connection - there might be a cold solder joint.</p>\n\n                <p>If none of the suggestions helped, please let us know. You can find more information or create a new issue on our <a href=\"https://github.com/prusa3d/Prusa-Link/issues\">GitHub</a> or contact support at info@prusa3d.com</p>\n\n                <div onclick=\"showPortsSection()\" id=\"ports_section_show\">Show more info...</div>\n                <div id=\"ports_section\">\n                        {% if wizard.daemon.prusa_link.model.serial_adapter.ports %}\n                            <h3>Below, you can find a list of detected ports and their status.</h3>\n                            <p class=\"align-center\" id=\"ports\"></p>\n                            <script>\n                                function showPortsSection() {\n                                  var ports_section = document.getElementById(\"ports_section\");\n                                  var textShow = document.getElementById(\"ports_section_show\");\n\n                                  if (ports_section.style.display === \"inline\") {\n                                    textShow.innerHTML = \"Show more info...\";\n                                    ports_section.style.display = \"none\";\n                                  } else {\n                                    textShow.innerHTML = \"Hide more info\";\n                                    ports_section.style.display = \"inline\";\n                                  }\n                                }\n\n                                let is_selected = false\n                                function getPortHtml(portData) {\n                                    let symbol = \"⌛\";\n                                    let status = \"WAITING\";\n\n                                    if (portData.checked) {\n                                        if (portData.selected) {\n                                            status = \"selected\";\n                                            symbol = \"〈\";\n                                            is_selected = true\n                                        } else if (portData.usable) {\n                                            status = \"usable\";\n                                            symbol = \"✓\";\n                                        } else {\n                                            symbol = \"✘\";\n                                            status = \"NOT USABLE\";\n                                        }\n                                    }\n\n                                    const rpi = portData.is_rpi_port ? \"[RPi]\" : \"\"\n                                    return `<li>${portData.path}: ${rpi} ${status} ... ${symbol}</li>`\n                                }\n\n                                function setPortsHtml(dataFromAPI) {\n                                    document.getElementById(\"ports\").innerHTML =\n                                        `<ul style=\"list-style-type:none;\">${dataFromAPI.ports.map(getPortHtml)}</ul>`\n\n                                    if (is_selected) {\n                                        window.location.reload(1);\n                                    }\n                                }\n\n                                const doUpdate = () => {\n                                    fetch('/api/ports')\n                                        .then((response) => response.json())\n                                        .then(setPortsHtml)\n                                        .catch((err) => console.error(err))\n                                        .finally(() => setTimeout(doUpdate, 1000));\n                                };\n                                doUpdate();\n                            </script>\n                        {% else %}\n                            <h3>Unfortunately we can't detect any active serial port.</h3>\n                            <script>\n                                setTimeout(function(){\n                                    window.location.reload(1);\n                                    }, 1000);\n                            </script>\n                        {% endif %}\n                    </div>\n\n                <a href={{ \"/wizard\" | prefixed }} class=\"btn btn-outline-light align-center\" style=\"width: 200px;margin: 2em auto;display: block;\">Refresh</a>\n\n            {% elif not wizard.daemon.prusa_link or not wizard.daemon.prusa_link.printer %}\n\n            <h2 class=\"align-center\">PrusaLink is starting...</h2>\n\n            {# TODO: some prusa-link log to see what's happen. #}\n\n            <a href={{ \"/wizard\" | prefixed }} class=\"btn btn-outline-light align-center\" style=\"width: 200px;margin: 2em auto;display: block;\">Refresh</a>\n            {# TODO: do some javascript call to check the printer, and then refresh is automaticaly #}\n\n            {% else %}\n\n            {% set printer = wizard.daemon.prusa_link.printer %}\n\n            <div class=\"img-center\">\n                <img src=\"img/printers/{{ printer.type if printer.type else 'unknown_printer' }}.svg\" height=\"200\" width=\"200\" />\n            </div>\n\n            <h2 class=\"align-center\">\n                {% if not printer.type_string %}\n                    {{ \"Printer not recognized.\" }}\n                {% else %}\n                    Original Prusa i3 {{ printer.type_string }}\n                {% endif %}\n            </h2>\n\n            <div class=\"container navigation\">\n                <div class=\"row\">\n                {% if not conditions.ID %}\n                    <div class=\"col\" style=\"text-align: center;\">Unfortunately your printer is not supported or running old firmware. To download the latest firmware, visit <a href=\"https://help.prusa3d.com/\">help.prusa3d.com.</a><br /><br />\n                        Only Original Prusa MK3, MK3S and MK3S+ printers are supported. If you think this is a bug, consider\n                        starting a new thread on <a href=\"https://forum.prusaprinters.org/forum/prusa-connect-prusalink/\">forum.prusaprinters.org</a>\n                        or contacting support at info@prusa3d.com\n                    </div>\n                {% elif not conditions.FW %}\n                    <div class=\"col\" style=\"text-align: center;\">Firmware is not up-to-date, please update it!<br /><br />\n                        You can download the latest stable firmware from the <a href=\"https://help.prusa3d.com/downloads\" target=\"_blank\">Prusa Downloads</a> page or <a href=\"https://github.com/prusa3d/Prusa-Firmware/releases/latest\" target=\"_blank\">Prusa-Firmware</a> github repository.<br />\n                        Follow the instructions in this <a href=\"https://help.prusa3d.com/en/guide/upgrading-the-firmware-original-prusa-i3_24720\" target=\"_blank\">guide</a>.<br /><br />\n                        After upgrading, run this wizard again.\n                    </div>\n                {% elif not conditions.SN %}\n                    <div class=\"col\"><b>Great!</b> You can continue to set Serial Number</div>\n                    <div class=\"col-xs-auto\">\n                        <a href={{ \"/wizard/serial\" | prefixed }} class=\"btn btn-outline-light\">Serial Number | NEXT <img src=\"img/arrow-right.svg\" height=\"16\" /></a>\n                    </div>\n                {% else %}\n                    <div class=\"col\"><b>Great!</b> You can continue to set up your credentials, <b>or</b> you can restore printer settings from your <i>prusa_printer_settings.ini</i> file</div>\n                    <br/>\n                    <div class=\"col-xs-auto\">\n\n\n                    </div>\n\n            <div class=\"container navigation\">\n              <div class=\"row\">\n                  <div class=\"col\">\n                    <a id=\"restore_btn\" href={{ \"/wizard/restore\" | prefixed }} class=\"btn btn-outline-light full-width\" onmouseover=\"changeIconImage('img/reset-icon-black.svg')\" onmouseout=\"changeIconImage('img/reset-icon-white.svg')\">Restore settings <img id=\"restore_icon\" height=\"25\" src=\"img/reset-icon-white.svg\">\n                    </a>\n                  </div>\n                  <div class=\"col-sm-auto\">\n                    <a href={{ \"/wizard/credentials\" | prefixed }} class=\"btn btn-outline-light full-width\">Setup credentials | NEXT <img src=\"img/arrow-right.svg\" height=\"16\" /></a>\n                  </div>\n              </div>\n            </div>\n\n                {% endif %}\n                </div>\n            </div>\n\n\n            {% endif %}\n        </div>{# /col-md-12 so end of content #}\n    </div>{# /row #}\n</div>{# /container #}\n\n<script>\n     function changeIconImage(img) {\n    document.getElementById('restore_icon').style.content = `url(${img})`;\n  }\n</script>\n\n{% include \"_wizard.html\" %}\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard_credentials.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard Credentials' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"credentials\" %}\n\n<div class=\"container\">\n  <div class=\"row\">\n      <div class=\"col-md-12\">\n          <h1>Credentials Setup</h1>\n            <p>Please set the administrator username and password.<br/></p>\n\n            <form method=\"post\" id=\"form\">\n            {% set errors = wizard.errors.get('credentials', {}) %}\n            <div class=\"form-group row\">\n              <label for=\"username\" class=\"col-sm-2 col-form-label\">Username:</label>\n              <div class=\"col-sm-10\">\n                <input type=\"text\" class=\"form-control {{ 'is-invalid' if errors.get('username') or errors.get('username_spaces') else '' }}\" id=\"username\" name=\"username\" value=\"{{ wizard.username or '' }}\" required>\n                <div class=\"invalid-feedback\">\n                  {% if errors.get('username_spaces') %}\n                    <p>Username cannot contain space at the beginning nor the end</p>\n                  {% elif errors.get('username') %}\n                    <p>Please provide a username in valid format and with sufficient length.</p>\n                  {% endif %}\n                </div>\n              </div>\n            </div>\n\n            {% if wizard.restored_digest %}\n              <div>Your password is restored from your configuration file. If you want to set a new one, please fill it into the form below.</div><br/>\n              {% set password_placeholder = \"Restored from configuration file\" %}\n            {% endif %}\n\n            <div class=\"form-group row\">\n              <label for=\"password\" class=\"col-sm-2 col-form-label\">Password:</label>\n                <div class=\"col-sm-10\">\n                  <input type=\"password\" class=\"form-control {{ 'is-invalid' if errors.get('password') or errors.get('password_spaces') else '' }}\" id=\"password\" name=\"password\" placeholder=\"{{ password_placeholder if password_placeholder else '' }}\">\n                  <div class=\"invalid-feedback\">\n                    {% if errors.get('password_spaces') %}\n                      <p>Password cannot contain space at the beginning nor the end</p>\n                    {% elif errors.get('password') %}\n                      <p>Please provide a strong password in correct format and with sufficient length.</p>\n                    {% endif %}\n                  </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row\">\n              <label for=\"repassword\" class=\"col-sm-2 col-form-label\">Password again:</label>\n              <div class=\"col-sm-10\">\n                <input type=\"password\" class=\"form-control {{ 'is-invalid' if errors.get('repassword') else '' }}\" id=\"repassword\" name=\"repassword\" placeholder=\"{{ password_placeholder if password_placeholder else '' }}\">\n                <div class=\"invalid-feedback\">\n                  Passwords must be same.\n                </div>\n              </div>\n            </div>\n\n              <p>PrusaLink will use HTTP Digest authorization. Username length must be at least <i>3 characters</i> long<br />\n              Passwords cannot contain spaces at the beginning nor the end, and must meet at least one of these conditions:<br />\n              <li>Minimal length 8 characters, including one lowercase letter, one uppercase letter and one number</li>\n              <li>Minimal length 8 characters, including one non-alphanumeric character (e.g. @)</li>\n              <li>Minimal length 15 characters</li>\n              </p>\n\n\n            <div class=\"container navigation\">\n              <div class=\"row\">\n                  <div class=\"col\">\n                    <a href={{ \"/wizard\" | prefixed }} class=\"btn btn-back btn-outline-light full-width\"><img src=\"../img/arrow-left.svg\" height=\"16\" /> BACK | Welcome</a>\n                  </div>\n                  <div class=\"col-sm-auto\">\n                    <button onclick=\"document.getElementById('form').submit();\" type=\"submit\" class=\"btn btn-outline-light full-width\">Printer info | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                  </div>\n              </div>\n            </div>\n\n          </form>\n        </div>{# /col-md-9 so end of content  #}\n      </div>{# /row #}\n    </div>{# /container #}\n\n    <script>\n        function changeIconImage(img) {\n            document.getElementById('restore_icon').style.content = `url(${img})`;\n        }\n\n        function useIniFile() {\n        var checked = document.getElementById(\"use_ini_file\").checked;\n        var ini_file = document.getElementById(\"ini_file\");\n        if (checked)\n        {\n          ini_file.style.display = \"block\";\n        } else {\n          ini_file.style.display = \"none\";\n        }\n        }\n    </script>\n\n\n    {% include \"_wizard.html\" %}\n    {% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard_finish.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"finish\" %}\n\n<div class=\"container\">\n  <div class=\"row\">\n    <div class=\"col-md-12\">\n        <h1>Local Settings Summary</h1>\n        {% set printer = wizard.daemon.prusa_link.printer %}\n        {% set cfg = wizard.daemon.cfg %}\n    </div>\n  </div>\n\n    <!-- Local Settings Row -->\n    <div class=\"row\">\n\n        <!-- Credentials -->\n        <div class=\"col\">\n          <h3>Credentials</h3>\n          <ul>\n            <li>Username: <span class=\"white\">{{ wizard.username }}</span></li>\n          </ul>\n        </div>\n        <!-- Credentials End -->\n\n        <!-- Network -->\n        {% if printer.network_info %}\n            <div class=\"col\">\n              <h3>Network</h3>\n              <ul>\n                  <li>Hostname: <span class=\"white\">{{ printer.network_info.hostname }}</span></li>\n\n                  {% if printer.network_info.wifi_ssid %}\n                    <li>WiFi SSID: <span class=\"white\">{{ printer.network_info.wifi_ssid }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.wifi_ipv4 %}\n                    <li>WiFi IPv4: <span class=\"white\">{{ printer.network_info.wifi_ipv4 }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.wifi_ipv6 %}\n                    <li>WiFi IPv6: <span class=\"white\">{{ printer.network_info.wifi_ipv6 }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.wifi_mac %}\n                    <li>WiFi MAC: <span class=\"white\">{{ printer.network_info.wifi_mac }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.lan_ipv4 %}\n                    <li>LAN IPv4: <span class=\"white\">{{ printer.network_info.lan_ipv4 }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.lan_ipv6 %}\n                    <li>LAN IPv6: <span class=\"white\">{{ printer.network_info.lan_ipv6 }}</span></li>\n                  {% endif %}\n\n                  {% if printer.network_info.lan_mac %}\n                    <li>LAN MAC: <span class=\"white\">{{ printer.network_info.lan_mac }}</span></li>\n                  {% endif %}\n\n              </ul>\n            </div>\n        {% endif %}\n        <!-- Network End -->\n\n        <!-- Printer -->\n        <div class=\"col\">\n          <h3>Printer</h3>\n          <ul>\n              {% if wizard.printer_name %}\n                <li>Name: <span class=\"white\">{{ wizard.printer_name }}</span></li>\n              {% endif %}\n              {% if wizard.printer_location %}\n                <li>Location: <span class=\"white\">{{ wizard.printer_location }}</span></li>\n              {% endif %}\n            <li>Type: <span class=\"white\">{{ printer.type | printer_type }}</span></li>\n            <li>Port: <span class=\"white\">{{ cfg.printer.port }}</span></li>\n            <li>Baudrate: <span class=\"white\">{{ cfg.printer.baudrate }}</span></li>\n              {% if wizard.serial_number %}\n                <li>SN: <span class=\"white\">{{ wizard.serial_number }}</span></li>\n              {% else %}\n                <li>SN could not be obtained from the FW!</li>\n              {% endif %}\n            <li>Firmware: <span class=\"white\">{{ printer.firmware }}</span></li>\n          </ul>\n        </div>\n        <!-- Printer End -->\n    </div>\n    <!-- Local Settings Row End -->\n\n    <!-- Connect Row -->\n    <div class=\"row\">\n        <div class=\"col\">\n            <h1>Prusa Connect</h1>\n            {% if wizard.restored_connect %}\n                <div>Your Prusa Connect settings was restored from configuration file</div>\n            {% else %}\n            <div>Prusa Connect is a web service that gives you a complete overview of all your 3D printers at all times.</br>\n                 PrusaLink with Prusa Connect right <b>now</b>, using the button below.</div>\n            {% endif %}\n        </div>\n    </div>\n    <!-- Connect Row End -->\n\n      <div class=\"row\">\n        <div class=\"col-md-12\">\n            <form method=\"post\" action=\"{{ \"/wizard/finish-register-skip\" | prefixed }}\" id=\"skip\"></form>\n            <form method=\"post\" action=\"{{ \"/wizard/finish-register\" | prefixed }}\" id=\"form\" target=\"_blank\">\n                <div class=\"container navigation\">\n                  <div class=\"row\">\n                      <div class=\"col\">\n                        <a href={{ \"/wizard/printer\" | prefixed }} class=\"btn btn-back btn-outline-light full-width\"><img src=\"../img/arrow-left.svg\" height=\"16\" /> BACK | Printer info</a>\n                      </div>\n                      <div class=\"col-sm-auto\">\n                        <button form=\"skip\" onclick=\"document.getElementById('skip').submit();\" type=\"submit\" class=\"btn btn-back btn-outline-light full-width\">Save printer | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                          {% if not wizard.restored_connect %}\n                            <button onclick=\"finish()\" type=\"button\" class=\"btn btn-outline-light full-width\">Save printer and link with Connect | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                          {% endif %}\n                      </div>\n                  </div>\n                </div>\n            </form>\n        </div>\n      </div>\n\n    </div>{# /container #}\n\n    <script>\n        function finish() {\n            setTimeout(() => {window.location.href = \"{{ \"/\" | prefixed }}\";}, 500);\n            document.getElementById('form').submit();\n        }\n    </script>\n\n    {% include \"_wizard.html\" %}\n    {% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard_printer.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"printer\" %}\n\n<div class=\"container\">\n  <div class=\"row\">\n      <div class=\"col-md-12\">\n          <h1>Printer Info</h1>\n\n          <form method=\"post\" id=\"form\">\n            <p>Please, fill in the name of the printer which will be used with PrusaLink, and its location.</p>\n\n            {% set errors = wizard.errors.get('printer', {}) %}\n            <div class=\"form-group row\">\n              <label for=\"name\" class=\"col-sm-2 col-form-label\">Name:</label>\n              <div class=\"col-sm-10\">\n                <input type=\"text\" class=\"form-control {{ 'is-invalid' if errors.get('name') else '' }}\" id=\"name\" name=\"name\" value=\"{{ wizard.printer_name or '' }}\" placeholder=\"Original Prusa i3 {{ wizard.daemon.prusa_link.printer.type_string }}\">\n                <div class=\"invalid-feedback\">\n                  Please provide a valid printer name.\n                </div>\n              </div>\n            </div>\n\n            <div class=\"form-group row\">\n              <label for=\"location\" class=\"col-sm-2 col-form-label\">Location:</label>\n              <div class=\"col-sm-10\">\n                <input type=\"text\" class=\"form-control {{ 'is-invalid' if errors.get('location') else '' }}\" id=\"location\" name=\"location\" value=\"{{ wizard.printer_location or '' }}\" placeholder=\"Warehouse top shelf\">\n                <div class=\"invalid-feedback\">\n                  Please provide a valid printer location.\n                </div>\n              </div>\n            </div>\n\n            <div class=\"container navigation\">\n              <div class=\"row\">\n                  <div class=\"col\">\n                    <a href={{ \"/wizard/credentials\" | prefixed }} class=\"btn btn-back btn-outline-light full-width\"><img src=\"../img/arrow-left.svg\" height=\"16\" /> BACK | Setup credentials</a>\n                  </div>\n                  <div class=\"col-sm-auto\">\n                    <button onclick=\"document.getElementById('form').submit();\" type=\"submit\" class=\"btn btn-outline-light full-width\">Recap and save | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                  </div>\n              </div>\n            </div>\n          </form>\n\n        </div>{# /col-md-9 so end of content  #}\n      </div>{# /row #}\n    </div>{# /container #}\n\n    {% include \"_wizard.html\" %}\n    {% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard_restore.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard Restore' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"restore\" %}\n\n<div class=\"container\">\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h1 class=\"align-center\">Restore Printer Settings</h1>\n            <div class=\"container navigation\">\n                <div class=\"row\">\n                    <p>You can download your <i>prusa_printer_settings.ini</i> file from the Connect website, or use the backup version from PrusaLink.\n                        The form will be populated with the values from the file, so you can check them before confirming.\n                    </p>\n\n                    <p>You can upload your <i>prusa_printer_settings.ini</i> file below</p>\n                </div>\n                <form id=\"ini_file_form\" class=\"col-sm-auto\" action = \"/wizard/restore\" method = \"POST\" enctype = \"multipart/form-data\">\n\n                    <div class=\"row\">\n                        <input type=\"file\" name=\"file\" class=\"btn btn-outline-light full-width\" style=\"white-space: nowrap;\"/>\n\n                        <div class=\"container navigation\">\n                          <div class=\"row\">\n                              <div class=\"col\">\n                                <a href={{ \"/wizard\" | prefixed }} class=\"btn btn-back btn-outline-light full-width\"><img src=\"../img/arrow-left.svg\" height=\"16\" /> BACK | Welcome</a>\n                              </div>\n                              <div class=\"col-sm-auto\">\n                                <button onclick=\"document.getElementById('form').submit();\" type=\"submit\" class=\"btn btn-outline-light full-width\">Restore and continue | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                              </div>\n                          </div>\n                        </div>\n\n                    </div>\n\n                </form>\n\n            </div>\n        </div>{# /col-md-12 so end of content #}\n    </div>{# /row #}\n</div>{# /container #}\n\n{% include \"_wizard.html\" %}\n{% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/templates/wizard_serial.html",
    "content": "{# vim:set softtabstop=2: -#}\n{% set title = 'Wizard' -%}\n{#% set refresh = 15 %#}\n{% include \"_header.html\" %}\n{% set active=\"serial\" %}\n\n<div class=\"container\">\n  <div class=\"row\">\n      <div class=\"col-md-12\">\n          <h1>Serial Number</h1>\n            {% set errors = wizard.errors.get('serial', {}) %}\n            {% if not errors.get('not_obtained') %}\n          <form method=\"post\" id=\"form\">\n              <p>Serial Number of your printer cannot be obtained. Please fill in the S/N, starting with <b>CZP</b>, from the label on the back of your printer.</p>\n\n            <div class=\"form-group row\">\n              <label for=\"serial\" class=\"col-sm-2 col-form-label\">S/N:</label>\n              <div class=\"col-sm-10\">\n                <input type=\"text\" class=\"form-control {{ 'is-invalid' if errors else '' }}\" id=\"serial\" name=\"serial\" value=\"{{ wizard.serial or '' }}\" required>\n                <div class=\"invalid-feedback\">\n                    {% if errors.get('not_valid') %}\n                        <p>Please provide a valid S/N.</p>\n                    {% elif errors.get('new_sn') %}\n                        <p>It looks like you have a new version of our S/N. Please contact our <a href=\"https://help.prusa3d.com/en/article/support_2287\" target=\"_blank\">Customer support</a> for help with your registration.</p>\n                    {% endif %}\n                </div>\n              </div>\n            </div>\n\n            <div class=\"container navigation\">\n              <div class=\"row\">\n                  <div class=\"col\">\n                    <a href={{ \"/wizard\" | prefixed }} class=\"btn btn-back btn-outline-light full-width\"><img src=\"../img/arrow-left.svg\" height=\"16\" /> BACK | Wizard</a>\n                  </div>\n                  <div class=\"col-sm-auto\">\n                    <button onclick=\"document.getElementById('form').submit();\" type=\"submit\" class=\"btn btn-outline-light full-width\">Setup Authorization | NEXT <img src=\"../img/arrow-right.svg\" height=\"16\" /></button>\n                  </div>\n              </div>\n            </div>\n          </form>\n          {% else %}\n          <div class=\"col\" style=\"text-align: center;\">Serial Number of your printer cannot be obtained, please contact our <a href=\"https://help.prusa3d.com/en/article/support_2287\" target=\"_blank\">Customer support</a>.</div>\n          {% endif %}\n\n        </div>{# /col-md-9 so end of content  #}\n      </div>{# /row #}\n    </div>{# /container #}\n\n    {% include \"_wizard.html\" %}\n    {% include \"_footer.html\" %}\n"
  },
  {
    "path": "prusa/link/util.py",
    "content": "\"\"\"Contains functions that might be useful outside of their modules\"\"\"\nimport datetime\nimport json\nimport logging\nimport multiprocessing\nimport os\nimport pwd\nimport socket\nimport struct\nimport typing\nfrom hashlib import sha256\nfrom pathlib import Path\nfrom threading import Event, current_thread\nfrom time import sleep, time\nfrom typing import Callable\n\nimport prctl  # type: ignore\nimport pyudev  # type: ignore\nimport unidecode\n\nfrom .const import (\n    MMU_SLOTS,\n    PP_MOVES_DELAY,\n    SD_STORAGE_NAME,\n    SUPPORTED_PRINTERS,\n)\nfrom .multi_instance.const import VALID_SN_REGEX\nfrom .printer_adapter.structures.model_classes import (\n    IndividualSlot,\n    PPData,\n    Slot,\n)\n\nlog = logging.getLogger(__name__)\n\n\ndef prctl_name():\n    \"\"\"Set system thread name with python thread name.\"\"\"\n    # pylint: disable=deprecated-method\n    # No current_thread is not deprecated, but currentThread is :-(\n    prctl.set_name(f\"pl#{current_thread().name}\")\n\n\ndef loop_until(loop_evt: Event, run_every_sec: Callable[[], float], to_run,\n               *arg_getters, **kwarg_getters):\n    \"\"\"\n    Call a function every X seconds, quit instantly\n    pass getters for arguments\n    \"\"\"\n    prctl_name()\n    while not loop_evt.is_set():\n        # if it's time to run the func\n\n        last_called = time()\n        args = []\n        for getter in arg_getters:\n            args.append(getter())\n\n        kwargs = {}\n        for name, getter in kwarg_getters.items():\n            kwargs[name] = getter()\n\n        to_run(*args, **kwargs)\n\n        run_again_in = max(0.0, (last_called + run_every_sec()) - time())\n        loop_evt.wait(run_again_in)\n\n\ndef get_local_ip():\n    \"\"\"\n    Gets the local ip used for connecting to MQTT_HOSTNAME\n    Code from https://stackoverflow.com/a/166589\n    Beware this throws socket errors\n    \"\"\"\n    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    # does not matter if host is reachable or not,\n    # any client interface that is UP should suffice\n    sock.connect((\"8.8.8.8\", 1))\n    local_ip = sock.getsockname()[0]\n    sock.close()\n    return local_ip\n\n\ndef get_local_ip6():\n    \"\"\"\n    Gets the local ipv6 used for connecting to MQTT_HOSTNAME\n    Code from https://stackoverflow.com/a/166589\n    Beware this throws socket errors\n    \"\"\"\n    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)\n    # does not matter if host is reachable or not,\n    # any client interface that is UP should suffice\n    sock.connect((\"2606:4700:4700::1111\", 1))\n    local_ip = sock.getsockname()[0]\n    return local_ip\n\n\ndef get_clean_path(path):\n    \"\"\"\n    Uses pathlib to load a path string, then gets a string for it,\n    ensuring consistent formatting\n    \"\"\"\n    return str(Path(path))\n\n\ndef ensure_directory(directory, chown_username=None):\n    \"\"\"If missing, makes directories, along the supplied path\"\"\"\n    if not os.path.exists(directory):\n        os.makedirs(directory)\n        if chown_username is None:\n            return\n        user_info = pwd.getpwnam(chown_username)\n        os.chown(directory, user_info.pw_uid, user_info.pw_gid)\n\n\ndef get_checksum(message: str):\n    \"\"\"\n    Goes over each byte of the supplied message and xors it onto the checksum\n    :param message: message to compute the checksum for (usually a gcode)\n    :return the computed checksum\n    \"\"\"\n    checksum = 0\n    for char in message.encode(\"ascii\"):\n        checksum ^= char\n\n\ndef persist_file(file: typing.TextIO):\n    \"\"\"\n    Tells the system to write and sync the file\n\n    Unused\n    \"\"\"\n    file.flush()\n    os.fsync(file.fileno())\n\n\ndef get_gcode(line):\n    \"\"\"\n    Removes comments after the supplied gcode command\n    Makes gcode encodeable in ascii\n    Put any other sanitization here\n    :param line: line of gcode most likely read from a file\n    :return: gcode without the comment at the end\n    \"\"\"\n    unicode_gcode = line.split(\";\", 1)[0].strip()\n    ascii_gcode = unidecode.unidecode(unicode_gcode)\n    return ascii_gcode\n\n\ndef file_is_on_sd(path_parts):\n    \"\"\"Checks if the file path starts wit the sd cards' storage name\"\"\"\n    if len(path_parts) < 2:\n        return False\n    return path_parts[1] == SD_STORAGE_NAME\n\n\ndef make_fingerprint(sn):\n    \"\"\"\n    Uses sha256 to hask the serial number for use as a fingerprint\n    Ideally, we would have the printer's UUID too, but MK3 printers\n    don't have it\n    \"\"\"\n    return sha256(sn.encode()).hexdigest()\n\n\ndef fat_datetime_to_tuple(fat_datetime):\n    \"\"\"\n    Converts datetime from FAT file header to touple of\n    (years, months, days, hours, minutes, seconds)\n\n    >>> assert fat_datetime_to_tuple(0x66a4d55) == \\\n            (1983, 3, 10, 9, 42, 42)\n    \"\"\"\n    seconds = (0b11111 & fat_datetime) * 2\n    minutes = (0b111111 << 5 & fat_datetime) >> 5\n    hours = (0b11111 << 11 & fat_datetime) >> 11\n    days = (0b11111 << 16 & fat_datetime) >> 16\n    months = (0b1111 << 21 & fat_datetime) >> 21\n    years = 1980 + ((0b11111111 << 25 & fat_datetime) >> 25)\n    # Date validation using the python standart library\n    datetime.datetime(year=years,\n                      month=months,\n                      day=days,\n                      hour=hours,\n                      minute=minutes,\n                      second=seconds)\n    return years, months, days, hours, minutes, seconds\n\n\n# pylint: disable=too-many-arguments\ndef get_print_stats_gcode(quiet_percent=-1,\n                          quiet_left=-1,\n                          quiet_change_in=-1,\n                          normal_percent=-1,\n                          normal_left=-1,\n                          normal_change_in=-1):\n    \"\"\"Returns the gcode for setting print stats\"\"\"\n    return (f\"M73 Q{quiet_percent} S{quiet_left} C{quiet_change_in} \"\n            f\"P{normal_percent} R{normal_left} D{normal_change_in}\")\n\n\ndef get_d3_code(address: int, byte_count: int):\n    \"\"\"\n    Gets the D-Code for reading the eeprom\n    :param address: - address in hex\n    :param byte_count: - the number of bytes to read\n\n    Address reference:\n    https://github.com/prusa3d/Prusa-Firmware/blob/MK3/Firmware/eeprom.cpp\n    \"\"\"\n    if not 0 < int(byte_count) < 1000:\n        raise AttributeError(\"Cannot read that many bytes\")\n    if address >= 2**16:\n        raise AttributeError(\"The address needs to be two bytes long\")\n    return f\"D3 Ax{format(address, 'x').upper()} C{byte_count}\"\n\n\ndef round_to_five(number: float):\n    \"\"\"Rounds a number to the nearest five\n\n    >>> round_to_five(23)\n    25\n    >>> round_to_five(22)\n    20\n    >>> round_to_five(22.6)\n    25\n    >>> round_to_five(22.4)\n    20\n    \"\"\"\n    return round(number / 5) * 5\n\n\ndef decode_line(line: bytes):\n    \"\"\"Decode a line read from the printer\"\"\"\n    return line.decode(\"cp437\").strip().replace('\\x00', '')\n\n\ndef is_potato_cpu():\n    \"\"\"Returns True if your CPU is a potato\"\"\"\n    return multiprocessing.cpu_count() == 1\n\n\nclass PrinterDevice:\n    \"\"\"The data model for the usb detected printer\"\"\"\n\n    def __init__(self, vendor_id: str,\n                 model_id: str,\n                 serial_number: str,\n                 path: str):\n        self.vendor_id = vendor_id\n        self.model_id = model_id\n        self.serial_number = serial_number\n        self.path = path\n\n\ndef get_usb_printers():\n    \"\"\"Gets serial devices that are on the supported list\n    and have a valid S/N\"\"\"\n    devices = []\n    context = pyudev.Context()\n    for device in context.list_devices(subsystem='tty'):\n        vendor_id = device.properties.get('ID_VENDOR_ID')\n        if isinstance(vendor_id, str) and vendor_id.startswith(\"0x\"):\n            vendor_id = device.properties.get('ID_USB_VENDOR_ID', \"\")\n\n        model_id = device.properties.get('ID_MODEL_ID')\n        if isinstance(model_id, str) and model_id.startswith(\"0x\"):\n            model_id = device.properties.get('ID_USB_MODEL_ID', \"\")\n\n        path = device.properties.get(\"DEVNAME\", \"\")\n\n        # If the vendor is not supported, we get an empty set\n        supported_models = SUPPORTED_PRINTERS.get(vendor_id, set())\n        is_supported = model_id in supported_models\n        serial_number = device.properties.get(\"ID_SERIAL_SHORT\", \"\")\n        if not serial_number:\n            serial_number = device.properties.get(\n                \"ID_USB_SERIAL_SHORT\", \"\")\n        valid_sn = VALID_SN_REGEX.match(serial_number)\n        if not is_supported or not valid_sn or not path:\n            continue\n\n        device = PrinterDevice(\n            vendor_id=vendor_id,\n            model_id=model_id,\n            serial_number=serial_number,\n            path=path,\n        )\n        devices.append(device)\n    return devices\n\n\ndef walk_dict(data: dict, key_path=None):\n    \"\"\"Walks a dict, yielding the path to each bottom-most value\"\"\"\n    if key_path is None:\n        key_path = []\n    for key, value in data.items():\n        if isinstance(value, dict):\n            yield from walk_dict(value, key_path + [key])\n        else:\n            yield key_path + [key], value\n\n\ndef slots_with_param(model, key, default, value):\n    \"\"\"Fills out the slot information with defaults, only the active one gets\n    the real value\"\"\"\n    slot: Slot = model.latest_telemetry.slot\n    if slot is None:\n        return None\n    active_slot = slot.active\n\n    slots = {}\n    for slot in range(1, MMU_SLOTS + 1):\n        slot_name = str(slot)\n        slots[slot_name] = IndividualSlot()\n        if slot == active_slot:\n            setattr(slots[slot_name], key, value)\n        else:\n            setattr(slots[slot_name], key, default)\n    return slots\n\n\ndef _parse_little_endian_uint32(match):\n    \"\"\"Decodes the D-Code specified little-endian uint32_t eeprom variable\"\"\"\n    str_data = match.group(\"data\").replace(\" \", \"\")\n    data = bytes.fromhex(str_data)\n    return struct.unpack(\"<I\", data)[0]\n\n\ndef power_panic_delay(cfg):\n    \"\"\"Adds a dynamic delay depending on power panic details.\n    This is needed so the printer reaches a stable state before we reset it.\"\"\"\n    pp_file_path = cfg.daemon.power_panic_file\n    if not os.path.exists(pp_file_path):\n        return\n\n    with open(pp_file_path, \"r\", encoding=\"UTF-8\") as pp_file:\n        pp_data = PPData(**json.load(pp_file))\n        if pp_data.using_rip_port:\n            return\n\n        log.info(\"Waiting an extra %ss for printer to heat up \"\n                 \"and finish its moves\", PP_MOVES_DELAY)\n        sleep(PP_MOVES_DELAY)\n"
  },
  {
    "path": "prusa/link/web/__init__.py",
    "content": "\"\"\"Init file for web application module.\"\"\"\nimport logging\nfrom threading import Thread\nfrom time import sleep\nfrom wsgiref.simple_server import make_server\n\nfrom ..util import prctl_name\nfrom .lib.auth import REALM\nfrom .lib.classes import RequestHandler, ThreadingServer\nfrom .lib.core import app\nfrom .lib.wizard import Wizard\nfrom .link_info import link_info\n\nlog = logging.getLogger(__name__)\n\n__import__('errors', globals=globals(), level=1)\n__import__('main', globals=globals(), level=1)\n__import__('wizard', globals=globals(), level=1)\n__import__('files', globals=globals(), level=1)\n__import__('files_legacy', globals=globals(), level=1)\n__import__('connection', globals=globals(), level=1)\n__import__('settings', globals=globals(), level=1)\n__import__('controls', globals=globals(), level=1)\n__import__('cameras', globals=globals(), level=1)\n\n\ndef init_web_app(daemon):\n    \"\"\"Initializes the app object for the web server to use\"\"\"\n    app.cfg = daemon.cfg\n    app.settings = daemon.settings\n    app.debug = daemon.cfg.debug\n\n    app.daemon = daemon\n\n    service_local = app.settings.service_local\n    if service_local.username and service_local.digest:\n        app.auth_map.set(REALM,\n                         service_local.username,\n                         service_local.digest)\n        log.info(\"Authentication was set\")\n    else:\n        log.info(\"No authentication was set\")\n\n    if service_local.api_key:\n        app.api_key = service_local.api_key\n        log.info(\"Api-Key was set.\")\n    else:\n        log.info(\"No Api-Key was set.\")\n\n    if app.settings.is_wizard_needed():\n        app.wizard = Wizard(app)\n\n    if app.cfg.http.link_info:\n        log.warning('Page /link-info is enabled!')\n        app.set_route('/link-info', link_info)\n\n\nclass WebServer:\n    \"\"\"A web server class for PrusaLink components\"\"\"\n\n    def __init__(self, application, address, port, exit_on_error=False):\n        \"\"\"Set application variables.\"\"\"\n        self.application = application\n        self.address = address\n        self.port = port\n        self.exit_on_error = exit_on_error\n\n        self.thread = None\n        self.httpd = None\n\n    def start(self):\n        \"\"\"Starts the server\"\"\"\n        self.thread = Thread(\n            target=self.run, daemon=True, name=\"httpd\")\n        self.thread.start()\n\n    def run(self):\n        \"\"\"Code for the server thread\"\"\"\n        prctl_name()\n\n        log.info('Starting server for http://%s:%d', self.address,\n                 self.port)\n        while True:\n            self.httpd = make_server(self.address,\n                                     self.port,\n                                     self.application,\n                                     server_class=ThreadingServer,\n                                     handler_class=RequestHandler)\n            self.httpd.timeout = 0.5\n\n            try:\n                self.httpd.serve_forever()\n            except Exception:  # pylint: disable=broad-except\n                log.exception(\"Exception in httpd\")\n                if self.exit_on_error:\n                    log.info(\"Shutdown http\")\n                    raise\n                log.info(\"Restarting httpd\")\n                sleep(1)\n                continue\n            else:\n                log.info(\"Shutdown http\")\n                return\n\n    def stop(self):\n        \"\"\"Stops the server\"\"\"\n        if not self.httpd:\n            return\n        self.httpd.shutdown()\n        self.thread.join()\n        log.info('Server stopped')\n\n\n__all__ = ['WebServer', 'init_web_app']\n"
  },
  {
    "path": "prusa/link/web/cameras.py",
    "content": "\"\"\"Camera web API - /api/v1/cameras handlers\"\"\"\nfrom datetime import datetime, timedelta\nfrom time import sleep, time\n\nfrom poorwsgi import state\nfrom poorwsgi.response import JSONResponse, Response\nfrom prusa.connect.printer.camera import Camera\nfrom prusa.connect.printer.const import (\n    TRIGGER_SCHEME_TO_SECONDS,\n    CameraAlreadyConnected,\n    CameraNotDetected,\n    CapabilityType,\n    ConfigError,\n    NotSupported,\n)\n\nfrom ..const import (\n    CAMERA_REGISTER_TIMEOUT,\n    HEADER_DATETIME_FORMAT,\n    QUIT_INTERVAL,\n    TIME_FOR_SNAPSHOT,\n)\nfrom .lib.auth import check_api_digest\nfrom .lib.core import app\n\nDEFAULT_PHOTO_EXPIRATION_TIMEOUT = 30  # 30s\n\n\ndef format_header(header):\n    \"\"\"Return datetime header in correct format\"\"\"\n    return header.strftime(HEADER_DATETIME_FORMAT)\n\n\ndef photo_by_camera_id(camera_id, req):\n    \"\"\"Returns the response for two endpoints\n    \"snap\" on the first camera in order and \"snap\" on a specific camera\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n\n    if not camera_configurator.is_connected(camera_id):\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} is\"\n                                    f\" not available\")\n    driver = camera_configurator.loaded[camera_id]\n    if driver.last_snapshot is None:\n        return JSONResponse(status_code=state.HTTP_NO_CONTENT,\n                            message=f\"Camera with id: {camera_id} did not \"\n                                    f\"take a photo yet.\")\n\n    trigger_scheme = camera_controller.get_camera(camera_id).trigger_scheme\n\n    photo_timeout = TRIGGER_SCHEME_TO_SECONDS.get(\n        trigger_scheme, DEFAULT_PHOTO_EXPIRATION_TIMEOUT)\n\n    # Give PrusaLink some time to take a new snapshot\n    timeout = photo_timeout + TIME_FOR_SNAPSHOT\n\n    last_modified_timestamp = driver.last_snapshot.timestamp\n    last_modified = datetime.utcfromtimestamp(last_modified_timestamp)\n    expires = last_modified + timedelta(seconds=timeout)\n\n    headers = {\n        'Date': format_header(datetime.utcnow()),\n        'Last-Modified': format_header(last_modified),\n        'Expires': format_header(expires),\n        'Cache-Control': f'private, max-age={timeout}',\n    }\n\n    if 'If-Modified-Since' in req.headers:\n        header_datetime = datetime.strptime(req.headers['If-Modified-Since'],\n                                            HEADER_DATETIME_FORMAT)\n\n        if last_modified <= header_datetime:\n            return Response(status_code=state.HTTP_NOT_MODIFIED,\n                            headers=headers)\n\n    return Response(driver.last_snapshot.data,\n                    headers=headers,\n                    content_type='image/jpeg')\n\n\n@app.route(\"/api/v1/cameras/snap\", method=state.METHOD_GET)\n@check_api_digest\ndef default_camera_snap(req):\n    \"\"\"Return the last photo of the default (first in order) camera\"\"\"\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n\n    for camera in camera_controller.cameras_in_order:\n        if not camera.supports(CapabilityType.IMAGING):\n            continue\n        return photo_by_camera_id(camera.camera_id, req)\n    return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                        message=\"Camera is not available\")\n\n\n@app.route(\"/api/v1/cameras\", method=state.METHOD_GET)\n@check_api_digest\ndef list_cameras(_):\n    \"\"\"List cameras in order, with disconnected cameras at the bottom\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    id_list = []\n    camera_list = []\n    for camera_id in camera_configurator.order:\n        if camera_id not in camera_configurator.loaded:\n            continue\n        id_list.append(camera_id)\n\n    for camera_id in camera_configurator.loaded:\n        if camera_id not in id_list:\n            id_list.append(camera_id)\n\n    for camera_id in id_list:\n        config = camera_configurator.loaded[camera_id].config\n        camera_controller = camera_configurator.camera_controller\n        connected = camera_configurator.is_connected(camera_id)\n        registered = False\n        if connected:\n            camera = camera_controller.get_camera(camera_id)\n            registered = camera.is_registered\n        list_item = {\n            \"camera_id\": camera_id,\n            \"config\": config,\n            \"connected\": connected,\n            \"detected\": camera_id in camera_configurator.detected,\n            \"stored\": camera_id in camera_configurator.stored,\n            \"registered\": registered,\n        }\n        camera_list.append(list_item)\n\n    return JSONResponse(**{\"camera_list\": camera_list})\n\n\n@app.route(\"/api/v1/cameras\", method=state.METHOD_PUT)\n@check_api_digest\ndef set_order(req):\n    \"\"\"Sets order of the cameras\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    camera_order = req.json\n    camera_configurator.set_order(camera_order)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/snap\", method=state.METHOD_GET)\n@check_api_digest\ndef get_photo_by_camera_id(req, camera_id):\n    \"\"\"Gets the last image from the specified camera\"\"\"\n    return photo_by_camera_id(camera_id, req)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/snap\", method=state.METHOD_POST)\n@check_api_digest\ndef take_photo_by_camera_id(_, camera_id):\n    \"\"\"Capture an image from the specified camera and return it\"\"\"\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n    if camera_id not in camera_controller:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} is\"\n                                    f\" not available\")\n    camera = camera_controller.get_camera(camera_id)\n    try:\n        photo = camera.take_a_photo()\n    except TimeoutError:\n        # see SDK CAMERA_WAIT_TIMEOUT - in const.py\n        return JSONResponse(status_code=state.HTTP_REQUEST_TIME_OUT,\n                            message=f\"Camera with id: {camera_id} did not \"\n                                    f\"return a photo in time\")\n    except NotSupported as error:\n        return JSONResponse(status_code=state.HTTP_CONFLICT,\n                            message=f\"Camera with id: {camera_id} \"\n                                    f\"cannot take the picture: {error}\")\n    return Response(photo, content_type='image/jpeg')\n\n\n@app.route(\"/api/v1/cameras/<camera_id>\", method=state.METHOD_GET)\n@check_api_digest\ndef camera_config(_, camera_id):\n    \"\"\"Gets the specified camera's config\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n    if camera_id not in camera_configurator.loaded:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} is not \"\n                                    f\"configured\")\n    if camera_id not in camera_controller:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} was not \"\n                                    f\"found among the connected cameras\")\n    camera = camera_controller.get_camera(camera_id)\n    settings = camera.get_settings()\n    json_settings = camera.json_from_settings(settings)\n    if CapabilityType.RESOLUTION in camera.capabilities:\n        json_settings[\"available_resolutions\"] = [\n            dict(resolution)\n            for resolution in camera.available_resolutions\n        ]\n    string_caps = map(lambda i: i.name, camera.capabilities)\n    json_settings[\"capabilities\"] = list(string_caps)\n    return JSONResponse(**json_settings)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>\", method=state.METHOD_POST)\n@check_api_digest\ndef add_camera(req, camera_id):\n    \"\"\"Either set up a new camera or fix a broken one.\n    Does not allow changing settings on a working one!\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n\n    config = req.json.get('config')\n    if config is None:\n        return JSONResponse(status_code=state.HTTP_BAD_REQUEST,\n                            message=\"Configuration is missing. \"\n                                    \"Cannot add a camera by ID alone.\")\n    try:\n        camera_configurator.add_camera(camera_id, config)\n    except CameraNotDetected as error:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera could not be added using \"\n                                    f\"the supplied ID: {error}\")\n    except CameraAlreadyConnected:\n        return JSONResponse(status_code=state.HTTP_CONFLICT,\n                            message=f\"Camera with id: {camera_id} is already \"\n                                    f\"running, modification is not allowed. \"\n                                    f\"Delete it first\")\n    except ConfigError as exception:\n        return JSONResponse(status_code=state.HTTP_BAD_REQUEST,\n                            message=f\"Camera could not be created using the \"\n                                    f\"supplied config: {exception}\")\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>\", method=state.METHOD_DELETE)\n@check_api_digest\ndef delete_camera(_, camera_id):\n    \"\"\"Removes the camera and its config\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    if camera_id not in camera_configurator.loaded:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} is not \"\n                                    f\"configured\")\n    if camera_id in camera_configurator.detected:\n        return JSONResponse(status_code=state.HTTP_CONFLICT,\n                            message=\"Cannot remove an auto-detected camera\")\n    camera_configurator.remove_camera(camera_id)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/config\", method=state.METHOD_PATCH)\n@check_api_digest\ndef set_settings(req, camera_id):\n    \"\"\"Set new settings to a working camera\"\"\"\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n    if camera_id not in camera_controller:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} was not \"\n                                    f\"found among the connected cameras\")\n    camera = camera_controller.get_camera(camera_id)\n    json_settings = req.json\n    settings = Camera.settings_from_json(json_settings)\n    try:\n        camera.set_settings(settings)\n    except TimeoutError:\n        return JSONResponse(status_code=state.HTTP_INTERNAL_SERVER_ERROR,\n                            message=f\"Camera with id: {camera_id} is busy \"\n                                    f\"for an unreasonably long time\")\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/config\", method=state.METHOD_DELETE)\n@check_api_digest\ndef reset_settings(_, camera_id):\n    \"\"\"Reset settings of a camera\"\"\"\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    if not camera_configurator.is_connected(camera_id):\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} was not \"\n                                    f\"found among the connected cameras\")\n    camera_configurator.reset_to_defaults(camera_id)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/connection\", method=state.METHOD_POST)\n@check_api_digest\ndef register_camera(_, camera_id):\n    \"\"\"Registers a camera to Connect\"\"\"\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n    if camera_id not in camera_controller:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} was not \"\n                                    f\"found among the connected cameras\")\n    camera = camera_controller.get_camera(camera_id)\n    if camera.is_registered:\n        return JSONResponse(status_code=state.HTTP_CONFLICT,\n                            message=f\"Camera: {camera_id} is already \"\n                                    \"registered.\")\n\n    camera_controller.register_camera(camera_id)\n    timeout_at = time() + CAMERA_REGISTER_TIMEOUT\n    while not camera.is_registered:\n        if time() > timeout_at:\n            return JSONResponse(status_code=state.HTTP_REQUEST_TIME_OUT,\n                                message=\"Timed out when registering \"\n                                        f\"camera: {camera_id}\")\n        sleep(QUIT_INTERVAL)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route(\"/api/v1/cameras/<camera_id>/connection\",\n           method=state.METHOD_DELETE)\n@check_api_digest\ndef unregister_camera(_, camera_id):\n    \"\"\"Un-registers a camera from Connect\"\"\"\n    camera_controller = app.daemon.prusa_link.printer.camera_controller\n    if camera_id not in camera_controller:\n        return JSONResponse(status_code=state.HTTP_NOT_FOUND,\n                            message=f\"Camera with id: {camera_id} was not \"\n                                    \"found among the connected cameras\")\n    camera = camera_controller.get_camera(camera_id)\n    if not camera.is_registered:\n        return JSONResponse(status_code=state.HTTP_CONFLICT,\n                            message=\"Cannot unregister a non-registered \"\n                                    f\"camera: {camera_id}\")\n\n    camera.set_token(None)\n    return Response(status_code=state.HTTP_OK)\n"
  },
  {
    "path": "prusa/link/web/connection.py",
    "content": "\"\"\"/api/connection endpoint handlers\"\"\"\nfrom socket import gethostbyname\nfrom urllib import parse\nfrom urllib.request import urlopen\n\nfrom poorwsgi import state\nfrom poorwsgi.response import JSONResponse\nfrom prusa.connect.printer import Printer\nfrom prusa.connect.printer.const import RegistrationStatus\n\nfrom .. import conditions\nfrom ..conditions import use_connect_errors\nfrom .lib.auth import check_api_digest\nfrom .lib.core import app\nfrom .main import PRINTER_STATES\n\n\ndef compose_register_url(printer, connect_url, name, location):\n    \"\"\"Compose and return url for Connect registration\"\"\"\n    printer.connection_from_settings(app.settings)\n    code = printer.register()\n    url = f\"{connect_url}/add-printer/connect/{printer.type}/{code}\"\n\n    printer_info = {}\n\n    # If the name and the location were an empty strings, don't add them to url\n    if name or location:\n        if name:\n            printer_info.update({\"name\": name})\n        if location:\n            printer_info.update({\"location\": location})\n\n        url += f\"?{parse.urlencode(printer_info)}\"\n    return url\n\n\n@app.route('/api/connection')\n@check_api_digest\ndef api_connection(req):\n    \"\"\"Returns printer connection info\"\"\"\n    # pylint: disable=unused-argument\n    service_connect = app.daemon.settings.service_connect\n    cfg = app.daemon.cfg\n    printer_state = app.daemon.prusa_link.printer.state\n\n    # Registration code from Connect - if code exists, there's registration\n    # in progress\n    code = app.daemon.prusa_link.printer.code\n\n    registration = RegistrationStatus.NO_REGISTRATION\n\n    # Token is available only after successful registration to Connect\n    if bool(service_connect.token):\n        registration = RegistrationStatus.FINISHED\n    elif code:\n        registration = RegistrationStatus.IN_PROGRESS\n\n    return JSONResponse(\n        **{\n            \"current\": {\n                \"baudrate\": cfg.printer.baudrate,\n                \"port\": cfg.printer.port,\n                \"printerProfile\": \"_default\",\n                \"state\": PRINTER_STATES[printer_state],\n            },\n            \"options\": {\n                \"ports\": [cfg.printer.port],\n                \"baudrates\": [cfg.printer.baudrate],\n                \"printerProfiles\": [{\n                    \"id\": \"_default\",\n                    \"name\": \"Prusa MK3S\",\n                }],\n                \"autoconnect\": True,\n            },\n            \"connect\": {\n                \"hostname\": service_connect.hostname,\n                \"port\": service_connect.port,\n                \"tls\": bool(service_connect.tls),\n                \"registration\": registration.value,\n                \"code\": code,\n            },\n            \"states\": {\n                \"printer\": conditions.printer_status(),\n                \"connect\": conditions.connect_status(),\n            },\n        })\n\n\n@app.route('/api/connection', method=state.METHOD_POST)\n@check_api_digest\ndef api_connection_set(req):\n    \"\"\"Returns URL for Connect registration completion\"\"\"\n    if app.settings.service_connect.token:\n        return JSONResponse(status_code=state.HTTP_CONFLICT)\n\n    service_connect = app.daemon.settings.service_connect\n    printer_settings = app.daemon.settings.printer\n    printer = app.daemon.prusa_link.printer\n\n    connect = req.json.get('connect')\n    hostname = connect.get('hostname')\n    port = connect.get('port')\n    tls = bool(connect.get('tls'))\n\n    try:\n        gethostbyname(hostname)\n    except Exception as exc:  # pylint: disable=broad-except\n        raise conditions.CantResolveHostname() from exc\n\n    connect_url = Printer.connect_url(hostname, tls, port)\n\n    try:\n        with urlopen(f'{connect_url}/info'):\n            pass\n    except Exception as exc:  # pylint: disable=broad-except\n        raise conditions.CantConnect() from exc\n\n    app.settings.service_connect.hostname = hostname\n    app.settings.service_connect.port = port\n    app.settings.service_connect.tls = tls\n\n    app.settings.update_sections()\n\n    register_url = compose_register_url(printer=printer,\n                                        connect_url=connect_url,\n                                        name=printer_settings.name,\n                                        location=printer_settings.location)\n\n    service_connect.hostname = hostname\n    service_connect.port = port\n    service_connect.tls = tls\n\n    return JSONResponse(status_code=state.HTTP_OK, url=register_url)\n\n\n@app.route('/api/connection', method=state.METHOD_DELETE)\n@check_api_digest\ndef api_connection_delete(req):\n    \"\"\"Cancel Connect registration and delete token from ini file\"\"\"\n    # pylint: disable=unused-argument\n    app.settings.service_connect.token = \"\"\n    use_connect_errors(False)\n\n    app.settings.update_sections()\n    app.daemon.prusa_link.printer.connection_from_settings(app.settings)\n\n    with open(app.daemon.cfg.printer.settings, 'w', encoding='utf-8') as ini:\n        app.daemon.settings.write(ini)\n\n    return JSONResponse(status_code=state.HTTP_OK)\n"
  },
  {
    "path": "prusa/link/web/controls.py",
    "content": "\"\"\"/api/printer endpoint handlers\"\"\"\n\nfrom poorwsgi import state\nfrom poorwsgi.response import JSONResponse\nfrom prusa.connect.printer.const import State\n\nfrom ..conditions import (\n    CantMoveAxis,\n    CantMoveAxisZ,\n    CurrentlyPrinting,\n    TemperatureTooHigh,\n    TemperatureTooLow,\n    ValueTooHigh,\n    ValueTooLow,\n)\nfrom ..const import LimitsMK3\nfrom ..serial.helpers import enqueue_instruction\nfrom .lib.auth import check_api_digest\nfrom .lib.core import app\n\n\ndef check_temperature_limits(temperature, min_temperature, max_temperature):\n    \"\"\"Check target temperature limits and raise error if out of limits\"\"\"\n    if temperature < min_temperature:\n        raise TemperatureTooLow()\n    if temperature > max_temperature:\n        raise TemperatureTooHigh()\n\n\ndef check_value_limits(value, min_value, max_value):\n    \"\"\"Check target value limits and raise error if out of limits\"\"\"\n    if value < min_value:\n        raise ValueTooLow()\n    if value > max_value:\n        raise ValueTooHigh()\n\n\ndef jog(req, serial_queue):\n    \"\"\"XYZ movement command\"\"\"\n    # pylint: disable=too-many-branches\n\n    # Compatibility with OctoPrint, OP speed == Prusa feedrate in mm/min\n    # If feedrate is not defined, use maximum value for X axis\n    feedrate = (req.json.get('feedrate')\n                or req.json.get('speed', LimitsMK3.feedrate_x_max))\n\n    check_value_limits(feedrate,\n                       LimitsMK3.feedrate_x_min, LimitsMK3.feedrate_x_max)\n\n    absolute = req.json.get('absolute')\n    axes = []\n\n    # --- Coordinates ---\n    x_axis = req.json.get('x')\n    y_axis = req.json.get('y')\n    z_axis = req.json.get('z')\n\n    if x_axis is not None:\n        if absolute:\n            check_value_limits(x_axis,\n                               LimitsMK3.position_x_min,\n                               LimitsMK3.position_x_max)\n        axes.append(f'X{x_axis}')\n\n    if y_axis is not None:\n        if absolute:\n            check_value_limits(y_axis,\n                               LimitsMK3.position_y_min,\n                               LimitsMK3.position_y_max)\n        axes.append(f'Y{y_axis}')\n\n    if z_axis is not None:\n        if absolute:\n            check_value_limits(z_axis,\n                               LimitsMK3.position_z_min,\n                               LimitsMK3.position_z_max)\n        axes.append(f'Z{z_axis}')\n\n    if absolute:\n        # G90 - absolute movement\n        enqueue_instruction(serial_queue, 'G90')\n    else:\n        # G91 - relative movement\n        enqueue_instruction(serial_queue, 'G91')\n\n    # G1 - linear movement in given axes\n    gcode = f'G1 F{feedrate} {axes}'\n    enqueue_instruction(serial_queue, gcode)\n\n\ndef home(req, serial_queue):\n    \"\"\"XYZ homing command\"\"\"\n    axes = req.json.get('axes')\n    if axes:\n        axes = list(map(str.upper, axes))\n    else:\n        axes = ['X', 'Y', 'Z']\n    gcode = f'G28 {axes}'\n    enqueue_instruction(serial_queue, gcode)\n\n\ndef set_speed(req, serial_queue):\n    \"\"\"Speed set command\"\"\"\n    factor = req.json.get('factor', 100)\n    check_value_limits(factor,\n                       LimitsMK3.print_speed_min, LimitsMK3.print_speed_max)\n\n    gcode = f'M220 S{factor}'\n    enqueue_instruction(serial_queue, gcode)\n\n\ndef disable_steppers(serial_queue):\n    \"\"\"Disable steppers command\"\"\"\n    gcode = 'M84'\n    enqueue_instruction(serial_queue, gcode)\n\n\ndef extrude(req, serial_queue):\n    \"\"\"Extrude given amount of filament in mm, negative value will retract\"\"\"\n    amount = req.json.get('amount')\n    # Compatibility with OctoPrint, OP speed == Prusa feedrate in mm/min\n    # If feedrate is not defined, use maximum value for E axis\n    feedrate = (req.json.get('feedrate')\n                or req.json.get('speed', LimitsMK3.feedrate_e_max))\n\n    check_value_limits(feedrate,\n                       LimitsMK3.feedrate_e_min, LimitsMK3.feedrate_e_max)\n\n    # M83 - relative movement for axis E\n    enqueue_instruction(serial_queue, 'M83')\n\n    gcode = f'G1 F{feedrate} E{amount}'\n    enqueue_instruction(serial_queue, gcode)\n\n\n@app.route('/api/printer/printhead', method=state.METHOD_POST)\n@check_api_digest\ndef api_printhead(req):\n    \"\"\"Control the printhead movement in XYZ axes\"\"\"\n    serial_queue = app.daemon.prusa_link.serial_queue\n    printer_state = app.daemon.prusa_link.model.state_manager.current_state\n    command = req.json.get('command')\n    status = state.HTTP_NO_CONTENT\n\n    if command == 'jog':\n        if req.json.get('z'):\n            if printer_state in \\\n                    (State.IDLE, State.READY, State.FINISHED, State.STOPPED):\n                jog(req, serial_queue)\n            else:\n                raise CantMoveAxisZ()\n        else:\n            if printer_state in (State.IDLE, State.READY, State.FINISHED,\n                                 State.PAUSED, State.STOPPED):\n                jog(req, serial_queue)\n            else:\n                raise CantMoveAxis()\n\n    if command == 'home':\n        if printer_state in (State.IDLE, State.READY):\n            home(req, serial_queue)\n        else:\n            raise CantMoveAxis()\n\n    # Compatibility with OctoPrint, OP feedrate == Prusa speed in %\n    if command in ('speed', 'feedrate'):\n        set_speed(req, serial_queue)\n\n    if command == \"disable_steppers\":\n        disable_steppers(serial_queue)\n\n    return JSONResponse(status_code=status)\n\n\n@app.route('/api/printer/tool', method=state.METHOD_POST)\n@check_api_digest\ndef api_tool(req):\n    \"\"\"Control the extruder, including E axis\"\"\"\n    serial_queue = app.daemon.prusa_link.serial_queue\n    tel = app.daemon.prusa_link.model.latest_telemetry\n    printer_state = app.daemon.prusa_link.printer.state\n    command = req.json.get('command')\n    status = state.HTTP_NO_CONTENT\n\n    if command == 'target':\n        targets = req.json.get('targets')\n\n        # Compability with OctoPrint, which uses more tools, here only tool0\n        tool = targets['tool0']\n\n        check_temperature_limits(tool,\n                                 LimitsMK3.temp_nozzle_min,\n                                 LimitsMK3.temp_nozzle_max)\n\n        gcode = f'M104 S{tool}'\n        enqueue_instruction(serial_queue, gcode)\n\n    if command == 'extrude':\n        if tel.temp_nozzle < LimitsMK3.min_temp_nozzle_e:\n            raise TemperatureTooLow()\n        if printer_state is State.PRINTING:\n            raise CurrentlyPrinting()\n\n        extrude(req, serial_queue)\n\n    if command == 'flowrate':\n        factor = req.json.get('factor')\n\n        check_value_limits(factor,\n                           LimitsMK3.print_flow_min, LimitsMK3.print_flow_max)\n\n        gcode = f'M221 S{factor}'\n        enqueue_instruction(serial_queue, gcode)\n\n    return JSONResponse(status_code=status)\n\n\n@app.route('/api/printer/bed', method=state.METHOD_POST)\n@check_api_digest\ndef api_bed(req):\n    \"\"\"Control the heatbed temperature\"\"\"\n    serial_queue = app.daemon.prusa_link.serial_queue\n    command = req.json.get('command')\n\n    if command == 'target':\n        target = req.json.get('target')\n\n        check_temperature_limits(target,\n                                 LimitsMK3.temp_bed_min,\n                                 LimitsMK3.temp_bed_max)\n\n        gcode = f'M140 S{target}'\n        enqueue_instruction(serial_queue, gcode)\n\n    return JSONResponse(status_code=state.HTTP_NO_CONTENT)\n"
  },
  {
    "path": "prusa/link/web/errors.py",
    "content": "\"\"\"Zakladní obecná obsluha url.\"\"\"\nimport logging\nfrom sys import exc_info\nfrom traceback import format_tb\n\nfrom poorwsgi.response import make_response\nfrom poorwsgi.state import METHOD_ALL\n\nfrom .. import conditions\nfrom .lib.core import app\nfrom .lib.view import generate_page\n\nlog = logging.getLogger(__name__)\n\n\ndef response_error(req, error: conditions.LinkError):\n    \"\"\"Create response from LinkError\"\"\"\n    error.set_url(req)\n    if req.accept_json:\n        return error.json_response()\n    if req.accept_html:\n        return make_response(generate_page(req,\n                                           error.template,\n                                           title=error.title,\n                                           text=error.text,\n                                           status_code=error.status_code),\n                             status_code=error.status_code)\n    return error.text_response()\n\n\n@app.http_state(500)\n@app.route('/error/internal-server-error')\ndef internal_server_error(req):\n    \"\"\"Error handler 500 Internal Server Error.\"\"\"\n    _, exception, traceback = exc_info()\n    if req.path != '/error/internal-server-error':\n        traceback = format_tb(traceback)\n        log.error('\\n%s%s', ''.join(traceback), repr(exception))\n\n    error = conditions.InternalServerError()\n    error.set_url(req)\n\n    try:\n        if req.accept_json:\n            return error.json_response()\n        if req.accept_html:\n            kwargs = {}\n            if app.debug and traceback:\n                kwargs[\"traceback\"] = traceback\n\n            return make_response(generate_page(req,\n                                               error.template,\n                                               error=repr(exception),\n                                               **kwargs),\n                                 status_code=500)\n\n    except Exception:  # pylint: disable=broad-except\n        log.exception()\n    return error.text_response()\n\n\n@app.http_state(403)\n@app.route('/error/forbidden')\ndef forbidden(req):\n    \"\"\"Error handler 403 forbidden.\"\"\"\n    return response_error(req, conditions.ForbiddenError())\n\n\n@app.http_state(404)\n@app.route('/error/not-found')\ndef not_found(req):\n    \"\"\"Error handler for 404 Not Found.\"\"\"\n    return response_error(req, conditions.NotFoundError())\n\n\n@app.route('/error/no-file-in-request')\ndef no_file_in_request(req):\n    \"\"\"Error handler for 400 File not found in request payload.\"\"\"\n    return response_error(req, conditions.NoFileInRequest())\n\n\n@app.route('/error/file-size-mismatch')\ndef file_size_mismatch(req):\n    \"\"\"Error handler for 400 File size mismatch.\"\"\"\n    return response_error(req, conditions.FileSizeMismatch())\n\n\n@app.route('/error/forbidden-characters')\ndef forbidden_characters(req):\n    \"\"\"Error handler for 400 Forbidden Characters.\"\"\"\n    return response_error(req, conditions.ForbiddenCharacters())\n\n\n@app.route('/error/filename-too-long')\ndef filename_too_long(req):\n    \"\"\"Error handler for 400 Filename Too Long\"\"\"\n    return response_error(req, conditions.FilenameTooLong())\n\n\n@app.route('/error/foldername-too-long')\ndef foldername_too_long(req):\n    \"\"\"Error handler for 400 Foldername Too Long\"\"\"\n    return response_error(req, conditions.FoldernameTooLong())\n\n\n@app.route('/error/sdcard-not-supported')\ndef sdcard_not_supported(req):\n    \"\"\"Error handler for 404 Some operations are not possible on SDCard.\"\"\"\n    return response_error(req, conditions.SDCardNotSupported())\n\n\n@app.route('/error/location-not-found')\ndef location_not_found(req):\n    \"\"\"Error handler for 404 Location from url not found.\"\"\"\n    return response_error(req, conditions.LocationNotFound())\n\n\n@app.route('/error/file-currently-printed')\ndef file_currently_printed(req):\n    \"\"\"Error handler for 409 File is currently printed.\"\"\"\n    return response_error(req, conditions.FileCurrentlyPrinted())\n\n\n@app.route('/error/transfer-conflict')\ndef transfer_conflict(req):\n    \"\"\"Error handler for 409 Already in transfer process.\"\"\"\n    return response_error(req, conditions.TransferConflict())\n\n\n@app.route('/error/unavailable-update')\ndef unavailable_update(req):\n    \"\"\"Error handler for 409 Unavailable update.\"\"\"\n    return response_error(req, conditions.UnavailableUpdate())\n\n\n@app.route('/error/unable-to-update')\ndef unable_to_update(req):\n    \"\"\"Error handler for 409 Unable to update.\"\"\"\n    return response_error(req, conditions.UnableToUpdate())\n\n\n@app.route('/error/entity-too-large')\ndef entity_too_large(req):\n    \"\"\"Error handler for 413 Payload Too Large\"\"\"\n    return response_error(req, conditions.EntityTooLarge())\n\n\n@app.route('/error/unsupported-media-type')\ndef unsupported_media_type(req):\n    \"\"\"Error handler for 415 Unsupported Media Type\"\"\"\n    return response_error(req, conditions.UnsupportedMediaError())\n\n\n@app.route('/error/response-timeout')\ndef response_timeout(req):\n    \"\"\"Error handler for 500 Response Timeout\"\"\"\n    return response_error(req, conditions.ResponseTimeout())\n\n\n@app.route('/error/cant-connect')\ndef cant_connect(req):\n    \"\"\"Error handler for 400 Can't connect\"\"\"\n    return response_error(req, conditions.CantConnect())\n\n\n@app.route('/error/cant-move-axis')\ndef cant_move_axis(req):\n    \"\"\"Error handler for 400 Can't move axis\"\"\"\n    return response_error(req, conditions.CantMoveAxis())\n\n\n@app.route('/error/cant-move-axis-z')\ndef cant_move_axis_z(req):\n    \"\"\"Error handler for 400 Can't move axis Z\"\"\"\n    return response_error(req, conditions.CantMoveAxisZ())\n\n\n@app.route('/error/cant-resolve-hostname')\ndef cant_resolve_hostname(req):\n    \"\"\"Error handler for 400 Can't resolve hostname\"\"\"\n    return response_error(req, conditions.CantResolveHostname())\n\n\n@app.route('/error/destination-same-as-source')\ndef destination_same_as_source(req):\n    \"\"\"Error handler for 400 Destination same as source\"\"\"\n    return response_error(req, conditions.DestinationSameAsSource())\n\n\n@app.route('/error/dir-not-empty')\ndef directory_not_empty(req):\n    \"\"\"Error handler for 409 Directory not empty\"\"\"\n    return response_error(req, conditions.DirectoryNotEmpty())\n\n\n@app.route('/error/file-already-exists')\ndef file_already_exists(req):\n    \"\"\"Error handler for 409 File already exists\"\"\"\n    return response_error(req, conditions.FileAlreadyExists())\n\n\n@app.route('/error/file-upload-failed')\ndef file_upload_failed(req):\n    \"\"\"Error handler for 400 File upload failed\"\"\"\n    return response_error(req, conditions.FileUploadFailed())\n\n\n@app.route('/error/folder-already-exists')\ndef folder_already_exists(req):\n    \"\"\"Error handler for 409 Folder already exists\"\"\"\n    return response_error(req, conditions.FolderAlreadyExists())\n\n\n@app.route('/error/invalid-boolean-header')\ndef invalid_boolean_header(req):\n    \"\"\"Error handler for 400 Invalid boolean header\"\"\"\n    return response_error(req, conditions.InvalidBooleanHeader())\n\n\n@app.route('/error/length-required')\ndef length_required(req):\n    \"\"\"Error handler for 411 Length required\"\"\"\n    return response_error(req, conditions.LengthRequired())\n\n\n@app.route('/error/not-state-to-print')\ndef not_state_to_print(req):\n    \"\"\"Error handler for 409 Not state to print\"\"\"\n    return response_error(req, conditions.NotStateToPrint())\n\n\n@app.route('/error/storage-not-exist')\ndef storage_not_exist(req):\n    \"\"\"Error handler for 409 Storage not exist\"\"\"\n    return response_error(req, conditions.StorageNotExist())\n\n\n@app.route('/error/temperature-too-high')\ndef temperature_too_high(req):\n    \"\"\"Error handler for 400 Temperature too high\"\"\"\n    return response_error(req, conditions.TemperatureTooHigh())\n\n\n@app.route('/error/temperature-too-low')\ndef temperature_too_low(req):\n    \"\"\"Error handler for 400 Temperature too low\"\"\"\n    return response_error(req, conditions.TemperatureTooLow())\n\n\n@app.route('/error/transfer-stopped')\ndef transfer_stopped(req):\n    \"\"\"Error handler for 409 Transfer stopped\"\"\"\n    return response_error(req, conditions.TransferStopped())\n\n\n@app.route('/error/value-too-high')\ndef value_too_high(req):\n    \"\"\"Error handler for 400 Value too high\"\"\"\n    return response_error(req, conditions.ValueTooHigh())\n\n\n@app.route('/error/value-too-low')\ndef value_too_low(req):\n    \"\"\"Error handler for 400 Value too low\"\"\"\n    return response_error(req, conditions.ValueTooLow())\n\n\n@app.http_state(410)\ndef gone(req):\n    \"\"\"Error handler for 410 Gone.\n\n    This handler is called only when wizard is done and someone try to\n    access it.\n    \"\"\"\n    return make_response(generate_page(req,\n                                       \"error-gone.html\",\n                                       error=exc_info()),\n                         status_code=410)\n\n\n@app.http_state(503)\n@app.route('/error/printer-unavailable')\ndef service_unavailable(req):\n    \"\"\"Error handler for 503 Service Unavailable.\"\"\"\n    _, error, traceback = exc_info()\n    traceback = format_tb(traceback)\n    log.error('\\n%s%s', ''.join(traceback), repr(error))\n\n    return response_error(req, conditions.PrinterUnavailable())\n\n\n@app.error_handler(conditions.LinkError, method=METHOD_ALL)\ndef link_error_handler(req, error):\n    \"\"\"Handle LinkError exception and generate right response.\"\"\"\n    return response_error(req, error)\n"
  },
  {
    "path": "prusa/link/web/files.py",
    "content": "\"\"\"/api/v1/files endpoint handlers\"\"\"\n\nimport logging\nfrom os import fsync, listdir, replace, rmdir, unlink\nfrom os.path import basename, exists, isdir, join, split\nfrom pathlib import Path\nfrom shutil import rmtree\nfrom time import monotonic, sleep\n\nfrom magic import Magic\nfrom poorwsgi import state\nfrom poorwsgi.response import JSONResponse, Response\nfrom prusa.connect.printer.const import (\n    Event,\n    FileType,\n    Source,\n    StorageType,\n    TransferType,\n)\n\nfrom .. import conditions\nfrom ..printer_adapter.command import FileNotFound, NotStateToPrint\nfrom ..printer_adapter.command_handlers import StartPrint\nfrom ..printer_adapter.job import Job\nfrom .lib.auth import check_api_digest\nfrom .lib.core import app\nfrom .lib.files import (\n    check_cache_headers,\n    check_job,\n    check_os_path,\n    check_read_only,\n    check_storage,\n    fill_file_data,\n    fill_printfile_data,\n    forbidden_characters,\n    get_boolean_header,\n    get_files_size,\n    get_last_modified,\n    get_os_path,\n    make_cache_headers,\n    make_headers,\n    partfilepath,\n    storage_display_path,\n)\n\nlog = logging.getLogger(__name__)\n\n\n@app.route('/api/v1/storage')\n@check_api_digest\ndef storage_info(req):\n    \"\"\"Returns info about each storage\"\"\"\n    # pylint: disable=unused-argument\n    storage_dict = app.daemon.prusa_link.printer.fs.storage_dict\n    storage_list = [{\n        'type': StorageType.LOCAL.value,\n        'path': '/local',\n        'available': False,\n    }, {\n        'type': StorageType.SDCARD.value,\n        'path': '/sdcard',\n        'available': False,\n    }]\n\n    for storage in storage_dict.values():\n        files = storage.to_dict()\n        storage_size = files['size']\n        print_files_size = get_files_size(files, FileType.PRINT_FILE.value)\n\n        if storage.path_storage:\n            # LOCAL\n            storage_ = storage_list[0]\n            storage_['free_space'] = files.get('free_space')\n            storage_['total_space'] = files.get('total_space')\n            storage_['read_only'] = False\n        else:\n            # SDCARD\n            storage_ = storage_list[1]\n            storage_['read_only'] = True\n\n        storage_['name'] = storage.storage\n        storage_['print_files'] = print_files_size\n        storage_['system_files'] = storage_size - print_files_size\n        storage_['available'] = True\n\n    return JSONResponse(storage_list=storage_list)\n\n\n@app.route('/api/v1/files/<storage>', method=state.METHOD_HEAD)\n@app.route('/api/v1/files/<storage>/', method=state.METHOD_HEAD)\n@app.route('/api/v1/files/<storage>/<path:re:.+(?!/raw)>',\n           method=state.METHOD_HEAD)\ndef head_file_info(req, storage, path=None):\n    \"\"\"Returns headers info about specific file or folder\"\"\"\n    # pylint: disable=unused-argument\n    file_system = app.daemon.prusa_link.printer.fs\n    last_modified = get_last_modified(file_system)\n\n    # If no path is inserted, return root of the storage\n    path = storage_display_path(storage, path)\n\n    file = file_system.get(path)\n    if not file:\n        raise conditions.FileNotFound()\n\n    headers = make_cache_headers(last_modified)\n    headers.update(make_headers(storage, path))\n    return Response(headers=headers)\n\n\n@app.route('/api/v1/files/<storage>', method=state.METHOD_GET)\n@app.route('/api/v1/files/<storage>/', method=state.METHOD_GET)\n@app.route('/api/v1/files/<storage>/<path:re:.+(?!/raw)>',\n           method=state.METHOD_GET)\n@check_api_digest\n@check_storage\ndef file_info(req, storage, path=None):\n    \"\"\"Returns info and metadata about specific file or folder\"\"\"\n    # pylint: disable=unused-argument\n    file_system = app.daemon.prusa_link.printer.fs\n    last_modified = get_last_modified(file_system)\n    headers = make_cache_headers(last_modified)\n\n    # If cache is up-to-date, return Not Modified response, otherwise continue\n    if check_cache_headers(req_headers=req.headers,\n                           headers=headers,\n                           last_modified=last_modified):\n        return Response(status_code=state.HTTP_NOT_MODIFIED, headers=headers)\n\n    # If no path is inserted, return root of the storage\n    path = storage_display_path(storage, path)\n\n    file = file_system.get(path)\n    if not file:\n        raise conditions.FileNotFound()\n\n    os_path = file_system.get_os_path(path)\n    file_tree = file.to_dict()\n    result = file_tree.copy()\n    file_type = result['type']\n    result['display_name'] = basename(path)\n\n    # --- FOLDER ---\n    # Fill children's tree data for the folder\n    if file_type is FileType.FOLDER.value:\n        for child in result.get('children', []):\n            child['display_name'] = child['name']\n            child_type = child['type']\n            child_path = join(path, child['name'])\n            child_os_path = join(os_path, child['name'])\n\n            if child_type is not FileType.FOLDER.value:\n                # Fill specific data for print files within children list\n                if child_type is FileType.PRINT_FILE.value:\n                    child.update(\n                        fill_printfile_data(child_path,\n                                            child_os_path,\n                                            storage,\n                                            simple=True))\n\n                # Fill specific data for firmware files within children list\n                elif child_type is FileType.FIRMWARE.value:\n                    child.update(fill_file_data(child_path, storage))\n\n                # Fill specific data for other files within children list\n                else:\n                    child.update(fill_file_data(child_path, storage))\n\n    # --- FILE ---\n    # Fill specific data and metadata for print file\n    elif file_type is FileType.PRINT_FILE.value:\n        result.update(fill_printfile_data(path, os_path, storage))\n\n    # Fill specific data for firmware file\n    elif file_type is FileType.FIRMWARE.value:\n        result.update(fill_file_data(path, storage))\n\n    # Fill specific data for other file\n    else:\n        result.update(fill_file_data(path, storage))\n\n    headers.update(make_headers(storage, path))\n    return JSONResponse(**result, headers=headers)\n\n\n@app.route('/api/v1/files/<storage>/<path:re:.+(?!/raw)>',\n           method=state.METHOD_PUT)\n@check_api_digest\n@check_storage\n@check_read_only\ndef file_upload(req, storage, path):\n    \"\"\"Upload a file via PUT method\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=too-many-return-statements\n    # pylint: disable=too-many-branches\n    # pylint: disable=too-many-statements\n    # pylint: disable=too-many-locals\n\n    if forbidden_characters(path):\n        raise conditions.ForbiddenCharacters()\n\n    abs_path = join(get_os_path(f'/{app.cfg.printer.directory_name}'), path)\n\n    if get_boolean_header(req.headers, 'Create-Folder'):\n        Path(abs_path).mkdir(parents=True, exist_ok=True)\n    else:\n        allowed_types = ['application/octet-stream', 'text/x.gcode']\n\n        # If the type is unknown, it will be checked after successful upload\n        mime_type = req.mime_type or 'application/octet-stream'\n\n        if mime_type not in allowed_types:\n            raise conditions.UnsupportedMediaError()\n\n        if not req.content_length > 0:\n            raise conditions.LengthRequired()\n\n        overwrite = get_boolean_header(req.headers, 'Overwrite')\n\n        if not overwrite:\n            if exists(abs_path):\n                raise conditions.FileAlreadyExists()\n\n        print_after_upload = get_boolean_header(req.headers,\n                                                'Print-After-Upload')\n\n        uploaded = 0\n        # checksum = sha256() # - # We don't use this value yet\n\n        # Create folders within the path\n        Path(split(abs_path)[0]).mkdir(parents=True, exist_ok=True)\n\n        filename = basename(abs_path)\n        part_path = partfilepath(filename)\n\n        transfer = app.daemon.prusa_link.printer.transfer\n        transfer.start(TransferType.FROM_CLIENT,\n                       filename,\n                       to_print=print_after_upload)\n        transfer.size = req.content_length\n        transfer.start_ts = monotonic()\n\n        with open(part_path, 'w+b') as temp:\n            block = min(app.cached_size, req.content_length)\n            data = req.read(block)\n            while data:\n                if transfer.stop_ts:\n                    break\n                uploaded += temp.write(data)\n                transfer.transferred = uploaded\n                # checksum.update(data) # - we don't use the value yet\n                block = min(app.cached_size, req.content_length - uploaded)\n                if block > 1:\n                    data = req.read(block)\n                else:\n                    data = b''\n            temp.flush()\n            fsync(temp.fileno())\n\n        event_cb = app.daemon.prusa_link.printer.event_cb\n        event_cb(Event.TRANSFER_FINISHED,\n                 Source.WUI,\n                 destination=transfer.path,\n                 transfer_id=transfer.transfer_id)\n\n        transfer.type = TransferType.NO_TRANSFER\n\n        if req.content_length > uploaded:\n            raise conditions.FileUploadFailed()\n\n        # Mine a real mime_type from the file using magic\n        if req.mime_type == 'application/octet-stream':\n            mime_type = Magic(mime=True).from_file(abs_path)\n            if mime_type not in allowed_types:\n                unlink(abs_path)\n                raise conditions.UnsupportedMediaError()\n\n        if not overwrite:\n            if exists(abs_path):\n                raise conditions.FileAlreadyExists()\n\n        replace(part_path, abs_path)\n\n        if print_after_upload:\n            tries = 0\n            print_path = storage_display_path(storage, path)\n\n            # Filesystem may need some time to update\n            while not app.daemon.prusa_link.printer.fs.get(print_path):\n                sleep(0.1)\n                tries += 1\n                if tries >= 10:\n                    raise conditions.RequestTimeout()\n            try:\n                app.daemon.prusa_link.command_queue.do_command(\n                    StartPrint(print_path, source=Source.WUI))\n            except NotStateToPrint as exception:\n                raise conditions.NotStateToPrint() from exception\n\n    return Response(status_code=state.HTTP_CREATED)\n\n\n@app.route('/api/v1/files/<storage>/<path:re:.+(?!/raw)>',\n           method=state.METHOD_DELETE)\n@check_api_digest\n@check_storage\n@check_read_only\ndef file_delete(req, storage, path):\n    \"\"\"Delete file or folder in local storage\"\"\"\n    # pylint: disable=unused-argument\n    path = storage_display_path(storage, path)\n    os_path = check_os_path(get_os_path(path))\n    check_job(Job.get_instance(), path)\n    force = get_boolean_header(req.headers, 'Force')\n\n    if isdir(os_path):\n        if force:\n            rmtree(os_path)\n        else:\n            if not listdir(os_path):\n                rmdir(os_path)\n            else:\n                raise conditions.DirectoryNotEmpty()\n    else:\n        unlink(os_path)\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/v1/files/<storage>/<path:re:.+(?!/raw)>',\n           method=state.METHOD_POST)\n@check_api_digest\n@check_storage\ndef file_start_print(req, storage, path):\n    \"\"\"Start print of file if there's no print job running\"\"\"\n    # pylint: disable=unused-argument\n    print_path = storage_display_path(storage, path)\n    try:\n        app.daemon.prusa_link.command_queue.do_command(\n            StartPrint(print_path, source=Source.WUI))\n    except NotStateToPrint as exception:\n        raise conditions.NotStateToPrint() from exception\n    except FileNotFound as exception:\n        raise conditions.FileNotFound from exception\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/v1/transfer')\n@check_api_digest\ndef transfer_info(req):\n    \"\"\"Returns info about current transfer\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=duplicate-code\n    transfer = app.daemon.prusa_link.printer.transfer\n    if transfer.in_progress:\n        return JSONResponse(\n            **{\n                \"type\": transfer.type.value,\n                \"display_name\": basename(transfer.path),\n                \"path\": \"/local\",\n                \"url\": transfer.url,\n                \"size\": transfer.size,\n                \"progress\": round(transfer.progress, 2),\n                \"transferred\": transfer.transferred,\n                \"time_remaining\": transfer.time_remaining(),\n                \"time_transferring\": transfer.time_transferring(),\n                \"to_print\": transfer.to_print,\n            })\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/v1/transfer', method=state.METHOD_DELETE)\n@check_api_digest\ndef transfer_abort(req):\n    \"\"\"Aborts the current transfer\"\"\"\n    # pylint: disable=unused-argument\n    transfer = app.daemon.prusa_link.printer.transfer\n    if transfer.in_progress:\n        transfer.stop()\n        return Response(status_code=state.HTTP_OK)\n    return Response(status_code=state.HTTP_NO_CONTENT)\n"
  },
  {
    "path": "prusa/link/web/files_legacy.py",
    "content": "\"\"\"/api/files legacy endpoint handlers\nThis is a deprecated legacy code\"\"\"\n\nimport logging\nfrom base64 import decodebytes\nfrom os import makedirs, replace, unlink\nfrom os.path import (\n    abspath,\n    basename,\n    dirname,\n    exists,\n    getctime,\n    getsize,\n    isdir,\n    join,\n)\nfrom shutil import move, rmtree\n\nimport validators  # type: ignore\nfrom gcode_metadata import FDMMetaData, get_metadata, get_preview\nfrom poorwsgi import state\nfrom poorwsgi.request import FieldStorage\nfrom poorwsgi.response import FileResponse, JSONResponse, Response\nfrom poorwsgi.results import hbytes\nfrom prusa.connect.printer import const\nfrom prusa.connect.printer.const import Source\nfrom prusa.connect.printer.download import forbidden_characters\n\nfrom .. import conditions\nfrom ..const import PATH_WAIT_TIMEOUT\nfrom ..printer_adapter.command_handlers import StartPrint\nfrom ..printer_adapter.job import Job, JobState\nfrom ..printer_adapter.prusa_link import TransferCallbackState\nfrom .lib.auth import check_api_digest\nfrom .lib.core import app\nfrom .lib.files import (\n    callback_factory,\n    check_cache_headers,\n    check_filename,\n    check_foldername,\n    check_job,\n    check_os_path,\n    check_read_only,\n    check_storage,\n    file_to_api,\n    gcode_analysis,\n    get_last_modified,\n    get_os_path,\n    local_refs,\n    make_cache_headers,\n    make_headers,\n    partfilepath,\n    sdcard_refs,\n    sort_files,\n    storage_display_path,\n)\n\nlog = logging.getLogger(__name__)\n\n\n@app.route('/api/files')\n@app.route('/api/files/path/<path:re:.+>')\n@check_api_digest\ndef api_files(req, path=''):\n    \"\"\"\n    Returns info about all available print files or\n    about print files in specific directory\n    \"\"\"\n    # pylint: disable=too-many-locals\n    # pylint: disable=too-many-branches\n    # pylint: disable=duplicate-code\n    file_system = app.daemon.prusa_link.printer.fs\n    last_modified = get_last_modified(file_system)\n    headers = make_cache_headers(last_modified)\n\n    # If cache is up-to-date, return Not Modified response, otherwise continue\n    if check_cache_headers(req_headers=req.headers,\n                           headers=headers,\n                           last_modified=last_modified):\n        return Response(status_code=state.HTTP_NOT_MODIFIED, headers=headers)\n\n    storage_path = ''\n    space_info = None\n\n    if path:\n        files = file_system.get(path)\n\n        # We need to find the storage in storage dict in order to find the\n        # information about free and total space\n\n        # If path contains only storage (e.g. /PrusaLink gcodes), check if it's\n        # present in storage dict and then assign it to the storage variable\n        storage = file_system.storage_dict.get(path)\n\n        # If path contains folder (e.g. /PrusaLink gcodes/examples), split the\n        # path, check if the first part is present in storage dict and if so,\n        # assign it to the storage variable\n        if not storage:\n            path = path.split(sep=\"/\", maxsplit=1)[0]\n            storage = file_system.storage_dict.get(path)\n\n        if files:\n            files_ = files.to_dict_legacy()[\"children\"]\n            files = [file_to_api(child) for child in files_]\n        else:\n            return Response(status_code=state.HTTP_NOT_FOUND, headers=headers)\n    else:\n        data = file_system.to_dict_legacy()\n\n        files = [file_to_api(child) for child in data.get(\"children\", [])]\n\n        for item in files:\n            if item['origin'] == 'local':\n                storage_path = item['name']\n                break\n\n        storage = file_system.storage_dict.get(storage_path)\n\n    # If the storage is SD Card, we are not able to get space info\n    if storage:\n        space_info = storage.get_space_info()\n\n    free = hbytes(space_info.get(\"free_space\")) if space_info else (0, \"B\")\n    total = hbytes(space_info.get(\"total_space\")) if space_info else (0, \"B\")\n\n    return JSONResponse(headers=headers,\n                        files=sort_files(filter(None, files)),\n                        free=f\"{int(free[0])} {free[1]}\",\n                        total=f\"{int(total[0])} {total[1]}\")\n\n\n@app.route('/api/files/<storage>', method=state.METHOD_POST)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_upload(req, storage):\n    \"\"\"Function for uploading G-CODE.\"\"\"\n    # pylint: disable=too-many-locals\n    def failed_upload_handler(transfer_):\n        \"\"\"Cancels the file transfer\"\"\"\n        event_cb = app.daemon.prusa_link.printer.event_cb\n        event_cb(const.Event.TRANSFER_ABORTED, const.Source.USER,\n                 transfer_id=transfer_.transfer_id)\n        transfer_.type = const.TransferType.NO_TRANSFER\n\n    transfer = app.daemon.prusa_link.printer.transfer\n    try:\n        form = FieldStorage(req,\n                            keep_blank_values=app.keep_blank_values,\n                            strict_parsing=app.strict_parsing,\n                            file_callback=callback_factory(req))\n    except TimeoutError as exception:\n        log.error(\"Oh no. Upload of a file timed out\")\n        failed_upload_handler(transfer)\n        raise conditions.RequestTimeout() from exception\n\n    if 'file' not in form:\n        raise conditions.NoFileInRequest()\n\n    filename = form['file'].filename\n    part_path = partfilepath(filename)\n    transfer.transferred = form.bytes_read\n\n    if form.bytes_read != req.content_length:\n        log.error(\"File uploading not complete\")\n        unlink(part_path)\n        failed_upload_handler(transfer)\n        raise conditions.FileSizeMismatch()\n\n    foldername = form.get('path', '/')\n    check_foldername(foldername)\n\n    if foldername.startswith('/'):\n        foldername = '.' + foldername\n    print_path = abspath(join(\n        f\"/{app.cfg.printer.directory_name}/\", foldername, filename))\n    foldername = abspath(join(app.cfg.printer.directory, foldername))\n    filepath = join(foldername, filename)\n\n    # post upload transfer fix from form fields\n    transfer.to_select = form.getfirst('select') == 'true'\n    transfer.to_print = form.getfirst('print') == 'true'\n    log.debug('select=%s, print=%s', transfer.to_select, transfer.to_print)\n    transfer.path = print_path  # post upload fix\n\n    job = Job.get_instance()\n\n    if exists(filepath) and job.data.job_state == JobState.IN_PROGRESS:\n        if print_path == job.data.selected_file_path:\n            unlink(part_path)\n            raise conditions.FileCurrentlyPrinted()\n\n    log.info(\"Store file to %s::%s\", storage, filepath)\n    makedirs(foldername, exist_ok=True)\n\n    if not job.printer.fs.wait_until_path(dirname(print_path),\n                                          PATH_WAIT_TIMEOUT):\n        raise conditions.ResponseTimeout()\n    replace(part_path, filepath)\n\n    if app.daemon.prusa_link.download_finished_cb(transfer) \\\n            == TransferCallbackState.NOT_IN_TREE:\n        raise conditions.ResponseTimeout()\n\n    if req.accept_json:\n        data = app.daemon.prusa_link.printer.fs.to_dict_legacy()\n\n        files = [file_to_api(child) for child in data.get(\"children\", [])]\n        return JSONResponse(done=True,\n                            files=sort_files(filter(None, files)),\n                            free=0,\n                            total=0,\n                            status_code=state.HTTP_CREATED)\n    return Response(status_code=state.HTTP_CREATED)\n\n\n@app.route(\"/api/files/<storage>/<path:re:.+>\", method=state.METHOD_POST)\n@check_api_digest\n@check_storage\ndef api_start_print(req, storage, path):\n    \"\"\"Start print if no print job is running\"\"\"\n    # pylint: disable=unused-argument\n    command = req.json.get('command')\n    job = Job.get_instance()\n\n    check_os_path(get_os_path('/' + path))\n\n    if command == 'select':\n        if job.data.job_state == JobState.IDLE:\n            job.deselect_file()\n            job.select_file(path)\n\n            if req.json.get('print', False):\n                command_queue = app.daemon.prusa_link.command_queue\n                command_queue.do_command(\n                    StartPrint(job.data.selected_file_path, source=Source.WUI))\n            return Response(status_code=state.HTTP_NO_CONTENT)\n\n    elif command == 'print':\n        if job.data.job_state == JobState.IDLE:\n            job.set_file_path(path,\n                              path_incomplete=False,\n                              prepend_sd_storage=False)\n            command_queue = app.daemon.prusa_link.command_queue\n            command_queue.do_command(StartPrint(path, source=Source.WUI))\n            return Response(status_code=state.HTTP_NO_CONTENT)\n\n        # job_state != IDLE\n        raise conditions.CurrentlyPrinting()\n\n    # only select command is supported now\n    return Response(status_code=state.HTTP_BAD_REQUEST)\n\n\n@app.route('/api/files/<storage>/<path:re:.+>/raw')\n@check_api_digest\n@check_storage\ndef api_downloads(req, storage, path):\n    \"\"\"Downloads intended gcode.\"\"\"\n    # pylint: disable=unused-argument\n    filename = basename(path)\n    os_path = check_os_path(get_os_path('/' + path))\n\n    headers = {\"Content-Disposition\": f\"attachment;filename=\\\"{filename}\\\"\"}\n    return FileResponse(os_path, headers=headers)\n\n\n@app.route('/api/files/<storage>/<path:re:.+(?!/raw)>')\n@check_api_digest\n@check_storage\ndef api_file_info(req, storage, path):\n    \"\"\"Returns info and metadata about specific file from its cache\"\"\"\n    # pylint: disable=unused-argument\n    file_system = app.daemon.prusa_link.printer.fs\n\n    path = '/' + path\n\n    result = {\n        'origin': storage,\n        'name': basename(path),\n        'path': path,\n        'type': '',\n        'typePath':  []}\n\n    if path.lower().endswith(const.GCODE_EXTENSIONS):\n        result['type'] = 'machinecode'\n        result['typePath'] = ['machinecode', 'gcode']\n    else:\n        result['type'] = None\n        result['typePath'] = None\n\n    if storage == 'local':\n        os_path = get_os_path(path)\n        if not os_path:\n            raise conditions.FileNotFound()\n        if isdir(os_path):\n            meta = FDMMetaData(os_path)\n            meta.load_from_path(path)\n        else:\n            meta = get_metadata(os_path)\n        result['refs'] = local_refs(path, meta)\n        if not meta.thumbnails:\n            result['refs']['thumbnail'] = None\n\n        result['size'] = getsize(os_path)\n        result['date'] = int(getctime(os_path))\n\n    else:  # sdcard\n        if not file_system.get(path):\n            raise conditions.FileNotFound()\n        meta = FDMMetaData(path)\n        meta.load_from_path(path)\n        result['refs'] = sdcard_refs()\n        result['read_only'] = True\n\n    headers = make_headers(storage, path)\n\n    result['gcodeAnalysis'] = gcode_analysis(meta)\n    return JSONResponse(**result, headers=headers)\n\n\n@app.route('/api/files/<storage>/<path:re:.+>', method=state.METHOD_DELETE)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_delete(req, storage, path):\n    \"\"\"Delete file on local storage.\"\"\"\n    # pylint: disable=unused-argument\n    path = storage_display_path(storage=storage, path=path)\n    os_path = check_os_path(get_os_path(path))\n    check_job(Job.get_instance(), path)\n    unlink(os_path)\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/download')\n@app.route('/api/transfer')\n@check_api_digest\ndef api_transfer_info(req):\n    \"\"\"Get info about the file currently being transfered\"\"\"\n    # pylint: disable=unused-argument\n    transfer = app.daemon.prusa_link.printer.transfer\n    if transfer.in_progress:\n        return JSONResponse(\n            **{\n                \"type\": transfer.type.value,\n                \"url\": transfer.url,\n                \"target\": \"local\",\n                \"destination\": transfer.path,\n                \"path\": transfer.path,\n                \"size\": transfer.size,\n                \"start_time\": int(transfer.start_ts),\n                \"progress\": transfer.progress\n                and round(transfer.progress / 100, 4),\n                \"remaining_time\": transfer.time_remaining(),\n                \"to_select\": transfer.to_select,\n                \"to_print\": transfer.to_print,\n            })\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/download/<storage>', method=state.METHOD_POST)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_download(req, storage):\n    \"\"\"Download intended file from a given url\"\"\"\n    # pylint: disable=unused-argument\n    download_mgr = app.daemon.prusa_link.printer.download_mgr\n\n    local = f'/{app.cfg.printer.directory_name}'\n    url = req.json.get('url')\n    if not validators.url(url):\n        return JSONResponse(status_code=state.HTTP_BAD_REQUEST,\n                            title=\"Invalid URL\",\n                            message=\"Inserted URL is not valid\")\n    filename = basename(url)\n    check_filename(filename)\n\n    path_name = req.json.get('path', req.json.get('destination'))\n    new_filename = req.json.get('rename').strip(\"/\")\n\n    path = join(local, path_name)\n    to_select = req.json.get('to_select', False)\n    to_print = req.json.get('to_print', False)\n    log.debug('select=%s, print=%s', to_select, to_print)\n\n    if new_filename:\n        if not new_filename.lower().endswith(const.GCODE_EXTENSIONS):\n            new_filename += '.gcode'\n        path = join(path, new_filename)\n    else:\n        path = join(path, filename)\n\n    if forbidden_characters(path):\n        return JSONResponse(\n            status_code=state.HTTP_BAD_REQUEST,\n            title=\"Forbidden characters in path\",\n            message=\"Folder or file name contains forbidden characters\")\n\n    job = Job.get_instance()\n\n    if job.data.job_state == JobState.IN_PROGRESS and \\\n            path == job.data.selected_file_path:\n        raise conditions.FileCurrentlyPrinted()\n\n    download_mgr.start(const.TransferType.FROM_WEB, path, url, to_print,\n                       to_select)\n\n    return Response(status_code=state.HTTP_CREATED)\n\n\n@app.route('/api/folder/<storage>/<path:re:.+>', method=state.METHOD_POST)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_create_folder(req, storage, path):\n    \"\"\"Create a folder in a path\"\"\"\n    # pylint: disable=unused-argument\n    os_path = get_os_path(f'/{app.cfg.printer.directory_name}')\n    path = join(os_path, path)\n\n    if exists(path):\n        raise conditions.FolderAlreadyExists()\n\n    makedirs(path)\n    return Response(status_code=state.HTTP_CREATED)\n\n\n@app.route('/api/folder/<storage>/<path:re:.+>', method=state.METHOD_DELETE)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_delete_folder(req, storage, path):\n    \"\"\"Delete a folder in a path\"\"\"\n    # pylint: disable=unused-argument\n    os_path = get_os_path(f'/{app.cfg.printer.directory_name}')\n    path = join(os_path, path)\n\n    if not exists(path):\n        raise conditions.FolderNotFound()\n\n    rmtree(path)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route('/api/modify/<storage>', method=state.METHOD_POST)\n@check_api_digest\n@check_storage\n@check_read_only\ndef api_modify(req, storage):\n    \"\"\"Move file to another directory or/and change its name\"\"\"\n    # pylint: disable=unused-argument\n\n    os_path = get_os_path(f'/{app.cfg.printer.directory_name}')\n\n    source = join(os_path, req.json.get('source'))\n    destination = join(os_path, req.json.get('destination'))\n\n    path = dirname(destination)\n\n    job = Job.get_instance()\n\n    if job.data.job_state == JobState.IN_PROGRESS and \\\n            source == get_os_path(job.data.selected_file_path):\n        raise conditions.FileCurrentlyPrinted()\n\n    if source == destination:\n        raise conditions.DestinationSameAsSource()\n\n    if not exists(source):\n        raise conditions.FileNotFound()\n\n    if not exists(path):\n        try:\n            makedirs(path)\n            move(source, destination)\n        except PermissionError as error:\n            raise error\n\n    return Response(status_code=state.HTTP_CREATED)\n\n\n@app.route('/api/download', method=state.METHOD_DELETE)\n@check_api_digest\ndef api_download_abort(req):\n    \"\"\"Aborts current download process\"\"\"\n    # pylint: disable=unused-argument\n    download_mgr = app.daemon.prusa_link.printer.download_mgr\n    download_mgr.transfer.stop()\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route('/api/thumbnails/<path:re:.+>.orig.<wanted_format:re:.{1,5}>')\n@check_api_digest\ndef api_thumbnails(req, path, wanted_format):\n    \"\"\"Returns preview from cache file.\"\"\"\n    # pylint: disable=unused-argument\n    headers = {'Cache-Control': 'private, max-age=604800'}\n    os_path = check_os_path(get_os_path('/' + path))\n\n    meta = FDMMetaData(os_path)\n    if not meta.is_cache_fresh():\n        raise conditions.FileNotFound()\n\n    meta.load_cache()\n    if not meta.thumbnails:\n        raise conditions.ThumbnailUnavailable()\n\n    info = get_preview(meta.thumbnails)\n    img_format = info.format.lower()\n    if wanted_format.lower() != img_format:\n        raise conditions.ThumbnailUnavailable()\n\n    data = meta.thumbnails[info.to_thumbnail_info()]\n    return Response(decodebytes(data), headers=headers,\n                    content_type=f\"image/{img_format}\")\n"
  },
  {
    "path": "prusa/link/web/lib/__init__.py",
    "content": "\"\"\"Root of web.lib module contains some shared tools for web interface.\"\"\"\n\n\ndef try_int(value):\n    \"\"\"Convertor to int wihout exception.\"\"\"\n    try:\n        return int(value)\n    except ValueError:\n        return None\n"
  },
  {
    "path": "prusa/link/web/lib/auth.py",
    "content": "\"\"\"Authorization tools and decorators\"\"\"\nimport logging\nfrom functools import wraps\n\nfrom poorwsgi import state\nfrom poorwsgi.digest import check_credentials, hexdigest\nfrom poorwsgi.response import HTTPException, Response\nfrom poorwsgi.session import check_token\n\nfrom ...printer_adapter.structures.regular_expressions import (\n    VALID_PASSWORD_REGEX,\n    VALID_USERNAME_REGEX,\n)\nfrom .core import app\n\nlog = logging.getLogger(__name__)\n\nREALM = 'Administrator'\n\n# --- Errors ---\nUSERNAME = \"Username is shorter than 3 characters or in invalid format\"\nUSERNAME_SPACES = \"Username cannot contain space at the beginning nor the end\"\nPASSWORD = \"New password is shorter than 8 characters or in invalid format\"\nPASSWORD_SPACES = \\\n    \"New password cannot contain space at the beginning nor the end\"\nREPASSWORD = \"New passwords are not same\"\nOLD_DIGEST = \"Password is not correct\"\nSAME_DIGEST = \"Nothing to change. All credentials are same as old ones\"\n\n\ndef check_digest(req):\n    \"\"\"Check HTTP Digest.\n\n    Use this as function, not as decorator\"\"\"\n    if 'Authorization' not in req.headers:\n        log.info('Digest: Authorization header not found')\n        raise HTTPException(state.HTTP_UNAUTHORIZED, realm=REALM)\n\n    if req.authorization['type'] != 'Digest':\n        log.error('Digest: Bad Authorization type')\n        raise HTTPException(state.HTTP_UNAUTHORIZED, realm=REALM)\n\n    if not check_token(req.authorization.get('nonce'),\n                       req.secret_key,\n                       req.user_agent,\n                       timeout=req.app.auth_timeout):\n        log.info(\"Digest: nonce value not match\")\n        raise HTTPException(state.HTTP_UNAUTHORIZED, realm=REALM, stale=True)\n\n    if not check_credentials(req, REALM, None):\n        raise HTTPException(state.HTTP_UNAUTHORIZED, realm=REALM)\n\n\ndef check_api_digest(func):\n    \"\"\"Check X-Api-Key header.\"\"\"\n\n    @wraps(func)\n    def handler(req, *args, **kwargs):\n        prusa_link = app.daemon.prusa_link\n        if not prusa_link or not prusa_link.printer:\n            raise HTTPException(state.HTTP_SERVICE_UNAVAILABLE)\n\n        if 'X-Api-Key' not in req.headers:\n            check_digest(req)\n            return func(req, *args, **kwargs)\n\n        api_key = req.headers.get('X-Api-Key')\n        if api_key != app.api_key:\n            res = Response(data=\"Bad X-Api-Key.\",\n                           status_code=state.HTTP_FORBIDDEN)\n            raise HTTPException(res)\n        # TODO: append printer object to kwargs\n        return func(req, *args, **kwargs)\n\n    return handler\n\n\ndef check_config(func):\n    \"\"\"Check if HTTP Digest is configured.\"\"\"\n\n    @wraps(func)\n    def handler(req, *args, **kwargs):\n        prusa_link = app.daemon.prusa_link\n        if not prusa_link or not prusa_link.printer:\n            log.error('prusa_link or prusa_link.printer is not available')\n            raise HTTPException(state.HTTP_SERVICE_UNAVAILABLE)\n\n        return func(req, *args, **kwargs)\n\n    return handler\n\n\ndef set_digest(username, password):\n    \"\"\"Set HTTP digest from password and self.username.\"\"\"\n    return hexdigest(username, REALM, password)\n\n\ndef valid_credentials(username, new_password, new_repassword, errors):\n    \"\"\"Check if auth credentials are valid.\"\"\"\n    _errors = {}\n    if username.startswith(\" \") or username.endswith(\" \"):\n        _errors['username_spaces'] = USERNAME_SPACES\n    if not VALID_USERNAME_REGEX.match(username):\n        _errors['username'] = USERNAME\n    if new_password:\n        if new_password.startswith(' ') or new_password.endswith(' '):\n            _errors['password_spaces'] = PASSWORD_SPACES\n        if not VALID_PASSWORD_REGEX.match(new_password):\n            _errors['password'] = PASSWORD\n        if new_password != new_repassword:\n            _errors['repassword'] = REPASSWORD\n    if _errors:\n        errors['user'] = _errors\n    return not _errors\n\n\ndef valid_digests(digest, old_digest, new_digest, errors):\n    \"\"\"Check auth credentials and compare to current ones.\n    :param digest: current digest, saved in system\n    :param old_digest: digest made from old password and old username\n    :param new_digest: digest made from new password and new username\n    :param errors: object with current errors\n    check, if OLD password is same as current one (old_digest),\n    check if NEW password is NOT same as current one (new_digest)\n    \"\"\"\n    _errors = {}\n    if old_digest != digest:\n        _errors['old_digest'] = OLD_DIGEST\n    if old_digest == new_digest:\n        _errors['same_digest'] = SAME_DIGEST\n    if _errors:\n        errors['user'] = _errors\n    return not _errors\n"
  },
  {
    "path": "prusa/link/web/lib/classes.py",
    "content": "\"\"\"Server Classes\n\nMain server classes for handling request.\n\"\"\"\nimport logging\nfrom socketserver import ThreadingMixIn\nfrom wsgiref.simple_server import ServerHandler, WSGIRequestHandler, WSGIServer\n\nfrom ... import __application__, __version__\n\nMAX_REQUEST_SIZE = 2048\nlog = logging.getLogger(__name__)\n\n\nclass ThreadingServer(ThreadingMixIn, WSGIServer):\n    \"\"\"WSGIServer which run request in thread.\n\n    * additional error handler\n    \"\"\"\n    daemon_threads = True\n    multithread = True\n\n    def handle_error(self, request, client_address):\n        log.exception(\"Error for client %s\", client_address[0])\n\n\nclass LinkHandler(ServerHandler):\n    \"\"\"For custom log_exception method and server_sofware\"\"\"\n\n    server_software = __application__\n    request_handler = None\n\n    def log_exception(self, exc_info):\n        \"\"\"Just skip old stderr functionality.\"\"\"\n        log.exception(\"Error handling\")\n\n\nclass RequestHandler(WSGIRequestHandler):\n    \"\"\"For custom handle, log_message and log_error methods.\"\"\"\n    server_version = f\"{__application__}/{__version__}\"\n\n    # pylint: disable=redefined-builtin\n    def log_message(self, format, *args):\n        \"\"\"Log a message, which is typical content of access.log\"\"\"\n        log.debug(\"%s - %s\", self.address_string(), format % args)\n\n    def log_error(self, *args):\n        \"\"\"Log an error.\"\"\"\n        log.error(args, self.address_string())\n\n    def handle(self):\n        \"\"\"Handle a single HTTP request\"\"\"\n\n        self.raw_requestline = self.rfile.readline(MAX_REQUEST_SIZE)\n        if len(self.raw_requestline) > MAX_REQUEST_SIZE:\n            self.requestline = ''\n            self.request_version = ''\n            self.command = ''\n            self.send_error(414)\n            return\n\n        if not self.parse_request():  # An error code has been sent, just exit\n            log.error(\"Parse request error.\")\n            return\n\n        handler = LinkHandler(\n            self.rfile,\n            self.wfile,\n            self.get_stderr(),\n            self.get_environ(),\n            multithread=True,\n        )\n        handler.request_handler = self  # backpointer for logging\n        handler.run(self.server.get_app())\n"
  },
  {
    "path": "prusa/link/web/lib/core.py",
    "content": "\"\"\"WSGI application initialization.\"\"\"\nimport os\nfrom hashlib import sha256\nfrom importlib.resources import files  # type: ignore\nfrom os.path import abspath, join\nfrom time import time\n\nfrom poorwsgi import Application\nfrom poorwsgi.digest import PasswordMap\n\nSTATIC_DIR = abspath(\n    os.environ.get('PRUSA_LINK_STATIC', join(str(files('prusa.link')),\n                                             'static')))\n\n\nclass LinkWebApp(Application):\n    \"\"\"Extended Application object.\"\"\"\n    cfg = None\n    settings = None\n    daemon = None\n    wizard = None\n    api_key = None\n\n\napp = application = LinkWebApp(__package__)\napp.keep_blank_values = 1\napp.auto_form = False  # only POST /api/files/<target> endpoints get HTML form\napp.document_root = STATIC_DIR\n\napp.secret_key = sha256(str(time()).encode()).hexdigest()\napp.auth_type = 'Digest'\napp.auth_timeout = 60\napp.auth_map = PasswordMap()\n"
  },
  {
    "path": "prusa/link/web/lib/files.py",
    "content": "\"\"\"Check and modify an input dictionary using recursion\"\"\"\n\nfrom datetime import datetime\nfrom functools import wraps\nfrom hashlib import md5\nfrom io import FileIO\nfrom os import fsync, statvfs\nfrom os.path import abspath, dirname, exists, join\nfrom time import sleep, time\n\nfrom gcode_metadata import (\n    FDMMetaData,\n    estimated_to_seconds,\n    get_metadata,\n    get_preview,\n)\nfrom poorwsgi.request import Headers, Request\nfrom prusa.connect.printer import Filesystem\nfrom prusa.connect.printer.const import (\n    GCODE_EXTENSIONS,\n    Event,\n    Source,\n    State,\n    TransferType,\n)\nfrom prusa.connect.printer.download import (\n    Transfer,\n    TransferRunningError,\n    filename_too_long,\n    foldername_too_long,\n    forbidden_characters,\n)\n\nfrom ... import conditions\nfrom ...const import HEADER_DATETIME_FORMAT, SD_STORAGE_NAME\nfrom ...printer_adapter.job import Job, JobState\nfrom .core import app\n\n\ndef get_os_path(abs_path):\n    \"\"\"Gets the OS file path of the file specified by abs_path.\n\n    >>> from mock import Mock\n    >>> from prusa.connect.printer.files import Filesystem\n    >>> fs = Filesystem()\n    >>> fs.from_dir('/tmp', 'Examples')\n    >>> app.daemon = Mock()\n    >>> app.daemon.prusa_link.printer.fs = fs\n    >>> get_os_path('/Examples/not_exist')\n    \"\"\"\n    file_system = app.daemon.prusa_link.printer.fs\n    file_ = file_system.get(abs_path)\n    if not file_:\n        return None\n    abs_path = abs_path.strip(file_system.sep)\n    storage_name = abs_path.split(file_system.sep)[0]\n    storage = file_system.storage_dict[storage_name]\n    return file_.abs_path(storage.path_storage)\n\n\ndef local_simple_refs(path: str):\n    \"\"\"Make refs structure for firmware and other files on local storage\"\"\"\n    return {\n        'download': f\"/api/files/local{path}/raw\",\n    }\n\n\ndef sdcard_simple_refs():\n    \"\"\"Make refs structure for firmware and other files on SD Card\"\"\"\n    return {\n        'download': None,\n    }\n\n\ndef local_refs(path: str, meta: FDMMetaData):\n    \"\"\"Make refs structure for print file on local storage.\"\"\"\n    thumbnail = None\n\n    info = get_preview(meta.thumbnails)\n\n    if info is not None:\n        img_format = info.format.lower()\n        thumbnail = f\"/api/thumbnails{path}.orig.{img_format}\"\n    return {\n        'download': f\"/api/files/local{path}/raw\",\n        'icon': None,\n        'thumbnail': thumbnail,\n    }\n\n\ndef sdcard_refs():\n    \"\"\"Make refs structure for print file on SD Card.\"\"\"\n    return {\n        'download': None,\n        'icon': None,\n        'thumbnail': None,\n    }\n\n\ndef gcode_analysis(meta):\n    \"\"\"Make gcodeAnalysis structure from metadata.\"\"\"\n    estimated = estimated_to_seconds(\n        meta.get('estimated printing time (normal mode)', ''))\n\n    return {\n        'estimatedPrintTime': estimated,\n        'material': meta.get('filament_type'),\n        'layerHeight': meta.get('layer_height'),\n        # filament struct\n        # dimensions\n        # printingArea\n    }\n\n\ndef fill_file_data(path: str, storage: str):\n    \"\"\"Get file data for firmware and other files and fill them to\n    the result dict\"\"\"\n    result = {}\n    if storage == \"local\":\n        result['refs'] = local_simple_refs(path)\n    else:\n        result['refs'] = sdcard_simple_refs()\n    return result\n\n\ndef fill_printfile_data(path: str, os_path: str, storage: str,\n                        simple: bool = False):\n    \"\"\"\n    Get file data for print file and fill them to the result dict\n    :param path: path to file\n    :param os_path: absolute path to file\n    :param storage: name of the storage\n    :param simple: whether to return file metadata or just its refs\n    \"\"\"\n    result = {}\n\n    # local\n    if storage == \"local\":\n        meta = FDMMetaData(os_path or path)\n\n        if os_path and meta.is_cache_fresh():\n            meta.load_cache()\n        result['refs'] = local_refs(path, meta)\n        if simple:\n            return result\n        meta = FDMMetaData(os_path)\n        meta.load_from_path(path)\n        meta = get_metadata(os_path)\n        if not meta.thumbnails:\n            result['refs']['thumbnail'] = None\n\n    # sdcard\n    else:\n        result['refs'] = sdcard_refs()\n        if simple:\n            return result\n        meta = FDMMetaData(path)\n        meta.load_from_path(path)\n\n    result['meta'] = meta.data\n    result['meta']['estimated_print_time'] = estimated_to_seconds(\n        meta.data.get('estimated printing time (normal mode)', ''))\n    return result\n\n\ndef file_to_api(node, origin: str = 'local', path: str = '/',\n                sort_by: str = 'folder,date'):\n    \"\"\"Convert Prusa SDK Files tree for API.\n\n    >>> from mock import Mock\n    >>> from prusa.connect.printer.files import Filesystem\n    >>> fs = Filesystem()\n    >>> fs.from_dir('/tmp', 'PrusaLink gcodes')\n    >>> fs.get('/PrusaLink gcodes/Examples')\n    >>> app.daemon = Mock()\n    >>> app.daemon.prusa_link.printer.fs = fs\n    >>> files = {'type': 'DIR', 'name': '/', 'read_only': True, 'children':[\n    ...     {'type': 'DIR', 'name': 'SD Card', 'children':[\n    ...         {'type': 'DIR', 'name': 'Examples', 'children':[\n    ...             {'type': 'FILE', 'name': '1.gcode'},\n    ...             {'type': 'FILE', 'name': 'b.gco'}]}]},\n    ...     {'type': 'DIR', 'name': 'PrusaLink gcodes', 'children':[\n    ...         {'type': 'DIR', 'name': 'Examples', 'children':[\n    ...             {'type': 'FILE', 'name': '1.gcode'},\n    ...             {'type': 'FILE', 'name': 'b.gco'}]}]},\n    ...     {'type': 'FILE', 'name': 'preview.png'},\n    ...     {'type': 'FILE', 'name': 'Big extension.GCO'},\n    ... ]}\n    >>> api_files = file_to_api(files)\n    >>> # /\n    >>> api_files['type']\n    'folder'\n    >>> # /SD Card\n    >>> api_files['children'][0]['type']\n    'folder'\n    >>> # /SD Card/Examples\n    >>> api_files['children'][0]['children'][0]['type']\n    'folder'\n    >>> api_files['children'][0]['children'][0]['path']\n    '/SD Card/Examples'\n    >>> #'/SD Card/Examples/1.gcode'\n    >>> api_files['children'][0]['children'][0]['children'][0]['type']\n    'machinecode'\n    >>> api_files['children'][0]['children'][0]['children'][0]['origin']\n    'sdcard'\n    >>> # /PrusaLink gcodes/Examples\n    >>> api_files['children'][1]['children'][0]['type']\n    'folder'\n    >>> # /PrusaLink gcodes/Examples/1.gcode\n    >>> api_files['children'][1]['children'][0]['children'][0]['type']\n    'machinecode'\n    >>> api_files['children'][1]['children'][0]['children'][0]['origin']\n    'local'\n    >>> api_files['children'][2]['name']\n    'Big extension.GCO'\n    >>> len(api_files['children'])\n    3\n    \"\"\"\n    name = node['name']\n    path = join(path, name)\n\n    result = {'name': name, 'path': path, 'display': name, 'date': None}\n\n    if \"m_timestamp\" in node:\n        result[\"date\"] = node[\"m_timestamp\"]\n\n    if 'size' in node:\n        result['size'] = node['size']\n\n    if node['type'] == 'DIR':\n        if name == SD_STORAGE_NAME:\n            origin = 'sdcard'\n            result['read_only'] = True\n\n        result['type'] = 'folder'\n        result['typePath'] = ['folder']\n        result['origin'] = origin\n        result['refs'] = {\"resource\": None}\n        children = [\n            file_to_api(child, origin, path, sort_by)\n            for child in node.get(\"children\", [])\n        ]\n        result['children'] = sort_files(filter(None, children), sort_by)\n\n    elif name.lower().endswith(GCODE_EXTENSIONS):\n        result['origin'] = origin\n        result['type'] = 'machinecode'\n        result['typePath'] = ['machinecode', 'gcode']\n        result['hash'] = None\n\n        os_path = get_os_path(path)\n        meta = FDMMetaData(os_path or path)\n\n        if origin != \"sdcard\":\n            # get metadata only for files with cache\n            os_path = get_os_path(path)\n            if os_path and meta.is_cache_fresh():\n                meta.load_cache()\n            result['refs'] = local_refs(path, meta)\n            if not meta.thumbnails:\n                result['refs']['thumbnail'] = None\n\n        else:\n            meta.load_from_path(path)\n            result['refs'] = sdcard_refs()\n            result['read_only'] = True\n\n        result['gcodeAnalysis'] = gcode_analysis(meta.data)\n\n    else:\n        return {}  # not folder or allowed extension\n\n    return result\n\n\ndef sort_files(files, sort_by='folder,date'):\n    \"\"\"Sort and filter files\n    >>> files_ = sort_files([\n    ...    {'name':'a','date': 1612348743, 'type': 'machinecode'},\n    ...    {'name':'b','date': 1612448743, 'type': 'machinecode'},\n    ...    {'name':'c'},\n    ...    {'name':'d', 'type': 'folder'},\n    ...    {'name':'e', 'type': 'folder', 'date': 1614168237},\n    ... ])\n    >>> [file['name'] for file in files_]\n    ['e', 'd', 'b', 'a', 'c']\n    \"\"\"\n    if sort_by == \"folder,date\":\n\n        def sort_key(file):\n            return file.get('type') == 'folder', file.get(\"date\") or 0\n\n    return sorted(files, key=sort_key, reverse=True)\n\n\ndef check_filename(filename: str):\n    \"\"\"Check filename length and format\"\"\"\n\n    # Filename length, including suffix must be <= 248 characters\n    if filename_too_long(filename):\n        raise conditions.FilenameTooLong()\n\n    # File name cannot contain any of forbidden characters e.g. '\\'\n    if forbidden_characters(filename):\n        raise conditions.ForbiddenCharacters()\n\n    if not filename.lower().endswith(GCODE_EXTENSIONS):\n        raise conditions.NotSupportedFileType()\n\n\ndef check_foldername(foldername: str):\n    \"\"\"Check foldername length and format\"\"\"\n\n    # All foldername lengths in path must be <= 255 characters\n    if foldername_too_long(foldername):\n        raise conditions.FoldernameTooLong()\n\n    # Foldername cannot contain any of forbidden characters e.g. '\\'\n    if forbidden_characters(foldername):\n        raise conditions.ForbiddenCharacters()\n\n\ndef check_os_path(os_path: str):\n    \"\"\"\"Check os_path if exists\"\"\"\n    if not os_path:\n        raise conditions.FileNotFound()\n    return os_path\n\n\ndef check_storage(func):\n    \"\"\"Check storage from request.\"\"\"\n    @wraps(func)\n    def handler(req, storage, *args, **kwargs):\n        if storage not in ('local', 'sdcard'):\n            raise conditions.LocationNotFound()\n        return func(req, storage, *args, **kwargs)\n    return handler\n\n\ndef check_read_only(func):\n    \"\"\"Check if storage from request is read only SD Card\"\"\"\n    @wraps(func)\n    def handler(req, storage, *args, **kwargs):\n        if storage == 'sdcard':\n            raise conditions.SDCardReadOnly()\n        return func(req, storage, *args, **kwargs)\n    return handler\n\n\ndef check_job(job: Job, path: str):\n    \"\"\"Check if the file is currently printed, if not, deselects the file\"\"\"\n    if job.data.selected_file_path == path:\n        if job.data.job_state != JobState.IDLE:\n            raise conditions.FileCurrentlyPrinted()\n        job.deselect_file()\n\n\ndef storage_display_name(storage: str):\n    \"\"\"Return display name of the storage\"\"\"\n    display_name = \"\"\n    if storage == 'local':\n        display_name = app.cfg.printer.directory_name  # type: ignore\n    elif storage == \"sdcard\":\n        display_name = SD_STORAGE_NAME\n    return display_name\n\n\ndef storage_display_path(storage: str, path: str):\n    \"\"\"Return display path of the storage\"\"\"\n    display_name = storage_display_name(storage)\n    if path is None:\n        return f\"/{display_name}\"\n    return f\"/{display_name}/{path}\"\n\n\ndef partfilepath(filename):\n    \"\"\"Return file path for part file name.\"\"\"\n    filename = '.' + filename + '.part'\n    return abspath(join(app.cfg.printer.directory, filename))\n\n\ndef get_local_free_space(path: str):\n    \"\"\"Return local storage free space.\"\"\"\n    if exists(path):\n        path_ = statvfs(path)\n        free_space = path_.f_bavail * path_.f_bsize\n        return free_space\n    return None\n\n\ndef get_files_size(files: dict, file_type: str):\n    \"\"\"Iterate through a list of print files and return size summary\"\"\"\n    size = 0\n    for item in files['children']:\n        if item['type'] == file_type:\n            size += item['size']\n    return size\n\n\nclass GCodeFile(FileIO):\n    \"\"\"Own file class to control processing data when POST\"\"\"\n\n    def __init__(self, filepath: str, transfer: Transfer):\n        assert (app.daemon and app.daemon.prusa_link\n                and app.daemon.prusa_link.printer)\n        self.transfer = transfer\n        job = Job.get_instance()\n        self.filepath = filepath\n        self.__uploaded = 0\n        self.job_data = job.data\n        self.printer = app.daemon.prusa_link.printer\n        super().__init__(filepath, 'w+b')\n\n    @property\n    def uploaded(self):\n        \"\"\"Return uploaded file size.\"\"\"\n        return self.__uploaded\n\n    def write(self, data):\n        \"\"\"Writes data\"\"\"\n        if self.transfer.stop_ts > 0:\n            event_cb = app.daemon.prusa_link.printer.event_cb\n            event_cb(Event.TRANSFER_STOPPED, Source.USER,\n                     transfer_id=self.transfer.transfer_id)\n            self.transfer.type = TransferType.NO_TRANSFER\n            raise conditions.TransferStopped()\n        if self.printer.state == State.PRINTING \\\n                and not self.job_data.from_sd:\n            sleep(0.01)\n        size = super().write(data)\n        self.__uploaded += size\n        self.transfer.transferred = self.__uploaded\n        return size\n\n    def close(self):\n        self.flush()\n        fsync(self.fileno())\n        super().close()\n        event_cb = app.daemon.prusa_link.printer.event_cb\n        event_cb(Event.TRANSFER_FINISHED,\n                 Source.CONNECT,\n                 destination=self.transfer.path,\n                 transfer_id=self.transfer.transfer_id)\n        self.transfer.type = TransferType.NO_TRANSFER\n\n\ndef callback_factory(req: Request):\n    \"\"\"Factory for creating file_callback.\"\"\"\n    if req.content_length <= 0:\n        raise conditions.LengthRequired()\n\n    def gcode_callback(filename):\n        \"\"\"Check filename and upload possibility.\n\n        When data can be accepted create and return file instance for writing\n        form data.\n        \"\"\"\n        if not filename:\n            raise conditions.NoFileInRequest()\n\n        check_filename(filename)\n\n        part_path = partfilepath(filename)\n\n        if not filename.lower().endswith(\n                GCODE_EXTENSIONS) or filename.startswith('.'):\n            raise conditions.UnsupportedMediaError()\n\n        # Content-Length is not file-size but it is good limit\n        if get_local_free_space(dirname(part_path)) <= req.content_length:\n            raise conditions.EntityTooLarge()\n\n        transfer = app.daemon.prusa_link.printer.transfer\n        # TODO: check if client is Slicer ;) and use another type\n        # TODO: read to_print and to_select first\n        try:\n            transfer.start(TransferType.FROM_CLIENT, filename)\n            transfer.size = req.content_length\n            transfer.start_ts = time()\n        except TransferRunningError as err:\n            raise conditions.TransferConflict() from err\n        return GCodeFile(part_path, transfer)\n\n    return gcode_callback\n\n\ndef make_headers(storage: str, path: str) -> dict:\n    \"\"\"Make headers for api(/v1)/files GET endpoints\"\"\"\n    headers = {\n        'Read-Only': str(storage != \"local\"),\n        'Currently-Printed':\n            str(Job.get_instance().data.selected_file_path == path),\n    }\n    return headers\n\n\ndef get_last_modified(file_system: Filesystem) -> datetime:\n    \"\"\"Get last modified datetime\"\"\"\n    last_updated = 0.0\n    for storage in file_system.storage_dict.values():\n        last_updated = max(last_updated, storage.last_updated)\n    last_modified = datetime.utcfromtimestamp(last_updated)\n    return last_modified\n\n\ndef generate_etag(last_modified_str: str) -> str:\n    \"\"\"Generate and return weak ETag from last_modified_str\"\"\"\n    etag = f'W/\"{md5(last_modified_str.encode()).hexdigest()[:10]}\"'\n    return etag\n\n\ndef make_cache_headers(last_modified: datetime) -> dict:\n    \"\"\"Make cache headers for api(/v1)/files GET endpoints\"\"\"\n    last_modified_str = last_modified.strftime(HEADER_DATETIME_FORMAT)\n    etag = generate_etag(last_modified_str)\n\n    headers = {\n        'Last-Modified': last_modified_str,\n        'ETag': etag,\n        'Date': datetime.utcnow().strftime(HEADER_DATETIME_FORMAT),\n    }\n\n    return headers\n\n\ndef check_cache_headers(req_headers: Headers, headers: dict,\n                        last_modified: datetime) -> bool:\n    \"\"\"Check cache headers and return True if there are no changes\"\"\"\n    if 'If-Modified-Since' in req_headers:  # check cache header\n        hdt = datetime.strptime(req_headers['If-Modified-Since'],\n                                HEADER_DATETIME_FORMAT)\n        if last_modified <= hdt:\n            return True\n\n    if 'If-None-Match' in req_headers:\n        if req_headers['If-None-Match'] == headers['ETag']:\n            return True\n\n    return False\n\n\ndef get_boolean_header(headers, variable):\n    \"\"\"Return boolean value based on header variable\"\"\"\n    header_boolean = headers.get(variable, \"?0\")\n    return header_boolean == \"?1\"\n"
  },
  {
    "path": "prusa/link/web/lib/view.py",
    "content": "\"\"\"Response generate module.\"\"\"\n\nfrom importlib.resources import files\nfrom os.path import join\n\nfrom jinja2 import Environment, FileSystemLoader, pass_context\nfrom jinja2.runtime import Context\nfrom jinja2_template_info import TemplateInfoExtension\nfrom poorwsgi import redirect\nfrom prusa.connect.printer.const import PrinterType\n\nfrom .core import app\n\nTEMPL_PATH = (str(files('jinja2_template_info')),\n              join(str(files('prusa.link')), 'templates'))\n\n\ndef printer_type(type_):\n    \"\"\"Return name of printer type.\"\"\"\n    # pylint: disable=unused-argument\n    if type_ == PrinterType.I3MK3:\n        return \"Original Prusa i3 MK3\"\n    if type_ == PrinterType.I3MK3S:\n        return \"Original Prusa i3 MK3S\"\n    if type_ == PrinterType.I3MK25:\n        return \"Original Prusa i3 MK2.5\"\n    if type_ == PrinterType.I3MK25S:\n        return \"Original Prusa i3 MK2.5S\"\n    return \"Unknown\"\n\n\ndef add_prefix(prefix, uri):\n    \"\"\"Add prefix to uri.\"\"\"\n    if uri[0] != '/':\n        raise ValueError(\"The supplied URI does not start with a slash, \"\n                         \"so it is probably not meant to be prefixed\")\n    if prefix:\n        return f\"{prefix}{uri}\"\n    return uri\n\n\n@pass_context\ndef prefix_filter(context: Context, uri):\n    \"\"\"Add prefix to uri.\"\"\"\n    prefix = context.get('uri_prefix')\n    return add_prefix(prefix, uri)\n\n\ndef redirect_with_proxy(req, uri):\n    \"\"\"Modifies the redirect uri to include the proxy prefix.\"\"\"\n    redirect(\n        add_prefix(\n            prefix=req.headers.get(\"X-Forwarded-Prefix\"),\n            uri=uri,\n        ),\n    )\n\n\nenv = Environment(loader=FileSystemLoader(TEMPL_PATH),\n                  extensions=[\n                      'jinja2.ext.i18n', 'jinja2.ext.do',\n                      'jinja2.ext.loopcontrols',\n                  ])\n\nenv.filters['printer_type'] = printer_type\nenv.filters['prefixed'] = prefix_filter\n\n\ndef package_to_api(pkg):\n    \"\"\"Convert pkg_resources.DistInfoDistribution to API.\"\"\"\n    return {\n        'name': pkg.project_name,\n        'version': pkg.version,\n        'path': pkg.module_path,\n    }\n\n\ndef generate_page(request, template, **kwargs):\n    \"\"\"Return generated ouptut fromjinja template.\"\"\"\n    if app.debug:\n        eval_env = env.overlay()\n        eval_env.add_extension(TemplateInfoExtension)\n        env.globals[\"template_info\"].data = kwargs.copy()\n        env.globals['template_info'].template = template\n        kwargs['debug'] = True\n    else:\n        eval_env = env\n\n    kwargs['this_uri'] = request.uri\n    kwargs['uri_prefix'] = request.headers.get(\"X-Forwarded-Prefix\")\n    tmpl = eval_env.get_template(template)\n    return tmpl.render(kwargs)\n"
  },
  {
    "path": "prusa/link/web/lib/wizard.py",
    "content": "\"\"\"Configuration wizard library.\"\"\"\n\nimport logging\nfrom socket import gethostbyname\nfrom threading import Event\nfrom urllib.request import urlopen\n\nfrom poorwsgi.digest import hexdigest\nfrom prusa.connect.printer import CondState, Printer\n\nfrom ...conditions import UPGRADED\nfrom ...const import PRINTER_CONF_TYPES\nfrom ...printer_adapter.printer_polling import PrinterPolling\nfrom ...printer_adapter.structures.item_updater import WatchedItem\nfrom ...printer_adapter.structures.regular_expressions import (\n    NEW_SN_REGEX,\n    VALID_PASSWORD_REGEX,\n    VALID_SN_REGEX,\n    VALID_USERNAME_REGEX,\n)\nfrom ...serial.helpers import enqueue_instruction\nfrom ..lib.auth import REALM\nfrom ..lib.core import app\n\nlog = logging.getLogger(__name__)\n\n\ndef valid_sn_format(serial):\n    \"\"\"Check serial number format.\"\"\"\n    return VALID_SN_REGEX.match(serial) is not None\n\n\ndef new_sn_format(serial):\n    \"\"\"Check if the entered serial number is new format (SN...)\"\"\"\n    return NEW_SN_REGEX.match(serial)\n\n\ndef sn_write_success() -> bool:\n    \"\"\"Check if the S/N was written successfully to the printer\"\"\"\n    polling: PrinterPolling\n    # Yes, there can be none in there, nothing I can do about it, sorry mypy\n    polling = app.daemon.prusa_link.printer_polling  # type: ignore\n    # Note: if there's more of things like this, consider integrating\n    # Set up an event to wait for\n    serial_number: WatchedItem = polling.serial_number\n    serial_event = Event()\n\n    def sn_became_valid(item):\n        assert item is not None\n        serial_event.set()\n\n    serial_number.became_valid_signal.connect(sn_became_valid)\n    polling.invalidate_serial_number()\n    # wait up to five second for S/N to become valid\n    success = serial_event.wait(5)\n    serial_number.became_valid_signal.disconnect(sn_became_valid)\n    return success\n\n\ndef execute_sn_gcode(serial_number: str, serial_queue):\n    \"\"\"Encode S/N to GCODE instruction and execute it\"\"\"\n    hex_serial = serial_number.encode(\"ascii\").hex() + \"00\"\n    # Add correct prefix\n    first_gcode = f\"D3 Ax0d15 C16 X{hex_serial[:32]}\"\n    second_gcode = f\"D3 Ax0d25 C4 X{hex_serial[32:]}\"\n\n    # Send GCODE instructions to printer\n    enqueue_instruction(serial_queue, first_gcode, True)\n    enqueue_instruction(serial_queue, second_gcode, True)\n\n\nclass Wizard:\n    \"\"\"Configuration wizard singleton with validation methods.\"\"\"\n    instance = None\n\n    def __init__(self, _app):\n        if Wizard.instance is not None:\n            raise RuntimeError('Wizard is singleton')\n\n        # locale\n        # self.locale = app.settings.printer.locale\n        # self.time_zone = None\n\n        # S/N\n        self.serial = None\n\n        # auth\n        self.username = _app.settings.service_local.username\n        self.digest = None\n        self.restored_digest = False\n\n        # network\n        self.net_hostname = _app.settings.network.hostname\n\n        # printer\n        self.printer_type = _app.settings.printer.type\n        self.printer_name = _app.settings.printer.name\n        self.printer_location = _app.settings.printer.location\n\n        # connect\n        self.connect_skip = False\n        self.restored_connect = False\n        self.connect_hostname = _app.settings.service_connect.hostname\n        self.connect_tls = _app.settings.service_connect.tls\n        self.connect_port = _app.settings.service_connect.port\n        self.connect_token = _app.settings.service_connect.token\n\n        self.daemon = _app.daemon\n        self.cfg = _app.daemon.cfg\n        self.settings = _app.settings\n\n        self.wifi = None\n\n        self.errors = {}\n        Wizard.instance = self\n\n    def set_digest(self, password):\n        \"\"\"Set HTTP digest from password and self.username.\"\"\"\n        self.digest = hexdigest(self.username, REALM, password)\n\n    @property\n    def serial_number(self):\n        \"\"\"Proxy property for daemon.prusa_link.printer.sn.\"\"\"\n        return self.daemon.prusa_link.printer.sn\n\n    def check_username(self):\n        \"\"\"Check if username is valid\"\"\"\n        errors = {}\n        if self.username.startswith(' ') or self.username.endswith(' '):\n            errors['username_spaces'] = True\n        if not VALID_USERNAME_REGEX.match(self.username):\n            errors['username'] = True\n        self.errors['credentials'] = errors\n        return not errors\n\n    def check_credentials(self, password, repassword):\n        \"\"\"Check if auth values are valid.\"\"\"\n        errors = {}\n        if self.username.startswith(' ') or self.username.endswith(' '):\n            errors['username_spaces'] = True\n        if not VALID_USERNAME_REGEX.match(self.username):\n            errors['username'] = True\n        if password.startswith(' ') or password.endswith(' '):\n            errors['password_spaces'] = True\n        if not VALID_PASSWORD_REGEX.match(password):\n            errors['password'] = True\n        if password != repassword:\n            errors['repassword'] = True\n        self.errors['credentials'] = errors\n        return not errors\n\n    def check_serial(self):\n        \"\"\"Check S/N is valid.\"\"\"\n        errors = {}\n        if new_sn_format(self.serial):\n            errors['new_sn'] = True\n        elif not valid_sn_format(self.serial):\n            errors['not_valid'] = True\n        self.errors['serial'] = errors\n        return not errors\n\n    def check_connect(self):\n        \"\"\"Check connect settings.\"\"\"\n        errors = {}\n        try:\n            gethostbyname(self.connect_hostname)\n        except Exception:  # pylint: disable=broad-except\n            errors['hostname'] = True\n        url = Printer.connect_url(self.connect_hostname,\n                                  bool(self.connect_tls), self.connect_port)\n        try:\n            with urlopen(f'{url}/info'):\n                pass\n        except Exception:  # pylint: disable=broad-except\n            errors['connection'] = True\n        self.errors['connect'] = errors\n        return not errors\n\n    def write_settings(self, settings):\n        \"\"\"Write settings configuration.\"\"\"\n        # auth\n        settings.service_local.digest = self.digest\n        settings.service_local.username = self.username\n\n        # network\n        settings.network.hostname = self.net_hostname\n\n        # printer\n        printer_type = PRINTER_CONF_TYPES.inverse[\n            self.daemon.prusa_link.printer.type]\n        settings.printer.type = printer_type\n        settings.printer.name = self.printer_name\n        settings.printer.location = self.printer_location\n\n        # connect\n        if not self.connect_skip:\n            settings.service_connect.hostname = self.connect_hostname\n            settings.service_connect.tls = self.connect_tls\n            settings.service_connect.port = self.connect_port\n            settings.service_connect.token = self.connect_token\n\n        settings.update_sections(self.connect_skip)\n        UPGRADED.state = CondState.OK\n        with open(self.cfg.printer.settings, 'w', encoding='utf-8') as ini:\n            settings.write(ini)\n"
  },
  {
    "path": "prusa/link/web/link_info.py",
    "content": "\"\"\"Debug page of Prusa-Link.\"\"\"\nfrom prusa.connect.printer import __version__ as sdk_version\n\nfrom .. import __version__, conditions\nfrom .lib.core import app\nfrom .lib.view import generate_page\n\n\ndef link_info(req):\n    \"\"\"Return link-info page.\"\"\"\n    prusa_link = app.daemon.prusa_link\n    printer = prusa_link.printer if prusa_link else None\n    transfer = printer.transfer if printer else None\n    return generate_page(req,\n                         \"link_info.html\",\n                         daemon=app.daemon,\n                         prusa_link=prusa_link,\n                         printer=printer,\n                         app=app,\n                         version=__version__,\n                         sdk_version=sdk_version,\n                         errors=conditions.status(),\n                         transfer=transfer)\n"
  },
  {
    "path": "prusa/link/web/main.py",
    "content": "\"\"\"Main pages and core API\"\"\"\n\nimport datetime\nimport logging\nimport shlex\nimport subprocess\nimport time\nfrom os import listdir\nfrom os.path import basename, getmtime, getsize, join\nfrom socket import gethostname\nfrom subprocess import CalledProcessError\nfrom sys import version\nfrom typing import BinaryIO, cast\n\nfrom gcode_metadata import get_metadata\nfrom pkg_resources import working_set  # type: ignore\nfrom poorwsgi import state\nfrom poorwsgi.digest import check_digest\nfrom poorwsgi.response import (\n    EmptyResponse,\n    FileResponse,\n    GeneratorResponse,\n    JSONResponse,\n    Response,\n)\nfrom prusa.connect.printer import __version__ as sdk_version\nfrom prusa.connect.printer.const import Source, State\nfrom prusa.connect.printer.models import filter_null\n\nfrom .. import __version__, conditions\nfrom ..const import GZ_SUFFIX, LOGS_FILES, LOGS_PATH, LimitsMK3S, instance_id\nfrom ..printer_adapter.command import CommandFailed\nfrom ..printer_adapter.command_handlers import (\n    PausePrint,\n    ResumePrint,\n    SetReady,\n    StartPrint,\n    StopPrint,\n    check_update_prusalink,\n    update_prusalink,\n)\nfrom ..printer_adapter.job import Job, JobState\nfrom .lib.auth import REALM, check_api_digest, check_config\nfrom .lib.core import app\nfrom .lib.files import fill_printfile_data, gcode_analysis, get_os_path\nfrom .lib.view import package_to_api\n\nlog = logging.getLogger(__name__)\n\nPRINTER_STATES = {\n    State.IDLE: \"Operational\",\n    State.READY: \"Operational\",\n    State.BUSY: \"Busy\",\n    State.PRINTING: \"Printing\",\n    State.PAUSED: \"Paused\",\n    State.FINISHED: \"Operational\",\n    State.STOPPED: \"Cancelling\",\n    State.ERROR: \"Error\",\n    State.ATTENTION: \"Error\",\n}\n\n# From which states the printer can be set to READY state\nSTATES_TO_READY = [State.IDLE, State.FINISHED, State.STOPPED]\n\nCONFIRM_TEXT = \"\"\"\n    <p>This action may disrupt any ongoing print jobs (depending on your\n    printer's controller and general setup that might also apply to prints\n    run directly from your printer's internal storage).\"\"\"\n\n\n@app.route('/', method=state.METHOD_HEAD)\ndef instance(req):\n    \"\"\"Return an instance ID for pairing instances\"\"\"\n    # pylint: disable=unused-argument\n    response = Response()\n    response.add_header(\"Instance-ID\", str(instance_id))\n    return response\n\n\n@app.route('/', method=state.METHOD_GET)\n@check_config\n@check_digest(REALM)\ndef index(req):\n    \"\"\"Return status page\"\"\"\n    # pylint: disable=unused-argument\n    return FileResponse(join(app.document_root, 'index.html'))\n\n\n@app.route('/sockjs/websocket')\ndef websocket(req):\n    \"\"\"No websocket support yet.\"\"\"\n    # pylint: disable=unused-argument\n    return EmptyResponse()\n\n\n@app.route('/api/logs')\n@check_api_digest\ndef api_logs(req):\n    \"\"\"Returns list of log files in var/log folder\"\"\"\n    # pylint: disable=unused-argument\n    logs_list = []\n\n    for file in listdir(LOGS_PATH):\n        if file.startswith(LOGS_FILES):\n            path = join(LOGS_PATH, file)\n            logs_list.append({\n                'name': file,\n                'size': getsize(path),\n                'date': int(getmtime(path)),\n            })\n    logs_list = sorted(logs_list, key=lambda key: key['name'])\n\n    if not logs_list:\n        try:\n            subprocess.run(shlex.split(\"which journalctl\"),\n                           check=True,\n                           stdout=subprocess.DEVNULL)\n        except CalledProcessError:\n            log.warning(\"journalctl not found\")\n        else:\n            logs_list.append({\n                'name': 'journal',\n                'size': None,\n                'date': int(time.time()),\n            })\n\n    return JSONResponse(files=logs_list)\n\n\n@app.route('/api/logs/<filename>')\n@check_api_digest\ndef api_log(req, filename):\n    \"\"\"Returns content of intended log file\"\"\"\n    # pylint: disable=unused-argument\n    if filename == \"journal\":\n        today = datetime.date.today()\n        week_ago = today - datetime.timedelta(days=7)\n        logs_from = week_ago.isoformat()\n        # pylint: disable=consider-using-with\n        # We cannot close the process when returning the response\n        # It needs to stay open until the response quits\n        # Then it will hopefully get garbage collected\n        result = subprocess.Popen(\n            shlex.split(f\"journalctl -S {logs_from} --no-pager\"),\n            stdout=subprocess.PIPE, bufsize=32768,\n        )\n        journal_output = result.stdout\n        if journal_output is None:\n            raise ValueError(\"No stdout from journalctl\")\n        slightly_different_journal_output = cast(BinaryIO, journal_output)\n        # Abusing a generator response because the file object one is broken\n        # Do not use an attribute if you didn't declare said attribute. EZ\n        return GeneratorResponse(\n            slightly_different_journal_output, content_type=\"text/plain\")\n\n    if not filename.startswith(LOGS_FILES):\n        return Response(status_code=state.HTTP_NOT_FOUND)\n\n    path_ = join(LOGS_PATH, filename)\n    headers_ = {}\n    if path_.endswith(GZ_SUFFIX):\n        headers_ = {\"Content-Encoding\": \"gzip\"}\n    return FileResponse(path_, content_type=\"text/plain\", headers=headers_)\n\n\n@app.route('/api/v1/info')\n@check_api_digest\ndef api_info(req):\n    \"\"\"Returns information about the printer\"\"\"\n    # pylint: disable=unused-argument\n    service_connect = app.daemon.settings.service_connect\n    printer_settings = app.daemon.settings.printer\n    printer = app.daemon.prusa_link.printer\n\n    info = {\n        'name': printer_settings.name,\n        'location': printer_settings.location,\n        'farm_mode': printer_settings.farm_mode,\n        \"network_error_chime\": printer_settings.network_error_chime,\n        'nozzle_diameter': printer.nozzle_diameter,\n        'min_extrusion_temp': LimitsMK3S.min_temp_nozzle_e,\n        'serial': printer.sn,\n        'hostname': service_connect.hostname,\n        'port': service_connect.port,\n    }\n\n    return JSONResponse(**info)\n\n\n@app.route('/api/v1/status')\n@check_api_digest\ndef api_status(req):\n    \"\"\"Returns telemetric data about printer, job and transfer\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=too-many-locals\n    job = app.daemon.prusa_link.model.job\n    tel = app.daemon.prusa_link.model.latest_telemetry\n    transfer = app.daemon.prusa_link.printer.transfer\n    printer = app.daemon.prusa_link.printer\n    camera_configurator = app.daemon.prusa_link.camera_configurator\n    storage_dict = app.daemon.prusa_link.printer.fs.storage_dict\n    status = {}\n\n    # --- Storage ---\n    storage_list = [\n        {\n            \"path\": \"/local\",\n            \"read_only\": False,\n        },\n        {\n            \"path\": \"/sdcard\",\n            \"read_only\": True,\n        }]\n\n    for storage in storage_dict.values():\n        free_space = storage.get_space_info().get(\"free_space\")\n        if storage.path_storage:\n            storage_ = storage_list[0]\n            storage_[\"free_space\"] = free_space\n        else:\n            storage_ = storage_list[1]\n        storage_[\"name\"] = storage.storage\n    status[\"storage\"] = storage_list\n\n    # --- Printer ---\n    status_printer = {\n        \"state\": printer.state.value,\n        \"temp_nozzle\": tel.temp_nozzle,\n        \"temp_bed\": tel.temp_bed,\n        \"axis_z\": tel.axis_z,\n        \"flow\": tel.flow,\n        \"speed\": tel.speed,\n        \"fan_hotend\": tel.fan_hotend,\n        \"fan_print\": tel.fan_print,\n        \"status_connect\": conditions.connect_status(),\n        \"status_printer\": conditions.printer_status(),\n        \"target_nozzle\": tel.target_nozzle,\n        \"target_bed\": tel.target_bed,\n    }\n\n    # X and Y axes data are available only when the axes are not moving\n    if printer.state not in (State.PRINTING, State.BUSY):\n        status_printer[\"axis_x\"] = tel.axis_x\n        status_printer[\"axis_y\"] = tel.axis_y\n    status[\"printer\"] = status_printer\n\n    # --- Camera ---\n    status[\"camera\"] = {\"id\": camera_configurator.order[0]} \\\n        if camera_configurator.order else None\n\n    # --- Job ---\n    if job.job_state is not JobState.IDLE:\n        progress = float(tel.progress or 0)\n        time_remaining = tel.time_remaining\n        time_printing = tel.time_printing\n\n        status_job = {\n            \"id\": job.job_id,\n            \"progress\": progress,\n            \"time_remaining\": time_remaining,\n            \"time_printing\": int(time_printing) if time_printing else None,\n        }\n        status[\"job\"] = status_job\n\n    # --- Transfer ---\n    if transfer.in_progress:\n        status_transfer = {\n            \"id\": transfer.transfer_id,\n            \"time_transferring\": transfer.time_transferring(),\n            \"progress\": round(transfer.progress, 2),\n            \"data_transferred\": transfer.transferred,\n        }\n        status[\"transfer\"] = status_transfer\n\n    return JSONResponse(**filter_null(status))\n\n\n@app.route('/api/version')\n@check_api_digest\ndef api_version(req):\n    \"\"\"Return api version\"\"\"\n    prusa_link = app.daemon.prusa_link\n    type_name = f\"PrusaLink {prusa_link.printer.type.name}\" \\\n        if prusa_link.printer.type else 'Unknown printer type'\n    retval = {\n        'api': \"2.0.0\",\n        'server': __version__,\n        'original': type_name,\n        'text': f\"PrusaLink {__version__}\",\n        'firmware': prusa_link.printer.firmware,\n        'sdk': sdk_version,\n        'capabilities': {\n            \"upload-by-put\": True,\n        },\n        'hostname': gethostname(),\n    }\n    try:\n        show_system_info = bool(int(req.args.get('system', False)))\n    except ValueError:\n        show_system_info = False\n\n    if show_system_info:\n        # pylint: disable=not-an-iterable\n        retval['python'] = [package_to_api(pkg) for pkg in working_set]\n        retval['system'] = {'python': version}\n        try:\n            # pylint: disable=import-outside-toplevel\n            # default in Rasbian OS\n            # ruff: noqa: PLC0415\n            import lsb_release  # type: ignore\n            lsb = lsb_release.get_distro_information()\n            retval['system'].update(lsb)\n        except ImportError:\n            pass\n    return JSONResponse(**retval)\n\n\n@app.route('/api/login', method=state.METHOD_POST)\n@check_api_digest\ndef api_login(req):\n    \"\"\"Always return 200 OK, when Api-Key or HTTP Digest is OK.\"\"\"\n    # pylint: disable=unused-argument\n    return JSONResponse(session=None,\n                        active=True,\n                        admin=True,\n                        user=True,\n                        name='_api')\n\n\n@app.route('/api/printer')\n@check_api_digest\ndef api_printer(req):\n    \"\"\"Returns printer telemetry info\"\"\"\n    # pylint: disable=unused-argument\n    prusa_link = app.daemon.prusa_link\n    tel = prusa_link.model.latest_telemetry\n    sd_ready = prusa_link.sd_ready\n    printer = prusa_link.printer\n    storage_dict = printer.fs.storage_dict\n    operational = printer.state in (State.IDLE, State.FINISHED, State.STOPPED)\n    link_state = printer.state.value\n\n    space_info = storage_dict[app.cfg.printer.directory_name].get_space_info()\n    free_space = space_info[\"free_space\"]\n    total_space = space_info[\"total_space\"]\n    return JSONResponse(\n        **{\n            \"temperature\": {\n                \"tool0\": {\n                    \"actual\": tel.temp_nozzle,\n                    \"target\": tel.target_nozzle,\n                },\n                \"bed\": {\n                    \"actual\": tel.temp_bed,\n                    \"target\": tel.target_bed,\n                },\n            },\n            \"sd\": {\n                \"ready\": sd_ready,\n            },\n            \"state\": {\n                \"text\": PRINTER_STATES[printer.state],\n                \"flags\": {\n                    \"operational\": operational,\n                    \"paused\": printer.state == State.PAUSED,\n                    \"printing\": printer.state == State.PRINTING,\n                    \"cancelling\": printer.state == State.STOPPED,\n                    \"pausing\": printer.state == State.PAUSED,\n                    \"sdReady\": sd_ready,\n                    \"error\": printer.state == State.ERROR,\n                    # Compatibility, READY will be changed to IDLE\n                    \"ready\": printer.state == State.IDLE,\n                    \"closedOrError\": False,\n                    \"finished\": printer.state == State.FINISHED,\n                    # Compatibility, PREPARED will be changed to READY\n                    \"prepared\": printer.ready,\n                    \"link_state\": link_state,\n                },\n            },\n            \"telemetry\": {\n                \"temp-bed\": tel.temp_bed,\n                \"temp-nozzle\": tel.temp_nozzle,\n                \"material\": \" - \",\n                \"z-height\": tel.axis_z,\n                \"print-speed\": tel.speed,\n                \"axis_x\": tel.axis_x,\n                \"axis_y\": tel.axis_y,\n                \"axis_z\": tel.axis_z,\n            },\n            \"storage\": {\n                \"local\": {\n                    \"free_space\": free_space,\n                    \"total_space\": total_space,\n                },\n                \"sd_card\": None,\n            },\n        })\n\n\n@app.route('/api/printer/sd')\n@check_api_digest\ndef api_printer_sd(req):\n    \"\"\"Returns sd state.\"\"\"\n    # pylint: disable=unused-argument\n    return JSONResponse(ready=app.daemon.prusa_link.sd_ready)\n\n\n@app.route('/api/printer/ready', method=state.METHOD_POST)\n@check_api_digest\ndef api_set_ready(req):\n    \"\"\"Set printer state to READY, if printer is in allowed state\"\"\"\n    # pylint: disable=unused-argument\n    command_queue = app.daemon.prusa_link.command_queue\n    try:\n        command_queue.do_command(SetReady(source=Source.WUI))\n    except CommandFailed:\n        return Response(status_code=state.HTTP_CONFLICT)\n    return Response(status_code=state.HTTP_OK)\n\n\n@app.route('/api/printer/ready', method=state.METHOD_DELETE)\n@check_api_digest\ndef api_cancel_ready(req):\n    \"\"\"Set printer state back to IDLE from READY\"\"\"\n    # pylint: disable=unused-argument\n    printer = app.daemon.prusa_link.printer\n    if printer.state == State.READY:\n        printer.cancel_printer_ready(printer.command)\n        return Response(status_code=state.HTTP_OK)\n    return Response(status_code=state.HTTP_CONFLICT)\n\n\n@app.route('/api/timelapse')\n@check_api_digest\ndef api_timelapse(req):\n    \"\"\"Returns timelapse information.\"\"\"\n    # pylint: disable=unused-argument\n    return JSONResponse(config={'type': 'off'},\n                        enabled=False,\n                        files=[],\n                        unrendered=[])\n\n\n@app.route('/api/job')\n@check_api_digest\ndef api_job(req):\n    \"\"\"Returns info about actual printing job\"\"\"\n    # pylint: disable=unused-argument\n    tel = app.daemon.prusa_link.model.latest_telemetry\n    job = app.daemon.prusa_link.model.job\n    printer = app.daemon.prusa_link.printer\n    is_printing = job.job_state == JobState.IN_PROGRESS\n    estimated_from_gcode = 0\n\n    if job.selected_file_path:\n        file_ = {\n            'name': basename(job.selected_file_path),\n            'path': job.selected_file_path,\n            'size': job.selected_file_size,\n            'origin': 'sdcard' if job.from_sd else 'local',\n        }\n\n        if file_['origin'] == 'local':\n            meta = get_metadata(get_os_path(job.selected_file_path))\n            analysis = gcode_analysis(meta.data)\n        else:\n            meta = printer.from_path(job.selected_file_path)\n            analysis = gcode_analysis(meta)\n\n        estimated_from_gcode = analysis.get('estimatedPrintTime')\n\n        if job.selected_file_m_timestamp:\n            file_['date'] = job.selected_file_m_timestamp\n    else:\n        file_ = {\n            'name': None,\n            'path': None,\n            'date': None,\n            'size': None,\n            'origin': None,\n        }\n\n    file_['display'] = file_['name']\n\n    progress = (tel.progress or 0) / 100.0 if is_printing else None\n    time_remaining = tel.time_remaining or estimated_from_gcode\n    time_printing = tel.time_printing or 0\n\n    # Prevent None divide if gcode name doesn't contain estimated time\n    estimated = int(time_remaining + time_printing) \\\n        if is_printing and time_remaining is not None else time_remaining\n\n    return JSONResponse(\n        **{\n            \"job\": {\n                \"estimatedPrintTime\": estimated,\n                \"averagePrintTime\": None,\n                \"lastPrintTime\": None,\n                \"filament\": None,\n                \"file\": file_,\n                \"user\": \"_api\",\n            },\n            \"progress\": {\n                \"completion\": progress,\n                \"filepos\": 0,\n                \"printTime\": time_printing if is_printing else None,\n                \"printTimeLeft\": time_remaining if is_printing else None,\n                \"printTimeLeftOrigin\": \"estimate\",\n                \"pos_z_mm\": tel.axis_z,\n                \"printSpeed\": tel.speed,\n                \"flow_factor\": tel.flow,\n            },\n            \"state\": PRINTER_STATES[printer.state],\n        })\n\n\n@app.route(\"/api/job\", method=state.METHOD_POST)\n@check_api_digest\ndef api_job_command(req):\n    \"\"\"Send command for job control\"\"\"\n    # pylint: disable=too-many-branches\n    job = Job.get_instance()\n    job_data = app.daemon.prusa_link.model.job\n    printer_state = app.daemon.prusa_link.printer.state\n\n    command = req.json.get(\"command\")\n    command_queue = app.daemon.prusa_link.command_queue\n\n    try:\n        if command == \"pause\":\n            if job_data.job_state != JobState.IN_PROGRESS:\n                raise conditions.NotPrinting()\n\n            action = req.json.get(\"action\")\n            if action == 'pause' and printer_state == State.PRINTING:\n                command_queue.do_command(PausePrint(source=Source.WUI))\n            elif action == 'resume' and printer_state == State.PAUSED:\n                command_queue.do_command(ResumePrint(source=Source.WUI))\n            elif action == 'toogle':\n                if printer_state == State.PAUSED:\n                    command_queue.do_command(ResumePrint(source=Source.WUI))\n                elif printer_state == State.PRINTING:\n                    command_queue.do_command(PausePrint(source=Source.WUI))\n\n        elif command == \"cancel\":\n            if job_data.job_state == JobState.IN_PROGRESS:\n                command_queue.do_command(StopPrint(source=Source.WUI))\n            elif job_data.job_state == JobState.IDLE:\n                job.deselect_file()\n            else:\n                raise conditions.NotPrinting()\n\n        elif command == \"start\":\n            if job_data.job_state != JobState.IDLE:\n                raise conditions.CurrentlyPrinting()\n            if job_data.selected_file_path:\n                command_queue.do_command(\n                    StartPrint(job.data.selected_file_path, source=Source.WUI))\n    except CommandFailed as err:\n        return JSONResponse(status_code=state.HTTP_INTERNAL_SERVER_ERROR,\n                            title='COMMAND FAILED',\n                            message=str(err),\n                            text=str(err))\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route(\"/api/v1/job\")\n@check_api_digest\ndef job_info(req):\n    \"\"\"Returns info about current job\"\"\"\n    # pylint: disable=unused-argument\n    job = app.daemon.prusa_link.model.job\n    tel = app.daemon.prusa_link.model.latest_telemetry\n    printer = app.daemon.prusa_link.printer\n    path = job.selected_file_path\n    file_system = app.daemon.prusa_link.printer.fs\n\n    if path and job.job_state is not JobState.IDLE:\n        file = file_system.get(path)\n        storage = \"sdcard\" if job.from_sd else \"local\"\n        os_path = file_system.get_os_path(path)\n        status_job = {\n            \"file\": {\n                \"name\": file.name,\n                \"display_name\": file.name,\n                \"path\": path,\n                \"display_path\": path,\n                \"size\": file.size,\n                \"m_timestamp\": file.attrs[\"m_timestamp\"],\n            },\n            \"id\": job.job_id,\n            \"state\": printer.state.value,\n            \"progress\": float(tel.progress or 0),\n            \"time_remaining\": tel.time_remaining,\n            \"time_printing\": int(tel.time_printing or 0),\n            \"inaccurate_estimates\": tel.inaccurate_estimates,\n        }\n        status_job[\"file\"].update(fill_printfile_data(\n            path=path, os_path=os_path, storage=storage))\n\n        return JSONResponse(**status_job)\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route(\"/api/v1/job/<job_id:int>\", method=state.METHOD_DELETE)\n@check_api_digest\ndef job_stop(req, job_id):\n    \"\"\"Stop job with given id\"\"\"\n    # pylint: disable=unused-argument\n    job = app.daemon.prusa_link.model.job\n    job_data = app.daemon.prusa_link.model.job\n    printer_state = app.daemon.prusa_link.printer.state\n    command_queue = app.daemon.prusa_link.command_queue\n\n    if job.job_id != job_id:\n        raise conditions.NotCurrentJob()\n\n    if printer_state in (State.PRINTING, State.PAUSED) \\\n            and job_data.job_state == JobState.IN_PROGRESS:\n        command_queue.enqueue_command(StopPrint(source=Source.WUI))\n    else:\n        raise conditions.NotPrinting()\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route(\"/api/v1/job/<job_id:int>/<command>\", method=state.METHOD_PUT)\n@check_api_digest\ndef job_command(req, job_id, command):\n    \"\"\"Execute command on job with given id\"\"\"\n    # pylint: disable=unused-argument\n    job = app.daemon.prusa_link.model.job\n    job_data = app.daemon.prusa_link.model.job\n    printer_state = app.daemon.prusa_link.printer.state\n    command_queue = app.daemon.prusa_link.command_queue\n\n    if job.job_id != job_id:\n        raise conditions.NotCurrentJob()\n\n    try:\n        # Pause job with given id\n        if command == \"pause\":\n            if printer_state == State.PRINTING \\\n                    and job_data.job_state == JobState.IN_PROGRESS:\n                command_queue.enqueue_command(PausePrint(source=Source.WUI))\n            else:\n                raise conditions.NotPrinting()\n\n        # Resume paused job with given id\n        elif command == \"resume\":\n            if printer_state == State.PAUSED:\n                command_queue.enqueue_command(ResumePrint(source=Source.WUI))\n            else:\n                raise conditions.NotPaused()\n\n        # Continue in job with given id after timelapse capture\n        elif command == \"continue\":  # Not implemented yet\n            pass\n\n    except CommandFailed as err:\n        return JSONResponse(status_code=state.HTTP_INTERNAL_SERVER_ERROR,\n                            title='COMMAND FAILED',\n                            message=str(err),\n                            text=str(err))\n\n    return Response(status_code=state.HTTP_NO_CONTENT)\n\n\n@app.route(\"/api/v1/update/<env>\")\n@check_api_digest\ndef api_update(req, env):\n    \"\"\"Retrieve information about available update of given environment\"\"\"\n    # pylint: disable=unused-argument\n    headers = {\"Update-Available\": \"False\"}\n\n    if env == \"prusalink\":\n        try:\n            output = check_update_prusalink()\n\n        # There's a problem with package installation, or it does not exist\n        except CalledProcessError as exception:\n            raise conditions.UnavailableUpdate(exception.output.decode()) \\\n                from exception\n\n        # New version is available to download and possible to install\n        if \"Would install\" in output:\n            output = output.splitlines()\n            for string in output:\n                if \"Would install\" in string:\n                    # Get available version number\n                    report = string.split()[-1].split(\"-\")[1]\n                    headers[\"Update-Available\"] = \"True\"\n                    return JSONResponse(new_version=report, headers=headers)\n        # No update available\n        return Response(status_code=state.HTTP_NO_CONTENT, headers=headers)\n\n    if env == \"system\":\n        return Response(status_code=state.HTTP_NOT_IMPLEMENTED)\n\n    return Response(status_code=state.HTTP_BAD_REQUEST)\n\n\n@app.route(\"/api/v1/update/<env>\", method=state.METHOD_POST)\n@check_api_digest\ndef api_update_post(req, env):\n    \"\"\"Update given environment\"\"\"\n    # pylint: disable=unused-argument\n    if env == \"prusalink\":\n        try:\n            output = update_prusalink()\n\n            # No update available\n            if \"Installing collected packages\" not in output:\n                return Response(status_code=state.HTTP_NO_CONTENT)\n\n            # New version was installed correctly - restart PrusaLink\n            app.daemon.restart([])\n            return Response(status_code=state.HTTP_OK)\n\n        # There's a problem with package installation, or it does not exist\n        except CalledProcessError as exception:\n            raise conditions.UnableToUpdate(exception.output.decode()) \\\n                from exception\n\n    if env == \"system\":\n        return Response(status_code=state.HTTP_NOT_IMPLEMENTED)\n\n    return Response(status_code=state.HTTP_BAD_REQUEST)\n"
  },
  {
    "path": "prusa/link/web/settings.py",
    "content": "\"\"\"/api/settings endpoint handlers\"\"\"\nfrom secrets import token_urlsafe\n\nfrom poorwsgi import state\nfrom poorwsgi.digest import check_digest\nfrom poorwsgi.response import JSONResponse\n\nfrom ..conditions import SN\nfrom .lib.auth import (\n    REALM,\n    check_api_digest,\n    set_digest,\n    valid_credentials,\n    valid_digests,\n)\nfrom .lib.core import app\nfrom .lib.wizard import (\n    execute_sn_gcode,\n    new_sn_format,\n    sn_write_success,\n    valid_sn_format,\n)\n\nerrors_titles = {\n    'username_spaces': 'Spaces in username',\n    'username': 'Invalid username',\n    'password': 'Invalid new password',\n    'repassword': 'Invalid re-password',\n    'old_digest': 'Invalid old password',\n    'same_digest': 'Nothing to change',\n}\n\n\ndef set_settings_user(new_username, new_digest):\n    \"\"\"Set new values to user settings\"\"\"\n    app.daemon.settings.service_local.username = new_username\n    app.daemon.settings.service_local.digest = new_digest\n    app.auth_map.clear()\n    app.auth_map.set(REALM, new_username, new_digest)\n\n\ndef save_settings():\n    \"\"\"Save new settings to file\"\"\"\n    with open(app.daemon.cfg.printer.settings, 'w', encoding='utf-8') as ini:\n        app.daemon.settings.write(ini)\n\n\ndef update_apikey(api_key):\n    \"\"\"Set new value to api-key\"\"\"\n    # Update API key in the app\n    app.api_key = api_key\n\n    # Update API key in the printer\n    app.daemon.prusa_link.printer.api_key = api_key\n\n    # Update API key in the prusa_printer_settings.ini file\n    app.daemon.settings.service_local.api_key = api_key\n    app.daemon.settings.update_sections()\n    save_settings()\n\n    # Send info about changes to Connect\n    printer = app.daemon.prusa_link.printer\n    printer.event_cb(**printer.get_info())\n\n\n@app.route('/api/ports')\ndef api_ports(req):\n    \"\"\"Returns dict of available ports and its parameters\"\"\"\n    # pylint: disable=unused-argument\n    if app.daemon.prusa_link:\n        ports_list = app.daemon.prusa_link.model.serial_adapter.ports\n        ports = []\n\n        for port in ports_list:\n            ports.append(port.dict())\n\n        return JSONResponse(**{\n            \"ports\": ports,\n        })\n    return JSONResponse(status_code=state.HTTP_SERVICE_UNAVAILABLE)\n\n\n@app.route('/api/settings')\n@check_api_digest\ndef api_settings(req):\n    \"\"\"Returns printer settings info\"\"\"\n    # pylint: disable=unused-argument\n    service_local = app.daemon.settings.service_local\n    printer_settings = app.daemon.settings.printer\n    return JSONResponse(\n        **{\n            \"api-key\": service_local.api_key,\n            \"username\": service_local.username,\n            \"printer\": {\n                \"name\": printer_settings.name,\n                \"location\": printer_settings.location,\n                \"farm_mode\": printer_settings.farm_mode,\n                \"network_error_chime\": printer_settings.network_error_chime,\n            },\n        })\n\n\n@app.route('/api/settings', method=state.METHOD_POST)\n@check_digest(REALM)\ndef api_settings_set(req):\n    \"\"\"Sets new printer and/or user settings and writes it to ini file\"\"\"\n    # pylint: disable=too-many-locals\n    # pylint: disable=too-many-branches\n    status = state.HTTP_OK\n    printer = req.json.get('printer')\n    user = req.json.get('user')\n    farm_mode = req.json.get('farm_mode')\n    network_error_chime = req.json.get('network_error_chime')\n    errors_ = {}\n    kwargs = {}\n\n    # user settings\n    if user:\n        password = user.get('password')\n        username = user.get('username')\n        if not username:\n            username = user['username'] = req.user\n        new_password = user.get('new_password', password)\n        new_repassword = user.get('new_repassword', password)\n\n        if valid_credentials(username, new_password, new_repassword, errors_):\n            # old_digest is for check if inserted old_password is correct\n            old_digest = set_digest(req.user, password)\n            # Create new_digest for compare with old_digest\n            new_digest = set_digest(username, new_password)\n            user['new_digest'] = new_digest\n            valid_digests(app.daemon.settings.service_local.digest, old_digest,\n                          new_digest, errors_)\n\n    if not errors_:\n        if printer:\n            if printer.get('name'):\n                app.daemon.settings.printer.name = printer['name'].strip()\n            if printer.get('location'):\n                app.daemon.settings.printer.location = \\\n                    printer['location'].strip()\n        if user:\n            set_settings_user(user['username'], user['new_digest'])\n        if farm_mode is not None:\n            app.daemon.settings.printer.farm_mode = farm_mode\n        if network_error_chime is not None:\n            app.daemon.settings.printer.network_error_chime = \\\n                network_error_chime\n\n        if printer or user or farm_mode is not None:\n            app.daemon.settings.update_sections()\n            save_settings()\n        else:\n            status = state.HTTP_NO_CONTENT\n    else:\n        if errors_.get('user'):\n            for key, value in errors_['user'].items():\n                title = key\n                message = value\n                break\n\n        errors_ = {'title': errors_titles[title], 'message': message}\n\n        kwargs = {**errors_}\n        status = state.HTTP_BAD_REQUEST\n\n    return JSONResponse(status_code=status, **kwargs)\n\n\n@app.route('/api/settings/apikey', method=state.METHOD_POST)\n@check_api_digest\ndef regenerate_api_key(req):\n    \"\"\"Regenerate Api-Key and save it to settings and config file\"\"\"\n    # pylint: disable=unused-argument\n    api_key = req.json.get('api-key')\n    if api_key:\n        if len(api_key) < 7:\n            message = \"Api-Key must be at least 7 characters long\"\n            return JSONResponse(status_code=state.HTTP_BAD_REQUEST,\n                                message=message)\n    else:\n        api_key = token_urlsafe(10)\n\n    update_apikey(api_key)\n\n    return JSONResponse(**{\"api-key\": api_key})\n\n\n@app.route('/api/settings/apikey', method=state.METHOD_DELETE)\n@check_api_digest\ndef delete_api_key(req):\n    \"\"\"Replace Api-Key in settings and ini file with empty string\"\"\"\n    # pylint: disable=unused-argument\n    update_apikey('')\n\n    return JSONResponse(status_code=state.HTTP_OK)\n\n\n@app.route('/api/settings/sn')\n@check_api_digest\ndef get_api_sn(req):\n    \"\"\"Get current S/N of the printer\"\"\"\n    # pylint: disable=unused-argument\n    return JSONResponse(**{\"serial\": app.daemon.prusa_link.printer.sn})\n\n\n@app.route('/api/settings/sn', method=state.METHOD_POST)\n@check_api_digest\ndef api_sn(req):\n    \"\"\"If printer is in SN error, user can insert new SN\"\"\"\n    # pylint: disable=unused-argument\n    serial_queue = app.daemon.prusa_link.serial_queue\n    status = state.HTTP_CONFLICT\n    msg = \"Printer already has a valid S/N\"\n\n    if SN:\n        serial = req.json.get('serial')\n        if valid_sn_format(serial):\n            execute_sn_gcode(serial, serial_queue)\n\n            # wait up to five second for S/N to be set\n            if sn_write_success():\n                return JSONResponse(status_code=state.HTTP_OK)\n\n            status = state.HTTP_INSUFFICIENT_STORAGE\n            msg = \"S/N was not successfully written to printer\"\n        else:\n            status = state.HTTP_BAD_REQUEST\n            if new_sn_format(serial):\n                title = \"New S/N format\"\n                msg = \\\n                    \"S/N is in new format. Please contact our Customer support\"\n            else:\n                title = \"Invalid S/N\"\n                msg = \"Please provide a valid S/N\"\n\n            errors_ = {'title': title, 'message': msg}\n            return JSONResponse(**errors_, status_code=status)\n    return JSONResponse(status_code=status, message=msg)\n"
  },
  {
    "path": "prusa/link/web/wizard.py",
    "content": "\"\"\"Wizard endpoints\"\"\"\n\nfrom configparser import ConfigParser, MissingSectionHeaderError\nfrom functools import wraps\nfrom time import sleep\n\nfrom poorwsgi import abort, redirect, state\nfrom poorwsgi.request import FieldStorage\nfrom prusa.connect.printer import Printer\n\nfrom .. import conditions\nfrom ..printer_adapter.structures.regular_expressions import URLS_FOR_WIZARD\nfrom ..web.connection import compose_register_url\nfrom .lib.auth import REALM\nfrom .lib.core import app\nfrom .lib.view import generate_page, redirect_with_proxy\nfrom .lib.wizard import execute_sn_gcode, sn_write_success\n\n# prusa_printer_settings.ini file sections\n# pylint: disable=invalid-name\nPRINTER = 'printer'\nNETWORK = 'network'\nCONNECT = 'service::connect'\nLOCAL = 'service::local'\n\n\ndef check_printer(fun):\n    \"\"\"Check if printer is initialized.\"\"\"\n\n    @wraps(fun)\n    def handler(req):\n        # printer must be initialized for wizard/printer\n        daemon = app.wizard.daemon\n        if not daemon.prusa_link \\\n                or not daemon.prusa_link.printer \\\n                or not conditions.SN:\n            redirect_with_proxy(req, '/wizard')\n        return fun(req)\n\n    return handler\n\n\ndef check_step(step):\n    \"\"\"Check a step of the wizard. If it was not OK, redirect back to it.\"\"\"\n\n    def wrapper(fun):\n        @wraps(fun)\n        def handler(req):\n            # if errors from step isn't empty, it is True too\n            if app.wizard.errors.get(step, True):\n                redirect_with_proxy(req, f'/wizard/{step}')\n            return fun(req)\n\n        return handler\n\n    return wrapper\n\n\nclass ConfigFile:\n    \"\"\"Configuration File object\"\"\"\n\n    def __init__(self):\n        self.buffer = \"\"\n\n    def write(self, data):\n        \"\"\"Count uploaded data size and fill buffer.\"\"\"\n        self.buffer += data.decode('utf-8')\n        size = len(data)\n        return size\n\n    def read(self):\n        \"\"\"File read\"\"\"\n        return self.buffer\n\n    def seek(self, size):\n        \"\"\"File seek\"\"\"\n        return size\n\n\ndef configfile_factory(req):\n    \"\"\"Factory for creating config file instance\"\"\"\n    if req.content_length <= 0:\n        raise conditions.LengthRequired()\n\n    def create(filename):\n        \"\"\"Create Config File object\"\"\"\n        if not filename.endswith('.ini'):\n            raise conditions.NotSupportedFileType()\n        return ConfigFile()\n\n    return create\n\n\ndef process_printer(config):\n    \"\"\"Process printer section\"\"\"\n    printer = config[PRINTER]\n    for option in config.options(PRINTER):\n        if option == 'type':\n            app.wizard.printer_type = printer['type']\n        if option == 'name':\n            app.wizard.printer_name = printer['name'].strip()\n        if option == 'location':\n            app.wizard.printer_location = printer['location'].strip()\n        if option == 'farm_mode':\n            app.wizard.settings.printer.farm_mode = \\\n                printer.getboolean('farm_mode')\n\n\ndef process_network(config):\n    \"\"\"Process network section\"\"\"\n    network = config[NETWORK]\n    if config.has_option(NETWORK, 'hostname'):\n        app.wizard.hostname = network['hostname']\n\n\ndef process_connect(config):\n    \"\"\"Process Connect section\"\"\"\n    connect = config[CONNECT]\n    app.wizard.connect_hostname = 'connect.prusa3d.com'\n    app.wizard.connect_tls = 1\n    app.wizard.connect_port = 0\n\n    for option in config.options(CONNECT):\n        if option == 'hostname':\n            app.wizard.connect_hostname = connect['hostname']\n        if option == 'tls':\n            app.wizard.connect_tls = connect.getboolean('tls')\n        if option == 'port':\n            app.wizard.connect_port = int(connect['port'])\n        if option == 'token':\n            if connect['token']:\n                app.wizard.connect_token = connect['token']\n                app.wizard.restored_connect = True\n\n\ndef process_local(config):\n    \"\"\"Process local section\"\"\"\n    local = config[LOCAL]\n    for option in config.options(LOCAL):\n        if option == 'enable':\n            if local['enable']:\n                app.wizard.enable = local['enable']\n        if option == 'username':\n            if local['username']:\n                app.wizard.username = local['username']\n        if option == 'digest':\n            if local['digest']:\n                app.wizard.digest = local['digest']\n                app.wizard.restored_digest = True\n\n\ndef parse_settings(buffer):\n    \"\"\"Parse printer settings from buffer to wizard\"\"\"\n    try:\n        config = ConfigParser(interpolation=None)\n        config.read_string(buffer)\n    except MissingSectionHeaderError as exception:\n        raise conditions.InvalidIniFileFormat() from exception\n\n    # [printer]\n    if config.has_section(PRINTER):\n        process_printer(config)\n\n    # [network]\n    if config.has_section(NETWORK):\n        process_network(config)\n\n    # [service::connect]\n    if config.has_section(CONNECT):\n        process_connect(config)\n\n    # [service::local]\n    if config.has_section(LOCAL):\n        process_local(config)\n\n\n@app.route('/wizard')\ndef wizard_root(req):\n    \"\"\"First wizard page.\"\"\"\n    return generate_page(req,\n                         \"wizard.html\",\n                         wizard=app.wizard,\n                         conditions=conditions)\n\n\n@app.route('/wizard/restore')\n@check_printer\ndef wizard_restore_(req):\n    \"\"\"Restore wizard settings from ini file\"\"\"\n    return generate_page(req, \"wizard_restore.html\", wizard=app.wizard)\n\n\n@app.route('/wizard/restore', method=state.METHOD_POST)\ndef wizard_restore_post(req):\n    \"\"\"Restore wizard settings from ini file\"\"\"\n    try:\n        form = FieldStorage(req,\n                            keep_blank_values=app.keep_blank_values,\n                            strict_parsing=app.strict_parsing,\n                            file_callback=configfile_factory(req))\n\n        buffer = form['file'].value\n        parse_settings(buffer)\n\n    except TimeoutError as exception:\n        raise conditions.RequestTimeout() from exception\n\n    redirect_with_proxy(req, '/wizard/credentials')\n\n\n@app.route('/wizard/credentials')\n@check_printer\ndef wizard_credentials(req):\n    \"\"\"Credentials configuration.\"\"\"\n    return generate_page(req, \"wizard_credentials.html\", wizard=app.wizard)\n\n\n@app.route('/wizard/credentials', method=state.METHOD_POST)\n@check_printer\ndef wizard_credentials_post(req):\n    \"\"\"Check and store values from wizard_credentials page.\"\"\"\n    form = FieldStorage(req,\n                        keep_blank_values=app.keep_blank_values,\n                        strict_parsing=app.strict_parsing)\n    app.wizard.username = form.get('username', '')\n\n    # Check, if the digest is loaded from an uploaded .ini file\n    if not app.wizard.digest or \\\n            (form.get('password') and form.get('repassword')):\n        password = form.get('password', '')\n        repassword = form.get('repassword', '')\n\n        if not app.wizard.check_credentials(password, repassword):\n            redirect_with_proxy(req, '/wizard/credentials')\n\n        app.wizard.set_digest(password)\n    else:\n\n        if not app.wizard.check_username():\n            redirect_with_proxy(req, '/wizard/credentials')\n\n    redirect_with_proxy(req, '/wizard/printer')\n\n\n@app.route('/wizard/printer')\n@check_step('credentials')\ndef wizard_printer(req):\n    \"\"\"Printer configuration.\"\"\"\n    return generate_page(req, \"wizard_printer.html\", wizard=app.wizard)\n\n\n@app.route('/wizard/printer', method=state.METHOD_POST)\n@check_step('credentials')\ndef wizard_printer_post(req):\n    \"\"\"Check and store values from wizard_printer page.\"\"\"\n    form = FieldStorage(req,\n                        keep_blank_values=app.keep_blank_values,\n                        strict_parsing=app.strict_parsing)\n    app.wizard.printer_name = form.get('name', '').strip()\n    app.wizard.printer_location = form.get('location', '').strip()\n    redirect_with_proxy(req, '/wizard/finish')\n\n\n@app.route('/wizard/finish')\ndef wizard_finish(req):\n    \"\"\"Show wizard status and link to homepage.\"\"\"\n    wizard = app.wizard\n    url = Printer.connect_url(wizard.connect_hostname,\n                              bool(wizard.connect_tls), wizard.connect_port)\n    return generate_page(req,\n                         \"wizard_finish.html\",\n                         wizard=app.wizard,\n                         connect_url=url)\n\n\n@app.route('/wizard/serial')\ndef wizard_serial(req):\n    \"\"\"Show template with S/N insertion\"\"\"\n\n    return generate_page(req, \"wizard_serial.html\", wizard=app.wizard)\n\n\n@app.route('/wizard/serial', method=state.METHOD_POST)\ndef wizard_serial_set(req):\n    \"\"\"Set given S/N to printer\"\"\"\n    wizard = app.wizard\n    serial_queue = app.daemon.prusa_link.serial_queue\n\n    form = FieldStorage(req,\n                        keep_blank_values=app.keep_blank_values,\n                        strict_parsing=app.strict_parsing)\n    wizard.serial = form.get('serial', '').strip()\n\n    if not app.wizard.check_serial():\n        redirect_with_proxy(req, '/wizard/serial')\n\n    execute_sn_gcode(wizard.serial, serial_queue)\n    if sn_write_success():\n        redirect_with_proxy(req, '/wizard/credentials')\n\n    # TODO: A redirect to \"please wait, ensure the printer is idle\"\n    #  and \"please try again or contact support\" after a timer could be nice\n\n    app.wizard.errors['serial']['not_obtained'] = True\n    redirect_with_proxy(req, '/wizard/serial')\n\n\n@app.route('/wizard/finish-register-skip', method=state.METHOD_POST)\ndef wizard_finish_skip_post(req):\n    \"\"\"Check and store values from wizard_connect page.\"\"\"\n    # pylint: disable=unused-argument\n    wizard = app.wizard\n    printer = wizard.daemon.prusa_link.printer\n\n    if wizard.restored_connect:\n        connect_url = Printer.connect_url(wizard.connect_hostname,\n                                          bool(wizard.connect_tls),\n                                          wizard.connect_port)\n        printer.set_connection(connect_url, wizard.connect_token)\n\n    wizard.write_settings(app.settings)\n\n    # set credentials\n    app.auth_map.clear()\n    app.auth_map.set(REALM, wizard.username, wizard.digest)\n\n    # wait up to one second for printer.sn to be set\n    for i in range(10):  # pylint: disable=unused-variable\n        if printer.sn:\n            break\n        sleep(.1)\n    redirect_with_proxy(req, '/')\n\n\n@app.route('/wizard/finish-register', method=state.METHOD_POST)\ndef wizard_finish_post(req):\n    \"\"\"Show wizard status and link to homepage.\"\"\"\n    # pylint: disable=unused-argument\n    wizard = app.wizard\n    printer = wizard.daemon.prusa_link.printer\n    wizard.write_settings(app.settings)\n    connect_url = Printer.connect_url(wizard.connect_hostname,\n                                      bool(wizard.connect_tls),\n                                      wizard.connect_port)\n\n    # set credentials\n    app.auth_map.clear()\n    app.auth_map.set(REALM, wizard.username, wizard.digest)\n\n    # wait up to one second for printer.sn to be set\n    for i in range(10):  # pylint: disable=unused-variable\n        if printer.sn:\n            break\n        sleep(.1)\n\n    # register printer\n    if wizard.connect_token:\n        printer.connection_from_settings(app.settings)\n        redirect_with_proxy(req, '/')\n    elif app.settings.service_connect.token:\n        redirect_with_proxy(req, '/')\n    else:\n        # set connect connection\n        name = wizard.printer_name\n        location = wizard.printer_location\n\n        register_url = compose_register_url(printer=printer,\n                                            connect_url=connect_url,\n                                            name=name,\n                                            location=location)\n        redirect(register_url)\n\n\n@app.before_response()\ndef check_wizard_access(req):\n    \"\"\"Check if wizard can be shown.\"\"\"\n    if not app.settings.is_wizard_needed() \\\n            and req.path.startswith('/wizard'):\n        abort(410)  # auth map is configured, wizard is denied\n\n    if app.settings.is_wizard_needed() \\\n            and URLS_FOR_WIZARD.fullmatch(req.path) and req.method != \"HEAD\":\n        redirect_with_proxy(req, '/wizard')\n"
  },
  {
    "path": "prusalink-boot",
    "content": "#!/bin/bash\nUSERNAME=$(id -nu 1000)\nHOME_DIR=$(eval echo \"~$USERNAME\")\nP_SOURCE=\"/boot/prusa_printer_settings.ini\"\nP_DESTINATION=\"$HOME_DIR/prusa_printer_settings.ini\"\nif test -f $P_SOURCE; then\n    echo \"Using the new printer settings from the boot partition!\" | logger\n    mv $P_SOURCE $P_DESTINATION\n    chown $USERNAME $P_DESTINATION\n    chgrp $USERNAME $P_DESTINATION\n    chmod 644 $P_DESTINATION\nelse\n    echo \"No file to overwrite the current printer settings with.\" | logger\nfi\n\nS_SOURCE=\"/boot/prusalink.ini\"\nS_DESTINATION=\"/etc/prusalink/prusalink.ini\"\nS_DESTINATION_DIR=\"/etc/prusalink\"\nif test -f $S_SOURCE; then\n    echo \"Using the new app config from the boot partition!\" | logger\n    if ! test -d S_DESTINATION_DIR ; then\n        echo \"Creating a folder at $S_DESTINATION_DIR\" | logger\n        mkdir $S_DESTINATION_DIR\n    fi\n    mv $S_SOURCE $S_DESTINATION\n    chmod 644 $S_DESTINATION\nelse\n    echo \"No file to overwrite the current app config with.\" | logger\nfi\n"
  },
  {
    "path": "public/prusalink.json",
    "content": "{\n    \"os_list\": [\n        {\n            \"name\": \"PrusaLink\",\n            \"description\": \"Connect your Prusa 3D printer to your network\",\n            \"icon\": \"https://prusa3d.github.io/Prusa-Link/favicon.png/favicon.png\",\n            \"url\": \"https://github.com/prusa3d/Prusa-Link/releases/download/0.8.1/prusalink-0.8.1.img.xz\",\n            \"extract_size\": 3448222208,\n            \"extract_sha256\": \"cb1ce29d3f9c0363512a699fc414087419b187f917186a91d15fdd9a2720517d\",\n            \"image_download_size\": 785930128,\n            \"release_date\": \"2024-06-28\",\n            \"init_format\": \"systemd\",\n            \"devices\": [\n                \"pi1-32bit\",\n                \"pi2-32bit\",\n                \"pi3-32bit\",\n                \"pi3-64bit\",\n                \"pi4-32bit\",\n                \"pi4-64bit\",\n                \"pi5-32bit\",\n                \"pi5-64bit\"\n            ]\n        },\n        {\n            \"name\": \"PrusaLink Multi-Instance\",\n            \"description\": \"Connect multiple Prusa 3D printers to your network using a single machine\",\n            \"icon\": \"https://prusa3d.github.io/Prusa-Link/favicon.png/favicon.png\",\n            \"url\": \"https://github.com/prusa3d/Prusa-Link/releases/download/0.8.1/prusalink-multi-instance-0.8.1.img.xz\",\n            \"extract_size\": 3452211712,\n            \"extract_sha256\": \"085d5e5f4f57e3878cf6b1b67ab34a782c195243e88169044cae481a8d8db621\",\n            \"image_download_size\": 789348620,\n            \"release_date\": \"2024-06-28\",\n            \"init_format\": \"systemd\",\n            \"devices\": [\n                \"pi1-32bit\",\n                \"pi2-32bit\",\n                \"pi3-32bit\",\n                \"pi3-64bit\",\n                \"pi4-32bit\",\n                \"pi4-64bit\",\n                \"pi5-32bit\",\n                \"pi5-64bit\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "requirements-multi.txt",
    "content": "ipcqueue~=0.9.7\n"
  },
  {
    "path": "requirements-pi.txt",
    "content": "wiringpi~=2.60.1\n"
  },
  {
    "path": "requirements.txt",
    "content": "prusa.connect.sdk.printer>=0.8.1\npy-gcode-metadata\npip>=22.2.0\nbidict~=0.22.1\nblinker~=1.5\nextendparser~=0.3.1\njinja2-template-info~=0.2.4\nlockfile~=0.12.2\npackaging~=23.0\nPoorWSGI~=2.5.0\npydantic==1.10.12 # to be kept pinned until https://github.com/pydantic/pydantic/issues/7689 is resolved\npyric~=0.1.6.3\npython-daemon~=3.0.1\npython-magic~=0.4.27\npython-prctl~=1.8.1\nPyTurboJPEG~=1.7.0\npyudev~=0.24.0\nPyYAML~=6.0\nrequests>=2.31.0\nsortedcontainers~=2.4.0\nunidecode~=1.3.6\nvalidators~=0.20.0\nzeroconf~=0.47.4\n"
  },
  {
    "path": "ruff.toml",
    "content": "lint.select = [\n    \"F\",    # pyflakes\n    \"E\",    # pycodestyle\n    \"W\",    # pycodestyle\n    \"C90\",  # mccabe\n    \"I\",    # isort\n    \"N\",    # pep8-naming\n    # \"D\",    # pydocstyle\n    # \"UP\",   # pyupgrade\n    \"YTT\",  # flake8-2020\n    # \"ANN\",  # flake8-annotations\n    \"S\",    # flake8-bandit\n    \"BLE\",  # flake8-blind-except\n    # \"FBT\",  # flake8-boolean-trap\n    \"B\",    # flake8-bugbear\n    \"A\",    # flake8-builtins\n    \"COM\",  # flake8-commas\n    \"C4\",   # flake8-comprehensions\n    # \"DTZ\",  # flake8-datetimez\n    \"T10\",  # flake8-debugger\n    \"DJ\",   # flake8-django\n    # \"EM\",   # flake8-errmsg\n    \"EXE\",  # flake8-executable\n    \"ISC\",  # flake8-implicit-str-concat\n    \"ICN\",  # flake8-import-conventions\n    \"G\",    # flake8-logging-format\n    \"INP\",  # flake8-no-pep420\n    \"PIE\",  # flake8-pie\n    \"T20\",  # flake8-print\n    \"PYI\",  # flake8-pyi\n    \"PT\",   # flake8-pytest-style\n    # \"Q\",    # flake8-quotes - error in version 0.3.5 https://github.com/astral-sh/ruff/issues/10724\n    \"RSE\",  # flake8-raise\n    \"RET\",  # flake8-return\n    \"SLF\",  # flake8-self\n    \"SIM\",  # flake8-simplify\n    # \"TID\",  # flake8-tidy-imports\n    # \"TCH\",  # flake8-type-checking\n    \"INT\",  # flake8-gettext\n    \"ARG\",  # flake8-unused-arguments\n    # \"PTH\",  # flake8-use-pathlib\n    \"ERA\",  # eradicate\n    \"PD\",   # pandas-vet\n    \"PGH\",  # pygrep-hooks\n    \"PL\",   # Pylint\n    \"TRY\",  # tryceratops\n    \"FLY\",  # flynt\n    \"NPY\",  # NumPy-specific rules\n    \"RUF\",  # Ruff-specific rules\n]\nlint.ignore = [\n    \"S101\",     # Use of `assert` detected\n    \"S105\",     # Possible hardcoded password assigned to: \"TOKEN\"\n    \"Q000\",     # Single quotes found but double quotes preferred\n    \"ARG001\",   # Unused function argument: `gcode`\n    \"SIM105\",   # Use `...` instead of `try`-`except`-`pass`\n    \"PGH003\",   # Use specific rule codes when ignoring type issues\n    \"PLR2004\",  # Magic value used in comparison\n    \"T201\",     # `print` found\n    \"RUF100\",   # Unused `noqa` directive (unused: `E501`)\n    \"A001\",     # Variable `dir` is shadowing a Python builtin\n    \"SIM115\",   # Use context handler for opening files,\n    \"C408\",     # Unnecessary `dict` call (rewrite as a literal)\n    \"PT022\",    # No teardown in fixture `...`, use `return` instead of `yield`\n    \"PT001\",    # Use `@pytest.fixture()` over `@pytest.fixture`\n    \"PT011\",    # `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception\n    \"PT012\",    # `pytest.raises()` block should contain a single simple statement\n    \"SIM117\",   # Use a single `with` statement with multiple contexts instead of nested `with` statements\n    \"S108\",     # Probable insecure usage of temporary file or directory: \"/tmp/c\"\n    \"ARG002\",   # Unused method argument\n    \"N802\",     # Function name should be lowercase\n    \"SIM201\",   # Use ` != ` instead of `not ==`\n    \"PT015\",    # Assertion always fails, replace with `pytest.fail()`\n    \"SLF001\",   # Private member accessed: `_running_loop`\n    \"ISC003\",   # Explicitly concatenated string should be implicitly concatenated\n    \"PLR0915\",  # Too many statements (63 > 50)\n    \"ARG005\",   # Unused lambda argument\n    \"TRY003\",   # Avoid specifying long messages outside the exception class\n    \"ERA001\",   # Found commented-out code\n    \"RET504\",   # Unnecessary variable assignment before `return` statement\n    \"PLR0913\",  # Too many arguments to function call\n    \"RET501\",   # Do not explicitly `return None` in function if it is the only possible return value\n    \"C402\",     # Unnecessary generator (rewrite as a `dict` comprehension)\n    \"PGH004\",   # Use specific rule codes when using `noqa`\n    \"ISC002\",   # Implicitly concatenated string literals over multiple lines\n    \"TRY300\",   # Consider moving this statement to an `else` block\n    \"TRY301\",   # Abstract `raise` to an inner function\n    \"TRY400\",   # `logging.exception` instead of `logging.error\n    \"N818\",     # Exception name `UnknownGcodeFileType` should be named with an Error suffix\n    \"B905\",     # `zip()` without an explicit `strict=` parameter\n    \"SIM108\",   # Use ternary operator\n    \"FLY002\",   # Consider `f'{ROOT}/{abs_path}'` instead of string join\n    \"SIM102\",   # Use a single `if` statement instead of nested `if` statements\n    \"BLE001\",   # Do not catch blind exception: `Exception`\n    \"PLR5501\",  # Consider using `elif` instead of `else` then `if` to remove one indentation level\n    \"RSE102\",   # Unnecessary parentheses on raised exception\n    \"A003\",     # Class attribute `hash` is shadowing a Python builtin\n    \"S311\",     # Standard pseudo-random generators are not suitable for cryptographic purposes\n    \"ISC001\",   # Implicitly concatenated string literals on one line\n    \"SIM110\",   # Use `return all(successor for successor in self)` instead of `for` loop\n    \"S110\",     # `try`-`except`-`pass` detected, consider logging the exception\n    \"C413\",     # Unnecessary `list` call around `sorted()`\n    \"SIM114\",   # Combine `if` branches using logical `or` operator\n    \"C417\",     # Unnecessary `map` usage (rewrite using a `set` comprehension\n    \"C901\",     # `loop_step` is too complex\n    \"PLR0912\",  # Too many branches\n    \"INP001\",   # File is part of an implicit namespace package.\n    \"B011\",     # Do not `assert False`\n    \"PLW2901\",  # `for` loop variable `line` overwritten by assignment target\n    \"S103\",     # `os.chmod` setting a permissive mask `0o777` on file or directory\n    \"C400\",     # Unnecessary generator (rewrite as a `list` comprehension)\n    \"PLR0911\",  # Too many return statements\n    \"B007\",     # Loop control variable `i` not used within loop body\n    \"PIE804\",   # Unnecessary `dict` kwargs\n    \"S603\",     #`subprocess` call: check for execution of untrusted input\n    \"S310\",     # Audit URL open for permitted schemes\n    \"S701\",     # By default, jinja2 sets `autoescape` to `False`\n    \"S324\",     # Probable use of insecure hash functions in `hashlib`: `md5`,\n    \"PT018\",    # Assertion should be broken down into multiple parts\n    \"A002\",     # Argument `format` is shadowing a Python builtin\n    \"TRY201\",   # Use `raise` without specifying exception name\n    \"TRY203\",   # Remove exception handler; error is immediately re-raised\n    \"Q003\",     # Change outer quotes to avoid escaping inner quotes\n    \"PLC1901\",  # `line == \"\"` can be simplified to `not line` as an empty string is falsey\n    \"B008\",     # Do not perform function call `Event` in argument defaults\n    \"B020\",     # Loop control variable `self` overrides iterable it iterates\n    \"TRY401\",   # Redundant exception object included in `logging.exception` call\n    \"PIE810\",   # Call `startswith` once with a `tuple`\n    \"S607\",     # Starting a process with a partial executable path\n    \"RUF005\",   # Consider `[sys.executable, \"-m\", \"prusa.link\", \"restart\", *argv]` instead of concatenation\n    \"RUF015\",   # Prefer `next(iter(screen.lines()))` over single element slice\n    \"S104\",     # Possible binding to all interfaces¸\n]\n\nline-length = 79"
  },
  {
    "path": "setup.py",
    "content": "\"\"\"Setup.py for PrusaLink software.\"\"\"\nimport logging\nimport os\nimport re\nfrom grp import getgrnam\nfrom shutil import copyfile, copytree\nfrom subprocess import run\nfrom sys import stderr\nfrom typing import ClassVar\n\nfrom setuptools import Command, find_namespace_packages, setup  # type: ignore\n\nfrom prusa.link import __author_email__, __author_name__, __version__\nfrom prusa.link import __doc__ as description  # type: ignore\n\nRPI_MODEL_PATH = \"/sys/firmware/devicetree/base/model\"\nRE_GIT = re.compile(r'(-e )?git\\+|:')\nRE_EGG = re.compile(r'#egg=(.*)$')\nREQUIRES = []\n\n\ndef fill_requires(filename):\n    \"\"\"Fill REQUIRES lists.\"\"\"\n    with open(filename, \"r\", encoding='utf-8') as requirements:\n        for line in requirements:\n            line = line.strip()\n            if RE_GIT.match(line):\n                match = RE_EGG.search(line)\n                if match:\n                    REQUIRES.append(f\"{match.groups()[0]} @ {line}\")\n                else:\n                    print(\n                        'Dependency to a git repository must have the format:',\n                        file=stderr)\n                    print(\n                        '\\tgit+ssh://git@github.com/xxx/xxx#egg=package_name',\n                        file=stderr)\n            else:\n                REQUIRES.append(line)\n\n\nfill_requires(\"requirements.txt\")\ntry:\n    if os.path.exists(RPI_MODEL_PATH):\n        with open(RPI_MODEL_PATH, encoding='utf-8') as model_file:\n            if \"Pi\" in model_file.read():\n                fill_requires(\"requirements-pi.txt\")\nexcept Exception:  # pylint: disable=broad-except\n    print(\"This is not a Raspberry Pi -> wiringpi installation won't be \"\n          \"attempted!\")\n\n\ndef doc():\n    \"\"\"Return README.md content.\"\"\"\n    with open('README.md', 'r', encoding='utf-8') as readme:\n        return readme.read().strip()\n\n\nclass BuildStatic(Command):\n    \"\"\"Build static html files, need docker.\"\"\"\n    description = __doc__\n    user_options: ClassVar[list[str]] = [\n            ('target-dir=', 't',\n             \"target build directory (default: './prusa/link/static')\"),\n            ]\n    target_dir = None\n\n    def initialize_options(self):\n        self.target_dir = None\n\n    def finalize_options(self):\n        if self.target_dir is None:\n            cwd = os.path.abspath(os.curdir)\n            self.target_dir = os.path.join(cwd, 'prusa', 'link', 'static')\n\n    def run(self):\n        logging.info(\"building html documentation\")\n        if self.dry_run:\n            if run(['docker', 'version'], check=False).returncode:\n                raise IOError(1, 'docker failed')\n            return\n\n        git_ret = run(['git', 'rev-parse', '--short', 'HEAD'],\n                      check=False, capture_output=True)\n        if git_ret.returncode:\n            raise IOError(1, \"Can't get git commit hash.\")\n        git_commit_hash = git_ret.stdout.strip()\n\n        if run(['docker', 'pull', 'node:latest'], check=False).returncode:\n            raise IOError(1, \"Can't get last node docker.\")\n\n        cwd = os.path.abspath(os.path.join(os.curdir, 'Prusa-Link-Web'))\n\n        copyfile(os.path.join(os.curdir, 'config.custom.js'),\n                 os.path.join(cwd, 'config.custom.js'))\n\n        args = ('docker', 'run', '-t', '--rm', '-u',\n                f\"{os.getuid()}:{getgrnam('docker').gr_gid}\", '-w', cwd,\n                '-v', f\"{cwd}:{cwd}\",\n                'node:latest', 'sh', '-c',\n                'npm install && npm run words:extract')\n        if run(args, check=False).returncode:\n            raise IOError(1, 'docker failed')\n\n        args = ('docker', 'run', '-t', '--rm', '-u',\n                f\"{os.getuid()}:{getgrnam('docker').gr_gid}\", '-w', cwd,\n                '-v', f\"{cwd}:{cwd}\",\n                '-e', f'GIT_COMMIT_HASH={git_commit_hash}',\n                'node:latest', 'sh', '-c',\n                'npm run build:custom')\n        if run(args, check=False).returncode:\n            raise IOError(1, 'docker failed')\n\n        # pylint: disable=unexpected-keyword-arg\n        # (python 3.7)\n        copytree(os.path.join(cwd, 'dist'),\n                 os.path.join(self.target_dir),\n                 dirs_exist_ok=True)\n\n\nsetup(\n    name=\"prusalink\",\n    version=__version__,\n    description=description.split(\"\\n\", maxsplit=1)[0],\n    author=__author_name__,\n    author_email=__author_email__,\n    maintainer=__author_name__,\n    maintainer_email=__author_email__,\n    license=\"Freeware\",\n    url=\"https://github.com/prusa3d/Prusa-Link\",\n    packages=find_namespace_packages(include=['prusa.*']),\n    include_package_data=True,\n    data_files=[('share/prusalink',\n                 ['README.md', 'ChangeLog', 'CONTRIBUTION.md'])],\n    scripts=['prusalink-boot'],\n    long_description=doc(),\n    long_description_content_type=\"text/markdown\",\n    classifiers=[\n        \"Development Status :: 4 - Beta\",\n        \"Natural Language :: English\",\n        \"License :: Freeware\",\n        \"Operating System :: POSIX :: Linux\",\n        \"Programming Language :: Python :: 3 :: Only\",\n    ],\n    python_requires=\">=3.9\",\n    install_requires=REQUIRES,\n    entry_points={'console_scripts': [\n        'prusalink = prusa.link.__main__:main',\n        'prusalink-manager = prusa.link.multi_instance.__main__:main',\n    ]},\n    cmdclass={'build_static': BuildStatic})\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_carousel.py",
    "content": "\"\"\"Tests of the LCD Printer component\"\"\"\nfrom time import time\n\nfrom prusa.link.printer_adapter.structures.carousel import (  # type:ignore\n    Carousel,\n    LCDLine,\n    Screen,\n)\n\n\ndef test_line():\n    \"\"\"Test that the LCDLine object works as expected\"\"\"\n    line = LCDLine(\"Derpy is best pony\",\n                   delay=5,\n                   resets_idle=True,\n                   chime_gcode=[\"M300 S900 P1\"])\n    assert line.text == \"Derpy is best pony\"\n    assert line.delay == 5\n    assert line.resets_idle\n    assert line.ends_at > time() + 4.5\n    assert line.chime_gcode == [\"M300 S900 P1\"]\n    line.ends_at = time() + 1\n    line.reset_end()\n    assert line.ends_at > time() + 4.5\n\n\ndef test_screen_lines():\n    \"\"\"Tests that a screen with set text outputs the correct lines\n    with the right properties attached\"\"\"\n    screen = Screen(resets_idle=False)\n    carousel = Carousel(screens=[screen])\n    carousel.set_text(\n        screen=screen,\n        text=\"I tried searching for an interesting text and got sidetracked.\",\n        scroll_delay=1,\n        first_line_extra=3,\n        last_line_extra=5,\n        scroll_amount=3)\n    lines = list(screen.lines())\n    first = lines[0]\n    second = lines[1]\n    second_to_last = lines[-2]\n    last = lines[-1]\n    assert first.text == \"I tried searching f\"\n    assert first.delay == 4\n    assert second.text == \"ried searching for \"\n    assert second.delay == 1\n    assert second_to_last.text == \"and got sidetracked\"\n    assert second_to_last.delay == 1\n    assert last.text == \"nd got sidetracked.\"\n    assert last.delay == 6\n    assert len(lines) == 16\n\n    carousel.set_text(screen=screen,\n                      text=\"A\",\n                      scroll_delay=1,\n                      first_line_extra=0,\n                      last_line_extra=0)\n    line = list(screen.lines())[0]\n    assert line.text == \"A\"\n    assert line.delay == 1\n\n\ndef test_priority():\n    \"\"\"Tests that the bigger priority gets shown and smaller gets hidden\"\"\"\n    screen_a = Screen(order=1)\n    screen_b = Screen(order=2)\n    screen_c = Screen(order=3)\n\n    carousel = Carousel(screens=[screen_a, screen_b, screen_c])\n    carousel.set_text(screen_a, \"A\")\n    carousel.set_text(screen_b, \"B\")\n    carousel.set_text(screen_c, \"C\")\n    carousel.set_priority(screen_a, 1)\n    carousel.set_priority(screen_b, 2)\n    carousel.set_priority(screen_c, 2)\n\n    carousel.enable(screen_b)\n    assert carousel.active_set == {screen_b}\n    carousel.enable(screen_a)\n    assert carousel.active_set == {screen_b}\n    carousel.enable(screen_c)\n    assert carousel.active_set == {screen_b, screen_c}\n    carousel.set_priority(screen_a, 3)\n    assert carousel.active_set == {screen_a}\n    carousel.disable(screen_a)\n    assert carousel.active_set == {screen_b, screen_c}\n    assert carousel.active_screens == [screen_b, screen_c]\n    carousel.set_priority(screen_a, 2)\n    carousel.enable(screen_a)\n    assert carousel.active_set == {screen_a, screen_b, screen_c}\n    assert carousel.active_screens == [screen_a, screen_b, screen_c]\n\n\ndef test_lines():\n    \"\"\"Tests the get_next output\"\"\"\n    screen_a = Screen(order=1, resets_idle=False)\n    screen_b = Screen(order=2, chime_gcode=[\"M300 S900 P1\"], resets_idle=True)\n    screen_c = Screen(order=3)\n    carousel = Carousel(screens=[screen_a, screen_b, screen_c])\n    carousel.set_text(screen_a, \"A\")\n    carousel.set_text(screen_b, \"B\")\n    carousel.set_text(screen_c, \"C\")\n    carousel.set_priority(screen_a, 2)\n    carousel.set_priority(screen_b, 2)\n    carousel.set_priority(screen_c, 1)\n    carousel.enable(screen_a)\n    carousel.enable(screen_b)\n    carousel.enable(screen_c)\n\n    # Test normal operation, shows lines in order\n    a_line = carousel.get_next()\n    assert a_line.text == \"A\"\n    assert not a_line.resets_idle\n    b_line = carousel.get_next()\n    assert b_line.text == \"B\"\n    assert b_line.chime_gcode == [\"M300 S900 P1\"]  # and chimes\n    assert b_line.resets_idle\n    # This time around it shouldn't chime\n    assert carousel.get_next().text == \"A\"\n    # Second time around, it should not chime\n    assert carousel.get_next().chime_gcode == []\n    # messages have priority\n    carousel.add_message(LCDLine(\"asdf\"))\n    assert carousel.get_next().text == \"asdf\"\n    # Setting a hidden Screen does not rewind\n    assert carousel.get_next().text == \"A\"\n    carousel.set_text(screen_c, \"Not C\")\n    assert carousel.get_next().text == \"B\"\n    assert carousel.get_next().text == \"A\"\n    # setting a shown screen rewinds\n    carousel.set_text(screen_b, \"Very much B\")\n    assert carousel.get_next().text == \"A\"\n    assert carousel.get_next().text == \"Very much B\"\n    carousel.disable(screen_a)\n    # Enabling again does not reset the chime\n    carousel.enable(screen_b)\n    assert carousel.get_next().chime_gcode == []\n    carousel.disable(screen_b)\n    carousel.enable(screen_b)\n    assert carousel.get_next().chime_gcode == [\"M300 S900 P1\"]\n    carousel.disable(screen_b)\n    carousel.enable(screen_c)\n    assert carousel.get_next().text == \"Not C\"\n    carousel.disable(screen_a)\n    carousel.disable(screen_c)\n    assert carousel.get_next() is None\n"
  },
  {
    "path": "tests/test_ipc_queue.py",
    "content": "\"\"\"Test for the IPC queue adapter.\"\"\"\nimport logging\nimport os\nimport signal\nimport threading\n\nimport pytest\n\nfrom prusa.link.const import QUIT_INTERVAL\nfrom prusa.link.multi_instance.ipc_queue_adapter import IPCConsumer, IPCSender\nfrom tests.util import EventSetMock\n\nTEST_QUEUE_NAME = \"/prusalink_test_ipc_queue\"\nlogging.basicConfig(level=logging.DEBUG)\n\n# pylint: disable=redefined-outer-name\n\n\n@pytest.fixture()\ndef ipc_consumer():\n    \"\"\"IPCConsumer setup fixture\"\"\"\n    ipc_consumer = IPCConsumer(TEST_QUEUE_NAME)\n    ipc_consumer.start()\n    yield ipc_consumer\n    ipc_consumer.stop()\n\n\ndef test_send_and_close(ipc_consumer):\n    \"\"\"Test sending a message to the ipc message queue\"\"\"\n    mock_handler = EventSetMock()\n    ipc_consumer.add_handler(\"test\", mock_handler)\n    IPCSender.send_and_close(TEST_QUEUE_NAME, \"test\")\n    mock_handler.event.wait(timeout=QUIT_INTERVAL)\n    mock_handler.assert_called_once()\n\n\ndef test_multiple_sends(ipc_consumer):\n    \"\"\"Test sending multiple messages to the ipc message queue\"\"\"\n    mock_handler_1 = EventSetMock()\n    mock_handler_2 = EventSetMock()\n    ipc_consumer.add_handler(\"test_1\", mock_handler_1)\n    ipc_consumer.add_handler(\"test_2\", mock_handler_2)\n    ipc_sender = IPCSender(TEST_QUEUE_NAME)\n    ipc_sender.send(\"test_1\")\n    ipc_sender.send(\"test_2\")\n\n    mock_handler_1.event.wait(timeout=QUIT_INTERVAL)\n    mock_handler_2.event.wait(timeout=QUIT_INTERVAL)\n    mock_handler_1.assert_called_once()\n    mock_handler_2.assert_called_once()\n\n\ndef test_args(ipc_consumer):\n    \"\"\"Test sending a message with arguments to the ipc message queue\"\"\"\n    mock_handler = EventSetMock()\n    ipc_consumer.add_handler(\"test\", mock_handler)\n    IPCSender.send_and_close(TEST_QUEUE_NAME,\n                             \"test\",\n                             \"foo\",\n                             42,\n                             arnold=\"rimmer\")\n    mock_handler.event.wait(timeout=QUIT_INTERVAL)\n    mock_handler.assert_called_once_with(\"foo\", 42, arnold=\"rimmer\")\n\n\ndef test_rights():\n    \"\"\"Test that the IPC queue can be created with the correct rights\"\"\"\n    ipc_consumer = IPCConsumer(TEST_QUEUE_NAME, chown_uid=0, chown_gid=0)\n    with pytest.raises(PermissionError):\n        ipc_consumer.start()\n\n\ndef test_signal_resistance(ipc_consumer):\n    \"\"\"Test that the IPC sender is resistant to POSIX signal interrupts\"\"\"\n    make_noise = True\n\n    def signal_handler(*_):\n        pass\n\n    def noisemaker():\n        while make_noise:\n            os.kill(os.getpid(), signal.SIGINT)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    noise_thread = threading.Thread(target=noisemaker)\n    noise_thread.start()\n\n    mock_handler = EventSetMock()\n    ipc_consumer.add_handler(\"test\", mock_handler)\n    ipc_sender = IPCSender(TEST_QUEUE_NAME)\n    for _ in range(100):\n        ipc_sender.send(\"test\")\n        mock_handler.event.wait(timeout=QUIT_INTERVAL)\n        mock_handler.assert_called_once()\n        mock_handler.reset_mock()\n    make_noise = False\n    noise_thread.join()\n    ipc_sender.close()\n\n\ndef test_signal_resistance_reverse():\n    \"\"\"Test that the IPC consumer is resistant to POSIX signal interrupts\"\"\"\n    # pylint: disable=protected-access\n    make_noise = True\n\n    ipc_consumer = IPCConsumer(TEST_QUEUE_NAME)\n    ipc_consumer._setup_queue()\n\n    def signal_handler(*_):\n        pass\n\n    def noisemaker():\n        while make_noise:\n            os.kill(os.getpid(), signal.SIGINT)\n\n    signal.signal(signal.SIGINT, signal_handler)\n    noise_thread = threading.Thread(target=noisemaker)\n    noise_thread.start()\n\n    mock_handler = EventSetMock()\n    ipc_consumer.add_handler(\"test\", mock_handler)\n    ipc_sender = IPCSender(TEST_QUEUE_NAME)\n\n    def actual_test():\n        nonlocal make_noise\n\n        for _ in range(100):\n            ipc_sender.send(\"test\")\n            mock_handler.event.wait(timeout=QUIT_INTERVAL)\n            mock_handler.assert_called_once()\n            mock_handler.reset_mock()\n        make_noise = False\n        noise_thread.join()\n        ipc_sender.close()\n        ipc_consumer.running = False\n\n    test_thread = threading.Thread(target=actual_test)\n    test_thread.start()\n    ipc_consumer.running = True\n    ipc_consumer._read_commands()\n    test_thread.join()\n"
  },
  {
    "path": "tests/test_item_updater.py",
    "content": "\"\"\"Test of the InfoUpdater component\"\"\"\n\n# pylint:disable=redefined-outer-name too-many-locals too-many-statements\n\nimport logging\nimport math\nfrom queue import PriorityQueue\nfrom time import sleep, time\nfrom unittest.mock import Mock\n\nimport pytest\n\nfrom prusa.link.printer_adapter.structures.item_updater import (  # type:ignore\n    ItemUpdater,\n    WatchedGroup,\n    WatchedItem,\n)\n\nfrom .util import EventSetMock, WaitingMock\n\nlogging.basicConfig(level=\"DEBUG\")\n\nTHRESHOLD = 0.05\n\n\n@pytest.fixture\ndef updater_instance():\n    \"\"\"\n    A fixture providing an instance of the ItemUpdater for the tests\n    \"\"\"\n    info_updater = ItemUpdater()\n    info_updater.start()\n    yield info_updater\n    info_updater.stop()\n\n\n@pytest.fixture\ndef validator():\n    \"\"\"A fixture providing a validator that only accepts 42 to the tests\"\"\"\n    def inner(value):\n        \"\"\"A validator that accepts only 42 as a valid value\"\"\"\n        return value == 42\n\n    return inner\n\n\ndef test_basics(updater_instance: ItemUpdater):\n    \"\"\"\n    Tests the basics, adding a watched item, gathering and writing of a value\n    and became valid/invalid signalling\n    :param updater_instance:\n    :return:\n    \"\"\"\n\n    gather = WaitingMock(return_value=42)\n    write = Mock()\n    # This empty spec makes it possible to pass this mock straight to a\n    # blinker signal\n    invalidated = EventSetMock(spec={})\n    valid = EventSetMock(spec={})\n    basic_item = WatchedItem(\"basic_item\",\n                             gather_function=gather,\n                             write_function=write)\n    basic_item.became_valid_signal.connect(valid)\n    basic_item.became_invalid_signal.connect(invalidated)\n    updater_instance.add_item(basic_item)\n\n    # Reminder that invalidation is only signalled when going from a\n    # valid state, so not at the beginning\n    invalidated.assert_not_called()\n    valid.assert_not_called()\n    write.assert_not_called()\n\n    gather.event.set()  # unstuck the gather\n    assert valid.event.wait(timeout=1), \"Didn't signal becoming valid\"\n\n    gather.assert_called_once()\n    write.assert_called_once_with(42)\n    valid.assert_called_once_with(basic_item)\n\n    updater_instance.invalidate(basic_item)\n    assert invalidated.event.wait(timeout=1), \"Didn't signal becoming invalid\"\n\n    invalidated.assert_called_once_with(basic_item)\n\n\ndef test_group(updater_instance: ItemUpdater):\n    \"\"\"\n    Tests that the WatchedGroup becomes valid only after all its\n    children are valid.\n    Tests that the WatchedGroup becomes invalid, if it was valid and one of\n    its members becomes invalid\n    Tests that the WatchedGroup does not signal invalidation unless it was\n    valid before\n    \"\"\"\n    gather_1 = WaitingMock(return_value=1)\n    gather_2 = WaitingMock(return_value=2)\n    group_valid = EventSetMock(spec={})\n    group_invalidated = EventSetMock(spec={})\n    item_1_valid = EventSetMock(spec={})\n    item_2_valid = EventSetMock(spec={})\n    item_2_invalidated = EventSetMock(spec={})\n    item_1 = WatchedItem(\"item_1\",\n                         gather_function=gather_1,\n                         write_function=Mock())\n    item_1.became_valid_signal.connect(item_1_valid)\n    item_2 = WatchedItem(\"item_2\",\n                         gather_function=gather_2,\n                         write_function=Mock())\n    item_2.became_valid_signal.connect(item_2_valid)\n    item_2.became_invalid_signal.connect(item_2_invalidated)\n    watched_group = WatchedGroup([item_1, item_2])\n    watched_group.became_valid_signal.connect(group_valid)\n    watched_group.became_invalid_signal.connect(group_invalidated)\n\n    updater_instance.add_item(item_1)\n    updater_instance.add_item(item_2)\n\n    group_valid.assert_not_called()\n\n    gather_1.event.set()\n\n    assert item_1_valid.event.wait(THRESHOLD)\n\n    group_valid.assert_not_called()\n\n    gather_2.event.set()\n\n    assert item_2_valid.event.wait(THRESHOLD)\n\n    group_valid.assert_called_once()\n    group_invalidated.assert_not_called()\n\n    updater_instance.invalidate(item_1)\n\n    assert group_invalidated.event.wait(THRESHOLD)\n\n    group_invalidated.assert_called_once()\n\n    updater_instance.invalidate(item_2)\n\n    assert item_2_invalidated.event.wait(THRESHOLD)\n\n    # Still only called once, not every time a member invalidates\n    group_invalidated.assert_called_once()\n\n\ndef test_scheduled_invalidation(updater_instance: ItemUpdater):\n    \"\"\"\n    Tests that scheduling an invalidation works properly\n\n    1. Scheduling without an interval has to throw an error\n    2. Scheduling without an interval when the item has a default uses the it\n    3. Scheduling with an interval overwrites the default\n    4. Re-scheduling the interval does nothing\n    5. Force re-scheduling resets the interval\n    6. Setting the value resets the scheduled invalidation\n    7. Auto invalidation works\n    8. Cancelling a scheduled invalidation works\n    9. Setting a value to an item scheduled for invalidation without default\n       interval does not cancel the scheduling (should it work this way?)\n    \"\"\"\n    base_interval = 0.2  # base invalidation interval\n    # for tests that refresh it, when to do so. should be < base_interval\n    refresh_offset = 0.1\n    offset_interval = base_interval + refresh_offset\n\n    time_of_start = 0.0  # set up later to time()\n    results = {}\n\n    item_1 = WatchedItem(\"item_1\",\n                         gather_function=Mock(),\n                         write_function=lambda value: None)\n    item_2 = WatchedItem(\"item_2\", gather_function=Mock())\n    item_2.became_invalid_signal.connect(\n        lambda item: results.update({2: time() - time_of_start}), weak=False)\n    item_3 = WatchedItem(\"item_3\", gather_function=Mock())\n    item_3.became_invalid_signal.connect(\n        lambda item: results.update({3: time() - time_of_start}), weak=False)\n    item_4 = WatchedItem(\"item_4\", gather_function=Mock())\n    item_4.became_invalid_signal.connect(\n        lambda item: results.update({4: time() - time_of_start}), weak=False)\n    item_5 = WatchedItem(\"item_5\", gather_function=Mock())\n    item_5.became_invalid_signal.connect(\n        lambda item: results.update({5: time() - time_of_start}), weak=False)\n    item_6 = WatchedItem(\"item_6\", gather_function=Mock())\n    item_6.became_invalid_signal.connect(\n        lambda item: results.update({6: time() - time_of_start}), weak=False)\n    item_7 = WatchedItem(\"item_7\",\n                         gather_function=Mock(),\n                         interval=base_interval)\n    item_7.became_invalid_signal.connect(\n        lambda item: results.update({7: time() - time_of_start}), weak=False)\n    item_8 = WatchedItem(\"item_8\",\n                         gather_function=Mock(),\n                         interval=base_interval)\n    item_8.became_invalid_signal.connect(\n        lambda item: results.update({8: time() - time_of_start}), weak=False)\n    item_9 = WatchedItem(\"item_9\",\n                         gather_function=Mock(),\n                         interval=base_interval)\n    item_9.became_invalid_signal.connect(\n        lambda item: results.update({9: time() - time_of_start}), weak=False)\n\n    group_valid = EventSetMock(spec={})\n    watched_group = WatchedGroup([\n        item_1, item_2, item_3, item_4, item_5, item_6, item_7, item_8, item_9,\n    ])\n    watched_group.became_valid_signal.connect(group_valid)\n\n    updater_instance.add_item(item_1)\n    updater_instance.add_item(item_2)\n    updater_instance.add_item(item_3)\n    updater_instance.add_item(item_4)\n    updater_instance.add_item(item_5)\n    updater_instance.add_item(item_6)\n    updater_instance.add_item(item_7)\n    updater_instance.add_item(item_8)\n    updater_instance.add_item(item_9)\n\n    assert group_valid.event.wait(THRESHOLD)\n\n    # set intervals after the items become valid for them to not auto schedule\n    item_2.interval = base_interval\n    item_3.interval = base_interval\n    item_4.interval = base_interval\n    item_5.interval = base_interval\n    item_6.interval = base_interval\n\n    time_of_start = time()\n\n    failed = False\n    try:\n        updater_instance.schedule_invalidation(item_1)\n    # pylint: disable=broad-except\n    except Exception:\n        failed = True\n    assert failed, \"Scheduling invalidation without an interval has to error\"\n\n    updater_instance.schedule_invalidation(item_2)\n    updater_instance.schedule_invalidation(item_3,\n                                           base_interval + refresh_offset)\n\n    updater_instance.schedule_invalidation(item_4)\n    updater_instance.schedule_invalidation(item_5)\n    updater_instance.schedule_invalidation(item_6)\n    updater_instance.schedule_invalidation(item_8, interval=base_interval)\n    updater_instance.schedule_invalidation(item_8, interval=base_interval)\n\n    sleep(refresh_offset)\n    updater_instance.cancel_scheduled_invalidation(item_8)\n    updater_instance.schedule_invalidation(item_4)\n    updater_instance.schedule_invalidation(item_5, reschedule=True)\n    updater_instance.set_value(item_6, 6)\n    updater_instance.set_value(item_9, 9)\n\n    # Reset the intervals to none, so the items won't auto re-schedule\n    item_2.interval = None\n    item_3.interval = None\n    item_4.interval = None\n    item_5.interval = None\n    item_6.interval = None\n    item_7.interval = None\n\n    # a \"busy\" wait in a test is fine\n    times_out_at = time() + refresh_offset + THRESHOLD\n    while not {2, 3, 4, 5, 6, 7, 9}.issubset(set(results.keys())):\n        if time() > times_out_at:\n            assert False, \"\"\"Timed out waiting for invalidation results\"\"\"\n        sleep(0.1)\n\n    assert base_interval <= results[2] <= base_interval + THRESHOLD\n    assert offset_interval <= results[3] <= offset_interval + THRESHOLD\n    assert base_interval <= results[4] <= base_interval + THRESHOLD\n    assert offset_interval <= results[5] <= offset_interval + THRESHOLD\n    assert offset_interval <= results[6] <= offset_interval + THRESHOLD\n    assert offset_interval <= results[9] <= offset_interval + THRESHOLD\n\n    # No precise syncing is done for this one, it's assumed it would have\n    # already invalidated because we were waiting more than THRESHOLD after\n    # it would if it was broken\n    assert 8 not in results\n\n\ndef test_validation(updater_instance: ItemUpdater, validator):\n    \"\"\"\n    Tests that an item which gathers a valid value validates and\n    that an item which gathers an invalid one errors out\n    \"\"\"\n    valid_valid = EventSetMock(spec={})\n    valid_errored = EventSetMock(spec={})\n    valid_item = WatchedItem(\"valid_item\",\n                             gather_function=Mock(return_value=42),\n                             validation_function=validator)\n    valid_item.became_valid_signal.connect(valid_valid)\n    valid_item.validation_error_signal.connect(valid_errored)\n    updater_instance.add_item(valid_item)\n\n    invalid_valid = EventSetMock(spec={})\n    invalid_errored = EventSetMock(spec={})\n    invalid_item = WatchedItem(\"invalid_item\",\n                               gather_function=Mock(return_value=69),\n                               validation_function=validator)\n    invalid_item.became_valid_signal.connect(invalid_valid)\n    invalid_item.validation_error_signal.connect(invalid_errored)\n    updater_instance.add_item(invalid_item)\n\n    assert valid_valid.event.wait(THRESHOLD)\n    assert invalid_errored.event.wait(THRESHOLD)\n\n    valid_valid.assert_called_once_with(valid_item)\n    valid_errored.assert_not_called()\n    invalid_valid.assert_not_called()\n    invalid_errored.assert_called_once()\n\n\ndef test_gather_error(updater_instance: ItemUpdater):\n    \"\"\"\n    Test gather exception handling\n    Test addressing items by their names\n    :param updater_instance:\n    :return:\n    \"\"\"\n    fail_interval = 0.1\n    threshold = 0.05\n\n    item_errored = EventSetMock(spec={})\n    write_mock = EventSetMock()\n    item = WatchedItem(\"item\",\n                       gather_function=Mock(side_effect=RuntimeError(\"Test\")),\n                       write_function=write_mock,\n                       on_fail_interval=fail_interval)\n    item.error_refreshing_signal.connect(item_errored)\n\n    time_of_start = time()\n    updater_instance.add_item(item)\n    assert item_errored.event.wait(threshold)\n    item_errored.event.clear()\n    assert item_errored.event.wait(fail_interval + threshold)\n    assert fail_interval < time() - time_of_start < fail_interval + threshold\n\n    write_mock.assert_not_called()\n    updater_instance.cancel_scheduled_invalidation(item)\n    updater_instance.set_value(item, 42)\n    assert write_mock.event.wait(threshold)\n\n\ndef test_timeouts(updater_instance: ItemUpdater, validator):\n    \"\"\"\n    1. Test that a stuck getter times out\n    2. Test that a failed getter which doesn't get re-scheduled times out\n    3. Test that a getter that keeps failing times out\n    4. Test that a getter which got stuck does not time out\n       if the value is provided from the outside\n    5. Test that an item which refreshes and then gets stuck times out after\n       the full time out amount\n    6. Test that an item which refreshes successfully doesn't time out\n    \"\"\"\n\n    base_interval = 0.2  # base timeout interval\n    wait_interval = 0.1\n    offset_interval = base_interval + wait_interval\n\n    stuck_gatherer = WaitingMock()\n\n    item_1_timeout_mock = EventSetMock(spec={})\n    item_1 = WatchedItem(\"item_1\",\n                         gather_function=stuck_gatherer,\n                         timeout=base_interval)\n    item_1.timed_out_signal.connect(item_1_timeout_mock)\n\n    item_2_timeout_mock = EventSetMock(spec={})\n    item_2 = WatchedItem(\"item_2\",\n                         gather_function=Mock(return_value=69),\n                         timeout=base_interval,\n                         on_fail_interval=1000,\n                         validation_function=validator)\n    item_2.timed_out_signal.connect(item_2_timeout_mock)\n\n    item_3_timeout_mock = EventSetMock(spec={})\n    item_3 = WatchedItem(\"item_3\",\n                         gather_function=Mock(return_value=69),\n                         timeout=base_interval,\n                         on_fail_interval=1000,\n                         validation_function=validator)\n    item_3.timed_out_signal.connect(item_3_timeout_mock)\n\n    item_4_timeout_mock = EventSetMock(spec={})\n    item_4 = WatchedItem(\"item_4\",\n                         gather_function=stuck_gatherer,\n                         timeout=base_interval)\n    item_4.timed_out_signal.connect(item_4_timeout_mock)\n\n    item_5_timeout_mock = EventSetMock(spec={})\n    item_5_valid_mock = EventSetMock(spec={})\n    item_5 = WatchedItem(\"item_5\",\n                         gather_function=stuck_gatherer,\n                         timeout=base_interval)\n    item_5.timed_out_signal.connect(item_5_timeout_mock)\n    item_5.became_valid_signal.connect(item_5_valid_mock)\n\n    item_6_timeout_mock = EventSetMock(spec={})\n    item_6 = WatchedItem(\"item_6\",\n                         gather_function=Mock(return_value=42),\n                         timeout=base_interval)\n    item_6.timed_out_signal.connect(item_6_timeout_mock)\n\n    time_of_start = time()\n    updater_instance.add_item(item_1)\n    assert item_1_timeout_mock.event.wait(base_interval + THRESHOLD)\n    assert base_interval < time() - time_of_start < base_interval + THRESHOLD\n    stuck_gatherer.event.set()\n    stuck_gatherer.event.clear()\n\n    time_of_start = time()\n    updater_instance.add_item(item_2)\n    assert item_2_timeout_mock.event.wait(base_interval + THRESHOLD)\n    assert base_interval < time() - time_of_start < base_interval + THRESHOLD\n    updater_instance.set_value(item_2, 42)\n    # make sure this does not get invalidated\n    updater_instance.cancel_scheduled_invalidation(item_2)\n\n    time_of_start = time()\n    updater_instance.add_item(item_3)\n    assert item_3_timeout_mock.event.wait(base_interval + THRESHOLD)\n    assert base_interval < time() - time_of_start < base_interval + THRESHOLD\n    updater_instance.set_value(item_3, 42)\n    # make sure this does not get invalidated\n    updater_instance.cancel_scheduled_invalidation(item_3)\n\n    updater_instance.add_item(item_4)\n    sleep(wait_interval)\n    updater_instance.set_value(item_4, 1)\n    assert not item_4_timeout_mock.event.wait(base_interval + THRESHOLD)\n    stuck_gatherer.event.set()\n    stuck_gatherer.event.clear()\n\n    time_of_start = time()\n    updater_instance.add_item(item_5)\n    sleep(wait_interval)\n    stuck_gatherer.event.set()\n    stuck_gatherer.event.clear()\n    assert item_5_valid_mock.event.wait(THRESHOLD)\n    updater_instance.invalidate(item_5)\n    assert item_5_timeout_mock.event.wait(base_interval + THRESHOLD)\n    # It is important that it's not less than the offset interval\n    offset_with_threshold = offset_interval + THRESHOLD\n    assert offset_interval < time() - time_of_start < offset_with_threshold\n    stuck_gatherer.event.set()\n    stuck_gatherer.event.clear()\n\n    updater_instance.add_item(item_6)\n    assert not item_6_timeout_mock.event.wait(base_interval + THRESHOLD)\n\n\ndef test_empty_group():\n    \"\"\"Test that an empty group raises an error on creation\"\"\"\n    with pytest.raises(ValueError):\n        WatchedGroup([])\n\n\ndef test_group_invalidation(updater_instance: ItemUpdater):\n    \"\"\"Test that group invalidation invalidates every member\"\"\"\n    item_1_invalidated = EventSetMock(spec={})\n    item_1 = WatchedItem(\"item_1\", gather_function=Mock())\n    item_1.became_invalid_signal.connect(item_1_invalidated)\n    item_2_invalidated = EventSetMock(spec={})\n    item_2 = WatchedItem(\"item_2\", gather_function=Mock())\n    item_2.became_invalid_signal.connect(item_2_invalidated)\n    group = WatchedGroup([item_1, item_2])\n\n    group_validated = EventSetMock(spec={})\n    group.became_valid_signal.connect(group_validated)\n\n    updater_instance.add_item(item_1)\n    updater_instance.add_item(item_2)\n\n    assert group_validated.event.wait(THRESHOLD)\n\n    updater_instance.invalidate_group(group)\n    assert item_1_invalidated.event.wait(THRESHOLD)\n    assert item_2_invalidated.event.wait(THRESHOLD)\n\n\ndef test_garb(updater_instance: ItemUpdater):\n    \"\"\"\n    Tests that addressing a non existing item throws a ValueError error\n    Tests that adding a garbage for tracking fails\n    \"\"\"\n    item = WatchedItem(\"item\", gather_function=Mock())\n    with pytest.raises(ValueError):\n        updater_instance.schedule_invalidation(item)\n\n\ndef test_valid_group_init(updater_instance: ItemUpdater):\n    \"\"\"\n    Test that adding valid items to a WatchedGroup works too\n    :param updater_instance:\n    :return:\n    \"\"\"\n    item_1_valid = EventSetMock(spec={})\n    item_1 = WatchedItem(\"item_1\", gather_function=Mock())\n    item_1.became_valid_signal.connect(item_1_valid)\n\n    item_2_valid = EventSetMock(spec={})\n    item_2 = WatchedItem(\"item_2\", gather_function=Mock())\n    item_2.became_valid_signal.connect(item_2_valid)\n\n    updater_instance.add_item(item_1)\n    updater_instance.add_item(item_2)\n\n    assert item_1_valid.event.wait(THRESHOLD)\n    assert item_2_valid.event.wait(THRESHOLD)\n\n    group_valid = EventSetMock(spec={})\n    group_invalid = EventSetMock(spec={})\n\n    group = WatchedGroup([item_1, item_2])\n    group.became_valid_signal.connect(group_valid)\n    group.became_valid_signal.connect(group_invalid)\n\n    updater_instance.invalidate(item_1)\n\n    assert group_invalid.event.wait(THRESHOLD)\n    assert group_valid.event.wait(THRESHOLD)\n\n\ndef test_valid_item_doesnt_gather(updater_instance: ItemUpdater):\n    \"\"\"\n    Test that an item which became valid while being scheduled for gather\n    does not actually gather\n    \"\"\"\n    item_1_gather = WaitingMock()\n    item_1 = WatchedItem(\"item_1\", gather_function=item_1_gather)\n\n    item_2_gather = EventSetMock(spec={}, return_value=69)\n    item_2 = WatchedItem(\"item_2\", gather_function=item_2_gather)\n\n    updater_instance.add_item(item_1)\n    updater_instance.add_item(item_2)\n\n    updater_instance.set_value(item_2, 42)\n    item_1_gather.event.set()\n\n    # Check that even when the get got unstuck an already valid item\n    # does not gather (Or should it?)\n    assert not item_2_gather.event.wait(THRESHOLD)\n\n\ndef test_subclasses_work(updater_instance: ItemUpdater):\n    \"\"\"Tests a subclass validates when being added to the Updater\"\"\"\n    class MyItem(WatchedItem):\n        \"\"\"Just a WatchedItem subclass for the test\"\"\"\n\n    my_item = MyItem(\"my_item\")\n    updater_instance.add_item(my_item)\n    assert my_item in updater_instance.items\n\n\ndef test_disabling(updater_instance: ItemUpdater):\n    \"\"\"\n    Can we disable and enable the item updating without affecting\n    any interval logic?\n    \"\"\"\n    item_gather = EventSetMock(spec={})\n    item = WatchedItem(\"item\", gather_function=item_gather, interval=0.2)\n\n    updater_instance.add_item(item, start_tracking=False)\n    assert not item_gather.event.wait(THRESHOLD)\n    assert item.interval == 0.2\n    updater_instance.disable(item)\n    updater_instance.invalidate(item)\n    assert not item_gather.event.wait(THRESHOLD), \\\n        \"Do not invalidate disabled items\"\n    updater_instance.schedule_invalidation(item, interval=0.1)\n    assert not item_gather.event.wait(THRESHOLD), \\\n        \"Do not invalidate \" \"disabled items\"\n    updater_instance.enable(item)\n    assert item_gather.event.wait(THRESHOLD)\n    assert item.invalidate_at <= time() + item.interval\n    updater_instance.disable(item)\n    assert item.invalidate_at == math.inf\n\n\ndef test_group_updating(updater_instance: ItemUpdater):\n    \"\"\"\n    Test a bug, where if a became_valid handler invalidated the same item\n    that just became balid, sometimes, the group the item was in got\n    notifiied after all has been done and got confused to the point of\n    raising a KeyError\n    \"\"\"\n    item_gather = EventSetMock(spec={})\n    item = WatchedItem(\"Item\",  gather_function=item_gather)\n    updater_instance.add_item(item, start_tracking=False)\n    group = WatchedGroup([item])\n    item.became_valid_signal.connect(\n        lambda _: updater_instance.invalidate_group(group), weak=False,\n    )\n    for _ in range(100):\n        updater_instance.invalidate(item)\n        item_gather.event.wait(THRESHOLD)\n        item_gather.event.clear()\n\n\ndef test_priority_queue():\n    \"\"\"Tests that you can have two watched items with the same priority\n    and the app does not throw an error\"\"\"\n    item_1 = WatchedItem(\"item_1\")\n    item_2 = WatchedItem(\"item_2\")\n    queue = PriorityQueue()\n    queue.put((1, item_1))\n    queue.put((1, item_2))\n    queue.get()\n    queue.get()\n"
  },
  {
    "path": "tests/test_serial_parser.py",
    "content": "\"\"\"Tests for the serial parser component\"\"\"\nimport re\nfrom unittest.mock import Mock\n\nfrom prusa.link.serial.serial_parser import SerialParser  # type:ignore\n\n# pylint: disable=protected-access\n\n\ndef test_basic():\n    \"\"\"Basic, one handler, call it on match\"\"\"\n    regex = re.compile(r\"(?P<a>Hello)\")\n    handler = Mock()\n    parser = SerialParser()\n    parser.add_handler(regex, handler)\n    parser.decide(\"Hello\")\n    handler.assert_called_once()\n    assert handler.call_args.kwargs[\"match\"].group(\"a\") == \"Hello\"\n    SerialParser._MCSingleton__instance = None\n\n\ndef test_inverted_basic():\n    \"\"\"Basic, don't call when it does not match\"\"\"\n    regex = re.compile(r\"(?P<a>Hello)\")\n    handler = Mock()\n    parser = SerialParser()\n    parser.add_handler(regex, handler)\n    parser.decide(\"Bye\")\n    handler.assert_not_called()\n    SerialParser._MCSingleton__instance = None\n\n\ndef test_basic_removal():\n    \"\"\"Basic, one handler, call it on match\"\"\"\n    regex = re.compile(r\"(?P<a>Hello)\")\n    handler = Mock()\n    parser = SerialParser()\n    parser.add_handler(regex, handler)\n    parser.remove_handler(regex, handler)\n    parser.decide(\"Hello\")\n    handler.assert_not_called()\n    SerialParser._MCSingleton__instance = None\n\n\ndef test_priority():\n    \"\"\"\n    Do call just the higher priority handler don't call the rest\n    Do not use the class like this, ideally there should not be two regexps\n    matching the same thing, but if yes, usually it's for an instruction to\n    take priority over some other thing.\n    \"\"\"\n    regex1 = re.compile(r\"(?P<a>Hell[o])\")\n    regex2 = re.compile(r\"(?P<a>[H]ello)\")\n    regex3 = re.compile(r\"(?P<a>H[e]llo)\")\n    handler1 = Mock()\n    handler2 = Mock()\n    handler3 = Mock()\n    parser = SerialParser()\n\n    parser.add_handler(regex1, handler1, 1)\n    parser.add_handler(regex3, handler3, 3)\n    parser.add_handler(regex2, handler2, 2)\n    parser.decide(\"Hello\")\n    handler3.assert_called_once()\n    handler2.assert_not_called()\n    handler1.assert_not_called()\n    assert handler3.call_args.kwargs[\"match\"].group(\"a\") == \"Hello\"\n    SerialParser._MCSingleton__instance = None\n\n\ndef test_bump_priority():\n    \"\"\"\n    If the same regex is already registered, adding a handler with a higher\n    priority should elevate all other handlers to the same priority.\n    This is mostly an implementation detail\n    \"\"\"\n    regex1 = re.compile(r\"(?P<a>Hell[o])\")\n    regex2 = re.compile(r\"(?P<a>[H]ello)\")\n    handler1 = Mock()\n    handler2 = Mock()\n    handler3 = Mock()\n    parser = SerialParser()\n\n    parser.add_handler(regex1, handler1, 1)\n    parser.add_handler(regex2, handler2, 2)\n    parser.add_handler(regex1, handler3, 3)\n    parser.decide(\"Hello\")\n    handler3.assert_called_once()\n    handler1.assert_called_once()\n    handler2.assert_not_called()\n    assert handler3.call_args.kwargs[\"match\"].group(\"a\") == \"Hello\"\n    assert handler1.call_args.kwargs[\"match\"].group(\"a\") == \"Hello\"\n    SerialParser._MCSingleton__instance = None\n"
  },
  {
    "path": "tests/util.py",
    "content": "\"\"\"Utility functions and classes for tests\"\"\"\nfrom threading import Event\nfrom unittest import mock\nfrom unittest.mock import Mock\n\n\ndef waiter(event: Event):\n    \"\"\"\n    Waits for the supplied event, returns a constant that allows a Mock\n    to behave nicely\n    \"\"\"\n    event.wait()\n    return mock.DEFAULT\n\n\nclass WaitingMock(Mock):\n    \"\"\"\n    Waits for its built in event when called, otherwise it's a regular mock\n    \"\"\"\n    def __init__(self, *args, side_effect=None, **kwargs):\n        if side_effect is not None:\n            raise AttributeError(\"Do not provide a side effect to this mock, \"\n                                 \"it has its own waiting one\")\n\n        super().__init__(\n            *args,\n            side_effect=lambda *args, **kwargs: waiter(self.event),\n            **kwargs)\n        self.event = Event()\n\n\nclass EventSetMock(Mock):\n    \"\"\"\n    Sets its built in event when called, otherwise it's a regular mock\n    \"\"\"\n    def __init__(self, *args, side_effect=None, **kwargs):\n        if side_effect is not None:\n            raise AttributeError(\"Do not provide a side effect to this mock, \"\n                                 \"it has its own waiting one\")\n\n        super().__init__(*args,\n                         side_effect=lambda *args, **kwargs: self.event.set(),\n                         **kwargs)\n        self.event = Event()\n\n    def reset_mock(self, *args, **kwargs) -> None:\n        super().reset_mock(*args, **kwargs)\n        self.event.clear()\n"
  }
]