diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/_modules/gallia/command/base.html b/_modules/gallia/command/base.html new file mode 100644 index 000000000..2e6f07186 --- /dev/null +++ b/_modules/gallia/command/base.html @@ -0,0 +1,675 @@ + + + + + + + + gallia.command.base — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.command.base

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import asyncio
+import dataclasses
+import json
+import os
+import os.path
+import shutil
+import signal
+import sys
+from abc import ABC, abstractmethod
+from collections.abc import MutableMapping
+from datetime import UTC, datetime
+from enum import Enum, unique
+from logging import Handler
+from pathlib import Path
+from subprocess import CalledProcessError, run
+from tempfile import gettempdir
+from typing import Any, Protocol, Self, cast
+
+from pydantic import ConfigDict, field_serializer, model_validator
+
+from gallia import exitcodes
+from gallia.command.config import Field, GalliaBaseModel, Idempotent
+from gallia.db.handler import DBHandler
+from gallia.dumpcap import Dumpcap
+from gallia.log import add_zst_log_handler, get_logger, tz
+from gallia.power_supply import PowerSupply
+from gallia.power_supply.uri import PowerSupplyURI
+from gallia.services.uds.core.exception import UDSException
+from gallia.transports import BaseTransport, TargetURI
+from gallia.utils import camel_to_snake, get_file_log_level
+
+
+@unique
+class FileNames(Enum):
+    PROPERTIES_PRE = "PROPERTIES_PRE.json"
+    PROPERTIES_POST = "PROPERTIES_POST.json"
+    META = "META.json"
+    ENV = "ENV"
+    LOGFILE = "log.json.zst"
+
+
+@unique
+class HookVariant(Enum):
+    PRE = "pre"
+    POST = "post"
+
+
+@dataclasses.dataclass
+class RunMeta:
+    command: str
+    start_time: str
+    end_time: str
+    exit_code: int
+    config: MutableMapping[str, Any]
+
+    def json(self) -> str:
+        return json.dumps(dataclasses.asdict(self))
+
+
+logger = get_logger(__name__)
+
+
+if sys.platform.startswith("linux") or sys.platform == "darwin":
+    import fcntl
+
+    class Flockable(Protocol):
+        @property
+        def _lock_file_fd(self) -> int | None: ...
+
+    class FlockMixin:
+        def _open_lockfile(self, path: Path) -> int | None:
+            if not path.exists():
+                path.touch()
+
+            logger.notice("opening lockfile…")
+            return os.open(path, os.O_RDONLY)
+
+        def _aquire_flock(self: Flockable) -> None:
+            assert self._lock_file_fd is not None
+
+            try:
+                # First do a non blocking flock. If waiting is required,
+                # log a message and do a blocking wait afterwards.
+                fcntl.flock(self._lock_file_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+            except BlockingIOError:
+                logger.notice("waiting for flock…")
+                fcntl.flock(self._lock_file_fd, fcntl.LOCK_EX)
+            logger.info("Acquired lock. Continuing…")
+
+        def _release_flock(self: Flockable) -> None:
+            assert self._lock_file_fd is not None
+            fcntl.flock(self._lock_file_fd, fcntl.LOCK_UN)
+            os.close(self._lock_file_fd)
+
+
+if sys.platform == "win32":
+
+    class FlockMixin:
+        def _open_lockfile(self, path: Path) -> int | None:
+            logger.warn("lockfile in windows is not supported")
+            return None
+
+        def _aquire_flock(self) -> None:
+            pass
+
+        def _release_flock(self) -> None:
+            pass
+
+
+class BaseCommandConfig(GalliaBaseModel, cli_group="generic", config_section="gallia"):
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    verbose: int = Field(0, description="increase verbosity on the console", short="v")
+    volatile_info: bool = Field(
+        True, description="Overwrite log lines with level info or lower in terminal output"
+    )
+    trace_log: bool = Field(False, description="set the loglevel of the logfile to TRACE")
+    pre_hook: str | None = Field(
+        None,
+        description="shell script to run before the main entry_point",
+        metavar="SCRIPT",
+        config_section="gallia.hooks",
+    )
+    post_hook: str | None = Field(
+        None,
+        description="shell script to run after the main entry_point",
+        metavar="SCRIPT",
+        config_section="gallia.hooks",
+    )
+    hooks: bool = Field(
+        True, description="execute pre and post hooks", config_section="gallia.hooks"
+    )
+    lock_file: Path | None = Field(
+        None, description="path to file used for a posix lock", metavar="PATH"
+    )
+    db: Path | None = Field(None, description="Path to sqlite3 database")
+    artifacts_dir: Path | None = Field(
+        None, description="Folder for artifacts", metavar="DIR", config_section="gallia.scanner"
+    )
+    artifacts_base: Path = Field(
+        Path(gettempdir()).joinpath("gallia"),
+        description="Base directory for artifacts",
+        metavar="DIR",
+        config_section="gallia.scanner",
+    )
+
+
+
+[docs] +class BaseCommand(FlockMixin, ABC): + """BaseCommand is the baseclass for all gallia commands. + This class can be used in standalone scripts via the + gallia command line interface facility. + + This class needs to be subclassed and all the abstract + methods need to be implemented. The artifacts_dir is + generated based on the COMMAND, GROUP, SUBGROUP + properties (falls back to the class name if all three + are not set). + + The main entry_point is :meth:`entry_point()`. + """ + + # The config type which is accepted by this class + # This is used for automatically creating the CLI + CONFIG_TYPE: type[BaseCommandConfig] = BaseCommandConfig + + #: The string which is shown on the cli with --help. + SHORT_HELP: str | None = None + #: The string which is shown at the bottom of --help. + EPILOG: str | None = None + + #: Enable a artifacts_dir. Setting this property to + #: True enables the creation of a logfile. + HAS_ARTIFACTS_DIR: bool = False + #: A list of exception types for which tracebacks are + #: suppressed at the top level. For these exceptions + #: a log message with level critical is logged. + CATCHED_EXCEPTIONS: list[type[Exception]] = [] + + log_file_handlers: list[Handler] + + def __init__(self, config: BaseCommandConfig) -> None: + self.id = camel_to_snake(self.__class__.__name__) + self.config = config + self.artifacts_dir = Path() + self.run_meta = RunMeta( + command=f"{type(self).__module__}.{type(self).__name__}", + start_time=datetime.now(tz).isoformat(), + exit_code=0, + end_time="", + config=json.loads(config.model_dump_json()), + ) + self._lock_file_fd: int | None = None + self.db_handler: DBHandler | None = None + self.log_file_handlers = [] + + @abstractmethod + def run(self) -> int: ... + + def run_hook(self, variant: HookVariant, exit_code: int | None = None) -> None: + script = self.config.pre_hook if variant == HookVariant.PRE else self.config.post_hook + if script is None or script == "": + return + + hook_id = f"{variant.value}-hook" + + argv = sys.argv[:] + argv[0] = Path(argv[0]).name + env = { + "GALLIA_ARTIFACTS_DIR": str(self.artifacts_dir), + "GALLIA_HOOK": variant.value, + "GALLIA_INVOCATION": " ".join(argv), + } | os.environ + + if variant == HookVariant.POST: + env["GALLIA_META"] = self.run_meta.json() + + if exit_code is not None: + env["GALLIA_EXIT_CODE"] = str(exit_code) + + try: + p = run(script, env=env, text=True, capture_output=True, shell=True, check=True) + stdout = p.stdout + stderr = p.stderr + except CalledProcessError as e: + logger.warning(f"{variant.value}-hook failed (exit code: {p.returncode})") + stdout = e.stdout + stderr = e.stderr + + if stdout: + logger.info(p.stdout.strip(), extra={"tags": [hook_id, "stdout"]}) + if stderr: + logger.info(p.stderr.strip(), extra={"tags": [hook_id, "stderr"]}) + + async def _db_insert_run_meta(self) -> None: + if self.config.db is not None: + self.db_handler = DBHandler(self.config.db) + await self.db_handler.connect() + + await self.db_handler.insert_run_meta( + script=self.run_meta.command, + config=self.config, + start_time=datetime.now(UTC).astimezone(), + path=self.artifacts_dir, + ) + + async def _db_finish_run_meta(self) -> None: + if self.db_handler is not None and self.db_handler.connection is not None: + if self.db_handler.meta is not None: + try: + await self.db_handler.complete_run_meta( + datetime.now(UTC).astimezone(), self.run_meta.exit_code, self.artifacts_dir + ) + except Exception as e: + logger.warning(f"Could not write the run meta to the database: {e!r}") + + try: + await self.db_handler.disconnect() + # CancelledError appears only on windows; it is unclear why this happens… + except Exception as e: + logger.error(f"Could not close the database connection properly: {e!r}") + except asyncio.exceptions.CancelledError as e: + logger.error(f"BUG: {e!r} occured. This only seems to happen on windows") + logger.error( + "If you can reproduce this, open an issue: https://github.com/Fraunhofer-AISEC/gallia" + ) + + def _dump_environment(self, path: Path) -> None: + environ = cast(dict[str, str], os.environ) + data = [f"{k}={v}" for k, v in environ.items()] + path.write_text("\n".join(data) + "\n") + + def _add_latest_link(self, path: Path) -> None: + dirs = list(path.glob("run-*")) + dirs.sort(key=lambda x: x.name) + + latest_dir = dirs[-1].relative_to(path) + + symlink = path.joinpath("LATEST") + symlink.unlink(missing_ok=True) + try: + symlink.symlink_to(latest_dir) + except (OSError, NotImplementedError) as e: + logger.warn(f"symlink error: {e}") + + def prepare_artifactsdir( + self, base_dir: Path | None = None, force_path: Path | None = None + ) -> Path: + if force_path is not None: + if force_path.is_dir(): + return force_path + + force_path.mkdir(parents=True) + return force_path + + if base_dir is not None: + _command_dir = self.id + + command_dir = base_dir.joinpath(_command_dir) + + _run_dir = f"run-{datetime.now().strftime('%Y%m%d-%H%M%S.%f')}" + artifacts_dir = command_dir.joinpath(_run_dir).absolute() + artifacts_dir.mkdir(parents=True) + + self._dump_environment(artifacts_dir.joinpath(FileNames.ENV.value)) + self._add_latest_link(command_dir) + + return artifacts_dir.absolute() + + raise ValueError("base_dir or force_path must be different from None") + + def entry_point(self) -> int: + if (p := self.config.lock_file) is not None: + try: + self._lock_file_fd = self._open_lockfile(p) + self._aquire_flock() + except OSError as e: + logger.critical(f"Unable to lock {p}: {e}") + return exitcodes.OSFILE + + if self.HAS_ARTIFACTS_DIR: + self.artifacts_dir = self.prepare_artifactsdir( + self.config.artifacts_base, self.config.artifacts_dir + ) + self.log_file_handlers.append( + add_zst_log_handler( + logger_name="gallia", + filepath=self.artifacts_dir.joinpath(FileNames.LOGFILE.value), + file_log_level=get_file_log_level(self.config), + ) + ) + + if self.config.hooks: + self.run_hook(HookVariant.PRE) + + asyncio.run(self._db_insert_run_meta()) + + exit_code = 0 + try: + exit_code = self.run() + except KeyboardInterrupt: + exit_code = 128 + signal.SIGINT + # Ensure that META.json gets written in the case a + # command calls sys.exit(). + except SystemExit as e: + match e.code: + case int(): + exit_code = e.code + case _: + exit_code = exitcodes.SOFTWARE + except Exception as e: + for t in self.CATCHED_EXCEPTIONS: + if isinstance(e, t): + # TODO: Map the exitcode to superclass of builtin exceptions. + exit_code = exitcodes.IOERR + logger.critical(f"Caught expected exception, stack trace on debug level: {e!r}") + logger.debug(e, exc_info=True) + break + else: + exit_code = exitcodes.SOFTWARE + logger.critical(e, exc_info=True) + finally: + self.run_meta.exit_code = exit_code + self.run_meta.end_time = datetime.now(tz).isoformat() + + asyncio.run(self._db_finish_run_meta()) + + if self.HAS_ARTIFACTS_DIR: + self.artifacts_dir.joinpath(FileNames.META.value).write_text( + self.run_meta.json() + "\n" + ) + logger.notice(f"Stored artifacts at {self.artifacts_dir}") + + if self.config.hooks: + self.run_hook(HookVariant.POST, exit_code) + + if self._lock_file_fd is not None: + self._release_flock() + + return exit_code
+ + + +class ScriptConfig( + BaseCommandConfig, + ABC, + cli_group=BaseCommandConfig._cli_group, + config_section=BaseCommandConfig._config_section, +): + pass + + +
+[docs] +class Script(BaseCommand, ABC): + """Script is a base class for a synchronous gallia command. + To implement a script, create a subclass and implement the + .main() method.""" + + GROUP = "script" + + def setup(self) -> None: ... + + @abstractmethod + def main(self) -> None: ... + + def teardown(self) -> None: ... + + def run(self) -> int: + self.setup() + try: + self.main() + finally: + self.teardown() + + return exitcodes.OK
+ + + +class AsyncScriptConfig( + BaseCommandConfig, + ABC, + cli_group=BaseCommandConfig._cli_group, + config_section=BaseCommandConfig._config_section, +): + pass + + +
+[docs] +class AsyncScript(BaseCommand, ABC): + """AsyncScript is a base class for a asynchronous gallia command. + To implement an async script, create a subclass and implement + the .main() method.""" + + GROUP = "script" + + async def setup(self) -> None: ... + + @abstractmethod + async def main(self) -> None: ... + + async def teardown(self) -> None: ... + + async def _run(self) -> None: + await self.setup() + try: + await self.main() + finally: + await self.teardown() + + def run(self) -> int: + asyncio.run(self._run()) + return exitcodes.OK
+ + + +class ScannerConfig(AsyncScriptConfig, cli_group="scanner", config_section="gallia.scanner"): + dumpcap: bool = Field( + sys.platform.startswith("linux"), description="Enable/Disable creating a pcap file" + ) + target: Idempotent[TargetURI] = Field( + description="URI that describes the target", metavar="TARGET" + ) + power_supply: Idempotent[PowerSupplyURI] | None = Field( + None, + description="URI specifying the location of the relevant opennetzteil server", + metavar="URI", + ) + power_cycle: bool = Field( + False, + description="use the configured power supply to power-cycle the ECU when needed (e.g. before starting the scan, or to recover bad state during scanning)", + ) + power_cycle_sleep: float = Field( + 5.0, description="time to sleep after the power-cycle", metavar="SECs" + ) + + @field_serializer("target", "power_supply") + def serialize_target_uri(self, target_uri: TargetURI | None) -> Any: + if target_uri is None: + return None + + return target_uri.raw + + @model_validator(mode="after") + def check_power_supply_required(self) -> Self: + if self.power_cycle and self.power_supply is None: + raise ValueError("power-cycle needs power-supply") + + return self + + +
+[docs] +class Scanner(AsyncScript, ABC): + """Scanner is a base class for all scanning related commands. + A scanner has the following properties: + + - It is async. + - It loads transports via TargetURIs; available via `self.transport`. + - Controlling PowerSupplies via the opennetzteil API is supported. + - `setup()` can be overwritten (do not forget to call `super().setup()`) + for preparation tasks, such as establishing a network connection or + starting background tasks. + - pcap logfiles can be recorded via a Dumpcap background task. + - `teardown()` can be overwritten (do not forget to call `super().teardown()`) + for cleanup tasks, such as terminating a network connection or background + tasks. + - `main()` is the relevant entry_point for the scanner and must be implemented. + """ + + HAS_ARTIFACTS_DIR = True + CATCHED_EXCEPTIONS: list[type[Exception]] = [ConnectionError, UDSException] + + def __init__(self, config: ScannerConfig): + super().__init__(config) + self.config: ScannerConfig = config + self.power_supply: PowerSupply | None = None + self.transport: BaseTransport + self.dumpcap: Dumpcap | None = None + + @abstractmethod + async def main(self) -> None: ... + + async def setup(self) -> None: + from gallia.plugins.plugin import load_transport + + if self.config.power_supply is not None: + self.power_supply = await PowerSupply.connect(self.config.power_supply) + if self.config.power_cycle is True: + await self.power_supply.power_cycle( + self.config.power_cycle_sleep, lambda: asyncio.sleep(2) + ) + + # Start dumpcap as the first subprocess; otherwise network + # traffic might be missing. + if self.config.dumpcap: + if shutil.which("dumpcap") is None: + raise RuntimeError("--dumpcap specified but `dumpcap` is not available") + self.dumpcap = await Dumpcap.start(self.config.target, self.artifacts_dir) + if self.dumpcap is None: + logger.error("`dumpcap` could not be started!") + else: + await self.dumpcap.sync() + + self.transport = await load_transport(self.config.target).connect(self.config.target) + + async def teardown(self) -> None: + await self.transport.close() + + if self.dumpcap: + await self.dumpcap.stop()
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/command/uds.html b/_modules/gallia/command/uds.html new file mode 100644 index 000000000..9d99b8273 --- /dev/null +++ b/_modules/gallia/command/uds.html @@ -0,0 +1,316 @@ + + + + + + + + gallia.command.uds — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.command.uds

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import json
+from abc import ABC
+
+from pydantic import field_validator
+
+from gallia.command.base import FileNames, Scanner, ScannerConfig
+from gallia.command.config import Field
+from gallia.log import get_logger
+from gallia.plugins.plugin import load_ecu, load_ecus
+from gallia.services.uds.core.service import NegativeResponse, UDSResponse
+from gallia.services.uds.ecu import ECU
+from gallia.services.uds.helpers import raise_for_error
+
+logger = get_logger(__name__)
+
+
+class UDSScannerConfig(ScannerConfig, cli_group="uds", config_section="gallia.protocols.uds"):
+    ecu_reset: int | None = Field(
+        None,
+        description="Trigger an initial ecu_reset via UDS; reset level is optional",
+        const=0x01,
+    )
+    oem: str = Field(
+        ECU.OEM,
+        description="The OEM of the ECU, used to choose a OEM specific ECU implementation",
+        metavar="OEM",
+    )
+    timeout: float = Field(
+        2, description="Timeout value to wait for a response from the ECU", metavar="SECONDS"
+    )
+    max_retries: int = Field(
+        3,
+        description="Number of maximum retries while sending UDS requests. If supported by the transport, this will trigger reconnects if required.",
+        metavar="INT",
+    )
+    ping: bool = Field(True, description="Enable/Disable initial TesterPresent request")
+    tester_present_interval: float = Field(
+        0.5,
+        description="Modify the interval of the cyclic tester present packets",
+        metavar="SECONDS",
+    )
+    tester_present: bool = Field(
+        True, description="Enable/Disable tester present background worker"
+    )
+    properties: bool = Field(
+        True, description="Read and store the ECU proporties prior and after scan"
+    )
+    compare_properties: bool = Field(
+        True, description="Compare properties before and after the scan"
+    )
+
+    @field_validator("oem")
+    @classmethod
+    def check_oem(cls, v: str) -> str:
+        ecu_names = [ecu.OEM for ecu in load_ecus()]
+
+        if v not in ecu_names:
+            raise ValueError(f"Not a valid OEM. Use any of {ecu_names}.")
+
+        return v
+
+
+
+[docs] +class UDSScanner(Scanner, ABC): + """UDSScanner is a baseclass, particularly for scanning tasks + related to the UDS protocol. The differences to Scanner are: + + - `self.ecu` contains a OEM specific UDS client object. + - A background tasks sends TesterPresent regularly to avoid timeouts. + """ + + SUBGROUP: str | None = "uds" + + def __init__(self, config: UDSScannerConfig): + super().__init__(config) + self.config: UDSScannerConfig = config + self.ecu: ECU + self._implicit_logging = True + + @property + def implicit_logging(self) -> bool: + return self._implicit_logging + + @implicit_logging.setter + def implicit_logging(self, value: bool) -> None: + self._implicit_logging = value + + if self.db_handler is not None: + self._apply_implicit_logging_setting() + + def _apply_implicit_logging_setting(self) -> None: + self.ecu.implicit_logging = self._implicit_logging + + async def setup(self) -> None: + await super().setup() + + self.ecu = load_ecu(self.config.oem)( + self.transport, + timeout=self.config.timeout, + max_retry=self.config.max_retries, + power_supply=self.power_supply, + ) + + self.ecu.db_handler = self.db_handler + + if self.db_handler is not None: + try: + # No idea, but str(args.target) fails with a strange traceback. + # Lets use the attribute directly… + await self.db_handler.insert_scan_run(self.config.target.raw) + self._apply_implicit_logging_setting() + except Exception as e: + logger.warning(f"Could not write the scan run to the database: {e:!r}") + + if self.config.ecu_reset is not None: + resp: UDSResponse = await self.ecu.ecu_reset(self.config.ecu_reset) + if isinstance(resp, NegativeResponse): + logger.warning(f"ECUReset failed: {resp}") + logger.warning("Switching to default session") + raise_for_error(await self.ecu.set_session(0x01)) + resp = await self.ecu.ecu_reset(self.config.ecu_reset) + if isinstance(resp, NegativeResponse): + logger.warning(f"ECUReset in session 0x01 failed: {resp}") + + # Handles connecting to the target and waits + # until it is ready. + if self.config.ping: + await self.ecu.wait_for_ecu() + + await self.ecu.connect() + + if self.config.tester_present: + await self.ecu.start_cyclic_tester_present(self.config.tester_present_interval) + + if self.config.properties is True: + path = self.artifacts_dir.joinpath(FileNames.PROPERTIES_PRE.value) + path.write_text(json.dumps(await self.ecu.properties(True), indent=4) + "\n") + + if self.db_handler is not None: + self._apply_implicit_logging_setting() + + if self.config.properties is True: + try: + await self.db_handler.insert_scan_run_properties_pre( + await self.ecu.properties() + ) + except Exception as e: + logger.warning(f"Could not write the properties_pre to the database: {e!r}") + + async def teardown(self) -> None: + if self.config.properties is True and (not self.ecu.transport.is_closed): + path = self.artifacts_dir.joinpath(FileNames.PROPERTIES_POST.value) + path.write_text(json.dumps(await self.ecu.properties(True), indent=4) + "\n") + + path_pre = self.artifacts_dir.joinpath(FileNames.PROPERTIES_PRE.value) + prop_pre = json.loads(path_pre.read_text()) + + if self.config.compare_properties and await self.ecu.properties(False) != prop_pre: + logger.warning("ecu properties differ, please investigate!") + + if self.db_handler is not None and self.config.properties is True: + try: + await self.db_handler.complete_scan_run(await self.ecu.properties(False)) + except Exception as e: + logger.warning(f"Could not write the scan run to the database: {e!r}") + + if self.config.tester_present: + await self.ecu.stop_cyclic_tester_present() + + # This must be the last one. + await super().teardown()
+ + + +class UDSDiscoveryScannerConfig(ScannerConfig): + timeout: float = Field(0.5, description="timeout value for request") + + +
+[docs] +class UDSDiscoveryScanner(Scanner, ABC): + def __init__(self, config: UDSDiscoveryScannerConfig): + super().__init__(config) + self.config: UDSDiscoveryScannerConfig = config + + async def setup(self) -> None: + await super().setup() + + if self.db_handler is not None: + try: + await self.db_handler.insert_discovery_run(self.config.target.url.scheme) + except Exception as e: + logger.warning(f"Could not write the discovery run to the database: {e!r}")
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/log.html b/_modules/gallia/log.html new file mode 100644 index 000000000..4c12ca885 --- /dev/null +++ b/_modules/gallia/log.html @@ -0,0 +1,979 @@ + + + + + + + + gallia.log — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.log

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import atexit
+import dataclasses
+import datetime
+import gzip
+import io
+import json
+import logging
+import mmap
+import os
+import shutil
+import socket
+import sys
+import tempfile
+import time
+import traceback
+from collections.abc import Iterator
+from enum import Enum, IntEnum, unique
+from logging.handlers import QueueHandler, QueueListener
+from pathlib import Path
+from queue import Queue
+from types import TracebackType
+from typing import TYPE_CHECKING, Any, BinaryIO, Self, TextIO, TypeAlias, cast
+
+import zstandard
+
+if TYPE_CHECKING:
+    from logging import _ExcInfoType
+
+
+gmt_offset = time.localtime().tm_gmtoff
+tz = datetime.timezone(datetime.timedelta(seconds=gmt_offset))
+
+
+
+[docs] +@unique +class ColorMode(Enum): + """ColorMode is used as an argument to :func:`set_color_mode`.""" + + #: Colors are always turned on. + ALWAYS = "always" + #: Colors are turned off if the target + #: stream (e.g. stderr) is not a tty. + AUTO = "auto" + #: No colors are used. In other words, + #: no ANSI escape codes are included. + NEVER = "never"
+ + + +
+[docs] +def resolve_color_mode(mode: ColorMode, stream: TextIO = sys.stderr) -> bool: + """Sets the color mode of the console log handler. + + :param mode: The available options are described in :class:`ColorMode`. + :param stream: Used as a reference for :attr:`ColorMode.AUTO`. + """ + if sys.platform == "win32": + return False + + match mode: + case ColorMode.ALWAYS: + return True + case ColorMode.AUTO: + if os.getenv("NO_COLOR") is not None: + return False + else: + return stream.isatty() + case ColorMode.NEVER: + return False
+ + + +# https://stackoverflow.com/a/35804945 +def _add_logging_level(level_name: str, level_num: int) -> None: + method_name = level_name.lower() + + if hasattr(logging, level_name): + raise AttributeError(f"{level_name} already defined in logging module") + if hasattr(logging, method_name): + raise AttributeError(f"{method_name} already defined in logging module") + if hasattr(logging.getLoggerClass(), method_name): + raise AttributeError(f"{method_name} already defined in logger class") + + # This method was inspired by the answers to Stack Overflow post + # http://stackoverflow.com/q/2183233/2988730, especially + # http://stackoverflow.com/a/13638084/2988730 + def for_level(self, message, *args, **kwargs): # type: ignore + if self.isEnabledFor(level_num): + self._log( + level_num, + message, + args, + **kwargs, + ) + + def to_root(message, *args, **kwargs): # type: ignore + logging.log(level_num, message, *args, **kwargs) + + logging.addLevelName(level_num, level_name) + setattr(logging, level_name, level_num) + setattr(logging.getLoggerClass(), method_name, for_level) + setattr(logging, method_name, to_root) + + +_add_logging_level("TRACE", 5) +_add_logging_level("NOTICE", 25) + + +
+[docs] +@unique +class Loglevel(IntEnum): + """A wrapper around the constants exposed by python's + ``logging`` module. Since gallia adds two additional + loglevel's (``NOTICE`` and ``TRACE``), this class + provides a type safe way to access the loglevels. + The level ``NOTICE`` was added to conform better to + RFC3164. Subsequently, ``TRACE`` was added to have + a facility for optional debug messages. + Loglevel describes python specific values for loglevels + which are required to integrate with the python ecosystem. + For generic priority values, see :class:`PenlogPriority`. + """ + + CRITICAL = logging.CRITICAL + ERROR = logging.ERROR + WARNING = logging.WARNING + NOTICE = logging.NOTICE # type: ignore + INFO = logging.INFO + DEBUG = logging.DEBUG + TRACE = logging.TRACE # type: ignore
+ + + +
+[docs] +@unique +class PenlogPriority(IntEnum): + """PenlogPriority holds the values which are written + to json log records. These values conform to RFC3164 + with the addition of ``TRACE``. Since Python uses different + int values for the loglevels, there are two enums in + gallia describing loglevels. PenlogPriority describes + generic priority values which are included in json + log records. + """ + + EMERGENCY = 0 + ALERT = 1 + CRITICAL = 2 + ERROR = 3 + WARNING = 4 + NOTICE = 5 + INFO = 6 + DEBUG = 7 + TRACE = 8 + +
+[docs] + @classmethod + def from_str(cls, string: str) -> PenlogPriority: + """Converts a string to an instance of PenlogPriority. + ``string`` can be a numeric value (0 to 8 inclusive) + or a string with a case insensitive name of the level + (e.g. ``debug``). + """ + if string.isnumeric(): + return cls(int(string, 0)) + + match string.lower(): + case "emergency": + return cls.EMERGENCY + case "alert": + return cls.ALERT + case "critical": + return cls.CRITICAL + case "error": + return cls.ERROR + case "warning": + return cls.WARNING + case "notice": + return cls.NOTICE + case "info": + return cls.INFO + case "debug": + return cls.DEBUG + case "trace": + return cls.TRACE + case _: + raise ValueError(f"{string} not a valid priority")
+ + +
+[docs] + @classmethod + def from_level(cls, value: int) -> PenlogPriority: + """Converts an int value (e.g. from python's logging module) + to an instance of this class. + """ + match value: + case Loglevel.TRACE: + return cls.TRACE + case Loglevel.DEBUG: + return cls.DEBUG + case Loglevel.INFO: + return cls.INFO + case Loglevel.NOTICE: + return cls.NOTICE + case Loglevel.WARNING: + return cls.WARNING + case Loglevel.ERROR: + return cls.ERROR + case Loglevel.CRITICAL: + return cls.CRITICAL + case _: + raise ValueError("invalid value")
+ + +
+[docs] + def to_level(self) -> Loglevel: + """Converts an instance of PenlogPriority to :class:`Loglevel`.""" + match self: + case self.TRACE: + return Loglevel.TRACE + case self.DEBUG: + return Loglevel.DEBUG + case self.INFO: + return Loglevel.INFO + case self.NOTICE: + return Loglevel.NOTICE + case self.WARNING: + return Loglevel.WARNING + case self.ERROR: + return Loglevel.ERROR + case self.CRITICAL: + return Loglevel.CRITICAL + case _: + raise ValueError("invalid value")
+
+ + + +
+[docs] +def setup_logging( + level: Loglevel | None = None, + color_mode: ColorMode = ColorMode.AUTO, + no_volatile_info: bool = False, + logger_name: str = "gallia", +) -> None: + """Enable and configure gallia's logging system. + If this fuction is not called as early as possible, + the logging system is in an undefined state und might + not behave as expected. Always use this function to + initialize gallia's logging. For instance, ``setup_logging()`` + initializes a QueueHandler to avoid blocking calls during + logging. + + :param level: The loglevel to enable for the console handler. + If this argument is None, the env variable + ``GALLIA_LOGLEVEL`` (see :doc:`../env`) is read. + :param file_level: The loglevel to enable for the file handler. + :param path: The path to the logfile containing json records. + :param color_mode: The color mode to use for the console. + """ + if level is None: + # FIXME: why is this here and not in config? + if (raw := os.getenv("GALLIA_LOGLEVEL")) is not None: + level = PenlogPriority.from_str(raw).to_level() + else: + level = Loglevel.DEBUG + + # These are slow and not used by gallia. + logging.logMultiprocessing = False + logging.logThreads = False + logging.logProcesses = False + + logger = logging.getLogger(logger_name) + # LogLevel cannot be 0 (NOTSET), because only the root logger sends it to its handlers then + logger.setLevel(1) + + # Clean up potentially existing handlers and create a new async QueueHandler for stderr output + while len(logger.handlers) > 0: + logger.handlers[0].close() + logger.removeHandler(logger.handlers[0]) + colored = resolve_color_mode(color_mode) + add_stderr_log_handler(logger_name, level, no_volatile_info, colored)
+ + + +def add_stderr_log_handler( + logger_name: str, + level: Loglevel, + no_volatile_info: bool, + colored: bool, +) -> None: + queue: Queue[Any] = Queue() + logger = logging.getLogger(logger_name) + logger.addHandler(QueueHandler(queue)) + + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setLevel(level) + console_formatter = _ConsoleFormatter() + + console_formatter.colored = colored + stderr_handler.terminator = "" # We manually handle the terminator while formatting + if no_volatile_info is False: + console_formatter.volatile_info = True + + stderr_handler.setFormatter(console_formatter) + + queue_listener = QueueListener( + queue, + *[stderr_handler], + respect_handler_level=True, + ) + queue_listener.start() + atexit.register(queue_listener.stop) + + +def add_zst_log_handler( + logger_name: str, filepath: Path, file_log_level: Loglevel +) -> logging.Handler: + queue: Queue[Any] = Queue() + logger = get_logger(logger_name) + logger.addHandler(QueueHandler(queue)) + + zstd_handler = _ZstdFileHandler( + filepath, + level=file_log_level, + ) + zstd_handler.setLevel(file_log_level) + zstd_handler.setFormatter(_JSONFormatter()) + + queue_listener = QueueListener( + queue, + *[zstd_handler], + respect_handler_level=True, + ) + queue_listener.start() + atexit.register(queue_listener.stop) + return zstd_handler + + +@dataclasses.dataclass +class _PenlogRecordV2: + module: str + host: str + data: str + datetime: str + priority: int + version: int + tags: list[str] | None = None + line: str | None = None + stacktrace: str | None = None + _python_level_no: int | None = None + _python_level_name: str | None = None + _python_func_name: str | None = None + + +_PenlogRecord: TypeAlias = _PenlogRecordV2 + + +def _colorize_msg(data: str, levelno: int) -> tuple[str, int]: + if sys.platform == "win32" or not sys.stderr.isatty(): + return data, 0 + + out = "" + match levelno: + case Loglevel.TRACE: + style = _Color.GRAY.value + case Loglevel.DEBUG: + style = _Color.GRAY.value + case Loglevel.INFO: + style = _Color.NOP.value + case Loglevel.NOTICE: + style = _Color.BOLD.value + case Loglevel.WARNING: + style = _Color.YELLOW.value + case Loglevel.ERROR: + style = _Color.RED.value + case Loglevel.CRITICAL: + style = _Color.RED.value + _Color.BOLD.value + case _: + style = _Color.NOP.value + + out += style + out += data + out += _Color.RESET.value + + return out, len(style) + + +def _format_record( # noqa: PLR0913 + dt: datetime.datetime, + name: str, + data: str, + levelno: int, + tags: list[str] | None, + stacktrace: str | None, + colored: bool = False, + volatile_info: bool = False, +) -> str: + msg = "" + if volatile_info: + msg += "\33[2K" + extra_len = 4 + msg += dt.strftime("%b %d %H:%M:%S.%f")[:-3] + msg += " " + msg += name + if tags is not None and len(tags) > 0: + msg += f" [{', '.join(tags)}]" + msg += ": " + + if colored: + tmp_msg, extra_len_tmp = _colorize_msg(data, levelno) + msg += tmp_msg + extra_len += extra_len_tmp + else: + msg += data + + if volatile_info and levelno <= Loglevel.INFO: + terminal_width, _ = shutil.get_terminal_size() + msg = msg[: terminal_width + extra_len - 1] # Adapt length to invisible ANSI colors + msg += _Color.RESET.value + msg += "\r" + else: + msg += "\n" + + if stacktrace is not None: + msg += "\n" + msg += stacktrace + + return msg + + +
+[docs] +@dataclasses.dataclass +class PenlogRecord: + module: str + host: str + data: str + datetime: datetime.datetime + # FIXME: Enums are slow. + priority: PenlogPriority + tags: list[str] | None = None + colored: bool = False + line: str | None = None + stacktrace: str | None = None + _python_level_no: int | None = None + _python_level_name: str | None = None + _python_func_name: str | None = None + + def __str__(self) -> str: + return _format_record( + dt=self.datetime, + name=self.module, + data=self.data, + levelno=self._python_level_no + if self._python_level_no is not None + else self.priority.to_level(), + tags=self.tags, + stacktrace=self.stacktrace, + colored=self.colored, + ) + + @classmethod + def parse_priority(cls, data: bytes) -> int | None: + if not data.startswith(b"<"): + return None + + prio_str = data[1 : data.index(b">")] + return int(prio_str) + + @classmethod + def parse_json(cls, data: bytes) -> Self: + if data.startswith(b"<"): + data = data[data.index(b">") + 1 :] + + record = json.loads(data.decode()) + if (v := record["version"]) != 2: + raise json.JSONDecodeError(f"invalid log record version {v}", data.decode(), 0) + + return cls( + module=record["module"], + host=record["host"], + data=record["data"], + datetime=datetime.datetime.fromisoformat(record["datetime"]), + priority=PenlogPriority(record["priority"]), + tags=record["tags"] if "tags" in record else None, + line=record["line"] if "line" in record else None, + stacktrace=record["stacktrace"] if "stacktrace" in record else None, + _python_level_no=record["_python_level_no"] if "_python_level_no" in record else None, + _python_level_name=record["_python_level_name"] + if "_python_level_name" in record + else None, + _python_func_name=record["_python_func_name"] + if "_python_func_name" in record + else None, + ) + + def to_log_record(self) -> logging.LogRecord: + level = self.priority.to_level() + timestamp = self.datetime.timestamp() + msecs = (timestamp - int(timestamp)) * 1000 + + lineno = 0 + pathname = "" + if (line := self.line) is not None: + pathname, lineno_str = line.rsplit(":", 1) + lineno = int(lineno_str) + + return logging.makeLogRecord( + { + "name": self.module, + "priority": self.priority, + "levelno": level, + "levelname": logging.getLevelName(level), + "msg": self.data, + "pathname": pathname, + "lineno": lineno, + "created": timestamp, + "msecs": msecs, + "host": self.host, + "tags": self.tags, + } + )
+ + + +class PenlogReader: + def __init__(self, path: Path) -> None: + self.path = path if str(path) != "-" else Path("/dev/stdin") + self.raw_file = self._prepare_for_mmap(self.path) + self.file_mmap = mmap.mmap(self.raw_file.fileno(), 0, access=mmap.ACCESS_READ) + self._current_line = b"" + self._current_record: PenlogRecord | None = None + self._current_record_index = 0 + self._parsed = False + self._record_offsets: list[int] = [] + + def _test_mmap(self, path: Path) -> bool: + with path.open("rb") as f: + try: + mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + return True + except ValueError: + return False + + def _prepare_for_mmap(self, path: Path) -> BinaryIO: + if path.is_file() and path.suffix in [".zst", ".gz"]: + tmpfile = tempfile.TemporaryFile() + match path.suffix: + case ".zst": + with self.path.open("rb") as f: + decomp = zstandard.ZstdDecompressor() + decomp.copy_stream(f, tmpfile) + case ".gz": + with gzip.open(self.path, "rb") as f: + shutil.copyfileobj(cast(BinaryIO, f), tmpfile) + + tmpfile.flush() + return cast(BinaryIO, tmpfile) + + if path.is_fifo() or self._test_mmap(path) is False: + tmpfile = tempfile.TemporaryFile() + with path.open("rb") as f: + shutil.copyfileobj(f, tmpfile) + tmpfile.flush() + return cast(BinaryIO, tmpfile) + + return self.path.open("rb") + + def _parse_file_structure(self) -> None: + old_offset = self.file_mmap.tell() + + while True: + self._record_offsets.append(self.file_mmap.tell()) + + line = self.file_mmap.readline() + if line == b"": + # The last newline char is not relevant, since + # no data is following. + del self._record_offsets[-1] + break + + self.file_mmap.seek(old_offset) + self._parsed = True + + def _lookup_offset(self, index: int) -> int: + if index == 0: + return 0 + if not self._parsed: + self._parse_file_structure() + return self._record_offsets[index] + + @property + def file_size(self) -> int: + old_offset = self.file_mmap.tell() + self.file_mmap.seek(0, io.SEEK_END) + size = self.file_mmap.tell() + self.file_mmap.seek(old_offset) + return size + + @property + def current_record(self) -> PenlogRecord: + if self._current_record is not None: + return self._current_record + return PenlogRecord.parse_json(self._current_line) + + @property + def current_priority(self) -> int: + prio = PenlogRecord.parse_priority(self._current_line) + if prio is None: + self._current_record = PenlogRecord.parse_json(self._current_line) + prio = self._current_record.priority + return prio + + def seek_to_record(self, n: int) -> None: + self.file_mmap.seek(self._lookup_offset(n)) + self._current_record_index = n + + def seek_to_current_record(self) -> None: + self.file_mmap.seek(self._lookup_offset(self._current_record_index)) + + def seek_to_previous_record(self) -> None: + self._current_record_index -= 1 + self.seek_to_record(self._current_record_index) + + def seek_to_next_record(self) -> None: + self._current_record_index += 1 + self.seek_to_record(self._current_record_index) + + def records( + self, + priority: PenlogPriority = PenlogPriority.TRACE, + offset: int = 0, + reverse: bool = False, + ) -> Iterator[PenlogRecord]: + self.seek_to_record(offset) + if reverse is False: + while True: + if self.readline() == b"": + break + if self.current_priority <= priority: + yield self.current_record + else: + while True: + self.readline() + if self.current_priority <= priority: + yield self.current_record + try: + self.seek_to_previous_record() + except IndexError: + break + + def readline(self) -> bytes: + self._current_record = None + self._current_line = self.file_mmap.readline() + return self._current_line + + def close(self) -> None: + self.file_mmap.close() + self.raw_file.close() + + def __len__(self) -> int: + if not self._parsed: + self._parse_file_structure() + return len(self._record_offsets) + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + if exc_type is not None: + self.close() + + +@unique +class _Color(Enum): + NOP = "" + RESET = "\033[0m" + BOLD = "\033[1m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + PURPLE = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + GRAY = "\033[0;38;5;245m" + + +class _JSONFormatter(logging.Formatter): + def __init__(self) -> None: + super().__init__() + self.hostname = socket.gethostname() + + def format(self, record: logging.LogRecord) -> str: + tags = record.__dict__["tags"] if "tags" in record.__dict__ else None + stacktrace = self.formatException(record.exc_info) if record.exc_info else None + + penlog_record = _PenlogRecordV2( + module=record.name, + host=self.hostname, + data=record.getMessage(), + priority=PenlogPriority.from_level(record.levelno).value, + datetime=datetime.datetime.fromtimestamp(record.created, tz=tz).isoformat(), + line=f"{record.pathname}:{record.lineno}", + stacktrace=stacktrace, + tags=tags, + _python_level_no=record.levelno, + _python_level_name=record.levelname, + _python_func_name=record.funcName, + version=2, + ) + return json.dumps(dataclasses.asdict(penlog_record)) + + +class _ConsoleFormatter(logging.Formatter): + colored: bool = False + volatile_info: bool = False + + def format( + self, + record: logging.LogRecord, + ) -> str: + stacktrace = None + + if record.exc_info: + exc_type, exc_value, exc_traceback = record.exc_info + assert exc_type + assert exc_value + assert exc_traceback + + stacktrace = "\n" + stacktrace += "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) + + return _format_record( + dt=datetime.datetime.fromtimestamp(record.created), + name=record.name, + data=record.getMessage(), + levelno=record.levelno, + tags=record.__dict__["tags"] if "tags" in record.__dict__ else None, + stacktrace=stacktrace, + colored=self.colored, + volatile_info=self.volatile_info, + ) + + +class _ZstdFileHandler(logging.Handler): + def __init__(self, path: Path, level: int | str = logging.NOTSET) -> None: + super().__init__(level) + self.file = zstandard.open( + filename=path, + mode="wb", + cctx=zstandard.ZstdCompressor( + write_checksum=True, + write_content_size=True, + threads=-1, + ), + ) + + def close(self) -> None: + self.file.flush() + self.file.close() + + def emit(self, record: logging.LogRecord) -> None: + prio = PenlogPriority.from_level(record.levelno).value + data = f"<{prio}>{self.format(record)}" + if not data.endswith("\n"): + data += "\n" + self.file.write(data.encode()) + + +
+[docs] +class Logger(logging.Logger): + def trace( + self, + msg: Any, + *args: Any, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + extra: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + if self.isEnabledFor(Loglevel.TRACE): + self._log( + Loglevel.TRACE, + msg, + args, + exc_info=exc_info, + extra=extra, + stack_info=stack_info, + **kwargs, + ) + + def notice( + self, + msg: Any, + *args: Any, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + extra: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + if self.isEnabledFor(Loglevel.NOTICE): + self._log( + Loglevel.NOTICE, + msg, + args, + exc_info=exc_info, + extra=extra, + stack_info=stack_info, + **kwargs, + ) + + def result( + self, + msg: Any, + *args: Any, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + extra: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + extra = extra if extra is not None else {} + extra["tags"] = ["result"] + if self.isEnabledFor(Loglevel.NOTICE): + self._log( + Loglevel.NOTICE, + msg, + args, + exc_info=exc_info, + extra=extra, + stack_info=stack_info, + **kwargs, + )
+ + + +logging.setLoggerClass(Logger) + + +def get_logger(name: str) -> Logger: + return cast(Logger, logging.getLogger(name)) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/services/uds/core/client.html b/_modules/gallia/services/uds/core/client.html new file mode 100644 index 000000000..04f2bea5a --- /dev/null +++ b/_modules/gallia/services/uds/core/client.html @@ -0,0 +1,1321 @@ + + + + + + + + gallia.services.uds.core.client — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for gallia.services.uds.core.client

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import asyncio
+import struct
+from collections.abc import Sequence
+from dataclasses import dataclass
+from typing import overload
+
+from gallia.log import get_logger
+from gallia.services.uds.core import service
+from gallia.services.uds.core.constants import UDSErrorCodes, UDSIsoServices
+from gallia.services.uds.core.exception import MissingResponse
+from gallia.services.uds.helpers import parse_pdu
+from gallia.transports import BaseTransport
+
+
+
+[docs] +@dataclass +class UDSRequestConfig: + # timeout for this request in sec + timeout: float | None = None + # maximum number of attempts in case of network errors + max_retry: int | None = None + # Skip the hooks which apply to session changes. + skip_hooks: bool = False + # tags to be applied to the logged output + tags: list[str] | None = None
+ + + +logger = get_logger(__name__) + + +class UDSClient: + def __init__( + self, + transport: BaseTransport, + timeout: float, + max_retry: int = 0, + ): + self.transport = transport + self.timeout = timeout + self.max_retry = max_retry + self.retry_wait = 0.2 + self.pending_timeout = 5 + self.mutex = asyncio.Lock() + + async def connect(self) -> None: ... + + async def reconnect(self, timeout: int | None = None) -> None: + """Calls the underlying transport to trigger a reconnect""" + async with self.mutex: + await self.reconnect_unsafe(timeout=timeout) + + async def reconnect_unsafe(self, timeout: int | None = None) -> None: + """Calls the underlying transport to trigger a reconnect without locking""" + self.transport = await self.transport.reconnect(timeout) + logger.debug("Reconnected transport successfully") + await self.connect() + + async def _read(self, timeout: float | None = None, tags: list[str] | None = None) -> bytes: + if timeout is None and self.timeout: + timeout = self.timeout + return await self.transport.read(timeout, tags) + + async def request_unsafe( + self, request: service.UDSRequest, config: UDSRequestConfig | None = None + ) -> service.UDSResponse: + """This method is the same as request() with the difference + that it does not hold the mutex in the underlying transport. + """ + config = config if config is not None else UDSRequestConfig() + tags: list[str] = [] if config.tags is None else config.tags + + last_exception: Exception = MissingResponse(request) + max_retry = config.max_retry if config.max_retry is not None else self.max_retry + timeout = config.timeout if config.timeout is not None else self.timeout + for i in range(max_retry + 1): + # Exponential backoff + wait_time = self.retry_wait * 2**i + + # Avoid pasting this very line in every error branch. + if i > 0: + logger.info(f"Requesting UDS PDU failed; retrying: {i} / {max_retry}…") + try: + logger.debug(request.pdu.hex(), extra={"tags": ["write", "uds"] + tags}) + raw_resp = await self.transport.request_unsafe(request.pdu, timeout, config.tags) + if raw_resp == b"": + raise BrokenPipeError("connection to target lost") + except TimeoutError as e: + logger.debug(f"{request} failed with: {repr(e)}") + last_exception = MissingResponse(request, str(e)) + if i < max_retry: + logger.debug(f"Sleeping for {wait_time}s") + await asyncio.sleep(wait_time) + continue + except ConnectionError as e: + logger.warning(f"{request} failed with: {e!r}") + last_exception = MissingResponse(request, str(e)) + if i < max_retry: + logger.info(f"Sleeping for {wait_time}s before attempting to reconnect") + await asyncio.sleep(wait_time) + await self.reconnect_unsafe() + continue + + logger.debug(raw_resp.hex(), extra={"tags": ["read", "uds"] + tags}) + resp = parse_pdu(raw_resp, request) + + if isinstance(resp, service.NegativeResponse): + if resp.response_code == UDSErrorCodes.busyRepeatRequest: + if i >= max_retry: + return resp + await asyncio.sleep(wait_time) + continue + # We already had ECUs which thought an infinite + # response_pending loop is a good idea… + # Let's limit this. + n_pending = 1 + MAX_N_PENDING = 120 + n_timeout = 0 + waiting_time = 0.5 + max_n_timeout = max(timeout if timeout else 0, 20) / waiting_time + while ( + isinstance(resp, service.NegativeResponse) + and resp.response_code == UDSErrorCodes.requestCorrectlyReceivedResponsePending + ): + logger.info( + f"Received ResponsePending: {n_pending}/{MAX_N_PENDING}; " + + f"waiting for next message: {n_timeout}/{int(max_n_timeout)}" + ) + try: + raw_resp = await self._read(timeout=waiting_time, tags=config.tags) + if raw_resp == b"": + raise BrokenPipeError("connection to target lost") + logger.debug(raw_resp.hex(), extra={"tags": ["read", "uds"] + tags}) + except TimeoutError as e: + # Send a tester present to indicate that + # we are still there. + # TODO: Is this really necessary? + await self._tester_present(suppress_resp=True) + n_timeout += 1 + if n_timeout >= max_n_timeout: + last_exception = MissingResponse(request, str(e)) + break + continue + resp = parse_pdu(raw_resp, request) + n_timeout = 0 # Only raise errors for consecutive timeouts + n_pending += 1 + if n_pending >= MAX_N_PENDING: + raise RuntimeError("ECU appears to be stuck in ResponsePending loop") + else: + # We reach this code here once all response pending + # and similar busy stuff is resolved. + return resp + + logger.debug(f"{request} failed after retry loop") + raise last_exception + + async def _tester_present( + self, suppress_resp: bool = False, config: UDSRequestConfig | None = None + ) -> service.UDSResponse | None: + config = config if config is not None else UDSRequestConfig() + timeout = config.timeout if config.timeout else self.timeout + if suppress_resp: + pdu = service.TesterPresentRequest(suppress_response=True).pdu + tags = config.tags if config.tags is not None else [] + logger.debug(pdu.hex(), extra={"tags": ["write", "uds"] + tags}) + await self.transport.write(pdu, timeout, config.tags) + # TODO: This is not fail safe: What if there is an answer??? + return None + return await self.tester_present(False, config) + + async def send_raw( + self, pdu: bytes, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.PositiveResponse: + """Raw request, which does not need to be compliant with the standard. + It can be used to send arbitrary data packets. + + :param pdu: The data. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request(service.RawRequest(pdu), config) + + async def diagnostic_session_control( + self, + diagnostic_session_type: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.DiagnosticSessionControlResponse: + """Sets the diagnostic session which is specified by a specific diagnosticSessionType + sub-function. + This is an implementation of the UDS request for service DiagnosticSessionControl (0x10). + + :param diagnostic_session_type: The session sub-function. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.DiagnosticSessionControlRequest(diagnostic_session_type, suppress_response), + config, + ) + + async def ecu_reset( + self, + reset_type: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ECUResetResponse: + """Resets the ECU using the specified reset type sub-function. + This is an implementation of the UDS request for service ECUReset (0x11). + + :param reset_type: The reset type sub-function. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request(service.ECUResetRequest(reset_type, suppress_response), config) + + async def security_access_request_seed( + self, + security_access_type: int, + security_access_data_record: bytes = b"", + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.SecurityAccessResponse: + """Requests a seed for a security access level. + This is an implementation of the UDS request for the requestSeed sub-function group + of the service SecurityAccess (0x27). + + :param security_access_type: The securityAccess type sub-function. + :param security_access_data_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.RequestSeedRequest( + security_access_type, security_access_data_record, suppress_response + ), + config, + ) + + async def security_access_send_key( + self, + security_access_type: int, + security_key: bytes, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.SecurityAccessResponse: + """Sends the key for a security access level. + This is an implementation of the UDS request for the sendKey sub-function group + of the service SecurityAccess (0x27). + + :param security_access_type: The securityAccess type sub-function. + :param security_key: The response to the seed challenge. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.SendKeyRequest(security_access_type, security_key, suppress_response), + config, + ) + + async def communication_control( + self, + control_type: int, + communication_type: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.CommunicationControlResponse: + """Controls communication of the ECU. + This is an implementation of the UDS request for service CommunicationControl (0x28). + + :param control_type: The control type sub-function. + :param communication_type: The communication type. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.CommunicationControlRequest( + control_type, communication_type, suppress_response + ), + config, + ) + + async def tester_present( + self, suppress_response: bool = False, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.TesterPresentResponse: + """Signals to the ECU, that the tester is still present. + This is an implementation of the UDS request for service TesterPresent (0x3E). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request(service.TesterPresentRequest(suppress_response), config) + + async def control_dtc_setting( + self, + dtc_setting_type: int, + dtc_setting_control_option_record: bytes = b"", + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ControlDTCSettingResponse: + """Control the setting of DTCs. + This is an implementation of the UDS request for service ControlDTCSetting (0x85). + + + :param dtc_setting_type: The setting type. + :param dtc_setting_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ControlDTCSettingRequest( + dtc_setting_type, dtc_setting_control_option_record, suppress_response + ), + config, + ) + + async def read_data_by_identifier( + self, + data_identifiers: int | Sequence[int], + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReadDataByIdentifierResponse: + """Reads data which is identified by a specific dataIdentifier. + This is an implementation of the UDS request for service ReadDataByIdentifier (0x22). + While this implementation supports requesting multiple dataIdentifiers at once, as is + permitted in the standard, it is recommended to request them separately, + because the support is optional on the server side. + Additionally, it is not possible to reliably determine each single dataRecord from a + corresponding response. + + :param data_identifiers: One or multiple dataIdentifiers. A dataIdentifier is a max two + bytes integer. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request(service.ReadDataByIdentifierRequest(data_identifiers), config) + + async def read_memory_by_address( + self, + memory_address: int, + memory_size: int, + address_and_length_format_identifier: int | None = None, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReadMemoryByAddressResponse: + """Reads data from a specific memory address on the UDS server. + This is an implementation of the UDS request for service ReadMemoryByAddress (0x3d). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param memory_address: The start address. + :param memory_size: The number of bytes to read. + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + parameters. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReadMemoryByAddressRequest( + memory_address, memory_size, address_and_length_format_identifier + ), + config, + ) + + async def write_data_by_identifier( + self, + data_identifier: int, + data_record: bytes, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.WriteDataByIdentifierResponse: + """Writes data which is identified by a specific dataIdentifier. + This is an implementation of the UDS request for service WriteDataByIdentifier (0x2E). + + :param data_identifier: The identifier. A dataIdentifier is a max two bytes integer. + :param data_record: The data to be written. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.WriteDataByIdentifierRequest(data_identifier, data_record), config + ) + + async def write_memory_by_address( # noqa: PLR0913 + self, + memory_address: int, + data_record: bytes, + memory_size: int | None = None, + address_and_length_format_identifier: int | None = None, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.WriteMemoryByAddressResponse: + """Writes data to a specific memory on the UDS server. + This is an implementation of the UDS request for service writeMemoryByAddress (0x3d). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param memory_address: The start address. + :param data_record: The data to be written. + :param memory_size: The number of bytes to write. + If omitted, the byte length of the data is used. + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + or data_record parameters. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.WriteMemoryByAddressRequest( + memory_address, + data_record, + memory_size, + address_and_length_format_identifier, + ), + config, + ) + + async def clear_diagnostic_information( + self, group_of_dtc: int, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.ClearDiagnosticInformationResponse: + """Clears diagnostic trouble codes according to a given mask. + This is an implementation of the UDS request for service clearDiagnosticInformation (0x14). + + :param group_of_dtc: The three byte mask, which determines the DTCs to be cleared. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request(service.ClearDiagnosticInformationRequest(group_of_dtc), config) + + async def read_dtc_information_report_number_of_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportNumberOfDTCByStatusMaskResponse: + """Read the number of DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the reportNumberOfDTCByStatusMask + sub-function of the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportNumberOfDTCByStatusMaskRequest(dtc_status_mask, suppress_response), + config, + ) + + async def read_dtc_information_report_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportDTCByStatusMaskResponse: + """Read DTCs and their state from the UDS server. + This is an implementation of the UDS request for the reportDTCByStatusMask sub-function of + the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportDTCByStatusMaskRequest(dtc_status_mask, suppress_response), + config, + ) + + async def read_dtc_information_report_mirror_memory_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportMirrorMemoryDTCByStatusMaskResponse: + """Read DTCs and their state from the UDS server's mirror memory. + This is an implementation of the UDS request for the reportMirrorMemoryDTCByStatusMask + sub-function of the + service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportMirrorMemoryDTCByStatusMaskRequest(dtc_status_mask, suppress_response), + config, + ) + + async def read_dtc_information_report_number_of_mirror_memory_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportNumberOfMirrorMemoryDTCByStatusMaskResponse: + """Read the number of DTCs with the specified state from the UDS server's mirror memory. + This is an implementation of the UDS request for the + reportNumberOfMirrorMemoryDTCByStatusMask sub-function of + the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportNumberOfMirrorMemoryDTCByStatusMaskRequest( + dtc_status_mask, suppress_response + ), + config, + ) + + async def read_dtc_information_report_number_of_emissions_related_obd_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> ( + service.NegativeResponse | service.ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskResponse + ): + """Read the number of emission related DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the + reportNumberOfEmissionsRelatedOBDDTCByStatusMask sub-function of the service + ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskRequest( + dtc_status_mask, suppress_response + ), + config, + ) + + async def read_dtc_information_report_emissions_related_obd_dtc_by_status_mask( + self, + dtc_status_mask: int, + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportEmissionsRelatedOBDDTCByStatusMaskResponse: + """Read the number of emission related DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the + reportNumberOfEmissionsRelatedOBDDTCByStatusMask + sub-function of the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReportEmissionsRelatedOBDDTCByStatusMaskRequest( + dtc_status_mask, suppress_response + ), + config, + ) + + async def input_output_control_by_identifier( + self, + data_identifier: int, + control_option_record: bytes, + control_enable_mask_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: + """Controls input or output values on the server. + This is an implementation of the UDS request for the service InputOutputControlByIdentifier + (0x2F). + This function exposes the parameters as in the corresponding specification, + hence is suitable for all variants of this service. + For the variants which use an inputOutputControlParameter as the first byte of the + controlOptionRecord, using the corresponding wrappers is recommended. + + :param data_identifier: The data identifier of the value(s) to be controlled. + :param control_option_record: The controlStates, which specify the intended values of the + input / output parameters, optionally prefixed with an + inputOutputControlParameter or only an + inputOutputControlParameter. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + pdu = struct.pack("!BH", UDSIsoServices.InputOutputControlByIdentifier, data_identifier) + pdu += control_option_record + control_enable_mask_record + return await self.request( + service.InputOutputControlByIdentifierRequest( + data_identifier, control_option_record, control_enable_mask_record + ), + config, + ) + + async def input_output_control_by_identifier_return_control_to_ecu( + self, + data_identifier: int, + control_enable_mask_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: + """Gives the control over input / output parameters back to the ECU. + This is a convenience wrapper for the generic input_output_control_by_id() for the case + where an inputOutputControlParameter is used and is set to returnControlToECU. + In that case no further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) for which control should be + returned to the ECU. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ReturnControlToECURequest(data_identifier, control_enable_mask_record), + config, + ) + + async def input_output_control_by_identifier_reset_to_default( + self, + data_identifier: int, + control_enable_mask_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: + """Sets the input / output parameters to the default value(s). + This is a convenience wrapper of the generic request for the case where an + inputOutputControlParameter is used and is set to resetToDefault. + In that case no further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) for which the values should be + reset. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ResetToDefaultRequest(data_identifier, control_enable_mask_record), + config, + ) + + async def input_output_control_by_identifier_freeze_current_state( + self, + data_identifier: int, + control_enable_mask_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: + """Freezes the input / output parameters at their current state. + This is a convenience wrapper of the generic request for the case where an + inputOutputControlParameter is used and is set to freezeCurrentState. + In that case no further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) to be frozen. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.FreezeCurrentStateRequest(data_identifier, control_enable_mask_record), + config, + ) + + async def input_output_control_by_identifier_short_term_adjustment( + self, + data_identifier: int, + control_states: bytes, + control_enable_mask_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: + """Sets the input / output parameters as specified in the controlOptionRecord. + This is a convenience wrapper of the generic request for the case + where an inputOutputControlParameter is used and is set to freezeCurrentState. + In that case controlState parameters are required. + + :param data_identifier: The data identifier of the value(s) to be adjusted. + :param control_states: The controlStates, which specify the intended values of the input / + output parameters. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.ShortTermAdjustmentRequest(data_identifier, control_enable_mask_record), + config, + ) + + async def routine_control_start_routine( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.StartRoutineResponse: + """Starts a specific routine on the server. + This is an implementation of the UDS request for the startRoutine sub-function of the + service routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.StartRoutineRequest( + routine_identifier, routine_control_option_record, suppress_response + ), + config, + ) + + async def routine_control_stop_routine( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.StopRoutineResponse: + """Stops a specific routine on the server. + This is an implementation of the UDS request for the stopRoutine sub-function of the service + routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.StopRoutineRequest( + routine_identifier, routine_control_option_record, suppress_response + ), + config, + ) + + async def routine_control_request_routine_results( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestRoutineResultsResponse: + """Requests the results of a specific routine on the server. + This is an implementation of the UDS request for the requestRoutineResults sub-function of + the service routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.RequestRoutineResultsRequest( + routine_identifier, routine_control_option_record, suppress_response + ), + config, + ) + + async def request_download( # noqa: PLR0913 + self, + memory_address: int, + memory_size: int, + compression_method: int = 0x0, + encryption_method: int = 0x0, + address_and_length_format_identifier: int | None = None, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestDownloadResponse: + """Requests the download of data, i.e. the possibility to send data from the client to the + server. + This is an implementation of the UDS request for requestDownload (0x34). + + :param memory_address: The address at which data should be downloaded. + :param memory_size: The number of bytes to be downloaded. + :param compression_method: Encodes the utilized compressionFormat (0x0 for none) + :param encryption_method: Encodes the utilized encryptionFormat (0x0 for none) + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + parameters. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.RequestDownloadRequest( + memory_address, + memory_size, + compression_method, + encryption_method, + address_and_length_format_identifier, + ), + config, + ) + + async def request_upload( # noqa: PLR0913 + self, + memory_address: int, + memory_size: int, + compression_method: int = 0x0, + encryption_method: int = 0x0, + address_and_length_format_identifier: int | None = None, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestUploadResponse: + """Requests the upload of data, i.e. the possibility to receive data from the server. + This is an implementation of the UDS request for requestUpload (0x35). + + :param memory_address: The address at which data should be uploaded. + :param memory_size: The number of bytes to be uploaded. + :param compression_method: Encodes the utilized compressionFormat (0x0 for none) + :param encryption_method: Encodes the utilized encryptionFormat (0x0 for none) + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + parameters. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.RequestUploadRequest( + memory_address, + memory_size, + compression_method, + encryption_method, + address_and_length_format_identifier, + ), + config, + ) + + async def transfer_data( + self, + block_sequence_counter: int, + transfer_request_parameter_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.TransferDataResponse: + """Transfers data to the server or requests the next data from the server. + This is an implementation of the UDS request for transferData (0x36). + + :param block_sequence_counter: The current block sequence counter. + Initialized with one and incremented for each new data. + After 0xff, the counter is resumed at 0 + :param transfer_request_parameter_record: Contains the data to be transferred if downloading + to the server. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.TransferDataRequest(block_sequence_counter, transfer_request_parameter_record), + config, + ) + + async def request_transfer_exit( + self, + transfer_request_parameter_record: bytes = b"", + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestTransferExitResponse: + """Ends the transfer of data. + This is an implementation of the UDS request for requestTransferExit (0x77). + + :param transfer_request_parameter_record: Optional data. + :param config: Passed on to request_pdu(). + :return: The response of the server. + """ + return await self.request( + service.RequestTransferExitRequest(transfer_request_parameter_record), + config, + ) + + async def define_by_identifier( + self, + dynamically_defined_data_identifier: int, + source_data_identifiers: int | Sequence[int], + positions_in_source_data_record: int | Sequence[int], + memory_sizes: int | Sequence[int], + suppress_response: bool = False, + ) -> service.NegativeResponse | service.DefineByIdentifierResponse: + """Defines a data identifier which combines data from multiple existing data identifiers on the UDS server. + This is an implementation of the UDS request for the defineByIdentifier sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + + :param dynamically_defined_data_identifier: The new data identifier. + :param source_data_identifiers: The source data identifiers which refer to the data to be included in the new data identifier. + :param positions_in_source_data_record: The start positions for each source data identifier. Note, that the position is 1-indexed. + :param memory_sizes: The number of bytes for each source data identifier, starting from the starting position. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + return await self.request( + service.DefineByIdentifierRequest( + dynamically_defined_data_identifier, + source_data_identifiers, + positions_in_source_data_record, + memory_sizes, + suppress_response, + ) + ) + + async def define_by_memory_address( + self, + dynamically_defined_data_identifier: int, + memory_addresses: int | Sequence[int], + memory_sizes: int | Sequence[int], + address_and_length_format_identifier: int | None = None, + suppress_response: bool = False, + ) -> service.NegativeResponse | service.DefineByMemoryAddressResponse: + """Defines a data identifier which combines data from multiple existing memory regions on the UDS server. + This is an implementation of the UDS request for the defineByMemoryAddress sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param dynamically_defined_data_identifier: The new data identifier. + :param memory_addresses: The memory addresses for each source data. + :param memory_sizes: The number of bytes for each source data, starting from the memory address. + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + or data_record parameters. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + return await self.request( + service.DefineByMemoryAddressRequest( + dynamically_defined_data_identifier, + memory_addresses, + memory_sizes, + address_and_length_format_identifier, + suppress_response, + ) + ) + + async def clear_dynamically_defined_data_identifier( + self, dynamically_defined_data_identifier: int | None, suppress_response: bool = False + ) -> service.ClearDynamicallyDefinedDataIdentifierResponse: + """Clears either a specific dynamically defined data identifier or all if no data identifier is given. + This is an implementation of the UDS request for the clearDynamicallyDefinedDataIdentifier sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + + :param dynamically_defined_data_identifier: The dynamically defined data identifier to be cleared, or None if all are to be cleared. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + return await self.request( + service.ClearDynamicallyDefinedDataIdentifierRequest( + dynamically_defined_data_identifier, suppress_response + ) + ) + + @overload + async def request( + self, request: service.RawRequest, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.PositiveResponse: ... + + @overload + async def request( + self, + request: service.DiagnosticSessionControlRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.DiagnosticSessionControlResponse: ... + + @overload + async def request( + self, + request: service.ECUResetRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ECUResetResponse: ... + + @overload + async def request( + self, + request: service.RequestSeedRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.SecurityAccessResponse: ... + + @overload + async def request( + self, request: service.SendKeyRequest, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.SecurityAccessResponse: ... + + @overload + async def request( + self, + request: service.CommunicationControlRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.CommunicationControlResponse: ... + + @overload + async def request( + self, + request: service.TesterPresentRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.TesterPresentResponse: ... + + @overload + async def request( + self, + request: service.ControlDTCSettingRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ControlDTCSettingResponse: ... + + @overload + async def request( + self, + request: service.ReadDataByIdentifierRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReadDataByIdentifierResponse: ... + + @overload + async def request( + self, + request: service.ReadMemoryByAddressRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReadMemoryByAddressResponse: ... + + @overload + async def request( + self, + request: service.WriteDataByIdentifierRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.WriteDataByIdentifierResponse: ... + + @overload + async def request( + self, + request: service.WriteMemoryByAddressRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.WriteMemoryByAddressResponse: ... + + @overload + async def request( + self, + request: service.ClearDiagnosticInformationRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ClearDiagnosticInformationResponse: ... + + @overload + async def request( + self, + request: service.ReportNumberOfDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportNumberOfDTCByStatusMaskResponse: ... + + @overload + async def request( + self, + request: service.ReportDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportDTCByStatusMaskResponse: ... + + @overload + async def request( + self, + request: service.ReportMirrorMemoryDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportMirrorMemoryDTCByStatusMaskResponse: ... + + @overload + async def request( + self, + request: service.ReportNumberOfMirrorMemoryDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportNumberOfMirrorMemoryDTCByStatusMaskResponse: ... + + @overload + async def request( + self, + request: service.ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> ( + service.NegativeResponse | service.ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskResponse + ): ... + + @overload + async def request( + self, + request: service.ReportEmissionsRelatedOBDDTCByStatusMaskRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.ReportEmissionsRelatedOBDDTCByStatusMaskResponse: ... + + @overload + async def request( + self, + request: service.InputOutputControlByIdentifierRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.InputOutputControlByIdentifierResponse: ... + + @overload + async def request( + self, + request: service.StartRoutineRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.StartRoutineResponse: ... + + @overload + async def request( + self, + request: service.StopRoutineRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.StopRoutineResponse: ... + + @overload + async def request( + self, + request: service.RequestRoutineResultsRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestRoutineResultsResponse: ... + + @overload + async def request( + self, + request: service.RequestDownloadRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestDownloadResponse: ... + + @overload + async def request( + self, + request: service.RequestUploadRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestUploadResponse: ... + + @overload + async def request( + self, + request: service.TransferDataRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.TransferDataResponse: ... + + @overload + async def request( + self, + request: service.RequestTransferExitRequest, + config: UDSRequestConfig | None = None, + ) -> service.NegativeResponse | service.RequestTransferExitResponse: ... + + @overload + async def request( + self, request: service.DefineByIdentifierRequest, config: UDSRequestConfig | None = None + ) -> service.DefineByIdentifierResponse: ... + + @overload + async def request( + self, request: service.DefineByMemoryAddressRequest, config: UDSRequestConfig | None = None + ) -> service.DefineByMemoryAddressResponse: ... + + @overload + async def request( + self, + request: service.ClearDynamicallyDefinedDataIdentifierRequest, + config: UDSRequestConfig | None = None, + ) -> service.ClearDynamicallyDefinedDataIdentifierResponse: ... + + async def request( + self, request: service.UDSRequest, config: UDSRequestConfig | None = None + ) -> service.UDSResponse: + """Sends a raw UDS request and returns the response. + Network errors are handled via exponential backoff. + Pending errors, triggered by the ECU are resolved as well. + + :param request: request to send + :param config: The request config parameters + :return: The response. + """ + return await self._request(request, config) + + async def _request( + self, request: service.UDSRequest, config: UDSRequestConfig | None = None + ) -> service.UDSResponse: + async with self.mutex: + return await self.request_unsafe(request, config) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/services/uds/core/constants.html b/_modules/gallia/services/uds/core/constants.html new file mode 100644 index 000000000..eb718d74d --- /dev/null +++ b/_modules/gallia/services/uds/core/constants.html @@ -0,0 +1,368 @@ + + + + + + + + gallia.services.uds.core.constants — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for gallia.services.uds.core.constants

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from enum import IntEnum, unique
+
+
+
+[docs] +@unique +class UDSIsoServices(IntEnum): + ShowCurrentData = 0x01 + ShowFreezeFrameData = 0x02 + ShowStoredDiagnosticTroubleCodes = 0x03 + ClearDiagnosticTroubleCodesAndStoredValues = 0x04 + TestResultsOxygenSensorMonitoring = 0x05 + TestResultsOtherComponentSystemMonitoring = 0x06 + ShowPendingDiagnosticTroubleCodes = 0x07 + ControlOperationOfOnBoardComponentSystem = 0x08 + RequestVehicleInformation = 0x09 + PermanentDiagnosticTroubleCodes = 0x0A + DiagnosticSessionControl = 0x10 + EcuReset = 0x11 + SecurityAccess = 0x27 + CommunicationControl = 0x28 + TesterPresent = 0x3E + Authentication = 0x29 + AccessTimingParameter = 0x83 + SecuredDataTransmission = 0x84 + ControlDTCSetting = 0x85 + ResponseOnEvent = 0x86 + LinkControl = 0x87 + ReadDataByIdentifier = 0x22 + ReadMemoryByAddress = 0x23 + ReadScalingDataByIdentifier = 0x24 + ReadDataByPeriodicIdentifier = 0x2A + DynamicallyDefineDataIdentifier = 0x2C + WriteDataByIdentifier = 0x2E + WriteMemoryByAddress = 0x3D + ClearDiagnosticInformation = 0x14 + ReadDTCInformation = 0x19 + InputOutputControlByIdentifier = 0x2F + RoutineControl = 0x31 + RequestDownload = 0x34 + RequestUpload = 0x35 + TransferData = 0x36 + RequestTransferExit = 0x37 + RequestFileTransfer = 0x38 + NegativeResponse = 0x7F
+ + + +
+[docs] +@unique +class UDSErrorCodes(IntEnum): + generalReject = 0x10 + serviceNotSupported = 0x11 + subFunctionNotSupported = 0x12 + incorrectMessageLengthOrInvalidFormat = 0x13 + responseTooLong = 0x14 + busyRepeatRequest = 0x21 + conditionsNotCorrect = 0x22 + requestSequenceError = 0x24 + noResponseFromSubnetComponent = 0x25 + failurePreventsExecutionOfRequestedAction = 0x26 + requestOutOfRange = 0x31 + securityAccessDenied = 0x33 + authenticationRequired = 0x34 + invalidKey = 0x35 + exceededNumberOfAttempts = 0x36 + requiredTimeDelayNotExpired = 0x37 + secureDataTransmissionRequired = 0x38 + secureDataTransmissionNotAllowed = 0x39 + secureDataVerificationFailed = 0x3A + certificateVerificationFailedInvalidTimePeriod = 0x50 + certificateVerificationFailedInvalidSignature = 0x51 + certificateVerificationFailedInvalidChainOfTrust = 0x52 + certificateVerificationFailedInvalidType = 0x53 + certificateVerificationFailedInvalidFormat = 0x54 + certificateVerificationFailedInvalidContent = 0x55 + certificateVerificationFailedInvalidScope = 0x56 + certificateVerificationFailedInvalidCertificateRevoked = 0x57 + ownershipVerificationFailed = 0x58 + challengeCalculationFailed = 0x59 + settingAccessRightsFailed = 0x5A + sessionKeyCreationOrDerivationFailed = 0x5B + configurationDataUsageFailed = 0x5C + deAuthenticationFailed = 0x5D + uploadDownloadNotAccepted = 0x70 + transferDataSuspended = 0x71 + generalProgrammingFailure = 0x72 + wrongBlockSequenceCounter = 0x73 + requestCorrectlyReceivedResponsePending = 0x78 + subFunctionNotSupportedInActiveSession = 0x7E + serviceNotSupportedInActiveSession = 0x7F + rpmTooHigh = 0x81 + rpmTooLow = 0x82 + engineIsRunning = 0x83 + engineIsNotRunning = 0x84 + engineRunTimeTooLow = 0x85 + temperatureTooHigh = 0x86 + temperatureTooLow = 0x87 + vehicleSpeedTooHigh = 0x88 + vehicleSpeedTooLow = 0x89 + throttlePedalTooHigh = 0x8A + throttlePedalTooLow = 0x8B + transmissionRangeNotInNeutral = 0x8C + transmissionRangeNotInGear = 0x8D + brakeSwitchNotClosed = 0x8F + shifterLeverNotInPark = 0x90 + torqueConverterClutchLocked = 0x91 + voltageTooHigh = 0x92 + voltageTooLow = 0x93 + resourceTemporarilyNotAvailable = 0x94 + vehicleManufacturerSpecificConditionsNotCorrectF0 = 0xF0 + vehicleManufacturerSpecificConditionsNotCorrectF1 = 0xF1 + vehicleManufacturerSpecificConditionsNotCorrectF2 = 0xF2 + vehicleManufacturerSpecificConditionsNotCorrectF3 = 0xF3 + vehicleManufacturerSpecificConditionsNotCorrectF4 = 0xF4 + vehicleManufacturerSpecificConditionsNotCorrectF5 = 0xF5 + vehicleManufacturerSpecificConditionsNotCorrectF6 = 0xF6 + vehicleManufacturerSpecificConditionsNotCorrectF7 = 0xF7 + vehicleManufacturerSpecificConditionsNotCorrectF8 = 0xF8 + vehicleManufacturerSpecificConditionsNotCorrectF9 = 0xF9 + vehicleManufacturerSpecificConditionsNotCorrectFA = 0xFA + vehicleManufacturerSpecificConditionsNotCorrectFB = 0xFB + vehicleManufacturerSpecificConditionsNotCorrectFC = 0xFC + vehicleManufacturerSpecificConditionsNotCorrectFD = 0xFD + vehicleManufacturerSpecificConditionsNotCorrectFE = 0xFE
+ + + +@unique +class DiagnosticSessionControlSubFuncs(IntEnum): + defaultSession = 0x01 + programmingSession = 0x02 + extendedDiagnosticSession = 0x03 + safetySystemDiagnosticSession = 0x04 + + +@unique +class RoutineControlSubFuncs(IntEnum): + startRoutine = 0x01 + stopRoutine = 0x02 + requestRoutineResults = 0x03 + + +@unique +class CCSubFuncs(IntEnum): + enableRxAndTx = 0x00 + enableRxAndDisableTx = 0x01 + disableRxAndEnableTx = 0x02 + disableRxAndTx = 0x03 + # Plus vendor specific stuff... + + +@unique +class CDTCSSubFuncs(IntEnum): + ON = 0x01 + OFF = 0x02 + # Plus vendor specific stuff... + + +@unique +class ReadDTCInformationSubFuncs(IntEnum): + reportNumberOfDTCByStatusMask = 0x01 + reportDTCByStatusMask = 0x02 + reportSupportedDTC = 0x0A + reportFirstTestFailedDTC = 0x0B + reportFirstConfirmedDTC = 0x0C + reportMostRecentTestFailedDTC = 0x0D + reportMostRecentConfirmedDTC = 0x0E + reportMirrorMemoryDTCByStatusMask = 0x0F + reportNumberOfMirrorMemoryDTCByStatusMask = 0x11 + reportNumberOfEmissionsRelatedOBDDTCByStatusMask = 0x12 + reportEmissionsRelatedOBDDTCByStatusMask = 0x13 + reportDTCFaultDetectionCounter = 0x14 + reportDTCWithPermanentStatus = 0x15 + + +@unique +class EcuResetSubFuncs(IntEnum): + hardReset = 0x01 + keyOffOnReset = 0x02 + softReset = 0x03 + enableRapidPowerShutDown = 0x04 + disableRapidPowerShutDown = 0x05 + + +@unique +class InputOutputControlParameter(IntEnum): + returnControlToECU = 0x00 + resetToDefault = 0x01 + freezeCurrentState = 0x02 + shortTermAdjustment = 0x03 + + +@unique +class DTCFormatIdentifier(IntEnum): + # ISO15031-6DTCFormat + ISO_15031_6 = 0x00 + # ISO14229-1DTCFormat + ISO_14229_1 = 0x01 + # SAEJ1939-73DTCFormat + SAE_J1939_73 = 0x02 + # ISO11992-4DTCFormat + ISO_11992_4 = 0x03 + + +# This dictionary maps UDS services to the echo length of their responses. +# Echos in that context are values which are identical to the corresponding entry in a request. +# Therefore they can be used to match responses to requests but are not adding any new information. +# Some services (e.g. rdbi) can accept more data records in a request and response accordingly. +# In that case there can be several echos. However, as of the time of writing, multiple data +# records are not considered in the rest of the code. +# For a complete handling one might need to transfer this to a function. +UDSIsoServicesEchoLength = { + UDSIsoServices.DiagnosticSessionControl: 1, + UDSIsoServices.EcuReset: 1, + UDSIsoServices.SecurityAccess: 1, + UDSIsoServices.CommunicationControl: 1, + UDSIsoServices.TesterPresent: 1, + UDSIsoServices.AccessTimingParameter: 1, + UDSIsoServices.ControlDTCSetting: 1, + UDSIsoServices.ResponseOnEvent: 1, # There are a number of echos but only one byte is a prefix + UDSIsoServices.LinkControl: 1, + UDSIsoServices.ReadDataByIdentifier: 2, + UDSIsoServices.ReadScalingDataByIdentifier: 2, + UDSIsoServices.ReadDataByPeriodicIdentifier: 1, # This one is a little weird + UDSIsoServices.DynamicallyDefineDataIdentifier: 3, + UDSIsoServices.WriteDataByIdentifier: 2, + # This one would require to parse the addressAndLengthFormatIdentifier field + # UDSIsoServices.WriteMemoryByAddress: None, + UDSIsoServices.ReadDTCInformation: 1, + UDSIsoServices.InputOutputControlByIdentifier: 2, + UDSIsoServices.RoutineControl: 3, + UDSIsoServices.TransferData: 1, +} + + +@unique +class DataIdentifier(IntEnum): + ActiveDiagnosticSessionDataIdentifier = 0xF186 + + +@unique +class DynamicallyDefineDataIdentifierSubFuncs(IntEnum): + defineByIdentifier = 0x01 + defineByMemoryAddress = 0x02 + clearDynamicallyDefinedDataIdentifier = 0x03 +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/services/uds/core/service.html b/_modules/gallia/services/uds/core/service.html new file mode 100644 index 000000000..5be882ce6 --- /dev/null +++ b/_modules/gallia/services/uds/core/service.html @@ -0,0 +1,3869 @@ + + + + + + + + gallia.services.uds.core.service — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for gallia.services.uds.core.service

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import inspect
+import struct
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from struct import pack
+from typing import Any, TypeVar
+
+from gallia.log import get_logger
+from gallia.services.uds.core.constants import (
+    DTCFormatIdentifier,
+    DynamicallyDefineDataIdentifierSubFuncs,
+    InputOutputControlParameter,
+    ReadDTCInformationSubFuncs,
+    RoutineControlSubFuncs,
+    UDSErrorCodes,
+    UDSIsoServices,
+    UDSIsoServicesEchoLength,
+)
+from gallia.services.uds.core.utils import (
+    address_and_length_fmt,
+    address_and_size_length,
+    any_repr,
+    bytes_repr,
+    check_data_identifier,
+    check_length,
+    check_range,
+    check_sub_function,
+    from_bytes,
+    int_repr,
+    service_repr,
+    sub_function_split,
+    to_bytes,
+    uds_memory_parameters,
+)
+
+logger = get_logger(__name__)
+
+# ****************
+# * Base classes *
+# ****************
+
+
+T_UDSRequest = TypeVar("T_UDSRequest", bound="UDSRequest")
+
+
+
+[docs] +class UDSRequest(ABC): + SERVICE_ID: int | None + RESPONSE_TYPE: type[PositiveResponse] + _MINIMAL_LENGTH: int + _MAXIMAL_LENGTH: int | None + + def __init_subclass__( + cls, + /, + service_id: int | None, + response_type: type[PositiveResponse], + minimal_length: int, + maximal_length: int | None, + **kwargs: Any, + ) -> None: + super().__init_subclass__(**kwargs) + + cls.SERVICE_ID = service_id + cls.RESPONSE_TYPE = response_type + cls._MINIMAL_LENGTH = minimal_length + cls._MAXIMAL_LENGTH = maximal_length + + @property + @abstractmethod + def pdu(self) -> bytes: + pass + + @classmethod + def from_pdu(cls: type[T_UDSRequest], pdu: bytes) -> T_UDSRequest: + cls._check_pdu(pdu) + result = cls._from_pdu(pdu) + + assert result.pdu == pdu + + return result + + @classmethod + @abstractmethod + def _from_pdu(cls: type[T_UDSRequest], pdu: bytes) -> T_UDSRequest: + pass + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + check_length(pdu, cls._MINIMAL_LENGTH, cls._MAXIMAL_LENGTH) + + if cls.SERVICE_ID is not None and pdu[0] != cls.SERVICE_ID: + raise ValueError(f"Service ID mismatch: {hex(pdu[0])} != {hex(cls.SERVICE_ID)}") + + @property + def service_id(self) -> int: + return self.pdu[0] + + @property + def data(self) -> bytes: + return self.pdu[1:] + + def __repr__(self) -> str: + title = self.__class__.__name__ + relevant_attributes = {} + + for attr, value in self.__dict__.items(): + if not attr.startswith("_"): + relevant_attributes[attr] = any_repr(value) + + attributes = ", ".join(f"{attr}={value}" for attr, value in relevant_attributes.items()) + return f"{title}({attributes})" + + @staticmethod + def parse_dynamic(pdu: bytes) -> UDSRequest: + try: + logger.trace("Dynamically parsing request") + logger.trace(f" - Got PDU {pdu.hex()}") + request_service = UDSService._SERVICES[UDSIsoServices(pdu[0])] + + logger.trace(f" - Inferred service {request_service.__name__}") + + if (request_type := request_service.Request) is not None: + logger.trace(f" - Trying {request_type.__name__}") + return request_type.from_pdu(pdu) + if issubclass(request_service, SpecializedSubFunctionService): + logger.trace(" - Trying to infer subFunction") + request_sub_function = request_service._sub_function_type(pdu) + logger.trace(f" - Inferred subFunction {request_sub_function.__name__}") + assert (request_type := request_sub_function.Request) is not None + logger.trace(f" - Trying {request_type.__name__}") + return request_type.from_pdu(pdu) + + raise ValueError("Request cannot be parsed") + except Exception as e: + logger.trace( + f" - Falling back to RawRequest because of the following problem: {repr(e)}" + ) + return RawRequest(pdu)
+ + + +T_UDSResponse = TypeVar("T_UDSResponse", bound="UDSResponse") + + +
+[docs] +class UDSResponse(ABC): + SERVICE_ID: int | None + RESPONSE_SERVICE_ID: int | None + _MINIMAL_LENGTH: int + _MAXIMAL_LENGTH: int | None + + def __init_subclass__( + cls, + /, + service_id: int | None, + minimal_length: int, + maximal_length: int | None, + **kwargs: Any, + ) -> None: + super().__init_subclass__(**kwargs) + + cls.SERVICE_ID = service_id + cls.RESPONSE_SERVICE_ID = None if service_id is None else service_id + 0x40 + cls._MINIMAL_LENGTH = minimal_length + cls._MAXIMAL_LENGTH = maximal_length + + def __init__(self) -> None: + self.trigger_request: UDSRequest | None = None + + @property + @abstractmethod + def pdu(self) -> bytes: + pass + + @classmethod + def from_pdu(cls: type[T_UDSResponse], pdu: bytes) -> T_UDSResponse: + cls._check_pdu(pdu) + return cls._from_pdu(pdu) + + @classmethod + @abstractmethod + def _from_pdu(cls: type[T_UDSResponse], pdu: bytes) -> T_UDSResponse: + pass + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + check_length(pdu, cls._MINIMAL_LENGTH, cls._MAXIMAL_LENGTH) + + if ( + cls.RESPONSE_SERVICE_ID is not None + and cls.SERVICE_ID is not None + and pdu[0] != cls.RESPONSE_SERVICE_ID + ): + raise ValueError( + f"Service ID mismatch: {hex(pdu[0])} != {hex(cls.RESPONSE_SERVICE_ID)}" + f" ({hex(cls.SERVICE_ID)} + 0x40)" + ) + + @property + def service_id(self) -> int: + assert self.SERVICE_ID is not None + + return self.SERVICE_ID + + @abstractmethod + def matches(self, request: UDSRequest) -> bool: + pass + + @staticmethod + def parse_dynamic(pdu: bytes) -> UDSResponse: + if pdu[0] == UDSIsoServices.NegativeResponse: + return NegativeResponse.from_pdu(pdu) + + response_type: type[PositiveResponse] + + logger.trace("Dynamically parsing response") + logger.trace(f" - Got PDU {pdu.hex()}") + + try: + response_service = UDSService._SERVICES[UDSIsoServices(pdu[0] - 0x40)] + except Exception: + logger.trace(" - Falling back to raw response because the service is unknown") + return RawPositiveResponse(pdu) + + logger.trace(f" - Inferred service {response_service.__name__}") + + if response_service.Response is not None: + response_type = response_service.Response + elif issubclass(response_service, SpecializedSubFunctionService): + if len(pdu) < 2: + raise ValueError("Message of subfunction service contains no subfunction") + + logger.trace(" - Trying to infer subfunction") + try: + response_sub_function = response_service._sub_function_type(pdu) + except ValueError as e: + logger.trace(f" - Falling back to raw response because {str(e)}") + return RawPositiveResponse(pdu) + + logger.trace(f" - Inferred subFunction {response_sub_function.__name__}") + assert (response_type_ := response_sub_function.Response) is not None + response_type = response_type_ + else: + logger.trace(" - Falling back to raw response because the response cannot be parsed") + return RawPositiveResponse(pdu) + + logger.trace(f" - Trying {response_type.__name__}") + return response_type.from_pdu(pdu)
+ + + +T_RawResponse = TypeVar("T_RawResponse", bound="RawResponse") + + +class RawResponse(UDSResponse, ABC, service_id=None, minimal_length=1, maximal_length=None): + def __init__(self, pdu: bytes) -> None: + super().__init__() + + self._pdu = pdu + + @classmethod + def _from_pdu(cls: type[T_RawResponse], pdu: bytes) -> T_RawResponse: + return cls(pdu) + + @property + def pdu(self) -> bytes: + return self._pdu + + @pdu.setter + def pdu(self, pdu: bytes) -> None: + self._pdu = pdu + + def __repr__(self) -> str: + return f"{type(self).__name__}(pdu={bytes_repr(self.pdu)})" + + +class NegativeResponseBase( + UDSResponse, + ABC, + service_id=UDSIsoServices.NegativeResponse, + minimal_length=1, + maximal_length=None, +): + pass + + +class RawNegativeResponse( + NegativeResponseBase, + RawResponse, + service_id=UDSIsoServices.NegativeResponse, + minimal_length=1, + maximal_length=None, +): + def matches(self, request: UDSRequest) -> bool: + return len(self.pdu) > 1 and self.pdu[1] == request.service_id + + +
+[docs] +class NegativeResponse( + NegativeResponseBase, + service_id=UDSIsoServices.NegativeResponse, + minimal_length=3, + maximal_length=3, +): + def __init__(self, request_service_id: int, response_code: UDSErrorCodes) -> None: + super().__init__() + + self.request_service_id = request_service_id + self.response_code = response_code + + @property + def pdu(self) -> bytes: + return pack("!BBB", self.SERVICE_ID, self.request_service_id, self.response_code) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> NegativeResponse: + return NegativeResponse(pdu[1], UDSErrorCodes(pdu[2])) + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + check_length(pdu, cls._MINIMAL_LENGTH, cls._MAXIMAL_LENGTH) + + if pdu[0] != UDSIsoServices.NegativeResponse: + raise ValueError( + f"Not a negative response: {hex(pdu[0])} != {hex(UDSIsoServices.NegativeResponse)}" + ) + + def matches(self, request: UDSRequest) -> bool: + return self.request_service_id == request.service_id + + def __str__(self) -> str: + return str(self.response_code.name) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(response_code={self.response_code.name}, " + f"request_service={service_repr(self.request_service_id)})" + )
+ + + +T_PositiveResponse = TypeVar("T_PositiveResponse", bound="PositiveResponse") + + +
+[docs] +class PositiveResponse(UDSResponse, ABC, service_id=None, minimal_length=0, maximal_length=None): + @property + def data(self) -> bytes: + return self.pdu[1:] + + def __repr__(self) -> str: + title = self.__class__.__name__ + relevant_attributes = {} + + for attr, value in self.__dict__.items(): + if not attr.startswith("_") and attr not in ["trigger_request"]: + relevant_attributes[attr] = any_repr(value) + + attributes = ", ".join(f"{attr}={value}" for attr, value in relevant_attributes.items()) + return f"{title}({attributes})" + + @classmethod + def parse_static( + cls: type[T_PositiveResponse], response_pdu: bytes + ) -> NegativeResponse | T_PositiveResponse: + if response_pdu[0] == 0x7F: + negative_response = NegativeResponse.from_pdu(response_pdu) + return negative_response + + response = cls.from_pdu(response_pdu) + return response
+ + + +class UDSService(ABC): + SERVICE_ID: UDSIsoServices | None + _SERVICES: dict[UDSIsoServices | None, type[UDSService]] = {} + Response: type[PositiveResponse] | None = None + Request: type[UDSRequest] | None = None + + @classmethod + def _response_type(cls, pdu: bytes) -> type[PositiveResponse] | None: + return cls.Response + + @classmethod + def _request_type(cls, pdu: bytes) -> type[UDSRequest] | None: + return cls.Request + + def __init_subclass__(cls, /, service_id: UDSIsoServices | None, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + + cls.SERVICE_ID = service_id + UDSService._SERVICES[service_id] = cls + + +class SubFunction(ABC): + SUB_FUNCTION_ID: int | None + Response: type[PositiveResponse] | None + Request: type[UDSRequest] | None + + def __init_subclass__(cls, /, sub_function_id: int | None, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + + cls.SUB_FUNCTION_ID = sub_function_id + + +class SpecializedSubFunctionService(UDSService, ABC, service_id=None): + @classmethod + def _sub_function_type(cls, pdu: bytes) -> type[SubFunction]: + sub_function_id = pdu[1] % 0x80 + + sub_functions = [ + x for x in cls.__dict__.values() if inspect.isclass(x) and issubclass(x, SubFunction) + ] + + for sub_function in sub_functions: + if sub_function.SUB_FUNCTION_ID == sub_function_id: + return sub_function + + raise ValueError(f"SubFunction not supported: {int_repr(sub_function_id)}") + + @classmethod + def _response_type(cls, pdu: bytes) -> type[PositiveResponse] | None: + return cls._sub_function_type(pdu).Response + + @classmethod + def _request_type(cls, pdu: bytes) -> type[UDSRequest] | None: + return cls._sub_function_type(pdu).Request + + +
+[docs] +class SubFunctionResponse( + PositiveResponse, ABC, service_id=None, minimal_length=2, maximal_length=None +): + def __init__(self) -> None: + super().__init__() + + check_sub_function(self.sub_function) + + @property + @abstractmethod + def sub_function(self) -> int: + pass + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + super()._check_pdu(pdu) + + check_sub_function(pdu[1])
+ + + +
+[docs] +class SubFunctionRequest( + UDSRequest, + ABC, + service_id=None, + response_type=SubFunctionResponse, # type: ignore + minimal_length=2, + maximal_length=None, +): + def __init__(self, suppress_response: bool) -> None: + check_sub_function(self.sub_function) + + self.suppress_response = suppress_response + + @property + @abstractmethod + def sub_function(self) -> int: + pass + + @property + def sub_function_with_suppress_response_bit(self) -> int: + return int(self.suppress_response) * 0x80 + self.sub_function + + @staticmethod + def suppress_response_set(pdu: bytes) -> bool: + return pdu[1] >= 0x80
+ + + +class SpecializedSubFunctionResponse( + SubFunctionResponse, ABC, service_id=None, minimal_length=2, maximal_length=None +): + SUB_FUNCTION_ID: int + + def __init_subclass__(cls, /, sub_function_id: int, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + + cls.SUB_FUNCTION_ID = sub_function_id + + def __init__(self) -> None: + super().__init__() + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + super()._check_pdu(pdu) + + if pdu[1] != cls.SUB_FUNCTION_ID: + raise ValueError( + f"Sub-function ID mismatch: {hex(pdu[1])} != {hex(cls.SUB_FUNCTION_ID)}" + ) + + @property + def sub_function(self) -> int: + return self.SUB_FUNCTION_ID + + +class SpecializedSubFunctionRequest( + SubFunctionRequest, + ABC, + service_id=None, + response_type=SpecializedSubFunctionResponse, # type: ignore + minimal_length=2, + maximal_length=None, +): + SUB_FUNCTION_ID: int + + def __init_subclass__(cls, /, sub_function_id: int, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + + cls.SUB_FUNCTION_ID = sub_function_id + + def __init__(self, suppress_response: bool) -> None: + super().__init__(suppress_response) + + @classmethod + def _check_pdu(cls, pdu: bytes) -> None: + super()._check_pdu(pdu) + + if pdu[1] % 0x80 != cls.SUB_FUNCTION_ID: + raise ValueError( + f"Sub-function ID mismatch: {hex(pdu[1])} != {hex(cls.SUB_FUNCTION_ID)}" + ) + + @property + def sub_function(self) -> int: + return self.SUB_FUNCTION_ID + + +# ****************************** +# * Raw requests and responses * +# ****************************** + + +class RawPositiveResponse( + RawResponse, + PositiveResponse, + service_id=None, + minimal_length=1, + maximal_length=None, +): + @property + def service_id(self) -> int: + return self.pdu[0] - 0x40 + + def matches(self, request: UDSRequest) -> bool: + if self.service_id != request.service_id: + return False + + # Use the old heuristic approach to detect as many mismatches as possible on responses which could not be parsed + try: + echo_length = UDSIsoServicesEchoLength[UDSIsoServices(self.service_id)] + return request.pdu[1 : echo_length + 1] == self.pdu[1 : echo_length + 1] + except Exception: + pass + + return True + + +class RawRequest( + UDSRequest, + service_id=None, + response_type=RawPositiveResponse, + minimal_length=1, + maximal_length=None, +): + def __init__(self, pdu: bytes) -> None: + """Raw request, which does not need to be compliant with the standard. + It can be used to send arbitrary data packets. + + :param pdu: The data. + """ + self._pdu = pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> RawRequest: + return RawRequest(pdu) + + @property + def service_id(self) -> int: + return self.pdu[0] + + @property + def pdu(self) -> bytes: + return self._pdu + + @pdu.setter + def pdu(self, pdu: bytes) -> None: + self._pdu = pdu + + def __repr__(self) -> str: + return f"{type(self).__name__}(pdu={bytes_repr(self.pdu)})" + + +class Raw(UDSService, service_id=None): + Response = RawPositiveResponse + Request = RawRequest + + +# ****************************** +# * Diagnostic session control * +# ****************************** + + +class DiagnosticSessionControlResponse( + SubFunctionResponse, + service_id=UDSIsoServices.DiagnosticSessionControl, + minimal_length=2, + maximal_length=None, +): + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.RESPONSE_SERVICE_ID, self.diagnostic_session_type) + + self.session_parameter_record + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> DiagnosticSessionControlResponse: + diagnostic_session_type = from_bytes(pdu[1:2]) + session_parameter_record = pdu[2:] + return DiagnosticSessionControlResponse(diagnostic_session_type, session_parameter_record) + + def __init__(self, diagnostic_session_type: int, session_parameter_record: bytes = b"") -> None: + self.diagnostic_session_type = diagnostic_session_type + self.session_parameter_record = session_parameter_record + + super().__init__() + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, DiagnosticSessionControlRequest) + and request.diagnostic_session_type == self.diagnostic_session_type + ) + + @property + def sub_function(self) -> int: + return self.diagnostic_session_type + + +class DiagnosticSessionControlRequest( + SubFunctionRequest, + service_id=UDSIsoServices.DiagnosticSessionControl, + response_type=DiagnosticSessionControlResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, diagnostic_session_type: int, suppress_response: bool = False) -> None: + """Sets the diagnostic session which is specified by a specific diagnosticSessionType + sub-function. + This is an implementation of the UDS request for service DiagnosticSessionControl (0x10). + + :param diagnostic_session_type: The session sub-function. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + self.diagnostic_session_type = diagnostic_session_type + + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return pack("!BB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> DiagnosticSessionControlRequest: + return DiagnosticSessionControlRequest(*sub_function_split(pdu[1])) + + @property + def sub_function(self) -> int: + return self.diagnostic_session_type + + +class DiagnosticSessionControl(UDSService, service_id=UDSIsoServices.DiagnosticSessionControl): + Response = DiagnosticSessionControlResponse + Request = DiagnosticSessionControlRequest + + +# ************* +# * ECU reset * +# ************* + + +class ECUResetResponse( + SubFunctionResponse, + service_id=UDSIsoServices.EcuReset, + minimal_length=2, + maximal_length=3, +): + def __init__(self, reset_type: int, power_down_time: int | None = None) -> None: + if power_down_time is not None: + check_range(power_down_time, "powerDownTime", 0, 0xFF) + + self.reset_type = reset_type + self.power_down_time = power_down_time + + super().__init__() + + @property + def pdu(self) -> bytes: + if self.power_down_time is None: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.reset_type) + + return pack("!BBB", self.RESPONSE_SERVICE_ID, self.reset_type, self.power_down_time) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ECUResetResponse: + reset_type = pdu[1] + power_down_time = pdu[2] if len(pdu) > 2 else None + + return ECUResetResponse(reset_type, power_down_time) + + def matches(self, request: UDSRequest) -> bool: + return isinstance(request, ECUResetRequest) and request.reset_type == self.reset_type + + @property + def sub_function(self) -> int: + return self.reset_type + + +class ECUResetRequest( + SubFunctionRequest, + service_id=UDSIsoServices.EcuReset, + response_type=ECUResetResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, reset_type: int, suppress_response: bool = False) -> None: + """Resets the ECU using the specified reset type sub-function. + This is an implementation of the UDS request for service ECUReset (0x11). + + :param reset_type: The reset type sub-function. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + self.reset_type = reset_type + + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return pack("!BB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ECUResetRequest: + return ECUResetRequest(*sub_function_split(pdu[1])) + + @property + def sub_function(self) -> int: + return self.reset_type + + +class ECUReset(UDSService, service_id=UDSIsoServices.EcuReset): + Response = ECUResetResponse + Request = ECUResetRequest + + +# ******************* +# * Security access * +# ******************* + + +class SecurityAccessResponse( + SubFunctionResponse, + service_id=UDSIsoServices.SecurityAccess, + minimal_length=2, + maximal_length=None, +): + def __init__(self, security_access_type: int, security_seed: bytes = b"") -> None: + self.security_access_type = security_access_type + self.security_seed = security_seed + + super().__init__() + + @property + def pdu(self) -> bytes: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.security_access_type) + self.security_seed + + @classmethod + def _from_pdu(cls, pdu: bytes) -> SecurityAccessResponse: + security_access_type = from_bytes(pdu[1:2]) + security_seed = pdu[2:] + return SecurityAccessResponse(security_access_type, security_seed) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, _SecurityAccessRequest) + and request.security_access_type == self.security_access_type + ) + + @property + def sub_function(self) -> int: + return self.security_access_type + + +class _SecurityAccessRequest( + SubFunctionRequest, + ABC, + service_id=UDSIsoServices.SecurityAccess, + response_type=SecurityAccessResponse, + minimal_length=2, + maximal_length=None, +): + def __init__(self, security_access_type: int, suppress_response: bool = False) -> None: + self.security_access_type = security_access_type + + super().__init__(suppress_response) + + @property + def sub_function(self) -> int: + return self.security_access_type + + +class RequestSeedRequest( + _SecurityAccessRequest, + service_id=UDSIsoServices.SecurityAccess, + response_type=SecurityAccessResponse, + minimal_length=2, + maximal_length=None, +): + def __init__( + self, + security_access_type: int, + security_access_data_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + """Requests a seed for a security access level. + This is an implementation of the UDS request for the requestSeed sub-function group + of the service SecurityAccess (0x27). + + :param security_access_type: The securityAccess type sub-function. + :param security_access_data_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(security_access_type, suppress_response) + + if security_access_type % 2 == 0: + raise ValueError( + f"RequestSeed requests must have an odd securityAccessType: " + f"{hex(security_access_type)}" + ) + + self.security_access_data_record = security_access_data_record + + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + self.security_access_data_record + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> RequestSeedRequest: + security_access_type, suppress_response = sub_function_split(pdu[1]) + security_access_data_record = pdu[2:] + return RequestSeedRequest( + security_access_type, security_access_data_record, suppress_response + ) + + +class SendKeyRequest( + _SecurityAccessRequest, + service_id=UDSIsoServices.SecurityAccess, + response_type=SecurityAccessResponse, + minimal_length=3, + maximal_length=None, +): + def __init__( + self, + security_access_type: int, + security_key: bytes, + suppress_response: bool = False, + ) -> None: + """Sends the key for a security access level. + This is an implementation of the UDS request for the sendKey sub-function group + of the service SecurityAccess (0x27). + + :param security_access_type: The securityAccess type sub-function. + :param security_key: The response to the seed challenge. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(security_access_type, suppress_response) + + if security_access_type % 2 == 1: + raise ValueError( + f"SendKey requests must have an even securityAccessType: " + f"{hex(security_access_type)}" + ) + + self.security_key = security_key + + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + self.security_key + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> SendKeyRequest: + security_access_type, suppress_response = sub_function_split(pdu[1]) + security_key = pdu[2:] + return SendKeyRequest(security_access_type, security_key, suppress_response) + + +class SecurityAccess(SpecializedSubFunctionService, service_id=UDSIsoServices.SecurityAccess): + class RequestSeed(SubFunction, sub_function_id=None): + Response = SecurityAccessResponse + Request = RequestSeedRequest + + class SendKey(SubFunction, sub_function_id=None): + Response = SecurityAccessResponse + Request = SendKeyRequest + + @classmethod + def _sub_function_type(cls, pdu: bytes) -> type[SubFunction]: + sub_function_id = pdu[1] + return SecurityAccess.RequestSeed if sub_function_id % 2 == 1 else SecurityAccess.SendKey + + +# ************************* +# * Communication control * +# ************************* + + +class CommunicationControlResponse( + SubFunctionResponse, + service_id=UDSIsoServices.CommunicationControl, + minimal_length=2, + maximal_length=2, +): + def __init__(self, control_type: int) -> None: + self.control_type = control_type + + super().__init__() + + @property + def pdu(self) -> bytes: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.control_type) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> CommunicationControlResponse: + control_type = pdu[1] + return CommunicationControlResponse(control_type) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, CommunicationControlRequest) + and request.control_type == self.control_type + ) + + @property + def sub_function(self) -> int: + return self.control_type + + +class CommunicationControlRequest( + SubFunctionRequest, + service_id=UDSIsoServices.CommunicationControl, + response_type=CommunicationControlResponse, + minimal_length=3, + maximal_length=3, +): + """Controls communication of the ECU. + This is an implementation of the UDS request for service CommunicationControl (0x28). + + :param control_type: The control type sub-function. + :param communication_type: The communication type. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + + def __init__( + self, + control_type: int, + communication_type: int, + suppress_response: bool = False, + ) -> None: + self.control_type = control_type + self.communication_type = communication_type + + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return pack( + "!BBB", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.communication_type, + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> CommunicationControlRequest: + control_type, suppress_response = sub_function_split(pdu[1]) + communication_type = pdu[2] + + return CommunicationControlRequest(control_type, communication_type, suppress_response) + + @property + def sub_function(self) -> int: + return self.control_type + + +class CommunicationControl(UDSService, service_id=UDSIsoServices.CommunicationControl): + Response = CommunicationControlResponse + Request = CommunicationControlRequest + + +# ****************** +# * Tester present * +# ****************** + + +class TesterPresentResponse( + SpecializedSubFunctionResponse, + service_id=UDSIsoServices.TesterPresent, + sub_function_id=0, + minimal_length=2, + maximal_length=2, +): + @property + def pdu(self) -> bytes: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.SUB_FUNCTION_ID) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> TesterPresentResponse: + return TesterPresentResponse() + + def matches(self, request: UDSRequest) -> bool: + return isinstance(request, TesterPresentRequest) + + +class TesterPresentRequest( + SpecializedSubFunctionRequest, + service_id=UDSIsoServices.TesterPresent, + sub_function_id=0, + response_type=TesterPresentResponse, + minimal_length=2, + maximal_length=2, +): + """Signals to the ECU, that the tester is still present. + This is an implementation of the UDS request for service TesterPresent (0x3E). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + + def __init__(self, suppress_response: bool = False) -> None: + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return pack("!BB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> TesterPresentRequest: + return TesterPresentRequest(cls.suppress_response_set(pdu)) + + +class TesterPresent(UDSService, service_id=UDSIsoServices.TesterPresent): + Response = TesterPresentResponse + Request = TesterPresentRequest + + +# ****************** +# * Authentication * +# ****************** + + +# *************************** +# * Access timing parameter * +# *************************** + + +# ***************************** +# * Secured data transmission * +# ***************************** + + +# *********************** +# * Control DTC setting * +# *********************** + + +class ControlDTCSettingResponse( + SubFunctionResponse, + service_id=UDSIsoServices.ControlDTCSetting, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_setting_type: int) -> None: + self.dtc_setting_type = dtc_setting_type + + super().__init__() + + @property + def pdu(self) -> bytes: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.dtc_setting_type) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ControlDTCSettingResponse: + dtc_setting_type = pdu[1] + return ControlDTCSettingResponse(dtc_setting_type) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, ControlDTCSettingRequest) + and request.dtc_setting_type == self.dtc_setting_type + ) + + @property + def sub_function(self) -> int: + return self.dtc_setting_type + + +class ControlDTCSettingRequest( + SubFunctionRequest, + service_id=UDSIsoServices.ControlDTCSetting, + response_type=ControlDTCSettingResponse, + minimal_length=2, + maximal_length=None, +): + def __init__( + self, + dtc_setting_type: int, + dtc_setting_control_option_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + """Control the setting of DTCs. + This is an implementation of the UDS request for service ControlDTCSetting (0x85). + + + :param dtc_setting_type: The setting type. + :param dtc_setting_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + self.dtc_setting_type = dtc_setting_type + self.dtc_setting_control_option_record = dtc_setting_control_option_record + + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.SERVICE_ID, self.dtc_setting_type) + + self.dtc_setting_control_option_record + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ControlDTCSettingRequest: + dtc_setting_type, suppress_response = sub_function_split(pdu[1]) + dtc_setting_control_option_record = pdu[2:] + + return ControlDTCSettingRequest( + dtc_setting_type, dtc_setting_control_option_record, suppress_response + ) + + @property + def sub_function(self) -> int: + return self.dtc_setting_type + + +class ControlDTCSetting(UDSService, service_id=UDSIsoServices.ControlDTCSetting): + Response = ControlDTCSettingResponse + Request = ControlDTCSettingRequest + + +# ********************* +# * Response on event * +# ********************* + + +# **************** +# * Link control * +# **************** + + +# *************************** +# * Read data by identifier * +# *************************** + + +class ReadDataByIdentifierResponse( + PositiveResponse, + service_id=UDSIsoServices.ReadDataByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + data_identifiers: int | Sequence[int], + data_records: bytes | Sequence[bytes], + ) -> None: + super().__init__() + + if not isinstance(data_identifiers, int): + self.data_identifiers = list(data_identifiers) + else: + self.data_identifiers = [data_identifiers] + + if not isinstance(data_records, bytes): + self.data_records = list(data_records) + else: + self.data_records = [data_records] + + if len(self.data_identifiers) != len(self.data_records): + raise ValueError( + f"The number of data identifiers does not match the number of " + f"data_records: " + f"{len(self.data_identifiers)} != {len(self.data_records)}" + ) + + for identifier in self.data_identifiers: + check_data_identifier(identifier) + + @property + def data_record(self) -> bytes: + return self.data_records[0] + + @data_record.setter + def data_record(self, data_record: bytes) -> None: + self.data_records[0] = data_record + + @property + def data_identifier(self) -> int: + return self.data_identifiers[0] + + @data_identifier.setter + def data_identifier(self, data_identifier: int) -> None: + self.data_identifiers[0] = data_identifier + + @property + def pdu(self) -> bytes: + pdu = pack("!B", self.RESPONSE_SERVICE_ID) + + for data_identifier, data_record in zip(self.data_identifiers, self.data_records): + pdu = pdu + to_bytes(data_identifier, 2) + data_record + + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ReadDataByIdentifierResponse: + # Without knowing the lengths of the dataRecords in a response with multiple dataIdentifiers + # and dataRecords it's not possible to recover all ids. + # Therefore, only the first identifier is used and the rest is simply attributed to the + # first dataRecord + data_identifier = from_bytes(pdu[1:3]) + data_record = pdu[3:] + + return ReadDataByIdentifierResponse(data_identifier, data_record) + + def matches(self, request: UDSRequest) -> bool: + if not isinstance(request, ReadDataByIdentifierRequest): + return False + + # Without knowing the lengths of the dataRecords in a response with multiple dataIdentifiers + # and dataRecords it's not possible to recover all ids. This is respected here, where only + # if this was possible,a complete check is done, while otherwise only the first id of the + # request is taken into account. + if len(self.data_identifiers) > 1: + if len(request.data_identifiers) != len(self.data_identifiers): + return False + + return all( + req_id == resp_id + for req_id, resp_id in zip(request.data_identifiers, self.data_identifiers) + ) + + return request.data_identifiers[0] == self.data_identifiers[0] + + @property + def _minimal_length(self) -> int: + return 4 + + +class ReadDataByIdentifierRequest( + UDSRequest, + service_id=UDSIsoServices.ReadDataByIdentifier, + response_type=ReadDataByIdentifierResponse, + minimal_length=3, + maximal_length=None, +): + def __init__(self, data_identifiers: int | Sequence[int]) -> None: + """Reads data which is identified by a specific dataIdentifier. + This is an implementation of the UDS request for service ReadDataByIdentifier (0x22). + While this implementation supports requesting multiple dataIdentifiers at once, as is + permitted in the standard, it is recommended to request them separately, because the support + is optional on the server side. + Additionally, it is not possible to reliably determine each single dataRecord from a + corresponding response. + + :param data_identifiers: One or multiple dataIdentifiers. A dataIdentifier is a max two + bytes integer. + """ + + if isinstance(data_identifiers, Sequence): + self.data_identifiers = list(data_identifiers) + else: + self.data_identifiers = [data_identifiers] + + for identifier in self.data_identifiers: + check_data_identifier(identifier) + + @property + def data_identifier(self) -> int: + return self.data_identifiers[0] + + @data_identifier.setter + def data_identifier(self, data_identifier: int) -> None: + self.data_identifiers[0] = data_identifier + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ReadDataByIdentifierRequest: + identifiers: list[int] = [] + + for i in range(1, len(pdu), 2): + identifiers.append(from_bytes(pdu[i : i + 2])) + + return ReadDataByIdentifierRequest(identifiers) + + @property + def pdu(self) -> bytes: + return pack( + f"!B{len(self.data_identifiers)}H", + UDSIsoServices.ReadDataByIdentifier, + *self.data_identifiers, + ) + + +class ReadDataByIdentifier(UDSService, service_id=UDSIsoServices.ReadDataByIdentifier): + Response = ReadDataByIdentifierResponse + Request = ReadDataByIdentifierRequest + + +# ************************** +# * Read memory by address * +# ************************** + + +class ReadMemoryByAddressResponse( + PositiveResponse, + service_id=UDSIsoServices.ReadMemoryByAddress, + minimal_length=2, + maximal_length=None, +): + @property + def pdu(self) -> bytes: + return pack("!B", self.RESPONSE_SERVICE_ID) + self.data_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ReadMemoryByAddressResponse: + data_record = pdu[1:] + + return ReadMemoryByAddressResponse(data_record) + + def __init__(self, data_record: bytes) -> None: + super().__init__() + + self.data_record = data_record + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, ReadMemoryByAddressRequest) + and len(self.data_record) == request.memory_size + ) + + +class ReadMemoryByAddressRequest( + UDSRequest, + service_id=UDSIsoServices.ReadMemoryByAddress, + response_type=ReadMemoryByAddressResponse, + minimal_length=4, + maximal_length=32, +): + def __init__( + self, + memory_address: int, + memory_size: int, + address_and_length_format_identifier: int | None = None, + ) -> None: + """Reads data from a specific memory address on the UDS server. + This is an implementation of the UDS request for service ReadMemoryByAddress (0x3d). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param memory_address: The start address. + :param memory_size: The number of bytes to read. + :param address_and_length_format_identifier: The byte lengths of the memory address and size. + If omitted, this parameter is computed based on + the memory_address and memory_size parameters. + """ + + self.memory_address = memory_address + self.memory_size = memory_size + + if address_and_length_format_identifier is None: + address_and_length_format_identifier, _, _ = uds_memory_parameters( + memory_address, memory_size, address_and_length_format_identifier + ) + + self.address_and_length_format_identifier = address_and_length_format_identifier + + @property + def pdu(self) -> bytes: + _, address_bytes, size_bytes = uds_memory_parameters( + self.memory_address, + self.memory_size, + self.address_and_length_format_identifier, + ) + + pdu = pack("!BB", self.SERVICE_ID, self.address_and_length_format_identifier) + pdu += address_bytes + size_bytes + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ReadMemoryByAddressRequest: + address_and_length_format_identifier = pdu[1] + address_length, size_length = address_and_size_length(address_and_length_format_identifier) + + if len(pdu) != 2 + address_length + size_length: + raise ValueError("The addressAndLengthIdentifier is incompatible with the PDU size") + + return ReadMemoryByAddressRequest( + from_bytes(pdu[2 : 2 + address_length]), + from_bytes(pdu[2 + address_length : 2 + address_length + size_length]), + address_and_length_format_identifier, + ) + + +class ReadMemoryByAddress(UDSService, service_id=UDSIsoServices.ReadMemoryByAddress): + Request = ReadMemoryByAddressRequest + Response = ReadMemoryByAddressResponse + + +# *********************************** +# * Read scaling data by identifier * +# *********************************** + + +# ************************************ +# * Read data by periodic identifier * +# ************************************ + + +# ************************************** +# * Dynamically define data identifier * +# ************************************** + + +T_DynamicallyDefineDataIdentifierResponse = TypeVar( + "T_DynamicallyDefineDataIdentifierResponse", bound="_DynamicallyDefineDataIdentifierResponse" +) + + +class _DynamicallyDefineDataIdentifierResponse( + SpecializedSubFunctionResponse, + ABC, + minimal_length=2, + maximal_length=4, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=0, +): + def __init__(self, dynamically_defined_data_identifier: int | None = None): + super().__init__() + + if dynamically_defined_data_identifier is not None: + check_data_identifier(dynamically_defined_data_identifier) + + self.dynamically_defined_data_identifier = dynamically_defined_data_identifier + + @property + def pdu(self) -> bytes: + if self.dynamically_defined_data_identifier is None: + return pack("!BB", self.RESPONSE_SERVICE_ID, self.SUB_FUNCTION_ID) + else: + return pack( + "!BBH", + self.RESPONSE_SERVICE_ID, + self.SUB_FUNCTION_ID, + self.dynamically_defined_data_identifier, + ) + + @classmethod + def _from_pdu( + cls: type[T_DynamicallyDefineDataIdentifierResponse], pdu: bytes + ) -> T_DynamicallyDefineDataIdentifierResponse: + dynamically_defined_data_identifier: int | None = None + + if len(pdu) > 2: + dynamically_defined_data_identifier = from_bytes(pdu[2:]) + + return cls(dynamically_defined_data_identifier) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, _DynamicallyDefineDataIdentifierRequest) + and self.sub_function == request.sub_function + ) + + +class _DynamicallyDefineDataIdentifierRequest( + SpecializedSubFunctionRequest, + ABC, + minimal_length=2, + maximal_length=None, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=0, + response_type=_DynamicallyDefineDataIdentifierResponse, +): + def __init__( + self, dynamically_defined_data_identifier: int | None, suppress_response: bool = False + ): + super().__init__(suppress_response) + + if dynamically_defined_data_identifier is not None: + check_data_identifier(dynamically_defined_data_identifier) + + self.dynamically_defined_data_identifier = dynamically_defined_data_identifier + + +class DefineByIdentifierResponse( + _DynamicallyDefineDataIdentifierResponse, + minimal_length=4, + maximal_length=4, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByIdentifier, +): + def __init__(self, dynamically_defined_data_identifier: int): + super().__init__(dynamically_defined_data_identifier) + + +class DefineByIdentifierRequest( + _DynamicallyDefineDataIdentifierRequest, + minimal_length=8, + maximal_length=None, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByIdentifier, + response_type=DefineByIdentifierResponse, +): + def __init__( + self, + dynamically_defined_data_identifier: int, + source_data_identifiers: int | Sequence[int], + positions_in_source_data_record: int | Sequence[int], + memory_sizes: int | Sequence[int], + suppress_response: bool = False, + ): + """Defines a data identifier which combines data from multiple existing data identifiers on the UDS server. + This is an implementation of the UDS request for the defineByIdentifier sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + + :param dynamically_defined_data_identifier: The new data identifier. + :param source_data_identifiers: The source data identifiers which refer to the data to be included in the new data identifier. + :param positions_in_source_data_record: The start positions for each source data identifier. Note, that the position is 1-indexed. + :param memory_sizes: The number of bytes for each source data identifier, starting from the starting position. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dynamically_defined_data_identifier, suppress_response) + + if not isinstance(source_data_identifiers, int): + self.source_data_identifiers = list(source_data_identifiers) + else: + self.source_data_identifiers = [source_data_identifiers] + + if not isinstance(positions_in_source_data_record, int): + self.positions_in_source_data_record = list(positions_in_source_data_record) + else: + self.positions_in_source_data_record = [positions_in_source_data_record] + + if not isinstance(memory_sizes, int): + self.memory_sizes = list(memory_sizes) + else: + self.memory_sizes = [memory_sizes] + + if len(self.source_data_identifiers) != len(self.positions_in_source_data_record): + raise ValueError( + f"The number of source data identifiers does not match the number of " + f"positions in source data records: " + f"{len(self.source_data_identifiers)} != {len(self.positions_in_source_data_record)}" + ) + + if len(self.source_data_identifiers) != len(self.memory_sizes): + raise ValueError( + f"The number of source data identifiers does not match the number of " + f"memory sizes: " + f"{len(self.source_data_identifiers)} != {len(self.memory_sizes)}" + ) + + for identifier in self.source_data_identifiers: + check_data_identifier(identifier) + + @property + def source_data_identifier(self) -> int: + return self.source_data_identifiers[0] + + @source_data_identifier.setter + def source_data_identifier(self, source_data_identifier: int) -> None: + self.source_data_identifiers[0] = source_data_identifier + + @property + def position_in_source_data_record(self) -> int: + return self.positions_in_source_data_record[0] + + @position_in_source_data_record.setter + def position_in_source_data_record(self, position_in_source_data_record: int) -> None: + self.positions_in_source_data_record[0] = position_in_source_data_record + + @property + def memory_size(self) -> int: + return self.memory_sizes[0] + + @memory_size.setter + def memory_size(self, memory_size: int) -> None: + self.memory_sizes[0] = memory_size + + @property + def pdu(self) -> bytes: + pdu = pack( + "!BBH", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.dynamically_defined_data_identifier, + ) + + for source_data_identifier, position_in_source_data_record, memory_size in zip( + self.source_data_identifiers, self.positions_in_source_data_record, self.memory_sizes + ): + pdu = ( + pdu + + to_bytes(source_data_identifier, 2) + + to_bytes(position_in_source_data_record, 1) + + to_bytes(memory_size, 1) + ) + + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> DefineByIdentifierRequest: + dynamically_defined_data_identifier = from_bytes(pdu[2:4]) + source_data_identifiers: list[int] = [] + positions_in_source_data_record: list[int] = [] + memory_sizes: list[int] = [] + + if len(pdu) % 4 != 0: + raise ValueError("The format of the PDU does not comply to the standard") + + for i in range(4, len(pdu), 4): + source_data_identifiers.append(from_bytes(pdu[i : i + 2])) + positions_in_source_data_record.append(pdu[i + 2]) + memory_sizes.append(pdu[i + 3]) + + return DefineByIdentifierRequest( + dynamically_defined_data_identifier, + source_data_identifiers, + positions_in_source_data_record, + memory_sizes, + cls.suppress_response_set(pdu), + ) + + +class DefineByMemoryAddressResponse( + _DynamicallyDefineDataIdentifierResponse, + minimal_length=4, + maximal_length=4, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByMemoryAddress, +): + def __init__(self, dynamically_defined_data_identifier: int): + super().__init__(dynamically_defined_data_identifier) + + +class DefineByMemoryAddressRequest( + _DynamicallyDefineDataIdentifierRequest, + minimal_length=7, + maximal_length=None, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByMemoryAddress, + response_type=DefineByMemoryAddressResponse, +): + def __init__( + self, + dynamically_defined_data_identifier: int, + memory_addresses: int | Sequence[int], + memory_sizes: int | Sequence[int], + address_and_length_format_identifier: int | None = None, + suppress_response: bool = False, + ): + """Defines a data identifier which combines data from multiple existing memory regions on the UDS server. + This is an implementation of the UDS request for the defineByMemoryAddress sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param dynamically_defined_data_identifier: The new data identifier. + :param memory_addresses: The memory addresses for each source data. + :param memory_sizes: The number of bytes for each source data, starting from the memory address. + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + or data_record parameters. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dynamically_defined_data_identifier, suppress_response) + + if not isinstance(memory_addresses, int): + self.memory_addresses = list(memory_addresses) + else: + self.memory_addresses = [memory_addresses] + + if not isinstance(memory_sizes, int): + self.memory_sizes = list(memory_sizes) + else: + self.memory_sizes = [memory_sizes] + + if len(self.memory_addresses) != len(self.memory_sizes): + raise ValueError( + f"The number of memory addresses does not match the number of " + f"memory sizes: " + f"{len(self.memory_addresses)} != {len(self.memory_sizes)}" + ) + + max_computed_address_length = 0 + max_computed_size_length = 0 + + # In case the address_and_length_format_identifier is None, this calculates it based on the longest address and size + # Otherwise it checks for all addresses and size if they can be represented with its length constraints. + for address, size in zip(self.memory_addresses, self.memory_sizes): + computed_address_and_length_format_identifier, _, _ = uds_memory_parameters( + address, size, address_and_length_format_identifier + ) + + computed_address_length, computed_size_length = address_and_size_length( + computed_address_and_length_format_identifier + ) + max_computed_address_length = max(computed_address_length, max_computed_address_length) + max_computed_size_length = max(computed_size_length, max_computed_size_length) + + if address_and_length_format_identifier is None: + address_and_length_format_identifier = address_and_length_fmt( + max_computed_address_length, max_computed_size_length + ) + + self.address_and_length_format_identifier = address_and_length_format_identifier + + @property + def memory_address(self) -> int: + return self.memory_addresses[0] + + @memory_address.setter + def memory_address(self, memory_address: int) -> None: + self.memory_addresses[0] = memory_address + + @property + def memory_size(self) -> int: + return self.memory_sizes[0] + + @memory_size.setter + def memory_size(self, memory_size: int) -> None: + self.memory_sizes[0] = memory_size + + @property + def pdu(self) -> bytes: + pdu = pack( + "!BBHB", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.dynamically_defined_data_identifier, + self.address_and_length_format_identifier, + ) + + for memory_address, memory_size in zip(self.memory_addresses, self.memory_sizes): + _, address, size = uds_memory_parameters( + memory_address, memory_size, self.address_and_length_format_identifier + ) + pdu = pdu + address + size + + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> DefineByMemoryAddressRequest: + dynamically_defined_data_identifier = from_bytes(pdu[2:4]) + address_and_length_format_identifier = pdu[4] + address_length, size_length = address_and_size_length(address_and_length_format_identifier) + memory_addresses: list[int] = [] + memory_sizes: list[int] = [] + + if (len(pdu) - 5) % (address_length + size_length) != 0: + raise ValueError("The format of the PDU does not comply to the standard") + + for i in range(5, len(pdu), address_length + size_length): + memory_addresses.append(from_bytes(pdu[i : i + address_length])) + memory_sizes.append( + from_bytes(pdu[i + address_length : i + address_length + size_length]) + ) + + return DefineByMemoryAddressRequest( + dynamically_defined_data_identifier, + memory_addresses, + memory_sizes, + address_and_length_format_identifier, + cls.suppress_response_set(pdu), + ) + + +class ClearDynamicallyDefinedDataIdentifierResponse( + _DynamicallyDefineDataIdentifierResponse, + minimal_length=2, + maximal_length=4, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.clearDynamicallyDefinedDataIdentifier, +): + pass + + +class ClearDynamicallyDefinedDataIdentifierRequest( + _DynamicallyDefineDataIdentifierRequest, + minimal_length=2, + maximal_length=4, + service_id=UDSIsoServices.DynamicallyDefineDataIdentifier, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.clearDynamicallyDefinedDataIdentifier, + response_type=ClearDynamicallyDefinedDataIdentifierResponse, +): + def __init__( + self, dynamically_defined_data_identifier: int | None, suppress_response: bool = False + ): + """Clears either a specific dynamically defined data identifier or all if no data identifier is given. + This is an implementation of the UDS request for the clearDynamicallyDefinedDataIdentifier sub-function of the + service DynamicallyDefineDataIdentifier (0x2C). + + :param dynamically_defined_data_identifier: The dynamically defined data identifier to be cleared, or None if all are to be cleared. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dynamically_defined_data_identifier, suppress_response) + + @property + def pdu(self) -> bytes: + if self.dynamically_defined_data_identifier is None: + return pack( + "!BBH", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.dynamically_defined_data_identifier, + ) + else: + return pack( + "!BB", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ClearDynamicallyDefinedDataIdentifierRequest: + dynamically_defined_data_identifier: int | None = None + + if len(pdu) > 2: + dynamically_defined_data_identifier = from_bytes(pdu[2:]) + + return cls(dynamically_defined_data_identifier) + + +class DynamicallyDefineDataIdentifier( + SpecializedSubFunctionService, service_id=UDSIsoServices.DynamicallyDefineDataIdentifier +): + class DefineByIdentifier( + SubFunction, sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByIdentifier + ): + Request = DefineByIdentifierRequest + Response = DefineByIdentifierResponse + + class DefineByMemoryAddress( + SubFunction, sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.defineByMemoryAddress + ): + Request = DefineByMemoryAddressRequest + Response = DefineByMemoryAddressResponse + + class ClearDynamicallyDefinedDataIdentifier( + SubFunction, + sub_function_id=DynamicallyDefineDataIdentifierSubFuncs.clearDynamicallyDefinedDataIdentifier, + ): + Request = ClearDynamicallyDefinedDataIdentifierRequest + Response = ClearDynamicallyDefinedDataIdentifierResponse + + +# ****************************** +# * Write memory by identifier * +# ****************************** + + +class WriteDataByIdentifierResponse( + PositiveResponse, + minimal_length=3, + maximal_length=3, + service_id=UDSIsoServices.WriteDataByIdentifier, +): + def __init__(self, data_identifier: int) -> None: + super().__init__() + + check_data_identifier(data_identifier) + + self.data_identifier = data_identifier + + @property + def pdu(self) -> bytes: + return pack("!BH", self.RESPONSE_SERVICE_ID, self.data_identifier) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> WriteDataByIdentifierResponse: + data_identifier = from_bytes(pdu[1:3]) + return WriteDataByIdentifierResponse(data_identifier) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, WriteDataByIdentifierRequest) + and request.data_identifier == self.data_identifier + ) + + +class WriteDataByIdentifierRequest( + UDSRequest, + service_id=UDSIsoServices.WriteDataByIdentifier, + response_type=WriteDataByIdentifierResponse, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, data_record: bytes) -> None: + """Writes data which is identified by a specific dataIdentifier. + This is an implementation of the UDS request for service WriteDataByIdentifier (0x2E). + + :param data_identifier: The identifier. A dataIdentifier is a max two bytes integer. + :param data_record: The data to be written. + """ + check_data_identifier(data_identifier) + + if len(data_record) < 1: + raise ValueError("The dataRecord must not be empty") + + self.data_identifier = data_identifier + self.data_record = data_record + + @property + def pdu(self) -> bytes: + return pack("!BH", self.SERVICE_ID, self.data_identifier) + self.data_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> WriteDataByIdentifierRequest: + data_identifier = from_bytes(pdu[1:3]) + data_record = pdu[3:] + return WriteDataByIdentifierRequest(data_identifier, data_record) + + +class WriteDataByIdentifier(UDSService, service_id=UDSIsoServices.WriteDataByIdentifier): + Response = WriteDataByIdentifierResponse + Request = WriteDataByIdentifierRequest + + +# *************************** +# * Write memory by address * +# *************************** + + +class WriteMemoryByAddressResponse( + PositiveResponse, + service_id=UDSIsoServices.WriteMemoryByAddress, + minimal_length=4, + maximal_length=32, +): + @property + def pdu(self) -> bytes: + _, address_bytes, size_bytes = uds_memory_parameters( + self.memory_address, + self.memory_size, + self.address_and_length_format_identifier, + ) + + pdu = pack("!BB", self.RESPONSE_SERVICE_ID, self.address_and_length_format_identifier) + pdu += address_bytes + size_bytes + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> WriteMemoryByAddressResponse: + address_and_length_format_identifier = pdu[1] + addr_len, size_len = address_and_size_length(address_and_length_format_identifier) + + if len(pdu) < 2 + addr_len + size_len: + raise ValueError( + "The PDU is smaller as specified by the addressAndLengthFormatIdentifier" + ) + + memory_address = from_bytes(pdu[2 : 2 + addr_len]) + memory_size = from_bytes(pdu[2 + addr_len : 2 + addr_len + size_len]) + + return WriteMemoryByAddressResponse( + memory_address, memory_size, address_and_length_format_identifier + ) + + def __init__( + self, + memory_address: int, + memory_size: int, + address_and_length_format_identifier: int | None = None, + ) -> None: + super().__init__() + + self.memory_address = memory_address + self.memory_size = memory_size + + if address_and_length_format_identifier is None: + address_and_length_format_identifier, _, _ = uds_memory_parameters( + memory_address, memory_size, address_and_length_format_identifier + ) + + self.address_and_length_format_identifier = address_and_length_format_identifier + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, WriteMemoryByAddressRequest) + and self.address_and_length_format_identifier + == request.address_and_length_format_identifier + and self.memory_address == request.memory_address + and self.memory_size == request.memory_size + ) + + +class WriteMemoryByAddressRequest( + UDSRequest, + service_id=UDSIsoServices.WriteMemoryByAddress, + response_type=WriteMemoryByAddressResponse, + minimal_length=5, + maximal_length=None, +): + def __init__( + self, + memory_address: int, + data_record: bytes, + memory_size: int | None = None, + address_and_length_format_identifier: int | None = None, + ) -> None: + """Writes data to a specific memory on the UDS server. + This is an implementation of the UDS request for service writeMemoryByAddress (0x3d). + While it exposes each parameter of the corresponding specification, + some parameters can be computed from the remaining ones and can therefore be omitted. + + :param memory_address: The start address. + :param data_record: The data to be written. + :param memory_size: The number of bytes to write. + If omitted, the byte length of the data is used. + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + or data_record parameters. + """ + + self.memory_address = memory_address + self.data_record = data_record + + # If the size is given explicitly, use it as is, otherwise take the size of the data + if memory_size is None: + memory_size = len(data_record) + + self.memory_size = memory_size + + if address_and_length_format_identifier is None: + address_and_length_format_identifier, _, _ = uds_memory_parameters( + memory_address, memory_size, address_and_length_format_identifier + ) + + self.address_and_length_format_identifier = address_and_length_format_identifier + + @property + def pdu(self) -> bytes: + _, address_bytes, size_bytes = uds_memory_parameters( + self.memory_address, + self.memory_size, + self.address_and_length_format_identifier, + ) + + pdu = pack("!BB", self.SERVICE_ID, self.address_and_length_format_identifier) + pdu += address_bytes + size_bytes + self.data_record + return pdu + + @classmethod + def _from_pdu(cls, pdu: bytes) -> WriteMemoryByAddressRequest: + address_and_length_format_identifier = pdu[1] + address_length, size_length = address_and_size_length(address_and_length_format_identifier) + + if len(pdu) < 2 + address_length + size_length: + raise ValueError("The addressAndLengthIdentifier is incompatible with the PDU size") + + return WriteMemoryByAddressRequest( + from_bytes(pdu[2 : 2 + address_length]), + pdu[2 + address_length + size_length :], + from_bytes(pdu[2 + address_length : 2 + address_length + size_length]), + address_and_length_format_identifier, + ) + + +class WriteMemoryByAddress(UDSService, service_id=UDSIsoServices.WriteMemoryByAddress): + Request = WriteMemoryByAddressRequest + Response = WriteMemoryByAddressResponse + + +# ******************************** +# * Clear diagnostic information * +# ******************************** + + +class ClearDiagnosticInformationResponse( + PositiveResponse, + minimal_length=1, + maximal_length=1, + service_id=UDSIsoServices.ClearDiagnosticInformation, +): + @property + def pdu(self) -> bytes: + assert self.RESPONSE_SERVICE_ID is not None + return bytes([self.RESPONSE_SERVICE_ID]) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ClearDiagnosticInformationResponse: + return ClearDiagnosticInformationResponse() + + def matches(self, request: UDSRequest) -> bool: + return isinstance(request, ClearDiagnosticInformationRequest) + + +class ClearDiagnosticInformationRequest( + UDSRequest, + minimal_length=4, + maximal_length=4, + service_id=UDSIsoServices.ClearDiagnosticInformation, + response_type=ClearDiagnosticInformationResponse, +): + def __init__(self, group_of_dtc: int) -> None: + """Clears diagnostic trouble codes according to a given mask. + This is an implementation of the UDS request for service clearDiagnosticInformation (0x14). + + :param group_of_dtc: The three byte mask, which determines the DTCs to be cleared. + """ + check_range(group_of_dtc, "groupOfDTC", 0, 0xFFFFFF) + + self.group_of_dtc = group_of_dtc + + @property + def pdu(self) -> bytes: + assert self.SERVICE_ID is not None + return bytes([self.SERVICE_ID]) + to_bytes(self.group_of_dtc, 3) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ClearDiagnosticInformationRequest: + group_of_dtc = from_bytes(pdu[1:]) + return ClearDiagnosticInformationRequest(group_of_dtc) + + +class ClearDiagnosticInformation(UDSService, service_id=UDSIsoServices.ClearDiagnosticInformation): + Response = ClearDiagnosticInformationResponse + Request = ClearDiagnosticInformationRequest + + +# ************************ +# * Read DTC information * +# ************************ + + +class _ReadDTCResponse( + SpecializedSubFunctionResponse, + ABC, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=None, + maximal_length=None, +): + def matches(self, request: UDSRequest) -> bool: + return isinstance(request, _ReadDTCRequest) and self.sub_function == request.sub_function + + +class _ReadDTCRequest( + SpecializedSubFunctionRequest, + ABC, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=None, + maximal_length=None, + response_type=_ReadDTCResponse, +): + pass + + +T_ReadDTCType0Response = TypeVar("T_ReadDTCType0Response", bound="_ReadDTCType0Response") + + +class _ReadDTCType0Response( + _ReadDTCResponse, + ABC, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=6, + maximal_length=6, +): + def __init__( + self, + dtc_status_availability_mask: int, + dtc_format_identifier: DTCFormatIdentifier, + dtc_count: int, + ) -> None: + super().__init__() + + check_range(dtc_status_availability_mask, "DTCStatusAvailabilityMask", 0, 0xFF) + check_range(dtc_count, "DTCCount", 0, 0xFFFF) + + self.dtc_status_availability_mask = dtc_status_availability_mask + self.dtc_format_identifier = dtc_format_identifier + self.dtc_count = dtc_count + + @property + def pdu(self) -> bytes: + return pack( + "!BBBBH", + self.RESPONSE_SERVICE_ID, + self.sub_function, + self.dtc_status_availability_mask, + self.dtc_format_identifier, + self.dtc_count, + ) + + @classmethod + def _from_pdu(cls: type[T_ReadDTCType0Response], pdu: bytes) -> T_ReadDTCType0Response: + dtc_status_availability_mask = pdu[2] + dtc_format_identifier = DTCFormatIdentifier(pdu[3]) + dtc_count = from_bytes(pdu[4:]) + return cls(dtc_status_availability_mask, dtc_format_identifier, dtc_count) + + +T_ReadDTCType1Response = TypeVar("T_ReadDTCType1Response", bound="_ReadDTCType1Response") + + +class _ReadDTCType1Response( + _ReadDTCResponse, + ABC, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=3, + maximal_length=None, +): + def __init__( + self, + dtc_status_availability_mask: int, + dtc_and_status_record: bytes | dict[int, int], + ) -> None: + super().__init__() + + check_range(dtc_status_availability_mask, "DTCStatusAvailabilityMask", 0, 0xFF) + + if isinstance(dtc_and_status_record, bytes): + if len(dtc_and_status_record) % 4 != 0: + raise ValueError("Not a valid dtc_and_status_record") + + self.dtc_and_status_record = { + from_bytes(dtc_and_status_record[i : i + 3]): dtc_and_status_record[i + 3] + for i in range(0, len(dtc_and_status_record), 4) + } + else: + for dtc, status in dtc_and_status_record.items(): + check_range(dtc, "DTC", 0, 0xFFFFFF) + check_range(status, "DTC Status", 0, 0xFF) + + self.dtc_and_status_record = dtc_and_status_record + + self.dtc_status_availability_mask = dtc_status_availability_mask + + def dtc_and_status_record_bytes(self) -> bytes: + return bytes( + bytearray().join( + bytearray(to_bytes(dtc, 3)) + to_bytes(status, 1) + for dtc, status in self.dtc_and_status_record.items() + ) + ) + + @property + def pdu(self) -> bytes: + return ( + pack( + "!BBB", + self.RESPONSE_SERVICE_ID, + self.sub_function, + self.dtc_status_availability_mask, + ) + + self.dtc_and_status_record_bytes() + ) + + @classmethod + def _from_pdu(cls: type[T_ReadDTCType1Response], pdu: bytes) -> T_ReadDTCType1Response: + dtc_status_availability_mask = pdu[2] + dtc_and_status_record = pdu[3:] + return cls(dtc_status_availability_mask, dtc_and_status_record) + + +T_ReadDTCType0Request = TypeVar("T_ReadDTCType0Request", bound="_ReadDTCType0Request") + + +class _ReadDTCType0Request( + _ReadDTCRequest, + ABC, + response_type=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=3, + maximal_length=3, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + super().__init__(suppress_response) + + check_range(dtc_status_mask, "DTCStatusMask", 0, 0xFF) + + self.dtc_status_mask = dtc_status_mask + + @property + def pdu(self) -> bytes: + return pack( + "!BBB", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.dtc_status_mask, + ) + + @classmethod + def _from_pdu(cls: type[T_ReadDTCType0Request], pdu: bytes) -> T_ReadDTCType0Request: + dtc_status_mask = pdu[2] + return cls(dtc_status_mask, cls.suppress_response_set(pdu)) + + +T_ReadDTCType6Request = TypeVar("T_ReadDTCType6Request", bound="_ReadDTCType6Request") + + +class _ReadDTCType6Request( + _ReadDTCRequest, + ABC, + response_type=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=0, + minimal_length=2, + maximal_length=2, +): + def __init__(self, suppress_response: bool = False) -> None: + super().__init__(suppress_response) + + @property + def pdu(self) -> bytes: + return pack("!BBB", self.SERVICE_ID, self.sub_function_with_suppress_response_bit) + + @classmethod + def _from_pdu(cls: type[T_ReadDTCType6Request], pdu: bytes) -> T_ReadDTCType6Request: + return cls(cls.suppress_response_set(pdu)) + + +class ReportNumberOfDTCByStatusMaskResponse( + _ReadDTCType0Response, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfDTCByStatusMask, + minimal_length=6, + maximal_length=6, +): + pass + + +class ReportNumberOfDTCByStatusMaskRequest( + _ReadDTCType0Request, + minimal_length=3, + maximal_length=3, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfDTCByStatusMask, + response_type=ReportNumberOfDTCByStatusMaskResponse, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the number of DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the reportNumberOfDTCByStatusMask + sub-function of the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportDTCByStatusMaskResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportDTCByStatusMask, +): + pass + + +class ReportDTCByStatusMaskRequest( + _ReadDTCType0Request, + minimal_length=3, + maximal_length=3, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportDTCByStatusMask, + response_type=ReportDTCByStatusMaskResponse, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read DTCs and their state from the UDS server. + This is an implementation of the UDS request for the reportDTCByStatusMask sub-function of + the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportMirrorMemoryDTCByStatusMaskResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMirrorMemoryDTCByStatusMask, +): + pass + + +class ReportMirrorMemoryDTCByStatusMaskRequest( + _ReadDTCType0Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMirrorMemoryDTCByStatusMask, + response_type=ReportMirrorMemoryDTCByStatusMaskResponse, + minimal_length=3, + maximal_length=3, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read DTCs and their state from the UDS server's mirror memory. + This is an implementation of the UDS request for the reportMirrorMemoryDTCByStatusMask + sub-function of the service ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportNumberOfMirrorMemoryDTCByStatusMaskResponse( + _ReadDTCType0Response, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfMirrorMemoryDTCByStatusMask, + minimal_length=6, + maximal_length=6, +): + pass + + +class ReportNumberOfMirrorMemoryDTCByStatusMaskRequest( + _ReadDTCType0Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfMirrorMemoryDTCByStatusMask, + response_type=ReportNumberOfMirrorMemoryDTCByStatusMaskResponse, + minimal_length=3, + maximal_length=3, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the number of DTCs with the specified state from the UDS server's mirror memory. + This is an implementation of the UDS request for the + reportNumberOfMirrorMemoryDTCByStatusMask sub-function of the service ReadDTCInformation + (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskResponse( + _ReadDTCType0Response, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfEmissionsRelatedOBDDTCByStatusMask, + minimal_length=6, + maximal_length=6, +): + pass + + +class ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskRequest( + _ReadDTCType0Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfEmissionsRelatedOBDDTCByStatusMask, + response_type=ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskResponse, + minimal_length=3, + maximal_length=3, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the number of emission related DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the + reportNumberOfEmissionsRelatedOBDDTCByStatusMask sub-function of the service + ReadDTCInformation (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportEmissionsRelatedOBDDTCByStatusMaskResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportEmissionsRelatedOBDDTCByStatusMask, +): + pass + + +class ReportEmissionsRelatedOBDDTCByStatusMaskRequest( + _ReadDTCType0Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportEmissionsRelatedOBDDTCByStatusMask, + response_type=ReportEmissionsRelatedOBDDTCByStatusMaskResponse, + minimal_length=3, + maximal_length=3, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the number of emission related DTCs with the specified state from the UDS server. + This is an implementation of the UDS request for the + reportEmissionsRelatedOBDDTCByStatusMask sub-function of the service ReadDTCInformation + (0x19). + + :param dtc_status_mask: Used to select a portion of the DTCs based on their state. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(dtc_status_mask, suppress_response) + + +class ReportSupportedDTCResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportSupportedDTC, +): + pass + + +class ReportSupportedDTCRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportSupportedDTC, + response_type=ReportSupportedDTCResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the supported DTCs from the UDS server. + This is an implementation of the UDS request for the + reportSupportedDTC sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReportFirstTestFailedDTCResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=7, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportFirstTestFailedDTC, +): + pass + + +class ReportFirstTestFailedDTCRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportFirstTestFailedDTC, + response_type=ReportFirstTestFailedDTCResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the first failed DTC since last clearance from the UDS server. + This is an implementation of the UDS request for the + reportFirstTestFailedDTC sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReportFirstConfirmedDTCResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=7, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportFirstConfirmedDTC, +): + pass + + +class ReportFirstConfirmedDTCRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportFirstConfirmedDTC, + response_type=ReportFirstConfirmedDTCResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the first confirmed DTC since last clearance from the UDS server. + This is an implementation of the UDS request for the + reportFirstConfirmedDTC sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReportMostRecentTestFailedDTCResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=7, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentTestFailedDTC, +): + pass + + +class ReportMostRecentFirstTestFailedDTCRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentTestFailedDTC, + response_type=ReportMostRecentTestFailedDTCResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the most recent failed DTC since last clearance from the UDS server. + This is an implementation of the UDS request for the + reportMostRecentTestFailedDTC sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReportMostrecentConfirmedDTCResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=7, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentConfirmedDTC, +): + pass + + +class ReportMostRecentConfirmedDTCRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentConfirmedDTC, + response_type=ReportMostrecentConfirmedDTCResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the most recent confirmed DTC since last clearance from the UDS server. + This is an implementation of the UDS request for the + reportMostRecentConfirmedDTC sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReportDTCWithPermanentStatusResponse( + _ReadDTCType1Response, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportDTCWithPermanentStatus, +): + pass + + +class ReportDTCWithPermanentStatusRequest( + _ReadDTCType6Request, + service_id=UDSIsoServices.ReadDTCInformation, + sub_function_id=ReadDTCInformationSubFuncs.reportDTCWithPermanentStatus, + response_type=ReportDTCWithPermanentStatusResponse, + minimal_length=2, + maximal_length=2, +): + def __init__(self, dtc_status_mask: int, suppress_response: bool = False) -> None: + """Read the DTCs with permanent status from the UDS server. + This is an implementation of the UDS request for the + reportDTCWithPermanentStatus sub-function of the service ReadDTCInformation (0x19). + + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(suppress_response) + + +class ReadDTCInformation( + SpecializedSubFunctionService, service_id=UDSIsoServices.ReadDTCInformation +): + class ReportNumberOfDTCByStatusMask( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfDTCByStatusMask, + ): + Response = ReportNumberOfDTCByStatusMaskResponse + Request = ReportNumberOfDTCByStatusMaskRequest + + class ReportDTCByStatusMask( + SubFunction, sub_function_id=ReadDTCInformationSubFuncs.reportDTCByStatusMask + ): + Response = ReportDTCByStatusMaskResponse + Request = ReportDTCByStatusMaskRequest + + class ReportMirrorMemoryDTCByStatusMask( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportMirrorMemoryDTCByStatusMask, + ): + Response = ReportMirrorMemoryDTCByStatusMaskResponse + Request = ReportMirrorMemoryDTCByStatusMaskRequest + + class ReportNumberOfMirrorMemoryDTCByStatusMask( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfMirrorMemoryDTCByStatusMask, + ): + Response = ReportNumberOfMirrorMemoryDTCByStatusMaskResponse + Request = ReportNumberOfMirrorMemoryDTCByStatusMaskRequest + + class ReportNumberOfEmissionsRelatedOBDDTCByStatusMask( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportNumberOfEmissionsRelatedOBDDTCByStatusMask, + ): + Response = ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskResponse + Request = ReportNumberOfEmissionsRelatedOBDDTCByStatusMaskRequest + + class ReportEmissionsRelatedOBDDTCByStatusMask( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportEmissionsRelatedOBDDTCByStatusMask, + ): + Response = ReportEmissionsRelatedOBDDTCByStatusMaskResponse + Request = ReportEmissionsRelatedOBDDTCByStatusMaskRequest + + class ReportSupportedDTC( + SubFunction, sub_function_id=ReadDTCInformationSubFuncs.reportSupportedDTC + ): + Response = ReportSupportedDTCResponse + Request = ReportSupportedDTCRequest + + class ReportFirstTestFailedDTC( + SubFunction, sub_function_id=ReadDTCInformationSubFuncs.reportFirstTestFailedDTC + ): + Response = ReportFirstTestFailedDTCResponse + Request = ReportFirstTestFailedDTCRequest + + class ReportFirstConfirmedDTC( + SubFunction, sub_function_id=ReadDTCInformationSubFuncs.reportFirstConfirmedDTC + ): + Response = ReportFirstConfirmedDTCResponse + Request = ReportFirstConfirmedDTCRequest + + class ReportMostRecentTestFailedDTC( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentTestFailedDTC, + ): + Response = ReportMostRecentTestFailedDTCResponse + Request = ReportMostRecentFirstTestFailedDTCRequest + + class ReportMostRecentConfirmedDTC( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportMostRecentConfirmedDTC, + ): + Response = ReportMostrecentConfirmedDTCResponse + Request = ReportMostRecentConfirmedDTCRequest + + class ReportDTCWithPermanentStatus( + SubFunction, + sub_function_id=ReadDTCInformationSubFuncs.reportDTCWithPermanentStatus, + ): + Response = ReportDTCWithPermanentStatusResponse + Request = ReportDTCWithPermanentStatusRequest + + +# ************************************** +# * Input output control by identifier * +# ************************************** + + +class InputOutputControlByIdentifierResponse( + PositiveResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_status_record: bytes) -> None: + super().__init__() + + check_data_identifier(data_identifier) + + if len(control_status_record) < 1: + raise ValueError("The controlStatusRecord must not be empty") + + self.data_identifier = data_identifier + self.control_status_record = control_status_record + + @property + def pdu(self) -> bytes: + return ( + pack("!BH", self.RESPONSE_SERVICE_ID, self.data_identifier) + self.control_status_record + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> InputOutputControlByIdentifierResponse: + data_identifier = from_bytes(pdu[1:3]) + control_status_record = pdu[3:] + return InputOutputControlByIdentifierResponse(data_identifier, control_status_record) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, InputOutputControlByIdentifierRequest) + and self.data_identifier == request.data_identifier + ) + + +class InputOutputControlByIdentifierRequest( + UDSRequest, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + response_type=InputOutputControlByIdentifierResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + data_identifier: int, + control_option_record: bytes, + control_enable_mask_record: bytes = b"", + ) -> None: + """Controls input or output values on the server. + This is an implementation of the UDS request for the service + InputOutputControlByIdentifier (0x2F). + This function exposes the parameters as in the corresponding specification, + hence is suitable for all variants of this service. + For the variants which use an inputOutputControlParameter as the first byte of the + controlOptionRecord, using the corresponding wrappers is recommended. + + :param data_identifier: The data identifier of the value(s) to be controlled. + :param control_option_record: The controlStates, which specify the intended values of the + input / output parameters, optionally prefixed with an + inputOutputControlParameter or only an + inputOutputControlParameter. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + """ + check_data_identifier(data_identifier) + + if len(control_option_record) < 1: + raise ValueError("The controlOptionRecord must not be empty") + + self.data_identifier = data_identifier + self.control_option_record = control_option_record + self.control_enable_mask_record = control_enable_mask_record + + @property + def pdu(self) -> bytes: + return ( + pack("!BH", self.SERVICE_ID, self.data_identifier) + + self.control_option_record + + self.control_enable_mask_record + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> InputOutputControlByIdentifierRequest: + # Because both the controlOptionRecord as well as the controlEnableMaskRecord are of + # variable size, and there is no field which describes those parameters, + # it is impossible for the server to determine those fields reliably without vendor or ECU + # specific knowledge. + # Therefore, similar to the implementation for ReadDataByIdentifier, + # the first variable parameter consumes all remaining data. + data_identifier = from_bytes(pdu[1:3]) + control_option_record = pdu[3:] + control_enable_mask_record = b"" + return InputOutputControlByIdentifierRequest( + data_identifier, control_option_record, control_enable_mask_record + ) + + +class ReturnControlToECUResponse( + InputOutputControlByIdentifierResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_states: bytes = b"") -> None: + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.returnControlToECU]) + control_states, + ) + + def matches(self, request: UDSRequest) -> bool: + return super().matches(request) and isinstance(request, ReturnControlToECURequest) + + +class ReturnControlToECURequest( + InputOutputControlByIdentifierRequest, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + response_type=ReturnControlToECUResponse, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_enable_mask_record: bytes = b"") -> None: + """Gives the control over input / output parameters back to the ECU. + This is a convenience wrapper of the generic request for the case where an + inputOutputControlParameter is used and is set to returnControlToECU. In that case no + further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) for which control should be + returned to the ECU. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + """ + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.returnControlToECU]), + control_enable_mask_record, + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ReturnControlToECURequest: + data_identifier = from_bytes(pdu[1:3]) + control_enable_mask_record = pdu[4:] + return ReturnControlToECURequest(data_identifier, control_enable_mask_record) + + +class ResetToDefaultResponse( + InputOutputControlByIdentifierResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_states: bytes = b"") -> None: + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.resetToDefault]) + control_states, + ) + + def matches(self, request: UDSRequest) -> bool: + return super().matches(request) and isinstance(request, ResetToDefaultRequest) + + +class ResetToDefaultRequest( + InputOutputControlByIdentifierRequest, + response_type=ResetToDefaultResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_enable_mask_record: bytes = b"") -> None: + """Sets the input / output parameters to the default value(s). + This is a convenience wrapper of the generic request for the case where an + inputOutputControlParameter is used and is set to resetToDefault. + In that case no further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) for which the values should be + reset. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + """ + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.resetToDefault]), + control_enable_mask_record, + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> ResetToDefaultRequest: + data_identifier = from_bytes(pdu[1:3]) + control_enable_mask_record = pdu[4:] + return ResetToDefaultRequest(data_identifier, control_enable_mask_record) + + +class FreezeCurrentStateResponse( + InputOutputControlByIdentifierResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_states: bytes = b"") -> None: + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.freezeCurrentState]) + control_states, + ) + + def matches(self, request: UDSRequest) -> bool: + return super().matches(request) and isinstance(request, FreezeCurrentStateResponse) + + +class FreezeCurrentStateRequest( + InputOutputControlByIdentifierRequest, + response_type=FreezeCurrentStateResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_enable_mask_record: bytes = b"") -> None: + """Freezes the input / output parameters at their current state. + This is a convenience wrapper of the generic request for the case where an + inputOutputControlParameter is used and is set to freezeCurrentState. + In that case no further controlState parameters can be submitted. + + :param data_identifier: The data identifier of the value(s) to be frozen. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + """ + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.freezeCurrentState]), + control_enable_mask_record, + ) + + @classmethod + def _from_pdu(cls, pdu: bytes) -> FreezeCurrentStateRequest: + data_identifier = from_bytes(pdu[1:3]) + control_enable_mask_record = pdu[4:] + return FreezeCurrentStateRequest(data_identifier, control_enable_mask_record) + + +class ShortTermAdjustmentResponse( + InputOutputControlByIdentifierResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=4, + maximal_length=None, +): + def __init__(self, data_identifier: int, control_states: bytes = b"") -> None: + super().__init__( + data_identifier, + bytes([InputOutputControlParameter.shortTermAdjustment]) + control_states, + ) + + +class ShortTermAdjustmentRequest( + InputOutputControlByIdentifierRequest, + response_type=ShortTermAdjustmentResponse, + service_id=UDSIsoServices.InputOutputControlByIdentifier, + minimal_length=5, + maximal_length=None, +): + def __init__( + self, + data_identifier: int, + control_states: bytes, + control_enable_mask_record: bytes = b"", + ) -> None: + """Sets the input / output parameters as specified in the controlOptionRecord. + This is a convenience wrapper of the generic request for the case + where an inputOutputControlParameter is used and is set to freezeCurrentState. + In that case controlState parameters are required. + + :param data_identifier: The data identifier of the value(s) to be adjusted. + :param control_states: The controlStates, which specify the intended values of the input / + output parameters. + :param control_enable_mask_record: In cases where the dataIdentifier corresponds to multiple + input / output parameters, this mask specifies which ones + should be affected by this request. + """ + control_option_record = ( + bytes([InputOutputControlParameter.shortTermAdjustment]) + control_states + ) + super().__init__(data_identifier, control_option_record, control_enable_mask_record) + + +class InputOutputControlByIdentifier( + UDSService, service_id=UDSIsoServices.InputOutputControlByIdentifier +): + Response = InputOutputControlByIdentifierResponse + Request = InputOutputControlByIdentifierRequest + + class ReturnControlToECU: + Response = ReturnControlToECUResponse + Request = ReturnControlToECURequest + + class ResetToDefault: + Response = ResetToDefaultResponse + Request = ResetToDefaultRequest + + class FreezeCurrentState: + Response = FreezeCurrentStateResponse + Request = FreezeCurrentStateRequest + + class ShortTermAdjustment: + Response = ShortTermAdjustmentResponse + Request = ShortTermAdjustmentRequest + + +# ******************* +# * Routine control * +# ******************* + + +T_RoutineControlResponse = TypeVar("T_RoutineControlResponse", bound="RoutineControlResponse") + + +class RoutineControlResponse( + SpecializedSubFunctionResponse, + ABC, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=0, + minimal_length=4, + maximal_length=None, +): + @property + def pdu(self) -> bytes: + return ( + pack( + "!BBH", + self.RESPONSE_SERVICE_ID, + self.sub_function, + self.routine_identifier, + ) + + self.routine_status_record + ) + + @classmethod + def _from_pdu(cls: type[T_RoutineControlResponse], pdu: bytes) -> T_RoutineControlResponse: + routine_identifier = from_bytes(pdu[2:4]) + routine_status_record = pdu[4:] + + return cls(routine_identifier, routine_status_record) + + def __init__(self, routine_identifier: int, routine_status_record: bytes = b"") -> None: + super().__init__() + + self.routine_control_type = self.sub_function + self.routine_identifier = routine_identifier + self.routine_status_record = routine_status_record + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, RoutineControlRequest) + and self.routine_control_type == request.routine_control_type + and self.routine_identifier == request.routine_identifier + ) + + +T_RoutineControlRequest = TypeVar("T_RoutineControlRequest", bound="RoutineControlRequest") + + +class RoutineControlRequest( + SpecializedSubFunctionRequest, + ABC, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=0, + response_type=RoutineControlResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + super().__init__(suppress_response) + + check_range(routine_identifier, "routineIdentifier", 0, 0xFFFF) + self.routine_control_type = self.sub_function + self.routine_identifier = routine_identifier + self.routine_control_option_record = routine_control_option_record + + @property + def pdu(self) -> bytes: + return ( + pack( + "!BBH", + self.SERVICE_ID, + self.sub_function_with_suppress_response_bit, + self.routine_identifier, + ) + + self.routine_control_option_record + ) + + @classmethod + def _from_pdu(cls: type[T_RoutineControlRequest], pdu: bytes) -> T_RoutineControlRequest: + routine_identifier = from_bytes(pdu[2:4]) + routine_control_option_record = pdu[4:] + + return cls( + routine_identifier, + routine_control_option_record, + cls.suppress_response_set(pdu), + ) + + +class StartRoutineResponse( + RoutineControlResponse, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.startRoutine, + minimal_length=4, + maximal_length=None, +): + pass + + +class StartRoutineRequest( + RoutineControlRequest, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.startRoutine, + response_type=StartRoutineResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + """Starts a specific routine on the server. + This is an implementation of the UDS request for the startRoutine sub-function of the + service routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(routine_identifier, routine_control_option_record, suppress_response) + + +class StopRoutineResponse( + RoutineControlResponse, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.stopRoutine, + minimal_length=4, + maximal_length=None, +): + pass + + +class StopRoutineRequest( + RoutineControlRequest, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.stopRoutine, + response_type=StopRoutineResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + """Stops a specific routine on the server. + This is an implementation of the UDS request for the stopRoutine sub-function of the service + routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(routine_identifier, routine_control_option_record, suppress_response) + + +class RequestRoutineResultsResponse( + RoutineControlResponse, + minimal_length=4, + maximal_length=None, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.requestRoutineResults, +): + pass + + +class RequestRoutineResultsRequest( + RoutineControlRequest, + service_id=UDSIsoServices.RoutineControl, + sub_function_id=RoutineControlSubFuncs.requestRoutineResults, + response_type=RequestRoutineResultsResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( + self, + routine_identifier: int, + routine_control_option_record: bytes = b"", + suppress_response: bool = False, + ) -> None: + """Requests the results of a specific routine on the server. + This is an implementation of the UDS request for the requestRoutineResults sub-function of + the service routineControl (0x31). + + :param routine_identifier: The identifier of the routine. + :param routine_control_option_record: Optional data. + :param suppress_response: If set to True, the server is advised to not send back a positive + response. + """ + super().__init__(routine_identifier, routine_control_option_record, suppress_response) + + +class RoutineControl(SpecializedSubFunctionService, service_id=UDSIsoServices.RoutineControl): + class StartRoutine(SubFunction, sub_function_id=RoutineControlSubFuncs.startRoutine): + Request = StartRoutineRequest + Response = StartRoutineResponse + + class StopRoutine(SubFunction, sub_function_id=RoutineControlSubFuncs.stopRoutine): + Request = StopRoutineRequest + Response = StopRoutineResponse + + class RequestRoutineResults( + SubFunction, sub_function_id=RoutineControlSubFuncs.requestRoutineResults + ): + Request = RequestRoutineResultsRequest + Response = RequestRoutineResultsResponse + + +# ******************** +# * Request download * +# ******************** + + +T_RequestUpOrDownloadResponse = TypeVar( + "T_RequestUpOrDownloadResponse", bound="_RequestUpOrDownloadResponse" +) + + +class _RequestUpOrDownloadResponse( + PositiveResponse, service_id=None, minimal_length=3, maximal_length=None +): + def __init__( + self, + max_number_of_block_length: int, + length_format_identifier: int | None = None, + ) -> None: + super().__init__() + + if length_format_identifier is not None: + if not 0 <= length_format_identifier <= 0xF0 or length_format_identifier % 2**4 > 0: + raise ValueError( + f"Invalid value for lengthFormatIdentifier: {length_format_identifier}" + ) + + uds_memory_parameters(0, max_number_of_block_length, length_format_identifier + 1) + else: + length_format_identifier, _, _ = uds_memory_parameters(0, max_number_of_block_length) + + self.max_number_of_block_length = max_number_of_block_length + self.length_format_identifier = length_format_identifier - (length_format_identifier % 2**4) + + @property + def pdu(self) -> bytes: + max_number_of_block_length = to_bytes( + self.max_number_of_block_length, self.length_format_identifier // 2**4 + ) + return ( + pack("BB", self.RESPONSE_SERVICE_ID, self.length_format_identifier) + + max_number_of_block_length + ) + + @classmethod + def _from_pdu( + cls: type[T_RequestUpOrDownloadResponse], pdu: bytes + ) -> T_RequestUpOrDownloadResponse: + length_format_identifier = pdu[1] + max_number_of_block_length = from_bytes(pdu[2:]) + return cls(max_number_of_block_length, length_format_identifier) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, _RequestUpOrDownloadRequest) + and request.SERVICE_ID == self.SERVICE_ID + ) + + +T_RequestUpOrDownloadRequest = TypeVar( + "T_RequestUpOrDownloadRequest", bound="_RequestUpOrDownloadRequest" +) + + +class _RequestUpOrDownloadRequest( + UDSRequest, + service_id=None, + response_type=_RequestUpOrDownloadResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( # noqa: PLR0913 + self, + memory_address: int, + memory_size: int, + compression_method: int = 0x0, + encryption_method: int = 0x0, + address_and_length_format_identifier: int | None = None, + ) -> None: + check_range(compression_method, "compressionMethod", 0, 0xF) + check_range(encryption_method, "encryptionMethod", 0, 0xF) + + if address_and_length_format_identifier is not None: + check_range( + address_and_length_format_identifier, + "addressAndLengthFormatIdentifier", + 0, + 0xFF, + ) + + self.memory_address = memory_address + self.memory_size = memory_size + self.compression_method = compression_method + self.encryption_method = encryption_method + self.address_and_length_format_identifier, _, _ = uds_memory_parameters( + memory_address, memory_size, address_and_length_format_identifier + ) + + @property + def pdu(self) -> bytes: + data_format_identifier = (self.compression_method << 4) | self.encryption_method + + addr_and_len_format_id, address_bytes, size_bytes = uds_memory_parameters( + self.memory_address, + self.memory_size, + self.address_and_length_format_identifier, + ) + + pdu = struct.pack("!BBB", self.SERVICE_ID, data_format_identifier, addr_and_len_format_id) + pdu += address_bytes + size_bytes + return pdu + + @classmethod + def _from_pdu( + cls: type[T_RequestUpOrDownloadRequest], pdu: bytes + ) -> T_RequestUpOrDownloadRequest: + data_format_identifier = pdu[1] + address_and_length_format_identifier = pdu[2] + address_length, size_length = address_and_size_length(address_and_length_format_identifier) + + if len(pdu) != 3 + address_length + size_length: + raise ValueError("The addressAndLengthIdentifier is incompatible with the PDU size") + + return cls( + from_bytes(pdu[3 : 3 + address_length]), + from_bytes(pdu[3 + address_length : 3 + address_length + size_length]), + data_format_identifier // 2**4, + data_format_identifier % 2**4, + address_and_length_format_identifier, + ) + + +class RequestDownloadResponse( + _RequestUpOrDownloadResponse, + minimal_length=3, + maximal_length=None, + service_id=UDSIsoServices.RequestDownload, +): + pass + + +class RequestDownloadRequest( + _RequestUpOrDownloadRequest, + service_id=UDSIsoServices.RequestDownload, + response_type=RequestDownloadResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( # noqa: PLR0913 + self, + memory_address: int, + memory_size: int, + compression_method: int = 0x0, + encryption_method: int = 0x0, + address_and_length_format_identifier: int | None = None, + ) -> None: + """Requests the download of data, i.e. the possibility to send data from the client to the + server. + This is an implementation of the UDS request for requestDownload (0x34). + + :param memory_address: The address at which data should be downloaded. + :param memory_size: The number of bytes to be downloaded. + :param compression_method: Encodes the utilized compressionFormat (0x0 for none) + :param encryption_method: Encodes the utilized encryptionFormat (0x0 for none) + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and + memory_size parameters. + """ + super().__init__( + memory_address, + memory_size, + compression_method, + encryption_method, + address_and_length_format_identifier, + ) + + +class RequestDownload(UDSService, service_id=UDSIsoServices.RequestDownload): + Response = RequestDownloadResponse + Request = RequestDownloadRequest + + +# ****************** +# * Request Upload * +# ****************** + + +class RequestUploadResponse( + _RequestUpOrDownloadResponse, + service_id=UDSIsoServices.RequestUpload, + minimal_length=3, + maximal_length=None, +): + pass + + +class RequestUploadRequest( + _RequestUpOrDownloadRequest, + service_id=UDSIsoServices.RequestUpload, + response_type=RequestUploadResponse, + minimal_length=4, + maximal_length=None, +): + def __init__( # noqa: PLR0913 + self, + memory_address: int, + memory_size: int, + compression_method: int = 0x0, + encryption_method: int = 0x0, + address_and_length_format_identifier: int | None = None, + ) -> None: + """Requests the upload of data, i.e. the possibility to receive data from the server. + This is an implementation of the UDS request for requestUpload (0x35). + + :param memory_address: The address at which data should be uploaded. + :param memory_size: The number of bytes to be uploaded. + :param compression_method: Encodes the utilized compressionFormat (0x0 for none) + :param encryption_method: Encodes the utilized encryptionFormat (0x0 for none) + :param address_and_length_format_identifier: The byte lengths of the memory address and + size. If omitted, this parameter is computed + based on the memory_address and memory_size + parameters. + """ + super().__init__( + memory_address, + memory_size, + compression_method, + encryption_method, + address_and_length_format_identifier, + ) + + +class RequestUpload(UDSService, service_id=UDSIsoServices.RequestUpload): + Response = RequestUploadResponse + Request = RequestUploadRequest + + +# ***************** +# * Transfer data * +# ***************** + + +class TransferDataResponse( + PositiveResponse, + service_id=UDSIsoServices.TransferData, + minimal_length=2, + maximal_length=None, +): + def __init__( + self, + block_sequence_counter: int, + transfer_response_parameter_record: bytes = b"", + ) -> None: + super().__init__() + + check_range(block_sequence_counter, "blockSequenceCounter", 0, 0xFF) + + self.block_sequence_counter = block_sequence_counter + self.transfer_response_parameter_record = transfer_response_parameter_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> TransferDataResponse: + block_sequence_counter = pdu[1] + transfer_response_parameter_record = pdu[2:] + return TransferDataResponse(block_sequence_counter, transfer_response_parameter_record) + + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.RESPONSE_SERVICE_ID, self.block_sequence_counter) + + self.transfer_response_parameter_record + ) + + def matches(self, request: UDSRequest) -> bool: + return ( + isinstance(request, TransferDataRequest) + and self.block_sequence_counter == request.block_sequence_counter + ) + + +class TransferDataRequest( + UDSRequest, + service_id=UDSIsoServices.TransferData, + response_type=TransferDataResponse, + minimal_length=2, + maximal_length=None, +): + def __init__( + self, + block_sequence_counter: int, + transfer_request_parameter_record: bytes = b"", + ) -> None: + """Transfers data to the server or requests the next data from the server. + This is an implementation of the UDS request for transferData (0x36). + + :param block_sequence_counter: The current block sequence counter. + Initialized with one and incremented for each new data. + After 0xff, the counter is resumed at 0 + :param transfer_request_parameter_record: Contains the data to be transferred if downloading + to the server. + """ + check_range(block_sequence_counter, "blockSequenceCounter", 0, 0xFF) + + self.block_sequence_counter = block_sequence_counter + self.transfer_request_parameter_record = transfer_request_parameter_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> TransferDataRequest: + block_sequence_counter = pdu[1] + transfer_request_parameter_record = pdu[2:] + return TransferDataRequest(block_sequence_counter, transfer_request_parameter_record) + + @property + def pdu(self) -> bytes: + return ( + pack("!BB", self.SERVICE_ID, self.block_sequence_counter) + + self.transfer_request_parameter_record + ) + + +class TransferData(UDSService, service_id=UDSIsoServices.TransferData): + Response = TransferDataResponse + Request = TransferDataRequest + + +# ************************* +# * Request transfer exit * +# ************************* + + +class RequestTransferExitResponse( + PositiveResponse, + service_id=UDSIsoServices.RequestTransferExit, + minimal_length=1, + maximal_length=None, +): + def __init__(self, transfer_response_parameter_record: bytes = b"") -> None: + super().__init__() + + self.transfer_response_parameter_record = transfer_response_parameter_record + + @property + def pdu(self) -> bytes: + assert self.RESPONSE_SERVICE_ID is not None + return bytes([self.RESPONSE_SERVICE_ID]) + self.transfer_response_parameter_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> RequestTransferExitResponse: + transfer_response_parameter_record = pdu[1:] + return RequestTransferExitResponse(transfer_response_parameter_record) + + def matches(self, request: UDSRequest) -> bool: + return isinstance(request, RequestTransferExitRequest) + + +class RequestTransferExitRequest( + UDSRequest, + service_id=UDSIsoServices.RequestTransferExit, + response_type=RequestTransferExitResponse, + minimal_length=1, + maximal_length=None, +): + def __init__(self, transfer_request_parameter_record: bytes = b"") -> None: + """Ends the transfer of data. + This is an implementation of the UDS request for requestTransferExit (0x77). + + :param transfer_request_parameter_record: Optional data. + """ + self.transfer_request_parameter_record = transfer_request_parameter_record + + @property + def pdu(self) -> bytes: + assert self.SERVICE_ID is not None + return bytes([self.SERVICE_ID]) + self.transfer_request_parameter_record + + @classmethod + def _from_pdu(cls, pdu: bytes) -> RequestTransferExitRequest: + transfer_request_parameter_record = pdu[1:] + return RequestTransferExitRequest(transfer_request_parameter_record) + + +class RequestTransferExit(UDSService, service_id=UDSIsoServices.RequestTransferExit): + Response = RequestTransferExitResponse + Request = RequestTransferExitRequest + + +# ************************* +# * Request file transfer * +# ************************* +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/services/uds/ecu.html b/_modules/gallia/services/uds/ecu.html new file mode 100644 index 000000000..d4b3652bf --- /dev/null +++ b/_modules/gallia/services/uds/ecu.html @@ -0,0 +1,645 @@ + + + + + + + + gallia.services.uds.ecu — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.services.uds.ecu

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import asyncio
+from asyncio import Task
+from datetime import UTC, datetime
+from typing import TYPE_CHECKING, Any
+
+from gallia.db.log import LogMode
+from gallia.log import get_logger
+from gallia.power_supply import PowerSupply
+from gallia.services.uds.core import service
+from gallia.services.uds.core.client import UDSClient, UDSRequestConfig
+from gallia.services.uds.core.constants import DataIdentifier
+from gallia.services.uds.core.exception import (
+    ResponseException,
+    UDSException,
+    UnexpectedNegativeResponse,
+)
+from gallia.services.uds.core.utils import from_bytes, g_repr
+from gallia.services.uds.helpers import (
+    as_exception,
+    raise_for_error,
+    suggests_identifier_not_supported,
+)
+from gallia.transports.base import BaseTransport
+from gallia.utils import handle_task_error, set_task_handler_ctx_variable
+
+if TYPE_CHECKING:
+    from gallia.db.handler import DBHandler
+
+
+class ECUState:
+    def __init__(self) -> None:
+        self.session = 1
+        self.security_access_level: int | None = None
+
+    def reset(self) -> None:
+        self.session = 1
+        self.security_access_level = None
+
+    def __repr__(self) -> str:
+        return f"{type(self).__name__}({', '.join(f'{key}={g_repr(value)}' for key, value in self.__dict__.items())})"
+
+
+logger = get_logger(__name__)
+
+
+
+[docs] +class ECU(UDSClient): + """ECU is a high level interface wrapping a UDSClient class. It provides + semantically correct higher level interfaces such as read_session() + or ping(). Vendor specific implementations can be derived from this + class. For the arguments of the constructor, please check uds.uds.UDS. + """ + + OEM = "default" + + def __init__( + self, + transport: BaseTransport, + timeout: float, + max_retry: int = 0, + power_supply: PowerSupply | None = None, + ) -> None: + super().__init__(transport, timeout, max_retry) + self.tester_present_task: Task[None] | None = None + self.tester_present_interval: float | None = None + self.power_supply = power_supply + self.state = ECUState() + self.db_handler: DBHandler | None = None + self.implicit_logging = True + + async def properties( + self, fresh: bool = False, config: UDSRequestConfig | None = None + ) -> dict[str, Any]: + return {} + +
+[docs] + async def ping( + self, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.TesterPresentResponse: + """Send an UDS TesterPresent message. + + Returns: + UDS response. + """ + return await self.tester_present(suppress_response=False, config=config)
+ + +
+[docs] + async def read_session(self, config: UDSRequestConfig | None = None) -> int: + """Read out current session. + + Returns: + The current session as int. + """ + resp = await self.read_data_by_identifier( + DataIdentifier.ActiveDiagnosticSessionDataIdentifier, config=config + ) + if isinstance(resp, service.NegativeResponse): + raise as_exception(resp) + return from_bytes(resp.data_record)
+ + +
+[docs] + async def set_session_pre(self, level: int, config: UDSRequestConfig | None = None) -> bool: + """set_session_pre() is called before the diagnostic session control + pdu is written on the wire. Implement this if there are special + preconditions for a particular session, such as disabling error + logging. + + Args: + uds: The UDSClient class where this hook is embedded. The caller typically + calls this function with `self` as the first argument. + session: The desired session identifier. + Returns: + True on success, False on error. + """ + return True
+ + +
+[docs] + async def set_session_post(self, level: int, config: UDSRequestConfig | None = None) -> bool: + """set_session_post() is called after the diagnostic session control + pdu was written on the wire. Implement this if there are special + cleanup routines or sleeping until a certain moment is required. + + Args: + uds: The UDSClient class where this hook is embedded. The caller typically + calls this function with `self` as the first argument. + session: The desired session identifier. + Returns: + True on success, False on error. + """ + return True
+ + +
+[docs] + async def check_and_set_session( + self, + expected_session: int, + retries: int = 3, + ) -> bool: + """check_and_set_session() reads the current session and (re)tries to set + the session to the expected session if they do not match. + + Returns True if the current session matches the expected session, + or if read_session is not supported by the ECU or in the current session.""" + + logger.debug(f"Checking current session, expecting {g_repr(expected_session)}") + + try: + current_session = await self.read_session(config=UDSRequestConfig(max_retry=retries)) + except UnexpectedNegativeResponse as e: + if suggests_identifier_not_supported(e.RESPONSE_CODE): + logger.info( + f"Read current session not supported: {e.RESPONSE_CODE.name}, skipping check_session" + ) + return True + raise e + except TimeoutError: + logger.warning("Reading current session timed out, skipping check_session") + return True + + logger.debug(f"Current session is {g_repr(current_session)}") + if current_session == expected_session: + return True + + for i in range(retries): + logger.warning( + f"Not in session {g_repr(expected_session)}, ECU replied with {g_repr(current_session)}" + ) + + logger.info( + f"Switching to session {g_repr(expected_session)}; attempt {i + 1} of {retries}" + ) + resp = await self.set_session(expected_session) + + if isinstance(resp, service.NegativeResponse): + logger.warning(f"Switching to session {g_repr(expected_session)} failed: {resp}") + + try: + current_session = await self.read_session( + config=UDSRequestConfig(max_retry=retries) + ) + logger.debug(f"Current session is {g_repr(current_session)}") + if current_session == expected_session: + return True + except UnexpectedNegativeResponse as e: + if suggests_identifier_not_supported(e.RESPONSE_CODE): + logger.info( + f"Read current session not supported: {e.RESPONSE_CODE.name}, skipping check_session" + ) + return True + raise e + except TimeoutError: + logger.warning("Reading current session timed out, skipping check_session") + return True + + logger.warning( + f"Failed to switch to session {g_repr(expected_session)} after {retries} attempts" + ) + return False
+ + + async def power_cycle(self, sleep: float = 5) -> bool: + if self.power_supply is None: + logger.debug("no power_supply available") + return False + + async def callback() -> None: + await self.wait_for_ecu() + + await self.power_supply.power_cycle(sleep, callback) + self.state.reset() + return True + +
+[docs] + async def leave_session( + self, + level: int, + config: UDSRequestConfig | None = None, + sleep: float | None = None, + ) -> bool: + """leave_session() is a hook which can be called explicitly by a + scanner when a session is to be disabled. Use this hook if resetting + the ECU is required, e.g. when disabling the programming session. + """ + resp: service.UDSResponse = await self.ecu_reset(0x01) + if isinstance(resp, service.NegativeResponse): + if sleep is not None: + await self.power_cycle(sleep=sleep) + else: + await self.power_cycle() + await self.reconnect() + await self.wait_for_ecu() + + resp = await self.set_session(0x01, config=config) + if isinstance(resp, service.NegativeResponse): + if sleep is not None: + await self.power_cycle(sleep=sleep) + else: + await self.power_cycle() + await self.reconnect() + return True
+ + + async def set_session( + self, + level: int, + config: UDSRequestConfig | None = None, + use_db: bool = True, + ) -> service.NegativeResponse | service.DiagnosticSessionControlResponse: + config = config if config is not None else UDSRequestConfig() + + if not config.skip_hooks: + await self.set_session_pre(level, config=config) + + resp = await self.diagnostic_session_control(level, config=config) + + if isinstance(resp, service.NegativeResponse) and self.db_handler is not None and use_db: + logger.debug("Could not switch to session. Trying with database transitions ...") + + if self.db_handler is not None: + steps = await self.db_handler.get_session_transition(level) + + logger.debug(f"Found the following steps in database: {steps}") + + if steps is not None: + for step in steps: + await self.set_session(step, use_db=False) + + resp = await self.diagnostic_session_control(level, config=config) + + if not isinstance(resp, service.NegativeResponse) and not config.skip_hooks: + await self.set_session_post(level, config=config) + + return resp + +
+[docs] + async def read_dtc( + self, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.ReportDTCByStatusMaskResponse: + """Read all dtc records from the ecu.""" + return await self.read_dtc_information_report_dtc_by_status_mask(0xFF, config=config)
+ + +
+[docs] + async def clear_dtc( + self, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.ClearDiagnosticInformationResponse: + """Clear all dtc records on the ecu.""" + return await self.clear_diagnostic_information(0xFFFFFF, config=config)
+ + +
+[docs] + async def read_vin( + self, config: UDSRequestConfig | None = None + ) -> service.NegativeResponse | service.ReadDataByIdentifierResponse: + """Read the VIN of the vehicle""" + return await self.read_data_by_identifier(0xF190, config=config)
+ + +
+[docs] + async def transmit_data( + self, + data: bytes, + block_length: int, + max_block_length: int = 0xFFF, + config: UDSRequestConfig | None = None, + ) -> None: + """transmit_data splits the data to be sent in several blocks of size block_length, + transfers all of them and concludes the transmission with RequestTransferExit""" + if block_length > max_block_length: + logger.warning(f"Limiting block size to {g_repr(max_block_length)}") + block_length = max_block_length + # block_length includes the service identifier and block counter; payload must be smaller + payload_size = block_length - 2 + counter = 0 + for i in range(0, len(data), payload_size): + counter += 1 + payload = data[i : i + payload_size] + logger.debug( + f"Transferring block {g_repr(counter)} with payload size {g_repr(len(payload))}" + ) + resp: service.UDSResponse = await self.transfer_data( + counter & 0xFF, payload, config=config + ) + raise_for_error(resp, f"Transmitting data failed at index {g_repr(i)}") + resp = await self.request_transfer_exit(config=config) + raise_for_error(resp)
+ + + async def _wait_for_ecu_endless_loop(self, sleep_time: float) -> None: + """Internal method with endless loop in case of no answer from ECU""" + config = UDSRequestConfig(timeout=0.5, max_retry=0, skip_hooks=True) + i = -1 + while True: + i = (i + 1) % 4 + logger.info(f"Waiting for ECU{'.' * i}") + try: + await asyncio.sleep(sleep_time) + await self.ping(config=config) + break + except ConnectionError as e: + logger.debug(f"ECU not ready: {e!r}, reconnecting…") + await self.reconnect() + except UDSException as e: + logger.debug(f"ECU not ready: {e!r}") + logger.info("ECU ready") + +
+[docs] + async def wait_for_ecu( + self, + timeout: float | None = 10, + ) -> bool: + """Wait for ecu to be alive again (e.g. after reset). + Sends a ping every 0.5s and waits at most timeout. + If timeout is None, wait endlessly""" + logger.info(f"Waiting for {timeout}s for ECU to respond") + if self.tester_present_task and self.tester_present_interval: + await self.stop_cyclic_tester_present() + + try: + await asyncio.wait_for(self._wait_for_ecu_endless_loop(0.5), timeout=timeout) + return True + except TimeoutError: + logger.critical("Timeout while waiting for ECU!") + return False + finally: + if self.tester_present_task and self.tester_present_interval: + await self.start_cyclic_tester_present(self.tester_present_interval)
+ + + async def _tester_present_worker(self, interval: float) -> None: + assert self.transport + logger.debug("tester present worker started") + task = asyncio.current_task() + while task is not None and task.cancelling() == 0: + try: + await asyncio.sleep(interval) + # TODO: Only ping if there was no other UDS traffic for `interval` amount of time + await self.ping(UDSRequestConfig(max_retry=0)) + except asyncio.CancelledError: + logger.debug("tester present worker terminated") + raise + except ConnectionError: + logger.info("connection lost; tester present waiting…") + except Exception as e: + logger.warning(f"Tester present worker got {e!r}") + logger.debug("Tester present worker was cancelled but received no asyncio.CancelledError") + + async def start_cyclic_tester_present(self, interval: float) -> None: + logger.debug("Starting tester present worker") + self.tester_present_interval = interval + coroutine = self._tester_present_worker(interval) + self.tester_present_task = asyncio.create_task(coroutine) + self.tester_present_task.add_done_callback( + handle_task_error, + context=set_task_handler_ctx_variable(__name__, "TesterPresent"), + ) + + # enforce context switch + # this ensures, that the task is executed at least once + # if the task is not executed, task.cancel will fail with CancelledError + await asyncio.sleep(0) + + async def stop_cyclic_tester_present(self) -> None: + logger.debug("Stopping tester present worker") + if self.tester_present_task is None: + logger.warning("BUG: stop_cyclic_tester_present() called but no task running") + return + + self.tester_present_task.cancel() + try: + await self.tester_present_task + except asyncio.CancelledError: + pass + + async def update_state( + self, request: service.UDSRequest, response: service.UDSResponse + ) -> None: + if isinstance(response, service.DiagnosticSessionControlResponse): + self.state.reset() + self.state.session = response.diagnostic_session_type + + if ( + isinstance(response, service.ReadDataByIdentifierResponse) + and response.data_identifier == DataIdentifier.ActiveDiagnosticSessionDataIdentifier + ): + new_session = int.from_bytes(response.data_record, "big") + + if self.state.session != new_session: + self.state.reset() + self.state.session = new_session + + if ( + isinstance(response, service.SecurityAccessResponse) + and response.security_access_type % 2 == 0 + ): + self.state.security_access_level = response.security_access_type - 1 + + if isinstance(response, service.ECUResetResponse): + self.state.reset() + +
+[docs] + async def refresh_state(self, reset_state: bool = False) -> None: + """ + Refresh the attributes of the ECU states, if possible. + By, default, old values are only overwritten in case the corresponding + information can be requested from the ECU and could be retrieved from a + positive response from the ECU. + + :param reset_state: If True, the ECU state is reset before updating it. + """ + if reset_state: + self.state.reset() + + await self.read_session()
+ + + async def _request( + self, request: service.UDSRequest, config: UDSRequestConfig | None = None + ) -> service.UDSResponse: + """Sends a raw UDS request and returns the response. + Network errors are handled via exponential backoff. + Pending errors, triggered by the ECU are resolved as well. + + :param request: request to send + :param config: The request config parameters + :return: The response. + """ + response = None + exception: Exception | None = None + send_time = datetime.now(UTC).astimezone() + receive_time = None + + try: + response = await super()._request(request, config) + receive_time = datetime.now(UTC).astimezone() + return response + except ResponseException as e: + exception = e + response = e.response + raise + except Exception as e: + exception = e + raise + finally: + try: + if self.implicit_logging and self.db_handler is not None: + mode = LogMode.implicit + + if config is not None and config.tags is not None and "ANALYZE" in config.tags: + mode = LogMode.emphasized + + await self.db_handler.insert_scan_result( + self.state.__dict__, + service.UDSRequest.parse_dynamic(request.pdu), + response, + exception, + send_time, + receive_time, + mode, + ) + except Exception as e: + logger.warning(f"Could not log messages to database: {g_repr(e)}") + + if response is not None: + await self.update_state(request, response)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/base.html b/_modules/gallia/transports/base.html new file mode 100644 index 000000000..3d3160359 --- /dev/null +++ b/_modules/gallia/transports/base.html @@ -0,0 +1,421 @@ + + + + + + + + gallia.transports.base — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.base

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import asyncio
+import binascii
+import io
+from abc import ABC, abstractmethod
+from typing import Any, Protocol, Self
+from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
+
+from gallia.log import get_logger
+from gallia.net import join_host_port
+from gallia.transports.schemes import TransportScheme
+
+logger = get_logger(__name__)
+
+
+
+[docs] +class TargetURI: + """TargetURI represents a target to which gallia can connect. + The target string must conform to a URI is specified by RFC3986. + + Basically, this is a wrapper around Python's ``urlparse()`` and + ``parse_qs()`` methods. TargetURI provides frequently used properties + for a more userfriendly usage. Instances are meant to be passed to + :meth:`BaseTransport.connect()` of transport implementations. + """ + + def __init__(self, raw: str) -> None: + self.raw = raw + self.url = urlparse(raw) + self.qs = parse_qs(self.url.query) + +
+[docs] + @classmethod + def from_parts( + cls, + scheme: str, + host: str, + port: int | None, + args: dict[str, Any], + ) -> Self: + """Constructs a instance of TargetURI with the given arguments. + The ``args`` dict is used for the query string. + """ + netloc = host if port is None else join_host_port(host, port) + return cls(urlunparse((scheme, netloc, "", "", urlencode(args), "")))
+ + + @property + def scheme(self) -> TransportScheme: + """The URI scheme""" + return TransportScheme(self.url.scheme) + + @property + def hostname(self) -> str | None: + """The hostname (without port)""" + return self.url.hostname + + @property + def port(self) -> int | None: + """The port number""" + return self.url.port + + @property + def netloc(self) -> str: + """The hostname and the portnumber, separated by a colon.""" + return self.url.netloc + + @property + def path(self) -> str: + """The path property of the url.""" + return self.url.path + + @property + def location(self) -> str: + """A URI string which only consists of the relevant scheme, + the host and the port. + """ + return f"{self.scheme}://{self.url.netloc}" + + @property + def qs_flat(self) -> dict[str, str]: + """A dict which contains the query string's key/value pairs. + In case a key appears multiple times, this variant only + contains the first found key/value pair. In contrast to + :attr:`qs`, this variant avoids lists and might be easier + to use for some cases. + """ + d = {} + for k, v in self.qs.items(): + d[k] = v[0] + return d + + def __str__(self) -> str: + return self.raw
+ + + +class TransportProtocol(Protocol): + mutex: asyncio.Lock + target: TargetURI + is_closed: bool + + def get_writer(self) -> asyncio.StreamWriter: + raise NotImplementedError + + def get_reader(self) -> asyncio.StreamReader: + raise NotImplementedError + + +
+[docs] +class BaseTransport(ABC): + """BaseTransport is the base class providing the required + interface for all transports used by gallia. + + A transport usually is some kind of network protocol which + carries an application level protocol. A good example is + DoIP carrying UDS requests which acts as a minimal middleware + on top of TCP. + + This class is to be used as a subclass with all abstractmethods + implemented and the SCHEME property filled. + + A few methods provide a ``tags`` argument. The debug logs of these + calls include these tags in the ``tags`` property of the relevant + :class:`gallia.log.PenlogRecord`. + """ + + #: The scheme for the implemented protocol, e.g. "doip". + SCHEME: str = "" + #: The buffersize of the transport. Might be used in read() calls. + #: Defaults to :const:`io.DEFAULT_BUFFER_SIZE`. + BUFSIZE: int = io.DEFAULT_BUFFER_SIZE + + def __init__(self, target: TargetURI) -> None: + self.mutex = asyncio.Lock() + self.target = target + self.is_closed = False + + def __init_subclass__( + cls, + /, + scheme: str, + bufsize: int = io.DEFAULT_BUFFER_SIZE, + **kwargs: Any, + ) -> None: + super().__init_subclass__(**kwargs) + cls.SCHEME = scheme + cls.BUFSIZE = bufsize + +
+[docs] + @classmethod + def check_scheme(cls, target: TargetURI) -> None: + """Checks if the provided URI has the correct scheme.""" + if target.scheme != cls.SCHEME: + raise ValueError(f"invalid scheme: {target.scheme}; expected: {cls.SCHEME}")
+ + +
+[docs] + @classmethod + @abstractmethod + async def connect( + cls, + target: str | TargetURI, + timeout: float | None = None, + ) -> Self: + """Classmethod to connect the transport to a relevant target. + The target argument is a URI, such as `doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d"` + An instance of the relevant transport class is returned. + """
+ + +
+[docs] + @abstractmethod + async def close(self) -> None: + """Terminates the connection and clean up all allocated ressources."""
+ + +
+[docs] + async def reconnect(self, timeout: float | None = None) -> Self: + """Closes the connection to the target and attempts to reconnect every + 100 ms until at max timeout. If timeout is None, only attempt to connect + once. + A new instance of this class is returned rendering the old one obsolete. + This method is safe for concurrent use. + """ + async with self.mutex: + try: + await self.close() + except ConnectionError as e: + logger.warning(f"close() failed during reconnect ({e!r}); ignoring") + + async with asyncio.timeout(timeout): + logger.debug( + f"Attempting to establish a new connection with a timeout of {timeout}" + ) + while True: + try: + return await self.connect(self.target) + except ConnectionError as e: + logger.info(f"Connection attempt failed while reconnecting: {e!r}") + if timeout is None: + logger.debug("Breaking out of the reconnect-loop since timeout is None") + raise e + await asyncio.sleep(0.1)
+ + +
+[docs] + @abstractmethod + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + """Reads one message and returns its raw byte representation. + An example for one message is 'one line, terminated by newline' + for a TCP transport yielding lines. + """
+ + +
+[docs] + @abstractmethod + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + """Writes one message and return the number of written bytes."""
+ + +
+[docs] + async def request( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + """Chains a :meth:`write()` call with a :meth:`read()` call. + The call is protected by a mutex and is thus safe for concurrent + use. + """ + async with self.mutex: + return await self.request_unsafe(data, timeout, tags)
+ + +
+[docs] + async def request_unsafe( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + """Chains a :meth:`write()` call with a :meth:`read()` call. + The call is **not** protected by a mutex. Only use this method + when you know what you are doing. + """ + await self.write(data, timeout, tags) + return await self.read(timeout, tags)
+
+ + + +class LinesTransportMixin: + async def write( + self: TransportProtocol, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + t = tags + ["write"] if tags is not None else ["write"] + + logger.trace(data.hex() + "0a", extra={"tags": t}) + + writer = self.get_writer() + writer.write(binascii.hexlify(data) + b"\n") + await asyncio.wait_for(writer.drain(), timeout) + return len(data) + + async def read( + self: TransportProtocol, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + data = await asyncio.wait_for(self.get_reader().readline(), timeout) + d = data.decode().strip() + + t = tags + ["read"] if tags is not None else ["read"] + logger.trace(d + "0a", extra={"tags": t}) + + return binascii.unhexlify(d) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/can.html b/_modules/gallia/transports/can.html new file mode 100644 index 000000000..5c5fee979 --- /dev/null +++ b/_modules/gallia/transports/can.html @@ -0,0 +1,392 @@ + + + + + + + + gallia.transports.can — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.can

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import asyncio
+import socket as s
+import struct
+import sys
+import time
+from dataclasses import dataclass
+from typing import Self
+
+assert sys.platform.startswith("linux"), "unsupported platform"
+
+from pydantic import BaseModel, field_validator
+
+from gallia.log import get_logger
+from gallia.transports._can_constants import (
+    CAN_EFF_FLAG,
+    CAN_EFF_MASK,
+    CAN_ERR_FLAG,
+    CAN_HEADER_FMT,
+    CAN_INV_FILTER,
+    CAN_RAW,
+    CAN_RAW_FD_FRAMES,
+    CAN_RAW_FILTER,
+    CAN_RAW_JOIN_FILTERS,
+    CAN_RTR_FLAG,
+    CAN_SFF_MASK,
+    CANFD_BRS,
+    CANFD_ESI,
+    CANFD_MTU,
+    SOL_CAN_RAW,
+)
+from gallia.transports.base import BaseTransport, TargetURI
+from gallia.utils import auto_int
+
+logger = get_logger(__name__)
+
+
+@dataclass(kw_only=True, slots=True, frozen=True)
+class CANMessage:
+    timestamp: float = 0.0
+    arbitration_id: int = 0
+
+    # TODO: Add a frametype attribute?
+    is_extended_id: bool = True
+    is_remote_frame: bool = False
+    is_error_frame: bool = False
+
+    dlc: int | None = None
+    data: bytes = b""
+    is_fd: bool = False
+    is_rx: bool = True
+    bitrate_switch: bool = False
+    error_state_indicator: bool = False
+
+    def __len__(self) -> int:
+        if self.dlc is None:
+            return len(self.data)
+        return self.dlc
+
+    def _compose_arbitration_id(self) -> int:
+        can_id = self.arbitration_id
+        if self.is_extended_id:
+            can_id |= CAN_EFF_FLAG
+        if self.is_remote_frame:
+            can_id |= CAN_RTR_FLAG
+        if self.is_error_frame:
+            can_id |= CAN_ERR_FLAG
+        return can_id
+
+    def pack(self) -> bytes:
+        can_id = self._compose_arbitration_id()
+        flags = 0
+        if self.bitrate_switch:
+            flags |= CANFD_BRS
+        if self.error_state_indicator:
+            flags |= CANFD_ESI
+        max_len = 64 if self.is_fd else 8
+        data = bytes(self.data).ljust(max_len, b"\x00")
+        return CAN_HEADER_FMT.pack(can_id, self.dlc, flags) + data
+
+    @staticmethod
+    def _dissect_can_frame(frame: bytes) -> tuple[int, int, int, bytes]:
+        can_id, can_dlc, flags = CAN_HEADER_FMT.unpack_from(frame)
+        if len(frame) != CANFD_MTU:
+            # Flags not valid in non-FD frames
+            flags = 0
+        return can_id, can_dlc, flags, frame[8 : 8 + can_dlc]
+
+    @classmethod
+    def unpack(cls, frame: bytes) -> Self:
+        can_id, can_dlc, flags, data = cls._dissect_can_frame(frame)
+
+        # EXT, RTR, ERR flags -> boolean attributes
+        #   /* special address description flags for the CAN_ID */
+        #   #define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
+        #   #define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
+        #   #define CAN_ERR_FLAG 0x20000000U /* error frame */
+        is_extended_frame_format = bool(can_id & CAN_EFF_FLAG)
+        is_remote_transmission_request = bool(can_id & CAN_RTR_FLAG)
+        is_error_frame = bool(can_id & CAN_ERR_FLAG)
+        is_fd = len(frame) == CANFD_MTU
+        bitrate_switch = bool(flags & CANFD_BRS)
+        error_state_indicator = bool(flags & CANFD_ESI)
+
+        if is_extended_frame_format:
+            arbitration_id = can_id & CAN_EFF_MASK
+        else:
+            arbitration_id = can_id & CAN_SFF_MASK
+
+        return cls(
+            arbitration_id=arbitration_id,
+            is_extended_id=is_extended_frame_format,
+            is_remote_frame=is_remote_transmission_request,
+            is_error_frame=is_error_frame,
+            is_fd=is_fd,
+            bitrate_switch=bitrate_switch,
+            error_state_indicator=error_state_indicator,
+            dlc=can_dlc,
+            data=data,
+        )
+
+
+class RawCANConfig(BaseModel):
+    is_extended: bool = False
+    is_fd: bool = False
+    dst_id: int | None = None
+
+    @field_validator(
+        "dst_id",
+        mode="before",
+    )
+    def auto_int(cls, v: str) -> int:
+        return auto_int(v)
+
+
+
+[docs] +class RawCANTransport(BaseTransport, scheme="can-raw"): + def __init__(self, target: TargetURI, config: RawCANConfig, sock: s.socket) -> None: + super().__init__(target) + + self._sock = sock + self.config = config + +
+[docs] + @classmethod + async def connect( + cls, + target: str | TargetURI, + timeout: float | None = None, + ) -> Self: + t = target if isinstance(target, TargetURI) else TargetURI(target) + cls.check_scheme(t) + + if t.hostname is None: + raise ValueError("empty interface") + + sock = s.socket(s.PF_CAN, s.SOCK_RAW, CAN_RAW) + sock.bind((t.hostname,)) + config = RawCANConfig(**t.qs_flat) + + if config.is_fd is True: + sock.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1) + + sock.setblocking(False) + + return cls(t, config, sock)
+ + + def set_filter(self, can_ids: list[int], inv_filter: bool = False) -> None: + if not can_ids: + return + filter_mask = CAN_EFF_MASK if self.config.is_extended else CAN_SFF_MASK + data = b"" + for can_id in can_ids: + if inv_filter: + can_id |= CAN_INV_FILTER # noqa: PLW2901 + data += struct.pack("@II", can_id, filter_mask) + self._sock.setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, data) + if inv_filter: + self._sock.setsockopt(SOL_CAN_RAW, CAN_RAW_JOIN_FILTERS, 1) + +
+[docs] + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + raise RuntimeError("RawCANTransport is a special snowflake")
+ + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + if self.config.dst_id: + return await self.sendto(data, self.config.dst_id, timeout, tags) + raise ValueError("dst_id not set")
+ + + async def sendto( + self, + data: bytes, + dst: int, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + msg = CANMessage( + arbitration_id=dst, + data=data, + is_extended_id=self.config.is_extended, + is_fd=self.config.is_fd, + ) + t = tags + ["write"] if tags is not None else ["write"] + if self.config.is_extended: + logger.trace(f"{dst:08x}#{data.hex()}", extra={"tags": t}) + else: + logger.trace(f"{dst:03x}#{data.hex()}", extra={"tags": t}) + + loop = asyncio.get_running_loop() + await asyncio.wait_for(loop.sock_sendall(self._sock, msg.pack()), timeout) + return len(data) + + async def recvfrom( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> tuple[int, bytes]: + loop = asyncio.get_running_loop() + can_frame = await asyncio.wait_for(loop.sock_recv(self._sock, self.BUFSIZE), timeout) + msg = CANMessage.unpack(can_frame) + + t = tags + ["read"] if tags is not None else ["read"] + if msg.is_extended_id: + logger.trace(f"{msg.arbitration_id:08x}#{msg.data.hex()}", extra={"tags": t}) + else: + logger.trace(f"{msg.arbitration_id:03x}#{msg.data.hex()}", extra={"tags": t}) + return msg.arbitration_id, msg.data + +
+[docs] + async def close(self) -> None: + pass
+ + +
+[docs] + async def get_idle_traffic(self, sniff_time: float) -> list[int]: + """Listen to traffic on the bus and return list of IDs + which are seen in the specified period of time. + The output of this function can be used as input to set_filter. + """ + addr_idle: list[int] = [] + t1 = time.time() + while time.time() - t1 < sniff_time: + try: + addr, _ = await self.recvfrom(timeout=1) + if addr not in addr_idle: + logger.info(f"Received a message from {addr:03x}") + addr_idle.append(addr) + except TimeoutError: + continue + addr_idle.sort() + return addr_idle
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/doip.html b/_modules/gallia/transports/doip.html new file mode 100644 index 000000000..8585743be --- /dev/null +++ b/_modules/gallia/transports/doip.html @@ -0,0 +1,1010 @@ + + + + + + + + gallia.transports.doip — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.doip

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import asyncio
+import socket
+import struct
+from dataclasses import dataclass
+from enum import IntEnum, unique
+from typing import Any, Self
+
+from pydantic import BaseModel, field_validator
+
+from gallia.log import get_logger
+from gallia.transports.base import BaseTransport, TargetURI
+from gallia.utils import (
+    auto_int,
+    handle_task_error,
+    set_task_handler_ctx_variable,
+)
+
+logger = get_logger(__name__)
+
+
+@unique
+class ProtocolVersions(IntEnum):
+    ISO_13400_2_2010 = 0x01
+    ISO_13400_2_2012 = 0x02
+    ISO_13400_2_2019 = 0x03
+
+
+@unique
+class RoutingActivationRequestTypes(IntEnum):
+    RESERVED = 0xFF
+    ManufacturerSpecific = 0xFE
+    Default = 0x00
+    WWH_OBD = 0x01
+    CentralSecurity = 0xE0
+
+    @classmethod
+    def _missing_(cls, value: Any) -> RoutingActivationRequestTypes:
+        if value in range(0xE1, 0x100):
+            return cls.ManufacturerSpecific
+        return cls.RESERVED
+
+
+@unique
+class RoutingActivationResponseCodes(IntEnum):
+    RESERVED = 0xFF
+    ManufacturerSpecific = 0xFE
+    UnknownSourceAddress = 0x00
+    NoResources = 0x01
+    InvalidConnectionEntry = 0x02
+    AlreadyActive = 0x03
+    AuthenticationMissing = 0x04
+    ConfirmationRejected = 0x05
+    UnsupportedActivationType = 0x06
+    TLSRequired = 0x07
+    Success = 0x10
+    SuccessConfirmationRequired = 0x11
+
+    @classmethod
+    def _missing_(cls, value: Any) -> RoutingActivationResponseCodes:
+        if value in range(0xE0, 0xFF):
+            return cls.ManufacturerSpecific
+        return cls.RESERVED
+
+
+class DoIPRoutingActivationDeniedError(ConnectionAbortedError):
+    rac_code: RoutingActivationResponseCodes
+
+    def __init__(self, rac_code: int):
+        self.rac_code = RoutingActivationResponseCodes(rac_code)
+        super().__init__(f"DoIP routing activation denied: {self.rac_code.name} ({rac_code})")
+
+
+@unique
+class PayloadTypes(IntEnum):
+    GenericDoIPHeaderNACK = 0x0000
+    VehicleIdentificationRequestMessage = 0x0001
+    VehicleIdentificationRequestMessageWithEID = 0x0002
+    VehicleIdentificationRequestMessageWithVIN = 0x0003
+    VehicleAnnouncementMessage = 0x004
+    RoutingActivationRequest = 0x0005
+    RoutingActivationResponse = 0x0006
+    AliveCheckRequest = 0x0007
+    AliveCheckResponse = 0x0008
+    DoIPEntityStatusRequest = 0x4001
+    DoIPEntityStatusResponse = 0x4002
+    DiagnosticPowerModeInformationRequest = 0x4003
+    DiagnosticPowerModeInformationResponse = 0x4004
+    DiagnosticMessage = 0x8001
+    DiagnosticMessagePositiveAcknowledgement = 0x8002
+    DiagnosticMessageNegativeAcknowledgement = 0x8003
+
+
+@unique
+class DiagnosticMessagePositiveAckCodes(IntEnum):
+    Success = 0x00
+
+
+@unique
+class DiagnosticMessageNegativeAckCodes(IntEnum):
+    RESERVED = 0xFF
+    InvalidSourceAddress = 0x02
+    UnknownTargetAddress = 0x03
+    DiagnosticMessageTooLarge = 0x04
+    OutOfMemory = 0x05
+    TargetUnreachable = 0x06
+    UnknownNetwork = 0x07
+    TransportProtocolError = 0x08
+
+    @classmethod
+    def _missing_(cls, value: Any) -> DiagnosticMessageNegativeAckCodes:
+        return cls.RESERVED
+
+
+class DoIPNegativeAckError(BrokenPipeError):
+    nack_code: DiagnosticMessageNegativeAckCodes
+
+    def __init__(self, negative_ack_code: int):
+        self.nack_code = DiagnosticMessageNegativeAckCodes(negative_ack_code)
+        super().__init__(f"DoIP negative ACK received: {self.nack_code.name} ({negative_ack_code})")
+
+
+@unique
+class GenericDoIPHeaderNACKCodes(IntEnum):
+    RESERVED = 0xFF
+    IncorrectPatternFormat = 0x00
+    UnknownPayloadType = 0x01
+    MessageTooLarge = 0x02
+    OutOfMemory = 0x03
+    InvalidPayloadLength = 0x04
+
+    @classmethod
+    def _missing_(cls, value: Any) -> GenericDoIPHeaderNACKCodes:
+        return cls.RESERVED
+
+
+class DoIPGenericHeaderNACKError(ConnectionAbortedError):
+    nack_code: GenericDoIPHeaderNACKCodes
+
+    def __init__(self, nack_code: int):
+        self.nack_code = GenericDoIPHeaderNACKCodes(nack_code)
+        super().__init__(f"DoIP generic header negative ACK: {self.nack_code.name} ({nack_code})")
+
+
+class TimingAndCommunicationParameters(IntEnum):
+    CtrlTimeout = 2000
+    AnnounceWait = 500
+    AnnounceInterval = 500
+    AnnounceNum = 3
+    DiagnosticMessageMessageAckTimeout = 2000
+    RoutingActivationResponseTimeout = 2000
+    DiagnosticMessageMessageTimeout = 2000
+    TCPGeneralInactivityTimeout = 5000
+    TCPInitialInactivityTimeout = 2000
+    TCPAliveCheckTimeout = 500
+    ProcessingTimeout = 2000
+    VehicleDiscoveryTimeout = 5000
+
+
+@dataclass
+class GenericHeader:
+    ProtocolVersion: int
+    PayloadType: int
+    PayloadLength: int
+
+    def pack(self) -> bytes:
+        return struct.pack(
+            "!BBHL",
+            self.ProtocolVersion,
+            self.ProtocolVersion ^ 0xFF,
+            self.PayloadType,
+            self.PayloadLength,
+        )
+
+    @classmethod
+    def unpack(cls, data: bytes) -> GenericHeader:
+        (
+            protocol_version,
+            inverse_protocol_version,
+            payload_type,
+            payload_length,
+        ) = struct.unpack("!BBHL", data)
+        if protocol_version != inverse_protocol_version ^ 0xFF:
+            raise ValueError("inverse protocol_version is invalid")
+        return cls(
+            protocol_version,
+            payload_type,
+            payload_length,
+        )
+
+
+@dataclass
+class GenericDoIPHeaderNACK:
+    GenericHeaderNACKCode: GenericDoIPHeaderNACKCodes
+
+    def pack(self) -> bytes:
+        return struct.pack(
+            "!B",
+            self.GenericHeaderNACKCode,
+        )
+
+    @classmethod
+    def unpack(cls, data: bytes) -> GenericDoIPHeaderNACK:
+        (generic_header_NACK_code,) = struct.unpack("!B", data)
+        return cls(
+            GenericDoIPHeaderNACKCodes(generic_header_NACK_code),
+        )
+
+
+@dataclass
+class VehicleIdentificationRequestMessage:
+    def pack(self) -> bytes:
+        return b""
+
+
+@dataclass
+class VehicleAnnouncementMessage:
+    VIN: bytes
+    LogicalAddress: int
+    EID: bytes
+    GID: bytes
+    FurtherActionRequired: FurtherActionCodes
+    VINGIDSyncStatus: SynchronisationStatusCodes | None
+
+    @classmethod
+    def unpack(cls, data: bytes) -> VehicleAnnouncementMessage:
+        if len(data) == 32:
+            # VINGIDSyncStatus is optional
+            (vin, logical_address, eid, gid, further_action_required) = struct.unpack(
+                "!17sH6s6sB", data
+            )
+            vin_gid_sync_status = None
+        else:
+            (
+                vin,
+                logical_address,
+                eid,
+                gid,
+                further_action_required,
+                vin_gid_sync_status,
+            ) = struct.unpack("!17sH6s6sBB", data)
+
+        return cls(
+            vin,
+            logical_address,
+            eid,
+            gid,
+            FurtherActionCodes(further_action_required),
+            SynchronisationStatusCodes(vin_gid_sync_status)
+            if vin_gid_sync_status is not None
+            else None,
+        )
+
+
+@unique
+class FurtherActionCodes(IntEnum):
+    RESERVED = 0x0F
+    ManufacturerSpecific = 0xFF
+    NoFurtherActionRequired = 0x00
+    RoutingActivationRequiredToInitiateCentralSecurity = 0x10
+
+    @classmethod
+    def _missing_(cls, value: Any) -> FurtherActionCodes:
+        if value in range(0x11, 0x100):
+            return cls.ManufacturerSpecific
+        return cls.RESERVED
+
+
+@unique
+class SynchronisationStatusCodes(IntEnum):
+    RESERVED = 0xFF
+    VINGIDSynchronized = 0x00
+    IncompleteVINGIDNotSynchronized = 0x10
+
+    @classmethod
+    def _missing_(cls, value: Any) -> SynchronisationStatusCodes:
+        return cls.RESERVED
+
+
+@dataclass
+class DoIPEntityStatusRequest:
+    def pack(self) -> bytes:
+        return b""
+
+
+@dataclass
+class DoIPEntityStatusResponse:
+    NodeType: NodeTypes
+    MaximumConcurrentTCP_DATASockets: int
+    CurrentlyOpenTCP_DATASockets: int
+    MaximumDataSize: int | None
+
+    @classmethod
+    def unpack(cls, data: bytes) -> DoIPEntityStatusResponse:
+        if len(data) == 3:
+            # MaximumDataSize is optional
+            (nt, mcts, ncts) = struct.unpack("!BBB", data)
+            mds = None
+        else:
+            (nt, mcts, ncts, mds) = struct.unpack("!BBBI", data)
+
+        return cls(NodeTypes(nt), mcts, ncts, mds)
+
+
+@unique
+class NodeTypes(IntEnum):
+    RESERVED = 0xFF
+    Gateway = 0x00
+    Node = 0x01
+
+    @classmethod
+    def _missing_(cls, value: Any) -> NodeTypes:
+        return cls.RESERVED
+
+
+@dataclass
+class RoutingActivationRequest:
+    SourceAddress: int
+    ActivationType: int
+    Reserved: int = 0x00000000  # Not used, default value.
+    # OEMReserved uint32
+
+    def pack(self) -> bytes:
+        return struct.pack("!HBI", self.SourceAddress, self.ActivationType, self.Reserved)
+
+
+@dataclass
+class RoutingActivationResponse:
+    SourceAddress: int
+    TargetAddress: int
+    RoutingActivationResponseCode: int
+    Reserved: int = 0x00000000  # Not used, default value.
+    # OEMReserved uint32
+
+    @classmethod
+    def unpack(cls, data: bytes) -> RoutingActivationResponse:
+        (
+            source_address,
+            target_address,
+            routing_activation_response_code,
+            reserved,
+        ) = struct.unpack("!HHBI", data)
+        if reserved != 0x00000000:
+            raise ValueError("reserved field contains data")
+        return cls(
+            source_address,
+            target_address,
+            routing_activation_response_code,
+            reserved,
+        )
+
+
+@dataclass
+class DiagnosticMessage:
+    SourceAddress: int
+    TargetAddress: int
+    UserData: bytes
+
+    def pack(self) -> bytes:
+        return (
+            struct.pack(
+                "!HH",
+                self.SourceAddress,
+                self.TargetAddress,
+            )
+            + self.UserData
+        )
+
+    @classmethod
+    def unpack(cls, data: bytes) -> DiagnosticMessage:
+        source_address, target_address = struct.unpack("!HH", data[:4])
+        data = data[4:]
+        return cls(source_address, target_address, data)
+
+
+@dataclass
+class DiagnosticMessageAcknowledgement:
+    SourceAddress: int
+    TargetAddress: int
+    ACKCode: int
+    PreviousDiagnosticMessageData: bytes
+
+    def pack(self) -> bytes:
+        return (
+            struct.pack(
+                "!HHB",
+                self.SourceAddress,
+                self.TargetAddress,
+                self.ACKCode,
+            )
+            + self.PreviousDiagnosticMessageData
+        )
+
+
+class DiagnosticMessagePositiveAcknowledgement(DiagnosticMessageAcknowledgement):
+    ACKCode: DiagnosticMessagePositiveAckCodes
+
+    @classmethod
+    def unpack(cls, data: bytes) -> DiagnosticMessagePositiveAcknowledgement:
+        source_address, target_address, ack_code = struct.unpack("!HHB", data[:5])
+        prev_data = data[5:]
+
+        return cls(
+            source_address,
+            target_address,
+            DiagnosticMessagePositiveAckCodes(ack_code),
+            prev_data,
+        )
+
+
+class DiagnosticMessageNegativeAcknowledgement(DiagnosticMessageAcknowledgement):
+    ACKCode: DiagnosticMessageNegativeAckCodes
+
+    @classmethod
+    def unpack(cls, data: bytes) -> DiagnosticMessageNegativeAcknowledgement:
+        source_address, target_address, ack_code = struct.unpack("!HHB", data[:5])
+        prev_data = data[5:]
+
+        return cls(
+            source_address,
+            target_address,
+            DiagnosticMessageNegativeAckCodes(ack_code),
+            prev_data,
+        )
+
+
+@dataclass
+class AliveCheckRequest:
+    pass
+
+
+@dataclass
+class AliveCheckResponse:
+    SourceAddress: int
+
+    def pack(self) -> bytes:
+        return struct.pack("!H", self.SourceAddress)
+
+
+# Messages expected to be sent by the DoIP gateway.
+DoIPInData = (
+    GenericDoIPHeaderNACK
+    | RoutingActivationResponse
+    | DiagnosticMessage
+    | DiagnosticMessagePositiveAcknowledgement
+    | DiagnosticMessageNegativeAcknowledgement
+    | AliveCheckRequest
+)
+
+# Messages expected to be sent by us.
+DoIPOutData = RoutingActivationRequest | DiagnosticMessage | AliveCheckResponse
+
+DoIPFrame = tuple[
+    GenericHeader,
+    DoIPInData | DoIPOutData,
+]
+DoIPDiagFrame = tuple[GenericHeader, DiagnosticMessage]
+
+
+class DoIPConnection:
+    def __init__(  # noqa: PLR0913
+        self,
+        reader: asyncio.StreamReader,
+        writer: asyncio.StreamWriter,
+        src_addr: int,
+        target_addr: int,
+        protocol_version: int,
+        separate_diagnostic_message_queue: bool = False,
+    ):
+        self.reader = reader
+        self.writer = writer
+        self.src_addr = src_addr
+        self.target_addr = target_addr
+        self.protocol_version = protocol_version
+        self.separate_diagnostic_message_queue = separate_diagnostic_message_queue
+        self._diagnostic_message_queue: asyncio.Queue[DoIPDiagFrame] = asyncio.Queue()
+        self._read_queue: asyncio.Queue[DoIPFrame] = asyncio.Queue()
+        self._read_task = asyncio.create_task(self._read_worker())
+        self._read_task.add_done_callback(
+            handle_task_error,
+            context=set_task_handler_ctx_variable(__name__, "DoipReader"),
+        )
+        self._is_closed = False
+        self._mutex = asyncio.Lock()
+
+    @classmethod
+    async def connect(  # noqa: PLR0913
+        cls,
+        host: str,
+        port: int,
+        src_addr: int,
+        target_addr: int,
+        so_linger: bool = False,
+        protocol_version: int = ProtocolVersions.ISO_13400_2_2019,
+        separate_diagnostic_message_queue: bool = False,
+    ) -> Self:
+        reader, writer = await asyncio.open_connection(host, port)
+
+        if so_linger is True:
+            # Depending on who will close the connection in the end, one party's socket
+            # will remain in a TIME_WAIT state, which occupies resources until enough
+            # time has passed. Setting the LINGER socket option tells our kernel to
+            # close the connection with a RST, which brings the TCP connection to an
+            # error state and thus avoids TIME_WAIT and instantly forces LISTEN or CLOSED
+            # For more info, see e.g. Note 3 of :
+            # https://www.ietf.org/rfc/rfc9293.html#name-state-machine-overview
+            sock = writer.get_extra_info("socket")
+            sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
+
+        return cls(
+            reader,
+            writer,
+            src_addr,
+            target_addr,
+            protocol_version,
+            separate_diagnostic_message_queue,
+        )
+
+    async def _read_frame(self) -> DoIPFrame | tuple[None, None]:
+        # Header is fixed size 8 byte.
+        hdr_buf = await self.reader.readexactly(8)
+        hdr = GenericHeader.unpack(hdr_buf)
+
+        payload_buf = await self.reader.readexactly(hdr.PayloadLength)
+        payload: DoIPInData
+        match hdr.PayloadType:
+            case PayloadTypes.GenericDoIPHeaderNACK:
+                payload = GenericDoIPHeaderNACK.unpack(payload_buf)
+            case PayloadTypes.RoutingActivationResponse:
+                payload = RoutingActivationResponse.unpack(payload_buf)
+            case PayloadTypes.DiagnosticMessagePositiveAcknowledgement:
+                payload = DiagnosticMessagePositiveAcknowledgement.unpack(payload_buf)
+            case PayloadTypes.DiagnosticMessageNegativeAcknowledgement:
+                payload = DiagnosticMessageNegativeAcknowledgement.unpack(payload_buf)
+            case PayloadTypes.DiagnosticMessage:
+                payload = DiagnosticMessage.unpack(payload_buf)
+            case PayloadTypes.AliveCheckRequest:
+                payload = AliveCheckRequest()
+            case _:
+                logger.warning(
+                    f"DoIP message with unhandled PayloadType: {hdr} {payload_buf.hex()}"
+                )
+                return None, None
+        logger.trace("Received DoIP message: %s, %s", hdr, payload)
+        return hdr, payload
+
+    async def _read_worker(self) -> None:
+        try:
+            while True:
+                hdr, data = await self._read_frame()
+                if hdr is None or data is None:
+                    continue
+                if hdr.PayloadType == PayloadTypes.AliveCheckRequest:
+                    await self.write_alive_check_response()
+                    continue
+                if isinstance(data, DiagnosticMessage) and self.separate_diagnostic_message_queue:
+                    await self._diagnostic_message_queue.put((hdr, data))
+                    continue
+                await self._read_queue.put((hdr, data))
+        except asyncio.CancelledError:
+            logger.debug("DoIP read worker got cancelled")
+        except asyncio.IncompleteReadError as e:
+            logger.debug(f"DoIP read worker received EOF: {e!r}")
+        except Exception as e:
+            logger.info(f"DoIP read worker died with {e!r}")
+        finally:
+            logger.debug("Feeding EOF to reader and requesting a close")
+            self.reader.feed_eof()
+            await self.close()
+
+    async def read_frame_unsafe(self) -> DoIPFrame:
+        # Avoid waiting on the queue forever when
+        # the connection has been terminated.
+        if self._is_closed:
+            raise ConnectionError
+        return await self._read_queue.get()
+
+    async def read_frame(self) -> DoIPFrame:
+        async with self._mutex:
+            return await self.read_frame_unsafe()
+
+    async def read_diag_request_raw(self) -> DoIPDiagFrame:
+        unexpected_packets: list[tuple[Any, Any]] = []
+        while True:
+            if self.separate_diagnostic_message_queue:
+                return await self._diagnostic_message_queue.get()
+            hdr, payload = await self.read_frame()
+            if not isinstance(payload, DiagnosticMessage):
+                logger.warning(f"expected DoIP DiagnosticMessage, instead got: {hdr} {payload}")
+                unexpected_packets.append((hdr, payload))
+                continue
+            if payload.SourceAddress != self.target_addr or payload.TargetAddress != self.src_addr:
+                logger.warning(
+                    f"DoIP-DiagnosticMessage: unexpected addresses (src:dst); expected {self.src_addr:#04x}:"
+                    + f"{self.target_addr:#04x} but got: {payload.SourceAddress:#04x}:{payload.TargetAddress:#04x}"
+                )
+                unexpected_packets.append((hdr, payload))
+                continue
+
+            # Do not consume unexpected packets, but re-add them to the queue for other consumers
+            for item in unexpected_packets:
+                await self._read_queue.put(item)
+
+            return hdr, payload
+
+    async def read_diag_request(self) -> bytes:
+        _, payload = await self.read_diag_request_raw()
+        return payload.UserData
+
+    async def _read_ack(self, prev_data: bytes) -> None:
+        unexpected_packets: list[tuple[Any, Any]] = []
+        while True:
+            hdr, payload = await self.read_frame_unsafe()
+            if not isinstance(payload, DiagnosticMessagePositiveAcknowledgement) and not isinstance(
+                payload, DiagnosticMessageNegativeAcknowledgement
+            ):
+                logger.warning(f"expected DoIP positive/negative ACK, instead got: {hdr} {payload}")
+                unexpected_packets.append((hdr, payload))
+                continue
+
+            if payload.SourceAddress != self.target_addr or payload.TargetAddress != self.src_addr:
+                logger.warning(
+                    f"DoIP-ACK: unexpected addresses (src:dst); expected {self.src_addr:#04x}:{self.target_addr:#04x} "
+                    + f"but got: {payload.SourceAddress:#04x}:{payload.TargetAddress:#04x}"
+                )
+                unexpected_packets.append((hdr, payload))
+                continue
+            if (
+                len(payload.PreviousDiagnosticMessageData) > 0
+                and payload.PreviousDiagnosticMessageData
+                != prev_data[: len(payload.PreviousDiagnosticMessageData)]
+            ):
+                logger.warning("ack: previous data differs from request")
+                logger.warning(
+                    f"DoIP-ACK: got: {payload.PreviousDiagnosticMessageData.hex()} expected {prev_data.hex()}"
+                )
+                unexpected_packets.append((hdr, payload))
+                continue
+
+            # Do not consume unexpected packets, but re-add them to the queue for other consumers
+            for item in unexpected_packets:
+                await self._read_queue.put(item)
+
+            if isinstance(payload, DiagnosticMessageNegativeAcknowledgement):
+                raise DoIPNegativeAckError(payload.ACKCode)
+            return
+
+    async def _read_routing_activation_response(self) -> None:
+        unexpected_packets: list[tuple[Any, Any]] = []
+        while True:
+            hdr, payload = await self.read_frame_unsafe()
+            if not isinstance(payload, RoutingActivationResponse):
+                logger.warning(
+                    f"expected DoIP RoutingActivationResponse, instead got: {hdr} {payload}"
+                )
+                unexpected_packets.append((hdr, payload))
+                continue
+
+            # Do not consume unexpected packets, but re-add them to the queue for other consumers
+            for item in unexpected_packets:
+                await self._read_queue.put(item)
+
+            if payload.RoutingActivationResponseCode != RoutingActivationResponseCodes.Success:
+                raise DoIPRoutingActivationDeniedError(payload.RoutingActivationResponseCode)
+            return
+
+    async def write_request_raw(self, hdr: GenericHeader, payload: DoIPOutData) -> None:
+        async with self._mutex:
+            buf = b""
+            buf += hdr.pack()
+            buf += payload.pack()
+            self.writer.write(buf)
+            await self.writer.drain()
+
+            logger.trace("Sent DoIP message: hdr: %s, payload: %s", hdr, payload)
+
+            try:
+                match payload:
+                    case DiagnosticMessage():
+                        # Now an ACK message is expected.
+                        await asyncio.wait_for(
+                            self._read_ack(payload.UserData),
+                            TimingAndCommunicationParameters.DiagnosticMessageMessageAckTimeout
+                            / 1000,
+                        )
+                    case RoutingActivationRequest():
+                        await asyncio.wait_for(
+                            self._read_routing_activation_response(),
+                            TimingAndCommunicationParameters.RoutingActivationResponseTimeout
+                            / 1000,
+                        )
+            except TimeoutError as e:
+                await self.close()
+                raise BrokenPipeError("Timeout while waiting for DoIP ACK message") from e
+
+    async def write_diag_request(self, data: bytes) -> None:
+        hdr = GenericHeader(
+            ProtocolVersion=self.protocol_version,
+            PayloadType=PayloadTypes.DiagnosticMessage,
+            PayloadLength=len(data) + 4,
+        )
+        payload = DiagnosticMessage(
+            SourceAddress=self.src_addr,
+            TargetAddress=self.target_addr,
+            UserData=data,
+        )
+        await self.write_request_raw(hdr, payload)
+
+    async def write_routing_activation_request(
+        self,
+        activation_type: int,
+    ) -> None:
+        hdr = GenericHeader(
+            ProtocolVersion=self.protocol_version,
+            PayloadType=PayloadTypes.RoutingActivationRequest,
+            PayloadLength=7,
+        )
+        payload = RoutingActivationRequest(
+            SourceAddress=self.src_addr,
+            ActivationType=activation_type,
+            Reserved=0x00,
+        )
+        await self.write_request_raw(hdr, payload)
+
+    async def write_alive_check_response(self) -> None:
+        hdr = GenericHeader(
+            ProtocolVersion=self.protocol_version,
+            PayloadType=PayloadTypes.AliveCheckResponse,
+            PayloadLength=2,
+        )
+        payload = AliveCheckResponse(
+            SourceAddress=self.src_addr,
+        )
+        await self.write_request_raw(hdr, payload)
+
+    async def close(self) -> None:
+        logger.debug("Closing DoIP connection...")
+        if self._is_closed:
+            logger.debug("Already closed!")
+            return
+        self._is_closed = True
+        logger.debug("Cancelling read worker")
+        self._read_task.cancel()
+        self.writer.close()
+        logger.debug("Awaiting confirmation of closed writer")
+        try:
+            await self.writer.wait_closed()
+        except ConnectionError as e:
+            logger.debug(f"Exception while waiting for the writer to close: {e!r}")
+
+
+class DoIPConfig(BaseModel):
+    src_addr: int
+    target_addr: int
+    activation_type: int = RoutingActivationRequestTypes.WWH_OBD.value
+    protocol_version: int = ProtocolVersions.ISO_13400_2_2019
+
+    @field_validator(
+        "src_addr",
+        "target_addr",
+        "activation_type",
+        "protocol_version",
+        mode="before",
+    )
+    def auto_int(cls, v: str) -> int:
+        return auto_int(v)
+
+
+
+[docs] +class DoIPTransport(BaseTransport, scheme="doip"): + def __init__( + self, + target: TargetURI, + port: int, + config: DoIPConfig, + conn: DoIPConnection, + ): + super().__init__(target) + self.port = port + self.config = config + self._conn = conn + self._is_closed = False + + @staticmethod + async def _connect( # noqa: PLR0913 + hostname: str, + port: int, + src_addr: int, + target_addr: int, + activation_type: int, + protocol_version: int, + ) -> DoIPConnection: + conn = await DoIPConnection.connect( + hostname, + port, + src_addr, + target_addr, + protocol_version=protocol_version, + ) + await conn.write_routing_activation_request(RoutingActivationRequestTypes(activation_type)) + return conn + +
+[docs] + @classmethod + async def connect( + cls, + target: str | TargetURI, + timeout: float | None = None, + ) -> Self: + t = target if isinstance(target, TargetURI) else TargetURI(target) + cls.check_scheme(t) + + if t.hostname is None: + raise ValueError("no hostname specified") + + port = t.port if t.port is not None else 13400 + config = DoIPConfig(**t.qs_flat) + conn = await asyncio.wait_for( + cls._connect( + t.hostname, + port, + config.src_addr, + config.target_addr, + config.activation_type, + config.protocol_version, + ), + timeout, + ) + return cls(t, port, config, conn)
+ + +
+[docs] + async def reconnect(self, timeout: float | None = None) -> Self: + # It might be that the DoIP endpoint is not immediately ready for another + # connection, so set the timeout to 10s by default. + return await super().reconnect(10 if timeout is None else timeout)
+ + +
+[docs] + async def close(self) -> None: + if self._is_closed: + return + self._is_closed = True + await self._conn.close()
+ + +
+[docs] + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + data = await asyncio.wait_for(self._conn.read_diag_request(), timeout) + + t = tags + ["read"] if tags is not None else ["read"] + logger.trace(data.hex(), extra={"tags": t}) + return data
+ + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + t = tags + ["write"] if tags is not None else ["write"] + logger.trace(data.hex(), extra={"tags": t}) + + try: + await asyncio.wait_for(self._conn.write_diag_request(data), timeout) + except DoIPNegativeAckError as e: + if e.nack_code != DiagnosticMessageNegativeAckCodes.TargetUnreachable: + raise e + # TargetUnreachable can be just a temporary issue. Thus, we do not raise + # BrokenPipeError but instead ignore it here and let upper layers handle + # missing responses + logger.debug("DoIP message was ACKed with TargetUnreachable") + + return len(data)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/hsfz.html b/_modules/gallia/transports/hsfz.html new file mode 100644 index 000000000..d37f59736 --- /dev/null +++ b/_modules/gallia/transports/hsfz.html @@ -0,0 +1,505 @@ + + + + + + + + gallia.transports.hsfz — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.hsfz

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import asyncio
+import errno
+import struct
+import sys
+from dataclasses import dataclass
+from enum import IntEnum
+from typing import Any, Self
+
+from pydantic import BaseModel, field_validator
+
+from gallia.log import get_logger
+from gallia.transports.base import BaseTransport, TargetURI
+from gallia.utils import auto_int, handle_task_error, set_task_handler_ctx_variable
+
+logger = get_logger(__name__)
+
+
+class HSFZStatus(IntEnum):
+    UNDEFINED = -0x01
+    Data = 0x01
+    Ack = 0x02
+    Klemme15 = 0x10
+    Vin = 0x11
+    AliveCheck = 0x12
+    StatusDataInquiry = 0x13
+    IncorrectTesterAddressError = 0x40
+    IncorrectControlWordError = 0x41
+    IncorrectFormatError = 0x42
+    IncorrectDestinationAddressError = 0x43
+    MessageTooLarge = 0x44
+    ApplicationNotReady = 0x45
+    OutOfMemory = 0xFF
+
+    @classmethod
+    def _missing_(cls, value: Any) -> HSFZStatus:
+        return cls.UNDEFINED
+
+
+@dataclass
+class HSFZHeader:
+    Len: int
+    CWord: int
+
+    def pack(self) -> bytes:
+        return struct.pack("!IH", self.Len, self.CWord)
+
+    @classmethod
+    def unpack(cls, data: bytes) -> Self:
+        len_, cword = struct.unpack("!IH", data)
+        return cls(len_, cword)
+
+
+@dataclass
+class HSFZDiagReqHeader:
+    src_addr: int
+    dst_addr: int
+
+    def pack(self) -> bytes:
+        return struct.pack("!BB", self.src_addr, self.dst_addr)
+
+    @classmethod
+    def unpack(cls, data: bytes) -> HSFZDiagReqHeader:
+        src_addr, dst_addr = struct.unpack("!BB", data)
+        return cls(src_addr, dst_addr)
+
+
+HSFZFrame = tuple[HSFZHeader, HSFZDiagReqHeader | None, bytes | None]
+HSFZDiagFrame = tuple[HSFZHeader, HSFZDiagReqHeader, bytes]
+
+
+class HSFZConnection:
+    def __init__(
+        self,
+        reader: asyncio.StreamReader,
+        writer: asyncio.StreamWriter,
+        src_addr: int,
+        dst_addr: int,
+        ack_timeout: float = 1.0,
+    ):
+        self.reader = reader
+        self.writer = writer
+        self.src_addr = src_addr
+        self.dst_addr = dst_addr
+        self.ack_timeout = ack_timeout
+        self._read_queue: asyncio.Queue[HSFZDiagFrame | int] = asyncio.Queue()
+        self._read_task = asyncio.create_task(self._read_worker())
+        self._read_task.add_done_callback(
+            handle_task_error,
+            context=set_task_handler_ctx_variable(__name__, "HsfzReader"),
+        )
+        self._closed = False
+        self._mutex = asyncio.Lock()
+
+    @classmethod
+    async def connect(
+        cls,
+        host: str,
+        port: int,
+        src_addr: int,
+        dst_addr: int,
+        ack_timeout: float,
+    ) -> HSFZConnection:
+        reader, writer = await asyncio.open_connection(host, port)
+        return cls(
+            reader,
+            writer,
+            src_addr,
+            dst_addr,
+            ack_timeout,
+        )
+
+    async def _read_frame(self) -> HSFZFrame:
+        # Header is fixed size 6 byte.
+        hdr_buf = await self.reader.readexactly(6)
+        hdr = HSFZHeader.unpack(hdr_buf)
+
+        # If a message without a RequestHeader is received,
+        # the whole message must be read before erroring out.
+        # Otherwise the partial read packet stays in the receive
+        # buffer and causes further breakage…
+        if hdr.Len < 2:
+            data = None
+            if hdr.Len > 0:
+                data = await self.reader.readexactly(hdr.Len)
+            data_str = data.hex() if data is not None else data
+            logger.trace(f"hdr: {hdr}, req_hdr: None, data: {data_str}", extra={"tags": ["read"]})
+            return hdr, None, data
+
+        # DiagReqHeader is fixed size 2 byte.
+        req_buf = await self.reader.readexactly(2)
+        req_hdr = HSFZDiagReqHeader.unpack(req_buf)
+
+        data_len = hdr.Len - 2
+        data = await self.reader.readexactly(data_len)
+        logger.trace(
+            f"hdr: {hdr}, req_hdr: {req_hdr}, data: {data.hex()}",
+            extra={"tags": ["read"]},
+        )
+        return hdr, req_hdr, data
+
+    async def write_frame(self, frame: HSFZFrame) -> None:
+        hdr, req_hdr, data = frame
+        buf = b""
+        buf += hdr.pack()
+        log_msg = f"hdr: {hdr}"
+        if req_hdr is not None:
+            buf += req_hdr.pack()
+            log_msg += f", req_hdr: {req_hdr}"
+            if data is not None:
+                buf += data
+                log_msg += f", data: {data.hex()}"
+        self.writer.write(buf)
+        await self.writer.drain()
+
+        logger.trace(log_msg, extra={"tags": ["write"]})
+
+    async def _read_worker(self) -> None:
+        try:
+            while True:
+                hdr, req_hdr, data = await self._read_frame()
+
+                match hdr.CWord:
+                    case HSFZStatus.AliveCheck:
+                        await self.send_alive_msg()
+                        continue
+                    case HSFZStatus.Ack | HSFZStatus.Data:
+                        if req_hdr is None:
+                            logger.warning("unexpected frame: no hsfz request header")
+                            continue
+                        if data is None:
+                            logger.warning("unexpected frame: no payload")
+                            continue
+                        await self._read_queue.put((hdr, req_hdr, data))
+                    case _:
+                        await self._read_queue.put(hdr.CWord)
+                        continue
+
+        except asyncio.CancelledError:
+            logger.debug("read worker cancelled")
+        except asyncio.IncompleteReadError as e:
+            logger.debug(f"read worker received EOF: {e}")
+        except Exception as e:
+            logger.critical(f"read worker died: {e}")
+
+    async def _unpack_frame(self, frame: HSFZDiagFrame | int) -> HSFZDiagFrame:
+        # I little hack, but it is either a tuple or an int….
+        match frame:
+            case tuple():
+                return frame
+            case int():
+                await self.close()
+                raise BrokenPipeError(f"I can't even: {HSFZStatus(frame).name}")
+            case _:
+                raise RuntimeError(f"unexpected frame: {frame}")
+
+    async def read_frame(self) -> HSFZDiagFrame | int:
+        if self._closed:
+            if sys.platform != "win32":
+                raise OSError(errno.EBADFD)
+            else:
+                raise RuntimeError("connection already closed")
+
+        return await self._read_queue.get()
+
+    async def read_diag_request(self) -> bytes:
+        unexpected_packets = []
+        while True:
+            hdr, req_hdr, data = await self._unpack_frame(await self.read_frame())
+            if hdr.CWord != HSFZStatus.Data:
+                logger.warning(
+                    f"expected HSFZ data, instead got: {HSFZStatus(hdr.CWord).name} with payload {data.hex()}"
+                )
+                unexpected_packets.append((hdr, req_hdr, data))
+                continue
+            if req_hdr.src_addr != self.dst_addr or req_hdr.dst_addr != self.src_addr:
+                logger.warning(
+                    f"HSFZ Data has unexpected addresses (src:dst); should be {self.dst_addr:#04x}:{self.src_addr:#04x}, but is {req_hdr.src_addr:#04x}:{req_hdr.dst_addr:#04x}"
+                )
+                unexpected_packets.append((hdr, req_hdr, data))
+                continue
+
+            # We do not want to consume packets that we were not expecting; add them to queue again
+            for item in unexpected_packets:
+                await self._read_queue.put(item)
+
+            return data
+
+    async def _read_ack(self, prev_data: bytes) -> None:
+        unexpected_packets = []
+        while True:
+            hdr, req_hdr, data = await self._unpack_frame(await self.read_frame())
+            if hdr.CWord != HSFZStatus.Ack:
+                logger.warning(
+                    f"expected HSFZ Ack for {prev_data.hex()}, instead got: {HSFZStatus(hdr.CWord).name} with payload {data.hex()}"
+                )
+                unexpected_packets.append((hdr, req_hdr, data))
+                continue
+            if req_hdr.src_addr != self.src_addr or req_hdr.dst_addr != self.dst_addr:
+                logger.warning(
+                    f"HSFZ Ack has unexpected addresses (src:dst); should be {self.src_addr:#04x}:{self.dst_addr:#04x}, but is {req_hdr.src_addr:#04x}:{req_hdr.dst_addr:#04x}"
+                )
+                unexpected_packets.append((hdr, req_hdr, data))
+                continue
+            if prev_data[:5] != data:
+                logger.warning(
+                    f"HSFZ Ack has unexpected data of {data.hex()}, should be {prev_data[:5].hex()}"
+                )
+                unexpected_packets.append((hdr, req_hdr, data))
+                continue
+
+            # We do not want to consume packets that we were not expecting; add them to queue again
+            for item in unexpected_packets:
+                await self._read_queue.put(item)
+
+            return
+
+    async def write_diag_request_raw(
+        self,
+        hdr: HSFZHeader,
+        req_hdr: HSFZDiagReqHeader,
+        data: bytes,
+    ) -> None:
+        async with self._mutex:
+            await self.write_frame((hdr, req_hdr, data))
+
+            try:
+                # Now an ACK message is expected.
+                await asyncio.wait_for(self._read_ack(data), self.ack_timeout)
+            except TimeoutError as e:
+                await self.close()
+                raise BrokenPipeError("no ack by gateway") from e
+
+    async def write_diag_request(self, data: bytes) -> None:
+        hdr = HSFZHeader(Len=len(data) + 2, CWord=HSFZStatus.Data)
+        req_hdr = HSFZDiagReqHeader(src_addr=self.src_addr, dst_addr=self.dst_addr)
+        await self.write_diag_request_raw(hdr, req_hdr, data)
+
+    async def send_alive_msg(self) -> None:
+        hdr = HSFZHeader(Len=2, CWord=HSFZStatus.AliveCheck)
+        buf = b""
+        buf += hdr.pack()
+        # For reasons, the tester address is two bytes large in this path.
+        buf += struct.pack("!H", self.src_addr)
+
+        self.writer.write(buf)
+        await self.writer.drain()
+
+    async def close(self) -> None:
+        if self._closed:
+            return
+
+        self._closed = True
+        self._read_task.cancel()
+        self.writer.close()
+        await self.writer.wait_closed()
+
+
+class HSFZConfig(BaseModel):
+    src_addr: int
+    dst_addr: int
+    ack_timeout: int = 1000
+
+    @field_validator(
+        "src_addr",
+        "dst_addr",
+        mode="before",
+    )
+    def auto_int(cls, v: str) -> int:
+        return auto_int(v)
+
+
+
+[docs] +class HSFZTransport(BaseTransport, scheme="hsfz"): + def __init__( + self, + target: TargetURI, + port: int, + config: HSFZConfig, + conn: HSFZConnection, + ): + super().__init__(target) + self._conn = conn + self.port = port + +
+[docs] + @classmethod + async def connect( + cls, + target: str | TargetURI, + timeout: float | None = None, + ) -> HSFZTransport: + t = TargetURI(target) if isinstance(target, str) else target + if t.hostname is None: + raise ValueError("no hostname specified") + + port = t.port if t.port is not None else 6801 + config = HSFZConfig(**t.qs_flat) + conn = await HSFZConnection.connect( + t.hostname, + port, + config.src_addr, + config.dst_addr, + config.ack_timeout / 1000, + ) + return cls( + t, + port, + config, + conn, + )
+ + +
+[docs] + async def close(self) -> None: + await self._conn.close()
+ + +
+[docs] + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + return await asyncio.wait_for(self._conn.read_diag_request(), timeout)
+ + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + await asyncio.wait_for(self._conn.write_diag_request(data), timeout) + return len(data)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/isotp.html b/_modules/gallia/transports/isotp.html new file mode 100644 index 000000000..128a75a7e --- /dev/null +++ b/_modules/gallia/transports/isotp.html @@ -0,0 +1,338 @@ + + + + + + + + gallia.transports.isotp — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.isotp

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import asyncio
+import errno
+import socket as s
+import struct
+import sys
+from typing import Self
+
+assert sys.platform == "linux", "unsupported platform"
+
+from pydantic import BaseModel, field_validator
+
+from gallia.log import get_logger
+from gallia.transports.base import BaseTransport, TargetURI
+from gallia.utils import auto_int
+
+logger = get_logger(__name__)
+
+# Socket Constants not available in the socket module,
+# see linux/can/isotp.h
+# TODO: Can be removed in the future…
+# https://github.com/python/cpython/pull/23794
+SOL_CAN_ISOTP = s.SOL_CAN_BASE + s.CAN_ISOTP
+
+# Valuetypes for SOL_CAN_ISOTP
+CAN_ISOTP_OPTS = 1
+CAN_ISOTP_RECV_FC = 2
+CAN_ISOTP_TX_STMIN = 3
+CAN_ISOTP_RX_STMIN = 4
+CAN_ISOTP_LL_OPTS = 5
+
+# Flags for setsockopt CAN_ISOTP_OPTS
+CAN_ISOTP_LISTEN_MODE = 0x001
+CAN_ISOTP_EXTEND_ADDR = 0x002
+CAN_ISOTP_TX_PADDING = 0x004
+CAN_ISOTP_RX_PADDING = 0x008
+CAN_ISOTP_CHK_PAD_LEN = 0x010
+CAN_ISOTP_CHK_PAD_DATA = 0x020
+CAN_ISOTP_HALF_DUPLEX = 0x040
+CAN_ISOTP_FORCE_TXSTMIN = 0x080
+CAN_ISOTP_FORCE_RXSTMIN = 0x100
+CAN_ISOTP_RX_EXT_ADDR = 0x200
+
+
+class ISOTPConfig(BaseModel):
+    src_addr: int
+    dst_addr: int
+    is_extended: bool = False
+    is_fd: bool = False
+    frame_txtime: int = 10
+    ext_address: int | None = None
+    rx_ext_address: int | None = None
+    tx_padding: int | None = None
+    rx_padding: int | None = None
+    tx_dl: int = 64
+
+    @field_validator(
+        "src_addr",
+        "dst_addr",
+        "ext_address",
+        "rx_ext_address",
+        "tx_padding",
+        "rx_padding",
+        mode="before",
+    )
+    def auto_int(cls, v: str) -> int:
+        return auto_int(v)
+
+
+
+[docs] +class ISOTPTransport(BaseTransport, scheme="isotp"): + def __init__(self, target: TargetURI, config: ISOTPConfig, sock: s.socket) -> None: + super().__init__(target) + self._sock = sock + self.config = config + +
+[docs] + @classmethod + async def connect( + cls, + target: str | TargetURI, + timeout: float | None = None, + ) -> Self: + t = target if isinstance(target, TargetURI) else TargetURI(target) + cls.check_scheme(t) + + if t.hostname is None: + raise ValueError("empty interface") + + config = ISOTPConfig(**t.qs_flat) + sock = s.socket(s.PF_CAN, s.SOCK_DGRAM, s.CAN_ISOTP) + sock.setblocking(False) + + src_addr = cls._calc_flags(config.src_addr, config.is_extended) + dst_addr = cls._calc_flags(config.dst_addr, config.is_extended) + + cls._setsockopts( + sock, + frame_txtime=config.frame_txtime, + ext_address=config.ext_address, + rx_ext_address=config.rx_ext_address, + tx_padding=config.tx_padding, + rx_padding=config.rx_padding, + ) + # If CAN-FD is used, jumbo frames are possible. + # This fails for non-fd configurations. + if config.is_fd: + cls._setsockllopts(sock, canfd=config.is_fd, tx_dl=config.tx_dl) + + sock.bind((t.hostname, dst_addr, src_addr)) + + return cls(t, config, sock)
+ + + @staticmethod + def _calc_flags(can_id: int, extended: bool = False) -> int: + if extended: + return (can_id & s.CAN_EFF_MASK) | s.CAN_EFF_FLAG + return can_id & s.CAN_SFF_MASK + + @staticmethod + def _setsockopts( # noqa: PLR0913 + sock: s.socket, + frame_txtime: int, + tx_padding: int | None = None, + rx_padding: int | None = None, + ext_address: int | None = None, + rx_ext_address: int | None = None, + ) -> None: + flags = 0 + if ext_address is not None: + flags |= CAN_ISOTP_EXTEND_ADDR + else: + ext_address = 0 + + if rx_ext_address is not None: + flags |= CAN_ISOTP_RX_EXT_ADDR + else: + rx_ext_address = 0 + + if tx_padding is not None: + flags |= CAN_ISOTP_TX_PADDING + else: + tx_padding = 0 + + if rx_padding is not None: + flags |= CAN_ISOTP_RX_PADDING + else: + rx_padding = 0 + + data = struct.pack( + "@IIBBBB", + flags, + frame_txtime, + ext_address, + tx_padding, + rx_padding, + rx_ext_address, + ) + sock.setsockopt(SOL_CAN_ISOTP, CAN_ISOTP_OPTS, data) + + @staticmethod + def _setsockfcopts( + sock: s.socket, + bs: int = 0, + stmin: int = 0, + wftmax: int = 0, + ) -> None: + data = struct.pack("@BBB", bs, stmin, wftmax) + sock.setsockopt(SOL_CAN_ISOTP, CAN_ISOTP_RECV_FC, data) + + @staticmethod + def _setsockllopts(sock: s.socket, canfd: bool, tx_dl: int) -> None: + canmtu = 72 if canfd else 16 + # The flags are set to 0, since the author marks this as obsolete. + data = struct.pack("@BBB", canmtu, tx_dl, 0) + sock.setsockopt(SOL_CAN_ISOTP, CAN_ISOTP_LL_OPTS, data) + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + t = tags + ["write"] if tags is not None else ["write"] + logger.trace(data.hex(), extra={"tags": t}) + + loop = asyncio.get_running_loop() + await asyncio.wait_for(loop.sock_sendall(self._sock, data), timeout) + return len(data)
+ + +
+[docs] + async def read(self, timeout: float | None = None, tags: list[str] | None = None) -> bytes: + loop = asyncio.get_running_loop() + try: + data = await asyncio.wait_for(loop.sock_recv(self._sock, self.BUFSIZE), timeout) + except OSError as e: + if e.errno == errno.ECOMM: + raise BrokenPipeError(f"isotp flow control frame missing: {e}") from e + if e.errno == errno.EILSEQ: + raise BrokenPipeError(f"invalid consecutive frame numbers: {e}") from e + raise e + logger.trace(data.hex(), extra={"tags": tags}) + return data
+ + +
+[docs] + async def close(self) -> None: + self._sock.close()
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/schemes.html b/_modules/gallia/transports/schemes.html new file mode 100644 index 000000000..ff8b30986 --- /dev/null +++ b/_modules/gallia/transports/schemes.html @@ -0,0 +1,157 @@ + + + + + + + + gallia.transports.schemes — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.schemes

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+from enum import StrEnum, unique
+
+TCP = "tcp"
+TCP_LINES = "tcp-lines"
+DOIP = "doip"
+
+
+if sys.platform.startswith("linux"):
+
+    @unique
+    class TransportScheme(StrEnum):
+        TCP = TCP
+        TCP_LINES = TCP_LINES
+        DOIP = DOIP
+        HSFZ = "hsfz"
+        UNIX = "unix"
+        UNIX_LINES = "unix-lines"
+
+        ISOTP = "isotp"
+        CAN_RAW = "can-raw"
+
+
+if sys.platform == "win32":
+
+
+[docs] + @unique + class TransportScheme(StrEnum): + TCP = TCP + TCP_LINES = TCP_LINES + DOIP = DOIP + + FLEXRAY_RAW = "fr-raw" + FLEXRAY_TP_LEGACY = "fr-tp-legacy"
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/tcp.html b/_modules/gallia/transports/tcp.html new file mode 100644 index 000000000..77005ec01 --- /dev/null +++ b/_modules/gallia/transports/tcp.html @@ -0,0 +1,209 @@ + + + + + + + + gallia.transports.tcp — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.tcp

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import asyncio
+from typing import Self
+
+from gallia.log import get_logger
+from gallia.transports.base import BaseTransport, LinesTransportMixin, TargetURI
+
+logger = get_logger(__name__)
+
+
+
+[docs] +class TCPTransport(BaseTransport, scheme="tcp"): + def __init__( + self, + target: TargetURI, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + ) -> None: + super().__init__(target) + self.reader = reader + self.writer = writer + +
+[docs] + @classmethod + async def connect(cls, target: str | TargetURI, timeout: float | None = None) -> Self: + t = target if isinstance(target, TargetURI) else TargetURI(target) + cls.check_scheme(t) + + reader, writer = await asyncio.wait_for( + asyncio.open_connection(t.hostname, t.port), timeout + ) + return cls(t, reader, writer)
+ + +
+[docs] + async def close(self) -> None: + if self.is_closed: + return + self.is_closed = True + self.writer.close() + await self.writer.wait_closed()
+ + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + t = tags + ["write"] if tags is not None else ["write"] + logger.trace(data.hex(), extra={"tags": t}) + + self.writer.write(data) + await asyncio.wait_for(self.writer.drain(), timeout) + return len(data)
+ + +
+[docs] + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + data = await asyncio.wait_for(self.reader.read(self.BUFSIZE), timeout) + + t = tags + ["read"] if tags is not None else ["read"] + logger.trace(data.hex(), extra={"tags": t}) + return data
+
+ + + +
+[docs] +class TCPLinesTransport(LinesTransportMixin, TCPTransport, scheme="tcp-lines"): + def get_reader(self) -> asyncio.StreamReader: + return self.reader + + def get_writer(self) -> asyncio.StreamWriter: + return self.writer
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/gallia/transports/unix.html b/_modules/gallia/transports/unix.html new file mode 100644 index 000000000..633c524a4 --- /dev/null +++ b/_modules/gallia/transports/unix.html @@ -0,0 +1,207 @@ + + + + + + + + gallia.transports.unix — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for gallia.transports.unix

+# SPDX-FileCopyrightText: AISEC Pentesting Team
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import asyncio
+import sys
+from typing import Self
+
+assert sys.platform.startswith("linux"), "unsupported platform"
+
+from gallia.log import get_logger
+from gallia.transports.base import BaseTransport, LinesTransportMixin, TargetURI
+
+logger = get_logger(__name__)
+
+
+
+[docs] +class UnixTransport(BaseTransport, scheme="unix"): + def __init__( + self, + target: TargetURI, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + ) -> None: + super().__init__(target) + self.reader = reader + self.writer = writer + +
+[docs] + @classmethod + async def connect(cls, target: str | TargetURI, timeout: float | None = None) -> Self: + t = target if isinstance(target, TargetURI) else TargetURI(target) + cls.check_scheme(t) + + reader, writer = await asyncio.wait_for(asyncio.open_unix_connection(t.path), timeout) + + return cls(t, reader, writer)
+ + +
+[docs] + async def close(self) -> None: + if self.is_closed: + return + self.writer.close() + await self.writer.wait_closed()
+ + +
+[docs] + async def write( + self, + data: bytes, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> int: + t = tags + ["write"] if tags is not None else ["write"] + logger.trace(data.hex(), extra={"tags": t}) + self.writer.write(data) + await asyncio.wait_for(self.writer.drain(), timeout) + + return len(data)
+ + +
+[docs] + async def read( + self, + timeout: float | None = None, + tags: list[str] | None = None, + ) -> bytes: + data = await self.reader.read() + t = tags + ["read"] if tags is not None else ["read"] + logger.trace(data.hex(), extra={"tags": t}) + return data
+
+ + + +
+[docs] +class UnixLinesTransport(LinesTransportMixin, UnixTransport, scheme="unix-lines"): + def get_reader(self) -> asyncio.StreamReader: + return self.reader + + def get_writer(self) -> asyncio.StreamWriter: + return self.writer
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..b6cc5d182 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,131 @@ + + + + + + + + Overview: module code — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_sources/api.md.txt b/_sources/api.md.txt new file mode 100644 index 000000000..45d369b84 --- /dev/null +++ b/_sources/api.md.txt @@ -0,0 +1,49 @@ + + +# Public API + +## gallia.command + +```{eval-rst} +.. automodule:: gallia.command + :members: + :show-inheritance: +``` + +## gallia.log + +```{eval-rst} +.. automodule:: gallia.log + :members: + :show-inheritance: +``` + +## gallia.plugins + +```{eval-rst} +.. automodule:: gallia.plugins + :members: + :show-inheritance: +``` + +## gallia.services.uds + +```{eval-rst} +.. automodule:: gallia.services.uds + :members: + :show-inheritance: +``` + +## gallia.transports + +All available transports are documented in {doc}`../transports`. + +```{eval-rst} +.. automodule:: gallia.transports + :members: + :show-inheritance: +``` diff --git a/_sources/automation.md.txt b/_sources/automation.md.txt new file mode 100644 index 000000000..baf0bc04f --- /dev/null +++ b/_sources/automation.md.txt @@ -0,0 +1,67 @@ + + +# Automation +## Power Supply + +`gallia` has support for controlling power supplies either directly via builtin drivers. +Power supplies are mostly used for power cycling the current device under test. +There is no limit in accessing power supplies, e.g. voltage or current settings can be controlled as well. + +Own drivers can be included by implementing the {class}`gallia.power_supply.BasePowerSupply` interface. +On the commandline there is the `--power-supply` argument to specify a relevant power supply. +Further, there is `--power-cycle` to automatically power-cycle the device under test. +There is an experimental cli tool `netzteil` included in `gallia`. +This cli tool can be used to control all supported power supplies via the cli. + +The argument for `--power-supply` is a URI of the following form: + +``` text +SCHEME://HOST:PORT/PATH?channel=CHANNEL?product_id=PRODUCT_ID +``` + +Some schemes might take additional arguments in the query string. + +SCHEME +: The used protocol scheme; could be `tcp`, `http`, … + +HOST:PORT +: For e.g. `tcp` or `https` this is the relevant host and port. + +PATH +: If the power supply is exposed as a local file, this might be the path. + +channel +: The relevant channel where the device is connected; the master channel is `0`. + +product_id +: The product_id of the used power supply. + +## Supported Power Supplies + +Power supplies are chosen depending on the `product_id` setting in the URI. +The following power supplies are supported. + +### [R&S®HMC804x](https://www.rohde-schwarz.com/de/produkt/hmc804x-produkt-startseite_63493-61542.html) + +product_id +: `hmc804` + +scheme +: `tcp` + +HOST +: IP address + +PORT +: TCP port, the device most likely uses `5025` + +Example: + +``` +tcp://192.0.2.5:5025?product_id=hmc804 +``` + diff --git a/_sources/config.md.txt b/_sources/config.md.txt new file mode 100644 index 000000000..7513c102d --- /dev/null +++ b/_sources/config.md.txt @@ -0,0 +1,66 @@ + + +# Configuration + +## gallia.toml + +All `gallia` settings stem from the commandline interface. +The documentation for all available settings per subcommand is available via `-h/--help`. +Frequently used settings can be put in a configfile which is called `gallia.toml`. +Settings from the config file set the **default** of the respective commandline option. +The config can always be overwritten by manually setting the relevant cli option. + +The configuration file `gallia.toml` is written in [TOML](https://toml.io/en/). +Inheritence is not supported; the first file is loaded. +The `gallia.toml` file is loaded from these locations (in this particular order): + +* path specified in the env variable `GALLIA_CONFIG`; see {doc}`../env`. +* current directory +* current Git root (if the current directory is a Git repository) +* `$XDG_CONFIG_HOME/gallia/gallia.toml` +* `~/.config/gallia/gallia.toml` + +Only some cli options are exposed to the config file. +The available config settings can be obtained from `gallia --template`. +The output of `--template` is maintained to be up to date and is intended as a starting point. + +## Hooks + +`gallia` supports hooks for preparation or cleanup/postprocessing tasks. +Alternatively, they can be useful for e.g. sending notifications about the exit_code via e.g. matrix or ntfy.sh. +Hooks are shell scripts which are executed before (= pre-hook) or after (= post-hook) the `main()` method. +These scripts can be specified via `--pre-hook` or `--post-hook` or via `gallia.toml` as well. + +The hook scripts have these environment variables set; some are optional and hook scripts are encouraged to check their presence before accessing them: + +GALLIA_HOOK +: Either `pre` or `post`. + +GALLIA_ARTIFACTS_DIR +: Path to the artifactsdir for the current testrun. + +GALLIA_EXIT_CODE (post) +: Is set to the exit_code which `gallia` will use after the hook terminates. + For instance GALLIA_EXIT_CODE different from zero means that the current testrun failed. + +GALLIA_META (post) +: Contains the JSON encoded content of `META.json`. + +GALLIA_INVOCATION +: The content os `sys.argv`, in other words the raw invocation of `gallia`. + +GALLIA_GROUP (optional) +: Usually the first part of the command on the cli. For instance, for `gallia scan uds identifiers` + `GALLIA_GROUP` is `scan`. + +GALLIA_SUBGROUP (optional) +: Usually the second part of the command on the cli. For instance, for `gallia scan uds identifiers` + `GALLIA_GROUP` is `uds`. + +GALLIA_COMMAND (optional) +: Usually the last part of the command on the cli. For instance, for `gallia scan uds identifiers` + `GALLIA_COMMAND` is `identifiers`. diff --git a/_sources/env.md.txt b/_sources/env.md.txt new file mode 100644 index 000000000..8264b5ed6 --- /dev/null +++ b/_sources/env.md.txt @@ -0,0 +1,24 @@ + + +# Environment Variables + +For some cases `gallia` can be configured with environment variables. +`gallia`-specific variables begin with `GALLIA_`. + +GALLIA_CONFIG +: The path to the config file usually called `gallia.toml`. + Disables autodiscovery of the config. + +GALLIA_LOGLEVEL +: When {meth}`gallia.log.setup_logging()` is called without an argument this environment variable is read to set the loglevel. + Supported value are: `trace`, `debug`, `info`, `notice`, `warning`, `error`, `critical`. + As an alternative, the int values from 0 to 7 can be used. + Mostly useful in own scripts or tests. + This variable is not read when using the gallia cli. + +NO_COLOR +: If this variable is set, `gallia` by default does not use color codes, see: https://no-color.org/ diff --git a/_sources/index.md.txt b/_sources/index.md.txt new file mode 100644 index 000000000..ac16794ba --- /dev/null +++ b/_sources/index.md.txt @@ -0,0 +1,60 @@ + + +# Gallia -- Extendable Pentesting Framework + +```{warning} +This project is intended for research and development usage only! +Inappropriate usage might cause irreversible damage to the device under test. +We do not take any responsibility for damage caused by the usage of this tool. +``` + +[Gallia](https://github.com/Fraunhofer-AISEC/gallia) is an extendable pentesting framework with the focus on the automotive domain. +The scope of the toolchain is conducting penetration tests from a single ECU up to whole cars. +Currently, the main focus lies on the [UDS](https://www.iso.org/standard/72439.html) interface. +Acting as a generic interface, the [logging](https://fraunhofer-aisec.github.io/gallia/logging.html) functionality implements reproducible tests and enables post-processing tasks. + +---- + +```{toctree} +:maxdepth: 1 +:caption: Usage + +setup +automation +config +transports +env +logging +plugins +``` + +---- + +```{toctree} +:maxdepth: 1 +:caption: UDS + +uds/database +uds/scan_modes +uds/virtual_ecu +``` + +The current main focus of `gallia` is the UDS protocol. +Several concepts and ideas are implemented in `gallia` in order to provide comprehensive tests. + +---- + +```{toctree} +:maxdepth: 1 +:caption: API + +api +``` + +`gallia` is designed as a pentesting framework where each test produces a lot of data. +It is possible to design own standalone tools or plugins utilizing the `gallia` Python modules. + diff --git a/_sources/logging.md.txt b/_sources/logging.md.txt new file mode 100644 index 000000000..829ee186a --- /dev/null +++ b/_sources/logging.md.txt @@ -0,0 +1,46 @@ + + +# Logging + +## Concept + +`gallia` uses structured structured logging implemented as line separated JSON records. +Each scanner creates a `artifacts_dir` under `artifacts_base`, which contains a zstd compressed logfile `log.json.zst`. +The logfile is created with loglevel `DEBUG`; for debugging purposes loglevel `TRACE` can be enabled with the setting `trace_log`. +Logfiles can be displayed with the `hr` tool which is included in `gallia`. + +The generic interface which represents a logrecord is {class}`gallia.log.PenlogRecord`. +The generic interface which is used to read a logfile {class}`gallia.log.PenlogReader`. + +## API + +`gallia` uses the [`logging`](https://docs.python.org/3/library/logging.html) module. +The loglevels `TRACE` and `NOTICE` have been added to the module. + +In own scripts {meth}`gallia.log.setup_logging` needs to be called as early as possible. +For creating a {class}`gallia.log.Logger`, there is {meth}`gallia.log.get_logger`. + +``` python +from gallia.log import get_logger, setup_logging, Loglevel + +# The logfile's loglevel is Loglevel.DEBUG. +# It can be set with the keyword argument file_level. +setup_logging(level=Loglevel.INFO) +logger = get_logger(__name__) +logger.info("hello world") +logger.debug("hello debug") +``` + +If processing of a logfile is needed, here is a minimal example; for custom functionality see {class}`gallia.log.PenlogReader` and {meth}`gallia.log.PenlogReader.records`. + +``` python +from gallia.log import PenlogReader + +reader = PenlogReader("/path/to/logfile") +for record in reader.records() + print(record) +``` diff --git a/_sources/plugins.md.txt b/_sources/plugins.md.txt new file mode 100644 index 000000000..b6db57e24 --- /dev/null +++ b/_sources/plugins.md.txt @@ -0,0 +1,64 @@ + + +# Plugins +## Entry Points + +`gallia` uses the [`entry_points` mechanism](https://docs.python.org/3/library/importlib.metadata.html#entry-points) for registering plugins. +These entry points are known by `gallia`: + +`gallia_commands` +: List of subclasses of {class}`gallia.command.BaseCommand` add new a command to the CLI. + +`gallia_cli_init` +: List of callables which get called during the initialization phase of the `ArgumentParser`; can be used to add new groups to the CLI. + +`gallia_transports` +: List of subclasses of {class}`gallia.transports.BaseTransport` add a new URI scheme for the `--target` flag. + +`gallia_uds_ecus` +: List of subclasses of {class}`gallia.services.uds.ECU` which add new choices for the `--oem` flag. + +## Example + +Below is an example that adds a new command to the CLI (using {class}`gallia.command.Script`). +Let's assume the following code snippet lives in the python module `hello.py` within the `hello_gallia` package. + +``` python +from argparse import Namespace + +from gallia.command import Script + + +class HelloWorld(Script): + """A hello world script showing gallia's plugin API.""" + + COMMAND = "hello" + SHORT_HELP = "say hello to the world" + + + def main(self, args: Namespace) -> None: + print("Hello World") + + +commands = [HelloWorld] +``` + +In `pyproject.toml` using `poetry` the following entry_point needs to be specified: + +``` toml +[tool.poetry.plugins."gallia_commands"] +"hello_world_commands" = "hello_gallia.hello:commands" +``` + +After issueing `poetry install`, the script can be called with `gallia script hello`. + +If a standalone script is desired, the `HelloWorld` class can be called like this: + +``` python +parser = argparse.ArgumentParser() +sys.exit(HelloWorld(parser).entry_point(parser.parse_args())) +``` diff --git a/_sources/setup.md.txt b/_sources/setup.md.txt new file mode 100644 index 000000000..e36286453 --- /dev/null +++ b/_sources/setup.md.txt @@ -0,0 +1,128 @@ + + +# Setup +## Dependencies + +This project has the following system level dependencies: + +* [Linux](https://kernel.org) >= 5.10 +* [Python](https://python.org) (latest and latest - 1) +* [uv](https://docs.astral.sh/uv/) (optional, for development) +* [dumpcap](https://www.wireshark.org/docs/man-pages/dumpcap.html) (optional, part of [wireshark](https://www.wireshark.org/)) + +Python dependencies are listed in `pyproject.toml`. + +## Install + +An overview of software repos where `gallia` is available is provided by [repology.org](https://repology.org/project/gallia/versions). + +### Docker + +Docker images are published via the [Github Container registry](https://github.com/Fraunhofer-AISEC/gallia/pkgs/container/gallia). + +### Arch Linux + +``` shell-session +$ paru -S gallia +``` + +### Debian/Ubuntu + +``` shell-session +$ sudo apt install pipx +$ pipx install gallia +``` + +### NixOS + +``` shell-session +$ nix shell nixpgks#gallia +``` + +For persistance add `gallia` to your `environment.systemPackages`, or when you use `home-manager` to `home.packages`. + +### Generic + +``` shell-session +$ pipx install gallia +``` + +### Without Install + +The `uvx` tool is provided by `uv`. + +``` shell-session +$ uvx gallia +``` + +## Development + +[uv](https://docs.astral.sh/uv/) is used to manage dependencies. + +### Clone repository + +```shell-session +$ git clone https://github.com/Fraunhofer-AISEC/gallia.git +``` + +### Environment + +`uv` manages the project environment, including the python version. +All `uv` commands must be invoked within the `gallia` repository. + +```shell-session +$ pipx install uv +$ uv sync +``` + +If you want to use a different Python version from the one defined in `.python-version`, the flags `--python-preference only-system` or `--python` for `uv sync` might be helpful; e.g. to use your system provided Python 3.11: + +```shell-session +$ uv sync --python-preference only-system --python 3.11 +``` + +#### shell + +Enable the venv under `.venv` manually by sourcing: + +``` shell-session +$ source .venv/bin/activate +$ source .venv/bin/activate.fish +``` + +#### run + +Run a single command inside the venv without changing the shell environment: + +```shell-session +$ uv run gallia +``` + +## Development with Plugins + +If you want to develop gallia and plugins at the same time, then you need to add `gallia` as a dependency to your plugin package. + +### Shell Completion +#### bash + +```shell-session +# register-python-argcomplete gallia > /etc/bash_completion.d/gallia +``` + +#### fish + +```shell-session +$ mkdir -p ~/.config/fish/completions +$ register-python-argcomplete --shell fish gallia > ~/.config/fish/completions/gallia.fish +``` + +### IDE Integration + +Just use [LSP](https://microsoft.github.io/language-server-protocol/). +Most editors (e.g. [neovim](https://neovim.io/)) support the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). +The required tools are listed as development dependencies in `pyproject.toml` and are automatically managed by `uv`. +Please refer to the documentation of your text editor of choice for configuring LSP support. diff --git a/_sources/transports.md.txt b/_sources/transports.md.txt new file mode 100644 index 000000000..dc8250fb9 --- /dev/null +++ b/_sources/transports.md.txt @@ -0,0 +1,152 @@ + + +# Transports + +All scanner share the same basic connection args. +Transports are subclasses of {class}`gallia.transports.BaseTransport`. + +## URIs + +The argument `--target` specifies **all** parameters which are required to establish a connection to the device under test. +The argument to `--target` is specified as a URI. +An URI consists of these components: + +``` text +scheme://host:port?param1=foo¶m2=bar + |location | +``` + +The parameters support: string, int (use 0x prefix for hex values) and bool (true/false) values. +The relevant transport protocol is specified in the scheme. + +### isotp + +ISO-TP (ISO 15765-2) as provided by the Linux [socket API](https://www.kernel.org/doc/html/latest/networking/can.html). + +The can interface is specified as a host, e.g. `can0`. +The following parameters are available (these are ISOTP transport settings): + +`src_addr` (required) +: The ISOTP source address as int. + +`dst_addr` (required) +: The ISOTP destination address as int. + +`is_extended` (optional) +: Use extended CAN identifiers. + +`is_fd` (optional) +: Use CAN-FD frames. + +`frame_txtime` (optional) +: The time in milliseconds the kernel waits before sending a ISOTP consecutive frame. + +`ext_address` (optional) +: The extended ISOTP address as int. + +`rx_ext_address` (optional) +: The extended ISOTP rx address. + +`tx_padding` (optional) +: Use padding in sent frames. + +`rx_padding` (optional) +: Expect padding in received frames. + +`tx_dl` (optional) +: CAN-FD max payload size. + +Example: + +``` text +isotp://can0?src_addr=0x6f4&dst_addr=0x654&rx_ext_address=0xf4&ext_address=0x54&is_fd=false +``` + +### can-raw + +`src_addr` (required) +: The ISOTP source address as int. + +`dst_addr` (required) +: The ISOTP destination address as int. + +`is_extended` (optional) +: Use extended CAN identifiers. + +`is_fd` (optional) +: Use CAN-FD frames. + +Example: + +``` text +can-raw://can1?is_fd=true +``` + +### doip + +The DoIP gateway address is specified in the location. + +`src_addr` (required) +: The source address as int. + +`target_addr` (required) +: The target address as int. + +Example: + +``` text +doip://169.254.100.100:13400?src_addr=0x0e00&target_addr=0x1d +``` + +### hsfz + +The gateway address is specified in the location. + +* `src_addr` (required): The source address as int. +* `dst_addr` (required): The destination address as int. +* `ack_timeout`: Specify the HSFZ acknowledge timeout in ms. +* `nocheck_src_addr`: Do not check the source address in received HSFZ frames. +* `nocheck_dst_addr`: Do not check the destination address in received HSFZ frames. + +Example: + +``` text +hsfz://169.254.100.100:6801?src_addr=0xf4&dst_addr=0x1d +``` + +### tcp-lines + +A simple tcp based transport. +UDS messages are sent linebased in ascii hex encoding. +Mainly useful for testing. + +Example: + +``` text +tcp-lines://127.0.0.1:1234 +``` + + +## API + +Transports can also be used in own standalone scripts; transports are created with the `.connect()` method which takes a URI. + +``` python +import asyncio + +from gallia.log import setup_logging +from gallia.transports import DOiPTransport + + +async def main(): + transport = await DOiPTransport.connect("doip://192.0.2.5:13400?src_addr=0xf4&dst_addr=0x1d") + await transport.write(bytes([0x10, 0x01])) + + +setup_logging() +asyncio.run(main()) +``` diff --git a/_sources/uds/database.md.txt b/_sources/uds/database.md.txt new file mode 100644 index 000000000..bdcdc59eb --- /dev/null +++ b/_sources/uds/database.md.txt @@ -0,0 +1,11 @@ + + +# Database + +This documentation describes the database layout gallia uses for storing its results. + +**Work in progress** \ No newline at end of file diff --git a/_sources/uds/scan_modes.md.txt b/_sources/uds/scan_modes.md.txt new file mode 100644 index 000000000..99c29a419 --- /dev/null +++ b/_sources/uds/scan_modes.md.txt @@ -0,0 +1,152 @@ + + +# Scan Modes + +A UDS scan usually covers multiple phases: + +1. Searching for ECUs on the relevant transport: **Discovery Scan** +2. Searching for UDS sessions on the found ECUs: **Session Scan** +3. Searching for UDS services on the found ECUs: **Service Scan** +4. Searching for UDS identifiers in discovered UDS services: **Identifier Scan** +5. Additional service specific scans, such as **Memory Scan**, **Fuzzing**, … + +## Discovery Scan + +Discovery scans are specific for the underlying transport, such as DoIP or ISO-TP. +The idea is crafting a valid UDS payload which is valid and at least some answer is expected. +A well working payload is `1001` which is a request to the DiagnosticSessionControl service. +This request instructs an ECU to change to the so called DefaultSession. +The DiagnosticSessionControl service and the DefaultSession should be always available, thus this payload is a good candidate for a discovery scan. +Payloads different from `1001` can be used as well; for instance `1003` to enable the ExtendedDiagnosticSession (session id `0x03`). +Another well working example is `3E00`, the TesterPresent service. + +The **addressing** of the ECU is provided by the underlying transport protocol. +Most of the time there are two addresses: the tester address and the ECU address. +The basic idea of a discovery scan is sending a valid UDS payload to all valid ECU addresses with a fixed tester address. +When a valid answer is received an ECU has been found. + +### DoIP + +The [Diagnostics Over Internet Protocol (DoIP)](https://www.iso.org/standard/74785.html) is a application level protocol on top of TCP enabling tunneling UDS messages. +As an advantage all features of modern operating systems can be used. +Furthermore, no custom hardware, such as expensive CAN bus adapters are required. + +```{note} +The user needs to provide a DHCP server enabling the communication stack on the DoIP gateway's side. +``` + +DoIP has three parameters which are required to establish a communication channel to an ECU: + +1. **SourceAddress**: In the automotive chargon also called *tester address*. It is often static and set to `0x0e00`. +2. **TargetAddress**: This is the address of a present ECU. This parameter needs to be discovered. +3. **RoutingActivationType**: DoIP provides further optional layers of authentication which can e.g. be vendor specific. Since `gallia` needs UDS endpoints this is most likely `WWH_OB`, which is a symbolic identifier for the constant `0x01`. Optionally, the RoutingActivationType can be scanned in order to find vendor specific routing methods. + +Assuming DHCP and the networking setup is running properly, the DoIP connection establishment looks like the following: + +1. TCP connect to the DoIP gateway. The IP address of the gateway is set by the mentioned user controlled DHCP server. +2. Sending a RoutingActivationRequest; the gateway must acknowledge this. +3. UDS requests can be tunneled to arbitrary ECUs using appropriate DoIP `DiagnosticMessage` packets. + +The mentioned `DiagnosticMessage` packets contain a `SourceAddress`, a `TargetAddress`, and a `UserData` field. +For the discovery scan on DoIP first step 1. and 2. from the described connection establishment process are performed. +Subsequently, a valid UDS message (c.f. previous section) is the put in the `UserData` field and the `TargetAddress` field is iterated. +When a valid answer is received an ECU has been found; when the gateway sends a NACK or just timeouts, no ECU is available on the tried `TargetAddress`. + +### ISO-TP + +[ISO-TP](https://www.iso.org/standard/66574.html) is a standard for a transport protocol on top of the [CAN bus](https://www.iso.org/standard/63648.html) system. +The CAN bus is a field bus which acts as a broadcast medium; any connected participant can read all messages. +On the CAN bus there is no concept of a connection. +Typically, there are cyclic messages on the CAN bus which are important for selected participants. +However, in order to implement a connection channel for the UDS protocol (which is required by law to be present in vecicles) the ISO-TP standard comes into play. +In contrast to DoIP special CAN hardware is required. +The ISO-TP protocol and the interaction with CAN interfaces is handled by the [networking stack](https://www.kernel.org/doc/html/latest/networking/can.html) of the Linux kernel. + +For a discovery scan it is important to distinguish whether the tester is connected to a filtered interface (e.g. the OBD connector) or to an unfiltered interface (e.g. an internal CAN bus). +In order to not confuse the discovery scanner, the so called *idle traffic* needs to be observed. +The idle traffic consists of the mentioned cyclic messages of the can bus. +Since there is no concept of a connection on the CAN bus itself and the parameters for the ISO-TP connection are unknown at the very first stage, an educated guess for a deny list is required. +Typically, `gallia` waits for a few seconds and observes the CAN bus traffic. +Subsequently, a deny filter is configured which filters out all CAN IDs seen in the idle traffic. + +ISO-TP provides multiple different addressing methods: +* normal addressing with normal CAN IDs, +* normal addressing with extended CAN IDs, +* extended addressing with normal CAN IDs, +* extended addressing with extended CAN IDs, +* the mentioned schemes but with CAN-FD below, +* … + +```{note} +For the detailed explanation of all these addressing modes we refer to the relevant ISO standard documents or further documents or presentations which are available online. +``` + +The tester needs to make assuptions about what addressing scheme is used; otherwise the scan does not yield any results. +ISO-TP provides the following parameters: + +* **source can_id**: + * Without extended addressing: Often set to an address with a static offset to the destination can_id. + * Extended addressing: Often set to a static value, e.g. `0x6f1`; a.k.a. *tester address*. +* **destination can_id**: + * Without extended addressing: Address of the ECU + * Extended addressing: Somehow part of the ECU address, e.g. `0x600 | ext_address` +* **extended source address**: When extended addressing is in use, often set to a static value, e.g. `0xf1`. +* **extended destination address**: When extended addressing is in use, it is the address of the ECU. + +The discovery procedure is dependend on the used addressing scheme. +From a high level perspective, the destination id is iterated and a valid payload is sent. +If a valid answer is received, an ECU has been found. + +## Session Scan + +UDS has the concept of sessions. +Different sessions can for example offer different services. +A session is identified by a 1 byte session ID. +The UDS standard defines a set of well known session IDs, but vendors are free to add their own sessions. +Some sessions might only be available from a specific ECU state (e.g. current session, enabled/configured ECU features, coding, ...). +Most of those preconditions cannot be detected automatically and might require vendor specific knowledge. + +The session scan tries to find all available session transitions. +Starting from the default session (0x01), all session IDs are iterated and enabling the relevant session is tried. +If the ECU replies with a positive response, the session is available. +In case of a negative response, the session is considered not available from the current state. +To detect sessions, which are only reachable from a session different to the default session, a recursive approach is used. +The scan for new sessions starts at each previously identified session. +The maximum depth is limited to avoid endless scans in case of transition cycles, such as `0x01 -> 0x03 -> 0x05 -> 0x03`. +The scan is finished, if no new session transition is found. + +## Service Scan + +The service scan operates at the UDS protocol level. +UDS provides several endpoints called *services*. +Each service has an identifier and a specific list of arguments or sub-functions. + +In order to identify available services, a reverse matching is applied. +According to the UDS standard, ECUs reply with the error codes `serviceNotSupported` or `serviceNotSupportedInActiveSession` when an unimplemented service is requested. +Therefore, each service which responds with a different error code is considered available. +To address the different services and their varying length of arguments and sub-functions the scanner automatically appends `\x00` bytes if the received response was `incorrectMessageLengthOrInvalidFormat`. + +## Identifier Scan + +The identifier scan operates at the UDS protocol level; to be more specific it operates at the level of a specific UDS service. +Most UDS services need identifiers is input arguments. +For instance, the ReadDataByIdentifier service requires a data identifier input for the requested ressource. +In order to find out the available identifiers for a specific service the Identifier Scan is employed. + +In order to identify available data identifiers, a reverse matching is applied. +According to the UDS standard, ECUs reply with the error codes `serviceNotSupported` or `serviceNotSupportedInActiveSession` when an unimplemented service is requested. +If the ECU responds with any of `serviceNotSupported`, `serviceNotSupportedInActiveSession`,`subFunctionNotSupported`, `subFunctionNotSupportedInActiveSession`, or `requestOutOfRange` the identifier is considered not available. + +A few services such as RoutineControl offer a `subFunction` as well. +SubFunction arguments can be discovered with the same technique but the error codes for the reverse matching are different. +For discovering available subFunctions the following error codes indicate the subFunction is not available: `serviceNotSupported`, `serviceNotSupportedInActiveSession`, `subFunctionNotSupported`, or `subFunctionNotSupportedInActiveSession`. + +Each identifier or subFunction which responds with a different error code is considered available. + +## Memory Scan + +TODO diff --git a/_sources/uds/virtual_ecu.md.txt b/_sources/uds/virtual_ecu.md.txt new file mode 100644 index 000000000..3966835c9 --- /dev/null +++ b/_sources/uds/virtual_ecu.md.txt @@ -0,0 +1,173 @@ + + +# Virtual ECUs + +For testing purposes, there exists the possibility to spawn virtual ECUs, against which the scanners can be run. +The virtual ECUs can however also be used independently of the remaining Gallia tools. + +The generic command to create a virtual ECU is as follows: + +```shell-session +$ gallia script vecu [vecu-arguments] [model-arguments] +``` + +The virtual ECUs support different transport schemes and answering models, +which are explained in the following sections. + +## transport + +The virtual ECU model is separated from the transport layer that is used for communication. +Currently, two different transport types are supported. +For each of them, a corresponding transport scheme exists on the scanner side, +which has to be used to enable communication between the scanner and the virtual ECU. + +### tcp-lines + +The transport scheme which is the easiest to use is the tcp-lines scheme. +It requires no additional setup and can be used immediately. +When using this transport scheme, the virtual ECU can handle requests from multiple UDS clients, +but conversely a scanner can only talk to a single virtual ECU. +For most scanners this is the intended behavior. +For discovery scanners instead, the tcp-lines transport is not suitable. + +For example, a random virtual ECU, which uses the tcp-lines protocol for communication +and listens for IPv4 connections on port 20162 can be started with the following command: + +```shell-session +$ gallia script vecu "tcp-lines://127.0.0.1:20162" rng +``` + +For IPv6, the command would look as follows: + +```shell-session +$ gallia script vecu "tcp-lines://[::1]:20162" rng +``` + +### iso-tp + +The iso-tp scheme operates on an existing can interface, which can be a physical interface or a virtual interface. +The following commands can be used to set up a virtual CAN interface with the name *vcan0*: + +```shell-session +# ip link add dev vcan0 type vcan +# ip link set up vcan0 +``` + +In contrast to the tcp-lines approach, +using the iso-tp transport scheme allows to simulate a whole bus of several virtual ECUs, +and is therefore also suitable for testing discovery scanners. + +For example, two random virtual ECUs, which uses the iso-tp protocol for communication +and use the *vcan0* interface can be started with the following commands: + +```shell-session +$ gallia script vecu "isotp://vcan0?src_addr=0x6aa&dst_addr=0x6f4&rx_ext_address=0xaa&ext_address=0xf4&is_fd=false" rng +$ gallia script vecu "isotp://vcan0?src_addr=0x6bb&dst_addr=0x6f4&rx_ext_address=0xbb&ext_address=0xf4&is_fd=false" rng +``` + +## model + +There are currently two different types of ECU models, which can be used together with any of the supported transports. + +### rng + +This type of model, creates an ECU, with a random set of supported sessions, services, sub-functions and identifiers, +where applicable. +By default, this model makes use of all available default behaviors, +thereby offering a very standard conformant behavior out of the box. +As with any model, these default mechanisms can be disabled via the *vecu-options*. +It can be further customized by specifying mandatory as well as optional sessions and services. +Additionally, the probabilities which are used to compute the previously mentioned set of available +functionality can be altered. + +For example, a random virtual ECU with no customization can be created with the following command: + +```shell-session +$ gallia vecu "tcp-lines://127.0.0.1:20162" rng +Storing artifacts at ... +Starting ... +Initialized random UDS server with seed 1623483675214623782 + "0x01": { + "0x01 (ShowCurrentData)": null, + "0x02 (ShowFreezeFrameData)": null, + "0x04 (ClearDiagnosticTroubleCodesAndStoredValues)": null, + "0x07 (ShowPendingDiagnosticTroubleCodes)": null, + "0x09 (RequestVehicleInformation)": null, + "0x10 (DiagnosticSessionControl)": "['0x01']", + "0x14 (ClearDiagnosticInformation)": null, + "0x27 (SecurityAccess)": "['0x29', '0x2a', '0x3f', '0x40', '0x63', '0x64']", + "0x31 (RoutineControl)": "['0x01', '0x02', '0x03']", + "0x3d (WriteMemoryByAddress)": null + } +} +``` + +After the startup, the output shows an overview of the supported sessions, services and sub-functions. +In this case only one session with ten services is offered. + +To enable reproducibility, at the startup, the output shows the seed, which has been used to initialize the random +number generator. +Using the same seed in combination with the same arguments, one can recreate the same virtual ECU: + +```shell-session +gallia vecu "tcp-lines://127.0.0.1:20162" rng --seed +``` + +The following command shows how to control the selection of services, by altering the mandatory and optional services, +as well as the probability that determines, how likely one of the optional services is included. +The same is possible for services. +For other categories, only the probabilities can be changed. + +```shell-session +$ gallia vecu "tcp-lines://127.0.0.1:20162" rng --mandatory_services "[DiagnosticSessionControl, ReadDataByIdentifier]" --optional_services "[RoutineControl]" --p_service 0.5 +Storing artifacts at ... +Starting ... +Initialized random UDS server with seed 222579011926250596 +{ + "0x01": { + "0x10 (DiagnosticSessionControl)": "['0x01', '0x46']", + "0x22 (ReadDataByIdentifier)": null, + "0x31 (RoutineControl)": "['0x01', '0x02', '0x03']" + }, + "0x46": { + "0x10 (DiagnosticSessionControl)": "['0x01']", + "0x22 (ReadDataByIdentifier)": null + } +} +``` + +### db + +This type of model creates a virtual ECU that mimics the behavior of an already scanned ECU. +This functionality is based on the database logging of the scanners. +For each request that the virtual ECU receives, it looks up if there is a corresponding request and response pair in +the database, which also satisfies other conditions, such as a matching ECU state. +By default, it will return either the corresponding response, or no response at all, if no such pair exists. +As with any model, default ECU mechanisms, such as a default request or correct handling of the +suppressPosRespMsgIndicationBit are supported, but are disabled by default. +They might be particularly useful to compensate for behavior, that is not intended to be handled explicitly, +while being able to include the corresponding ECU in the testing procedure. + +When using the db model, the virtual ECU requires the path of the database to which scan results have been +logged for the corresponding ECU. +For example, a virtual ECU that mimics the behavior of an ECU, +that was logged in */path/to/db* can be created with the following command: + +```shell-session +$ gallia vecu "tcp-lines://127.0.0.1:20162" db /path/to/db +``` + +If the database contains logs of multiple ECUs, one particular ECU can be chosen by its name. +Note, that the name is not filled in automatically +and may have to be manually added and referenced in one or more addresses. +Additionally, it is possible to include only those scan runs, for which the ECU satisfied a set of properties. +For example, the following command creates a virtual ECU that mimics the behavior of the ECU *XYZ* based on the logs +in */path/to/db* where the *software_version* was *1.2.3*: + +```shell-session +$ gallia vecu "tcp-lines://127.0.0.1:20162" db /path/to/db --ecu "XYZ" --properties '{"software_version": "1.2.3"}' +``` diff --git a/_static/_sphinx_javascript_frameworks_compat.js b/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 000000000..81415803e --- /dev/null +++ b/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,123 @@ +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 000000000..7ebbd6d07 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,914 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css new file mode 100644 index 000000000..88ba55b96 --- /dev/null +++ b/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff b/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 000000000..6cb600001 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff2 b/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 000000000..7059e2314 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff b/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 000000000..f815f63f9 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff2 b/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 000000000..f2c76e5bd Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/_static/css/fonts/fontawesome-webfont.eot b/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/_static/css/fonts/fontawesome-webfont.svg b/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/css/fonts/fontawesome-webfont.ttf b/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/_static/css/fonts/fontawesome-webfont.woff b/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/_static/css/fonts/fontawesome-webfont.woff2 b/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/_static/css/fonts/lato-bold-italic.woff b/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 000000000..88ad05b9f Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff differ diff --git a/_static/css/fonts/lato-bold-italic.woff2 b/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 000000000..c4e3d804b Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/_static/css/fonts/lato-bold.woff b/_static/css/fonts/lato-bold.woff new file mode 100644 index 000000000..c6dff51f0 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff differ diff --git a/_static/css/fonts/lato-bold.woff2 b/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 000000000..bb195043c Binary files /dev/null and b/_static/css/fonts/lato-bold.woff2 differ diff --git a/_static/css/fonts/lato-normal-italic.woff b/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 000000000..76114bc03 Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff differ diff --git a/_static/css/fonts/lato-normal-italic.woff2 b/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 000000000..3404f37e2 Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/_static/css/fonts/lato-normal.woff b/_static/css/fonts/lato-normal.woff new file mode 100644 index 000000000..ae1307ff5 Binary files /dev/null and b/_static/css/fonts/lato-normal.woff differ diff --git a/_static/css/fonts/lato-normal.woff2 b/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 000000000..3bf984332 Binary files /dev/null and b/_static/css/fonts/lato-normal.woff2 differ diff --git a/_static/css/theme.css b/_static/css/theme.css new file mode 100644 index 000000000..0f14f1064 --- /dev/null +++ b/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 000000000..0398ebb9f --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 000000000..7e4c114f2 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 000000000..a858a410e Binary files /dev/null and b/_static/file.png differ diff --git a/_static/fonts/Lato/lato-bold.eot b/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 000000000..3361183a4 Binary files /dev/null and b/_static/fonts/Lato/lato-bold.eot differ diff --git a/_static/fonts/Lato/lato-bold.ttf b/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 000000000..29f691d5e Binary files /dev/null and b/_static/fonts/Lato/lato-bold.ttf differ diff --git a/_static/fonts/Lato/lato-bold.woff b/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 000000000..c6dff51f0 Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff differ diff --git a/_static/fonts/Lato/lato-bold.woff2 b/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 000000000..bb195043c Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/_static/fonts/Lato/lato-bolditalic.eot b/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 000000000..3d4154936 Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/_static/fonts/Lato/lato-bolditalic.ttf b/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 000000000..f402040b3 Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/_static/fonts/Lato/lato-bolditalic.woff b/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 000000000..88ad05b9f Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/_static/fonts/Lato/lato-bolditalic.woff2 b/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 000000000..c4e3d804b Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/_static/fonts/Lato/lato-italic.eot b/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 000000000..3f826421a Binary files /dev/null and b/_static/fonts/Lato/lato-italic.eot differ diff --git a/_static/fonts/Lato/lato-italic.ttf b/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 000000000..b4bfc9b24 Binary files /dev/null and b/_static/fonts/Lato/lato-italic.ttf differ diff --git a/_static/fonts/Lato/lato-italic.woff b/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 000000000..76114bc03 Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff differ diff --git a/_static/fonts/Lato/lato-italic.woff2 b/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 000000000..3404f37e2 Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/_static/fonts/Lato/lato-regular.eot b/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 000000000..11e3f2a5f Binary files /dev/null and b/_static/fonts/Lato/lato-regular.eot differ diff --git a/_static/fonts/Lato/lato-regular.ttf b/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 000000000..74decd9eb Binary files /dev/null and b/_static/fonts/Lato/lato-regular.ttf differ diff --git a/_static/fonts/Lato/lato-regular.woff b/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 000000000..ae1307ff5 Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff differ diff --git a/_static/fonts/Lato/lato-regular.woff2 b/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 000000000..3bf984332 Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 000000000..79dc8efed Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 000000000..df5d1df27 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 000000000..6cb600001 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 000000000..7059e2314 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 000000000..2f7ca78a1 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 000000000..eb52a7907 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 000000000..f815f63f9 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 000000000..f2c76e5bd Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/_static/jquery.js b/_static/jquery.js new file mode 100644 index 000000000..c4c6022f2 --- /dev/null +++ b/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); + + const languagesHTML = ` +
+
Languages
+ ${languages + .map( + (translation) => ` +
+ ${translation.language.code} +
+ `, + ) + .join("\n")} +
+ `; + return languagesHTML; + } + + function renderVersions(config) { + if (!config.versions.active.length) { + return ""; + } + const versionsHTML = ` +
+
Versions
+ ${config.versions.active + .map( + (version) => ` +
+ ${version.slug} +
+ `, + ) + .join("\n")} +
+ `; + return versionsHTML; + } + + function renderDownloads(config) { + if (!Object.keys(config.versions.current.downloads).length) { + return ""; + } + const downloadsNameDisplay = { + pdf: "PDF", + epub: "Epub", + htmlzip: "HTML", + }; + + const downloadsHTML = ` +
+
Downloads
+ ${Object.entries(config.versions.current.downloads) + .map( + ([name, url]) => ` +
+ ${downloadsNameDisplay[name]} +
+ `, + ) + .join("\n")} +
+ `; + return downloadsHTML; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const flyout = ` +
+ + Read the Docs + v: ${config.versions.current.slug} + + +
+
+ ${renderLanguages(config)} + ${renderVersions(config)} + ${renderDownloads(config)} +
+
On Read the Docs
+
+ Project Home +
+
+ Builds +
+
+ Downloads +
+
+
+
Search
+
+
+ +
+
+
+
+ + Hosted by Read the Docs + +
+
+ `; + + // Inject the generated flyout into the body HTML element. + document.body.insertAdjacentHTML("beforeend", flyout); + + // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. + document + .querySelector("#flyout-search-form") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); + }) +} + +if (themeLanguageSelector || themeVersionSelector) { + function onSelectorSwitch(event) { + const option = event.target.selectedIndex; + const item = event.target.options[option]; + window.location.href = item.dataset.url; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const versionSwitch = document.querySelector( + "div.switch-menus > div.version-switch", + ); + if (themeVersionSelector) { + let versions = config.versions.active; + if (config.versions.current.hidden || config.versions.current.type === "external") { + versions.unshift(config.versions.current); + } + const versionSelect = ` + + `; + + versionSwitch.innerHTML = versionSelect; + versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + + const languageSwitch = document.querySelector( + "div.switch-menus > div.language-switch", + ); + + if (themeLanguageSelector) { + if (config.projects.translations.length) { + // Add the current language to the options on the selector + let languages = config.projects.translations.concat( + config.projects.current, + ); + languages = languages.sort((a, b) => + a.language.name.localeCompare(b.language.name), + ); + + const languageSelect = ` + + `; + + languageSwitch.innerHTML = languageSelect; + languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + else { + languageSwitch.remove(); + } + } + }); +} + +document.addEventListener("readthedocs-addons-data-ready", function (event) { + // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. + document + .querySelector("[role='search'] input") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); +}); \ No newline at end of file diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 000000000..c7fe6c6fa --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 000000000..d96755fda Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 000000000..7107cec93 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 000000000..6f8b210a1 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #F00 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #04D } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ +.highlight .no { color: #800 } /* Name.Constant */ +.highlight .nd { color: #A2F } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #00F } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #BBB } /* Text.Whitespace */ +.highlight .mb { color: #666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666 } /* Literal.Number.Float */ +.highlight .mh { color: #666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00F } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 000000000..2c774d17a --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 000000000..8a96c69a1 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/api.html b/api.html new file mode 100644 index 000000000..d5162d3a4 --- /dev/null +++ b/api.html @@ -0,0 +1,1256 @@ + + + + + + + + + Public API — gallia documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Public API

+
+

gallia.command

+
+
+class gallia.command.AsyncScript(config: BaseCommandConfig)[source]
+

Bases: BaseCommand, ABC

+

AsyncScript is a base class for a asynchronous gallia command. +To implement an async script, create a subclass and implement +the .main() method.

+
+ +
+
+class gallia.command.BaseCommand(config: BaseCommandConfig)[source]
+

Bases: FlockMixin, ABC

+

BaseCommand is the baseclass for all gallia commands. +This class can be used in standalone scripts via the +gallia command line interface facility.

+

This class needs to be subclassed and all the abstract +methods need to be implemented. The artifacts_dir is +generated based on the COMMAND, GROUP, SUBGROUP +properties (falls back to the class name if all three +are not set).

+

The main entry_point is entry_point().

+
+
+CATCHED_EXCEPTIONS: list[type[Exception]] = []
+

A list of exception types for which tracebacks are +suppressed at the top level. For these exceptions +a log message with level critical is logged.

+
+ +
+
+CONFIG_TYPE
+

alias of BaseCommandConfig

+
+ +
+
+EPILOG: str | None = None
+

The string which is shown at the bottom of –help.

+
+ +
+
+HAS_ARTIFACTS_DIR: bool = False
+

Enable a artifacts_dir. Setting this property to +True enables the creation of a logfile.

+
+ +
+
+SHORT_HELP: str | None = None
+

The string which is shown on the cli with –help.

+
+ +
+ +
+
+class gallia.command.Scanner(config: ScannerConfig)[source]
+

Bases: AsyncScript, ABC

+

Scanner is a base class for all scanning related commands. +A scanner has the following properties:

+
    +
  • It is async.

  • +
  • It loads transports via TargetURIs; available via self.transport.

  • +
  • Controlling PowerSupplies via the opennetzteil API is supported.

  • +
  • setup() can be overwritten (do not forget to call super().setup()) +for preparation tasks, such as establishing a network connection or +starting background tasks.

  • +
  • pcap logfiles can be recorded via a Dumpcap background task.

  • +
  • teardown() can be overwritten (do not forget to call super().teardown()) +for cleanup tasks, such as terminating a network connection or background +tasks.

  • +
  • main() is the relevant entry_point for the scanner and must be implemented.

  • +
+
+
+CATCHED_EXCEPTIONS: list[type[Exception]] = [<class 'ConnectionError'>, <class 'gallia.services.uds.core.exception.UDSException'>]
+

A list of exception types for which tracebacks are +suppressed at the top level. For these exceptions +a log message with level critical is logged.

+
+ +
+
+HAS_ARTIFACTS_DIR: bool = True
+

Enable a artifacts_dir. Setting this property to +True enables the creation of a logfile.

+
+ +
+ +
+
+class gallia.command.Script(config: BaseCommandConfig)[source]
+

Bases: BaseCommand, ABC

+

Script is a base class for a synchronous gallia command. +To implement a script, create a subclass and implement the +.main() method.

+
+ +
+
+class gallia.command.UDSDiscoveryScanner(config: UDSDiscoveryScannerConfig)[source]
+

Bases: Scanner, ABC

+
+ +
+
+class gallia.command.UDSScanner(config: UDSScannerConfig)[source]
+

Bases: Scanner, ABC

+

UDSScanner is a baseclass, particularly for scanning tasks +related to the UDS protocol. The differences to Scanner are:

+
    +
  • self.ecu contains a OEM specific UDS client object.

  • +
  • A background tasks sends TesterPresent regularly to avoid timeouts.

  • +
+
+ +
+
+

gallia.log

+
+
+class gallia.log.ColorMode(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: Enum

+

ColorMode is used as an argument to set_color_mode().

+
+
+ALWAYS = 'always'
+

Colors are always turned on.

+
+ +
+
+AUTO = 'auto'
+

Colors are turned off if the target +stream (e.g. stderr) is not a tty.

+
+ +
+
+NEVER = 'never'
+

No colors are used. In other words, +no ANSI escape codes are included.

+
+ +
+ +
+
+class gallia.log.Logger(name, level=0)[source]
+

Bases: Logger

+
+ +
+
+class gallia.log.Loglevel(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: IntEnum

+

A wrapper around the constants exposed by python’s +logging module. Since gallia adds two additional +loglevel’s (NOTICE and TRACE), this class +provides a type safe way to access the loglevels. +The level NOTICE was added to conform better to +RFC3164. Subsequently, TRACE was added to have +a facility for optional debug messages. +Loglevel describes python specific values for loglevels +which are required to integrate with the python ecosystem. +For generic priority values, see PenlogPriority.

+
+ +
+
+class gallia.log.PenlogPriority(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: IntEnum

+

PenlogPriority holds the values which are written +to json log records. These values conform to RFC3164 +with the addition of TRACE. Since Python uses different +int values for the loglevels, there are two enums in +gallia describing loglevels. PenlogPriority describes +generic priority values which are included in json +log records.

+
+
+classmethod from_level(value: int) PenlogPriority[source]
+

Converts an int value (e.g. from python’s logging module) +to an instance of this class.

+
+ +
+
+classmethod from_str(string: str) PenlogPriority[source]
+

Converts a string to an instance of PenlogPriority. +string can be a numeric value (0 to 8 inclusive) +or a string with a case insensitive name of the level +(e.g. debug).

+
+ +
+
+to_level() Loglevel[source]
+

Converts an instance of PenlogPriority to Loglevel.

+
+ +
+ +
+
+class gallia.log.PenlogRecord(module: 'str', host: 'str', data: 'str', datetime: 'datetime.datetime', priority: 'PenlogPriority', tags: 'list[str] | None' = None, colored: 'bool' = False, line: 'str | None' = None, stacktrace: 'str | None' = None, _python_level_no: 'int | None' = None, _python_level_name: 'str | None' = None, _python_func_name: 'str | None' = None)[source]
+

Bases: object

+
+ +
+
+gallia.log.resolve_color_mode(mode: ~gallia.log.ColorMode, stream: ~typing.TextIO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>) bool[source]
+

Sets the color mode of the console log handler.

+
+
Parameters:
+
+
+
+
+ +
+
+gallia.log.setup_logging(level: Loglevel | None = None, color_mode: ColorMode = ColorMode.AUTO, no_volatile_info: bool = False, logger_name: str = 'gallia') None[source]
+

Enable and configure gallia’s logging system. +If this fuction is not called as early as possible, +the logging system is in an undefined state und might +not behave as expected. Always use this function to +initialize gallia’s logging. For instance, setup_logging() +initializes a QueueHandler to avoid blocking calls during +logging.

+
+
Parameters:
+
    +
  • level – The loglevel to enable for the console handler. +If this argument is None, the env variable +GALLIA_LOGLEVEL (see Environment Variables) is read.

  • +
  • file_level – The loglevel to enable for the file handler.

  • +
  • path – The path to the logfile containing json records.

  • +
  • color_mode – The color mode to use for the console.

  • +
+
+
+
+ +
+
+

gallia.plugins

+
+
+

gallia.services.uds

+
+
+class gallia.services.uds.ECU(transport: BaseTransport, timeout: float, max_retry: int = 0, power_supply: PowerSupply | None = None)[source]
+

Bases: UDSClient

+

ECU is a high level interface wrapping a UDSClient class. It provides +semantically correct higher level interfaces such as read_session() +or ping(). Vendor specific implementations can be derived from this +class. For the arguments of the constructor, please check uds.uds.UDS.

+
+
+async check_and_set_session(expected_session: int, retries: int = 3) bool[source]
+

check_and_set_session() reads the current session and (re)tries to set +the session to the expected session if they do not match.

+

Returns True if the current session matches the expected session, +or if read_session is not supported by the ECU or in the current session.

+
+ +
+
+async clear_dtc(config: UDSRequestConfig | None = None) NegativeResponse | ClearDiagnosticInformationResponse[source]
+

Clear all dtc records on the ecu.

+
+ +
+
+async leave_session(level: int, config: UDSRequestConfig | None = None, sleep: float | None = None) bool[source]
+

leave_session() is a hook which can be called explicitly by a +scanner when a session is to be disabled. Use this hook if resetting +the ECU is required, e.g. when disabling the programming session.

+
+ +
+
+async ping(config: UDSRequestConfig | None = None) NegativeResponse | TesterPresentResponse[source]
+

Send an UDS TesterPresent message.

+
+
Returns:
+

UDS response.

+
+
+
+ +
+
+async read_dtc(config: UDSRequestConfig | None = None) NegativeResponse | ReportDTCByStatusMaskResponse[source]
+

Read all dtc records from the ecu.

+
+ +
+
+async read_session(config: UDSRequestConfig | None = None) int[source]
+

Read out current session.

+
+
Returns:
+

The current session as int.

+
+
+
+ +
+
+async read_vin(config: UDSRequestConfig | None = None) NegativeResponse | ReadDataByIdentifierResponse[source]
+

Read the VIN of the vehicle

+
+ +
+
+async refresh_state(reset_state: bool = False) None[source]
+

Refresh the attributes of the ECU states, if possible. +By, default, old values are only overwritten in case the corresponding +information can be requested from the ECU and could be retrieved from a +positive response from the ECU.

+
+
Parameters:
+

reset_state – If True, the ECU state is reset before updating it.

+
+
+
+ +
+
+async set_session_post(level: int, config: UDSRequestConfig | None = None) bool[source]
+

set_session_post() is called after the diagnostic session control +pdu was written on the wire. Implement this if there are special +cleanup routines or sleeping until a certain moment is required.

+
+
Parameters:
+
    +
  • uds – The UDSClient class where this hook is embedded. The caller typically +calls this function with self as the first argument.

  • +
  • session – The desired session identifier.

  • +
+
+
Returns:
+

True on success, False on error.

+
+
+
+ +
+
+async set_session_pre(level: int, config: UDSRequestConfig | None = None) bool[source]
+

set_session_pre() is called before the diagnostic session control +pdu is written on the wire. Implement this if there are special +preconditions for a particular session, such as disabling error +logging.

+
+
Parameters:
+
    +
  • uds – The UDSClient class where this hook is embedded. The caller typically +calls this function with self as the first argument.

  • +
  • session – The desired session identifier.

  • +
+
+
Returns:
+

True on success, False on error.

+
+
+
+ +
+
+async transmit_data(data: bytes, block_length: int, max_block_length: int = 4095, config: UDSRequestConfig | None = None) None[source]
+

transmit_data splits the data to be sent in several blocks of size block_length, +transfers all of them and concludes the transmission with RequestTransferExit

+
+ +
+
+async wait_for_ecu(timeout: float | None = 10) bool[source]
+

Wait for ecu to be alive again (e.g. after reset). +Sends a ping every 0.5s and waits at most timeout. +If timeout is None, wait endlessly

+
+ +
+ +
+
+class gallia.services.uds.NegativeResponse(request_service_id: int, response_code: UDSErrorCodes)[source]
+

Bases: NegativeResponseBase

+
+ +
+
+class gallia.services.uds.PositiveResponse[source]
+

Bases: UDSResponse, ABC

+
+ +
+
+class gallia.services.uds.SubFunctionRequest(suppress_response: bool)[source]
+

Bases: UDSRequest, ABC

+
+
+RESPONSE_TYPE
+

alias of SubFunctionResponse

+
+ +
+ +
+
+class gallia.services.uds.SubFunctionResponse[source]
+

Bases: PositiveResponse, ABC

+
+ +
+
+class gallia.services.uds.UDSErrorCodes(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: IntEnum

+
+ +
+
+class gallia.services.uds.UDSIsoServices(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: IntEnum

+
+ +
+
+class gallia.services.uds.UDSRequest[source]
+

Bases: ABC

+
+ +
+
+class gallia.services.uds.UDSRequestConfig(timeout: 'float | None' = None, max_retry: 'int | None' = None, skip_hooks: 'bool' = False, tags: 'list[str] | None' = None)[source]
+

Bases: object

+
+ +
+
+class gallia.services.uds.UDSResponse[source]
+

Bases: ABC

+
+ +
+
+

gallia.transports

+

All available transports are documented in Transports.

+
+
+class gallia.transports.BaseTransport(target: TargetURI)[source]
+

Bases: ABC

+

BaseTransport is the base class providing the required +interface for all transports used by gallia.

+

A transport usually is some kind of network protocol which +carries an application level protocol. A good example is +DoIP carrying UDS requests which acts as a minimal middleware +on top of TCP.

+

This class is to be used as a subclass with all abstractmethods +implemented and the SCHEME property filled.

+

A few methods provide a tags argument. The debug logs of these +calls include these tags in the tags property of the relevant +gallia.log.PenlogRecord.

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = ''
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+classmethod check_scheme(target: TargetURI) None[source]
+

Checks if the provided URI has the correct scheme.

+
+ +
+
+abstract async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+abstract async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+abstract async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async reconnect(timeout: float | None = None) Self[source]
+

Closes the connection to the target and attempts to reconnect every +100 ms until at max timeout. If timeout is None, only attempt to connect +once. +A new instance of this class is returned rendering the old one obsolete. +This method is safe for concurrent use.

+
+ +
+
+async request(data: bytes, timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Chains a write() call with a read() call. +The call is protected by a mutex and is thus safe for concurrent +use.

+
+ +
+
+async request_unsafe(data: bytes, timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Chains a write() call with a read() call. +The call is not protected by a mutex. Only use this method +when you know what you are doing.

+
+ +
+
+abstract async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.DoIPTransport(target: TargetURI, port: int, config: DoIPConfig, conn: DoIPConnection)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'doip'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async reconnect(timeout: float | None = None) Self[source]
+

Closes the connection to the target and attempts to reconnect every +100 ms until at max timeout. If timeout is None, only attempt to connect +once. +A new instance of this class is returned rendering the old one obsolete. +This method is safe for concurrent use.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.HSFZTransport(target: TargetURI, port: int, config: HSFZConfig, conn: HSFZConnection)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'hsfz'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) HSFZTransport[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.ISOTPTransport(target: TargetURI, config: ISOTPConfig, sock: socket)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'isotp'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.RawCANTransport(target: TargetURI, config: RawCANConfig, sock: socket)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'can-raw'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async get_idle_traffic(sniff_time: float) list[int][source]
+

Listen to traffic on the bus and return list of IDs +which are seen in the specified period of time. +The output of this function can be used as input to set_filter.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.TCPLinesTransport(target: TargetURI, reader: StreamReader, writer: StreamWriter)[source]
+

Bases: LinesTransportMixin, TCPTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'tcp-lines'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+ +
+
+class gallia.transports.TCPTransport(target: TargetURI, reader: StreamReader, writer: StreamWriter)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'tcp'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+class gallia.transports.TargetURI(raw: str)[source]
+

Bases: object

+

TargetURI represents a target to which gallia can connect. +The target string must conform to a URI is specified by RFC3986.

+

Basically, this is a wrapper around Python’s urlparse() and +parse_qs() methods. TargetURI provides frequently used properties +for a more userfriendly usage. Instances are meant to be passed to +BaseTransport.connect() of transport implementations.

+
+
+classmethod from_parts(scheme: str, host: str, port: int | None, args: dict[str, Any]) Self[source]
+

Constructs a instance of TargetURI with the given arguments. +The args dict is used for the query string.

+
+ +
+
+property hostname: str | None
+

The hostname (without port)

+
+ +
+
+property location: str
+

A URI string which only consists of the relevant scheme, +the host and the port.

+
+ +
+
+property netloc: str
+

The hostname and the portnumber, separated by a colon.

+
+ +
+
+property path: str
+

The path property of the url.

+
+ +
+
+property port: int | None
+

The port number

+
+ +
+
+property qs_flat: dict[str, str]
+

A dict which contains the query string’s key/value pairs. +In case a key appears multiple times, this variant only +contains the first found key/value pair. In contrast to +qs, this variant avoids lists and might be easier +to use for some cases.

+
+ +
+
+property scheme: TransportScheme
+

The URI scheme

+
+ +
+ +
+
+class gallia.transports.TransportScheme(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: StrEnum

+
+ +
+
+class gallia.transports.UnixLinesTransport(target: TargetURI, reader: StreamReader, writer: StreamWriter)[source]
+

Bases: LinesTransportMixin, UnixTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'unix-lines'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+ +
+
+class gallia.transports.UnixTransport(target: TargetURI, reader: StreamReader, writer: StreamWriter)[source]
+

Bases: BaseTransport

+
+
+BUFSIZE: int = 8192
+

The buffersize of the transport. Might be used in read() calls. +Defaults to io.DEFAULT_BUFFER_SIZE.

+
+ +
+
+SCHEME: str = 'unix'
+

The scheme for the implemented protocol, e.g. “doip”.

+
+ +
+
+async close() None[source]
+

Terminates the connection and clean up all allocated ressources.

+
+ +
+
+async classmethod connect(target: str | TargetURI, timeout: float | None = None) Self[source]
+

Classmethod to connect the transport to a relevant target. +The target argument is a URI, such as doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d” +An instance of the relevant transport class is returned.

+
+ +
+
+async read(timeout: float | None = None, tags: list[str] | None = None) bytes[source]
+

Reads one message and returns its raw byte representation. +An example for one message is ‘one line, terminated by newline’ +for a TCP transport yielding lines.

+
+ +
+
+async write(data: bytes, timeout: float | None = None, tags: list[str] | None = None) int[source]
+

Writes one message and return the number of written bytes.

+
+ +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/automation.html b/automation.html new file mode 100644 index 000000000..42f827e4c --- /dev/null +++ b/automation.html @@ -0,0 +1,187 @@ + + + + + + + + + Automation — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Automation

+
+

Power Supply

+

gallia has support for controlling power supplies either directly via builtin drivers. +Power supplies are mostly used for power cycling the current device under test. +There is no limit in accessing power supplies, e.g. voltage or current settings can be controlled as well.

+

Own drivers can be included by implementing the gallia.power_supply.BasePowerSupply interface. +On the commandline there is the --power-supply argument to specify a relevant power supply. +Further, there is --power-cycle to automatically power-cycle the device under test. +There is an experimental cli tool netzteil included in gallia. +This cli tool can be used to control all supported power supplies via the cli.

+

The argument for --power-supply is a URI of the following form:

+
SCHEME://HOST:PORT/PATH?channel=CHANNEL?product_id=PRODUCT_ID
+
+
+

Some schemes might take additional arguments in the query string.

+
+
SCHEME

The used protocol scheme; could be tcp, http, …

+
+
HOST:PORT

For e.g. tcp or https this is the relevant host and port.

+
+
PATH

If the power supply is exposed as a local file, this might be the path.

+
+
channel

The relevant channel where the device is connected; the master channel is 0.

+
+
product_id

The product_id of the used power supply.

+
+
+
+
+

Supported Power Supplies

+

Power supplies are chosen depending on the product_id setting in the URI. +The following power supplies are supported.

+
+

R&S®HMC804x

+
+
product_id

hmc804

+
+
scheme

tcp

+
+
HOST

IP address

+
+
PORT

TCP port, the device most likely uses 5025

+
+
+

Example:

+
tcp://192.0.2.5:5025?product_id=hmc804
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/config.html b/config.html new file mode 100644 index 000000000..ab36913ec --- /dev/null +++ b/config.html @@ -0,0 +1,185 @@ + + + + + + + + + Configuration — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Configuration

+
+

gallia.toml

+

All gallia settings stem from the commandline interface. +The documentation for all available settings per subcommand is available via -h/--help. +Frequently used settings can be put in a configfile which is called gallia.toml. +Settings from the config file set the default of the respective commandline option. +The config can always be overwritten by manually setting the relevant cli option.

+

The configuration file gallia.toml is written in TOML. +Inheritence is not supported; the first file is loaded. +The gallia.toml file is loaded from these locations (in this particular order):

+
    +
  • path specified in the env variable GALLIA_CONFIG; see Environment Variables.

  • +
  • current directory

  • +
  • current Git root (if the current directory is a Git repository)

  • +
  • $XDG_CONFIG_HOME/gallia/gallia.toml

  • +
  • ~/.config/gallia/gallia.toml

  • +
+

Only some cli options are exposed to the config file. +The available config settings can be obtained from gallia --template. +The output of --template is maintained to be up to date and is intended as a starting point.

+
+
+

Hooks

+

gallia supports hooks for preparation or cleanup/postprocessing tasks. +Alternatively, they can be useful for e.g. sending notifications about the exit_code via e.g. matrix or ntfy.sh. +Hooks are shell scripts which are executed before (= pre-hook) or after (= post-hook) the main() method. +These scripts can be specified via --pre-hook or --post-hook or via gallia.toml as well.

+

The hook scripts have these environment variables set; some are optional and hook scripts are encouraged to check their presence before accessing them:

+
+
GALLIA_HOOK

Either pre or post.

+
+
GALLIA_ARTIFACTS_DIR

Path to the artifactsdir for the current testrun.

+
+
GALLIA_EXIT_CODE (post)

Is set to the exit_code which gallia will use after the hook terminates. +For instance GALLIA_EXIT_CODE different from zero means that the current testrun failed.

+
+
GALLIA_META (post)

Contains the JSON encoded content of META.json.

+
+
GALLIA_INVOCATION

The content os sys.argv, in other words the raw invocation of gallia.

+
+
GALLIA_GROUP (optional)

Usually the first part of the command on the cli. For instance, for gallia scan uds identifiers +GALLIA_GROUP is scan.

+
+
GALLIA_SUBGROUP (optional)

Usually the second part of the command on the cli. For instance, for gallia scan uds identifiers +GALLIA_GROUP is uds.

+
+
GALLIA_COMMAND (optional)

Usually the last part of the command on the cli. For instance, for gallia scan uds identifiers +GALLIA_COMMAND is identifiers.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/env.html b/env.html new file mode 100644 index 000000000..0191a4de9 --- /dev/null +++ b/env.html @@ -0,0 +1,145 @@ + + + + + + + + + Environment Variables — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Environment Variables

+

For some cases gallia can be configured with environment variables. +gallia-specific variables begin with GALLIA_.

+
+
GALLIA_CONFIG

The path to the config file usually called gallia.toml. +Disables autodiscovery of the config.

+
+
GALLIA_LOGLEVEL

When gallia.log.setup_logging() is called without an argument this environment variable is read to set the loglevel. +Supported value are: trace, debug, info, notice, warning, error, critical. +As an alternative, the int values from 0 to 7 can be used. +Mostly useful in own scripts or tests. +This variable is not read when using the gallia cli.

+
+
NO_COLOR

If this variable is set, gallia by default does not use color codes, see: https://no-color.org/

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 000000000..1f6405dff --- /dev/null +++ b/genindex.html @@ -0,0 +1,590 @@ + + + + + + + + Index — gallia documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | L + | M + | N + | P + | Q + | R + | S + | T + | U + | W + +
+

A

+ + + +
+ +

B

+ + +
+ +

C

+ + + +
+ +

D

+ + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
    +
  • + gallia.command + +
  • +
  • + gallia.log + +
  • +
  • + gallia.plugins + +
  • +
+ +

H

+ + + +
+ +

I

+ + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + + +
+ +

P

+ + + +
+ +

Q

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

W

+ + +
+ + + +
+
+
+ +
+ +
+

© Copyright 2018, AISEC Pentest Team.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..2b8f2dd18 --- /dev/null +++ b/index.html @@ -0,0 +1,171 @@ + + + + + + + + + Gallia – Extendable Pentesting Framework — gallia documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Gallia – Extendable Pentesting Framework

+
+

Warning

+

This project is intended for research and development usage only! +Inappropriate usage might cause irreversible damage to the device under test. +We do not take any responsibility for damage caused by the usage of this tool.

+
+

Gallia is an extendable pentesting framework with the focus on the automotive domain. +The scope of the toolchain is conducting penetration tests from a single ECU up to whole cars. +Currently, the main focus lies on the UDS interface. +Acting as a generic interface, the logging functionality implements reproducible tests and enables post-processing tasks.

+
+ +
+ +

The current main focus of gallia is the UDS protocol. +Several concepts and ideas are implemented in gallia in order to provide comprehensive tests.

+
+
+

API

+ +
+

gallia is designed as a pentesting framework where each test produces a lot of data. +It is possible to design own standalone tools or plugins utilizing the gallia Python modules.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/logging.html b/logging.html new file mode 100644 index 000000000..3ebbaf838 --- /dev/null +++ b/logging.html @@ -0,0 +1,168 @@ + + + + + + + + + Logging — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Logging

+
+

Concept

+

gallia uses structured structured logging implemented as line separated JSON records. +Each scanner creates a artifacts_dir under artifacts_base, which contains a zstd compressed logfile log.json.zst. +The logfile is created with loglevel DEBUG; for debugging purposes loglevel TRACE can be enabled with the setting trace_log. +Logfiles can be displayed with the hr tool which is included in gallia.

+

The generic interface which represents a logrecord is gallia.log.PenlogRecord. +The generic interface which is used to read a logfile gallia.log.PenlogReader.

+
+
+

API

+

gallia uses the logging module. +The loglevels TRACE and NOTICE have been added to the module.

+

In own scripts gallia.log.setup_logging() needs to be called as early as possible. +For creating a gallia.log.Logger, there is gallia.log.get_logger().

+
from gallia.log import get_logger, setup_logging, Loglevel
+
+# The logfile's loglevel is Loglevel.DEBUG.
+# It can be set with the keyword argument file_level.
+setup_logging(level=Loglevel.INFO)
+logger = get_logger(__name__)
+logger.info("hello world")
+logger.debug("hello debug")
+
+
+

If processing of a logfile is needed, here is a minimal example; for custom functionality see gallia.log.PenlogReader and gallia.log.PenlogReader.records().

+
from gallia.log import PenlogReader
+
+reader = PenlogReader("/path/to/logfile")
+for record in reader.records()
+    print(record)
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 000000000..b3daa3a86 Binary files /dev/null and b/objects.inv differ diff --git a/plugins.html b/plugins.html new file mode 100644 index 000000000..e8a113581 --- /dev/null +++ b/plugins.html @@ -0,0 +1,184 @@ + + + + + + + + + Plugins — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Plugins

+
+

Entry Points

+

gallia uses the entry_points mechanism for registering plugins. +These entry points are known by gallia:

+
+
gallia_commands

List of subclasses of gallia.command.BaseCommand add new a command to the CLI.

+
+
gallia_cli_init

List of callables which get called during the initialization phase of the ArgumentParser; can be used to add new groups to the CLI.

+
+
gallia_transports

List of subclasses of gallia.transports.BaseTransport add a new URI scheme for the --target flag.

+
+
gallia_uds_ecus

List of subclasses of gallia.services.uds.ECU which add new choices for the --oem flag.

+
+
+
+
+

Example

+

Below is an example that adds a new command to the CLI (using gallia.command.Script). +Let’s assume the following code snippet lives in the python module hello.py within the hello_gallia package.

+
from argparse import Namespace
+
+from gallia.command import Script
+
+
+class HelloWorld(Script):
+    """A hello world script showing gallia's plugin API."""
+
+    COMMAND = "hello"
+    SHORT_HELP = "say hello to the world"
+
+
+    def main(self, args: Namespace) -> None:
+        print("Hello World")
+
+
+commands = [HelloWorld]
+
+
+

In pyproject.toml using poetry the following entry_point needs to be specified:

+
[tool.poetry.plugins."gallia_commands"]
+"hello_world_commands" = "hello_gallia.hello:commands"
+
+
+

After issueing poetry install, the script can be called with gallia script hello.

+

If a standalone script is desired, the HelloWorld class can be called like this:

+
parser = argparse.ArgumentParser()
+sys.exit(HelloWorld(parser).entry_point(parser.parse_args()))
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 000000000..2a0e1abae --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,161 @@ + + + + + + + + Python Module Index — gallia documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ g +
+ + + + + + + + + + + + + + + + + + + + + + +
 
+ g
+ gallia +
    + gallia.command +
    + gallia.log +
    + gallia.plugins +
    + gallia.services.uds +
    + gallia.transports +
+ + +
+
+
+ +
+ +
+

© Copyright 2018, AISEC Pentest Team.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 000000000..faa6a896a --- /dev/null +++ b/search.html @@ -0,0 +1,136 @@ + + + + + + + + Search — gallia documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2018, AISEC Pentest Team.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 000000000..99ec077d2 --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"API": [[4, null], [5, "api"], [8, "api"]], "Arch Linux": [[7, "arch-linux"]], "Automation": [[1, null]], "Clone repository": [[7, "clone-repository"]], "Concept": [[5, "concept"]], "Configuration": [[2, null]], "Database": [[9, null]], "Debian/Ubuntu": [[7, "debian-ubuntu"]], "Dependencies": [[7, "dependencies"]], "Development": [[7, "development"]], "Development with Plugins": [[7, "development-with-plugins"]], "Discovery Scan": [[10, "discovery-scan"]], "DoIP": [[10, "doip"]], "Docker": [[7, "docker"]], "Entry Points": [[6, "entry-points"]], "Environment": [[7, "environment"]], "Environment Variables": [[3, null]], "Example": [[6, "example"]], "Gallia \u2013 Extendable Pentesting Framework": [[4, null]], "Generic": [[7, "generic"]], "Hooks": [[2, "hooks"]], "IDE Integration": [[7, "ide-integration"]], "ISO-TP": [[10, "iso-tp"]], "Identifier Scan": [[10, "identifier-scan"]], "Install": [[7, "install"]], "Logging": [[5, null]], "Memory Scan": [[10, "memory-scan"]], "NixOS": [[7, "nixos"]], "Plugins": [[6, null]], "Power Supply": [[1, "power-supply"]], "Public API": [[0, null]], "R&S\u00aeHMC804x": [[1, "r-shmc804x"]], "Scan Modes": [[10, null]], "Service Scan": [[10, "service-scan"]], "Session Scan": [[10, "session-scan"]], "Setup": [[7, null]], "Shell Completion": [[7, "shell-completion"]], "Supported Power Supplies": [[1, "supported-power-supplies"]], "Transports": [[8, null]], "UDS": [[4, null]], "URIs": [[8, "uris"]], "Usage": [[4, null]], "Virtual ECUs": [[11, null]], "Without Install": [[7, "without-install"]], "bash": [[7, "bash"]], "can-raw": [[8, "can-raw"]], "db": [[11, "db"]], "doip": [[8, "doip"]], "fish": [[7, "fish"]], "gallia.command": [[0, "module-gallia.command"]], "gallia.log": [[0, "module-gallia.log"]], "gallia.plugins": [[0, "module-gallia.plugins"]], "gallia.services.uds": [[0, "module-gallia.services.uds"]], "gallia.toml": [[2, "gallia-toml"]], "gallia.transports": [[0, "gallia-transports"]], "hsfz": [[8, "hsfz"]], "iso-tp": [[11, "iso-tp"]], "isotp": [[8, "isotp"]], "model": [[11, "model"]], "rng": [[11, "rng"]], "run": [[7, "run"]], "shell": [[7, "shell"]], "tcp-lines": [[8, "tcp-lines"], [11, "tcp-lines"]], "transport": [[11, "transport"]]}, "docnames": ["api", "automation", "config", "env", "index", "logging", "plugins", "setup", "transports", "uds/database", "uds/scan_modes", "uds/virtual_ecu"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.viewcode": 1}, "filenames": ["api.md", "automation.md", "config.md", "env.md", "index.md", "logging.md", "plugins.md", "setup.md", "transports.md", "uds/database.md", "uds/scan_modes.md", "uds/virtual_ecu.md"], "indexentries": {"always (gallia.log.colormode attribute)": [[0, "gallia.log.ColorMode.ALWAYS", false]], "asyncscript (class in gallia.command)": [[0, "gallia.command.AsyncScript", false]], "auto (gallia.log.colormode attribute)": [[0, "gallia.log.ColorMode.AUTO", false]], "basecommand (class in gallia.command)": [[0, "gallia.command.BaseCommand", false]], "basetransport (class in gallia.transports)": [[0, "gallia.transports.BaseTransport", false]], "bufsize (gallia.transports.basetransport attribute)": [[0, "gallia.transports.BaseTransport.BUFSIZE", false]], "bufsize (gallia.transports.doiptransport attribute)": [[0, "gallia.transports.DoIPTransport.BUFSIZE", false]], "bufsize (gallia.transports.hsfztransport attribute)": [[0, "gallia.transports.HSFZTransport.BUFSIZE", false]], "bufsize (gallia.transports.isotptransport attribute)": [[0, "gallia.transports.ISOTPTransport.BUFSIZE", false]], "bufsize (gallia.transports.rawcantransport attribute)": [[0, "gallia.transports.RawCANTransport.BUFSIZE", false]], "bufsize (gallia.transports.tcplinestransport attribute)": [[0, "gallia.transports.TCPLinesTransport.BUFSIZE", false]], "bufsize (gallia.transports.tcptransport attribute)": [[0, "gallia.transports.TCPTransport.BUFSIZE", false]], "bufsize (gallia.transports.unixlinestransport attribute)": [[0, "gallia.transports.UnixLinesTransport.BUFSIZE", false]], "bufsize (gallia.transports.unixtransport attribute)": [[0, "gallia.transports.UnixTransport.BUFSIZE", false]], "catched_exceptions (gallia.command.basecommand attribute)": [[0, "gallia.command.BaseCommand.CATCHED_EXCEPTIONS", false]], "catched_exceptions (gallia.command.scanner attribute)": [[0, "gallia.command.Scanner.CATCHED_EXCEPTIONS", false]], "check_and_set_session() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.check_and_set_session", false]], "check_scheme() (gallia.transports.basetransport class method)": [[0, "gallia.transports.BaseTransport.check_scheme", false]], "clear_dtc() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.clear_dtc", false]], "close() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.close", false]], "close() (gallia.transports.doiptransport method)": [[0, "gallia.transports.DoIPTransport.close", false]], "close() (gallia.transports.hsfztransport method)": [[0, "gallia.transports.HSFZTransport.close", false]], "close() (gallia.transports.isotptransport method)": [[0, "gallia.transports.ISOTPTransport.close", false]], "close() (gallia.transports.rawcantransport method)": [[0, "gallia.transports.RawCANTransport.close", false]], "close() (gallia.transports.tcptransport method)": [[0, "gallia.transports.TCPTransport.close", false]], "close() (gallia.transports.unixtransport method)": [[0, "gallia.transports.UnixTransport.close", false]], "colormode (class in gallia.log)": [[0, "gallia.log.ColorMode", false]], "config_type (gallia.command.basecommand attribute)": [[0, "gallia.command.BaseCommand.CONFIG_TYPE", false]], "connect() (gallia.transports.basetransport class method)": [[0, "gallia.transports.BaseTransport.connect", false]], "connect() (gallia.transports.doiptransport class method)": [[0, "gallia.transports.DoIPTransport.connect", false]], "connect() (gallia.transports.hsfztransport class method)": [[0, "gallia.transports.HSFZTransport.connect", false]], "connect() (gallia.transports.isotptransport class method)": [[0, "gallia.transports.ISOTPTransport.connect", false]], "connect() (gallia.transports.rawcantransport class method)": [[0, "gallia.transports.RawCANTransport.connect", false]], "connect() (gallia.transports.tcptransport class method)": [[0, "gallia.transports.TCPTransport.connect", false]], "connect() (gallia.transports.unixtransport class method)": [[0, "gallia.transports.UnixTransport.connect", false]], "doiptransport (class in gallia.transports)": [[0, "gallia.transports.DoIPTransport", false]], "ecu (class in gallia.services.uds)": [[0, "gallia.services.uds.ECU", false]], "epilog (gallia.command.basecommand attribute)": [[0, "gallia.command.BaseCommand.EPILOG", false]], "from_level() (gallia.log.penlogpriority class method)": [[0, "gallia.log.PenlogPriority.from_level", false]], "from_parts() (gallia.transports.targeturi class method)": [[0, "gallia.transports.TargetURI.from_parts", false]], "from_str() (gallia.log.penlogpriority class method)": [[0, "gallia.log.PenlogPriority.from_str", false]], "gallia.command": [[0, "module-gallia.command", false]], "gallia.log": [[0, "module-gallia.log", false]], "gallia.plugins": [[0, "module-gallia.plugins", false]], "gallia.services.uds": [[0, "module-gallia.services.uds", false]], "gallia.transports": [[0, "module-gallia.transports", false]], "get_idle_traffic() (gallia.transports.rawcantransport method)": [[0, "gallia.transports.RawCANTransport.get_idle_traffic", false]], "has_artifacts_dir (gallia.command.basecommand attribute)": [[0, "gallia.command.BaseCommand.HAS_ARTIFACTS_DIR", false]], "has_artifacts_dir (gallia.command.scanner attribute)": [[0, "gallia.command.Scanner.HAS_ARTIFACTS_DIR", false]], "hostname (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.hostname", false]], "hsfztransport (class in gallia.transports)": [[0, "gallia.transports.HSFZTransport", false]], "isotptransport (class in gallia.transports)": [[0, "gallia.transports.ISOTPTransport", false]], "leave_session() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.leave_session", false]], "location (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.location", false]], "logger (class in gallia.log)": [[0, "gallia.log.Logger", false]], "loglevel (class in gallia.log)": [[0, "gallia.log.Loglevel", false]], "module": [[0, "module-gallia.command", false], [0, "module-gallia.log", false], [0, "module-gallia.plugins", false], [0, "module-gallia.services.uds", false], [0, "module-gallia.transports", false]], "negativeresponse (class in gallia.services.uds)": [[0, "gallia.services.uds.NegativeResponse", false]], "netloc (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.netloc", false]], "never (gallia.log.colormode attribute)": [[0, "gallia.log.ColorMode.NEVER", false]], "path (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.path", false]], "penlogpriority (class in gallia.log)": [[0, "gallia.log.PenlogPriority", false]], "penlogrecord (class in gallia.log)": [[0, "gallia.log.PenlogRecord", false]], "ping() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.ping", false]], "port (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.port", false]], "positiveresponse (class in gallia.services.uds)": [[0, "gallia.services.uds.PositiveResponse", false]], "qs_flat (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.qs_flat", false]], "rawcantransport (class in gallia.transports)": [[0, "gallia.transports.RawCANTransport", false]], "read() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.read", false]], "read() (gallia.transports.doiptransport method)": [[0, "gallia.transports.DoIPTransport.read", false]], "read() (gallia.transports.hsfztransport method)": [[0, "gallia.transports.HSFZTransport.read", false]], "read() (gallia.transports.isotptransport method)": [[0, "gallia.transports.ISOTPTransport.read", false]], "read() (gallia.transports.rawcantransport method)": [[0, "gallia.transports.RawCANTransport.read", false]], "read() (gallia.transports.tcptransport method)": [[0, "gallia.transports.TCPTransport.read", false]], "read() (gallia.transports.unixtransport method)": [[0, "gallia.transports.UnixTransport.read", false]], "read_dtc() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.read_dtc", false]], "read_session() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.read_session", false]], "read_vin() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.read_vin", false]], "reconnect() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.reconnect", false]], "reconnect() (gallia.transports.doiptransport method)": [[0, "gallia.transports.DoIPTransport.reconnect", false]], "refresh_state() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.refresh_state", false]], "request() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.request", false]], "request_unsafe() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.request_unsafe", false]], "resolve_color_mode() (in module gallia.log)": [[0, "gallia.log.resolve_color_mode", false]], "response_type (gallia.services.uds.subfunctionrequest attribute)": [[0, "gallia.services.uds.SubFunctionRequest.RESPONSE_TYPE", false]], "scanner (class in gallia.command)": [[0, "gallia.command.Scanner", false]], "scheme (gallia.transports.basetransport attribute)": [[0, "gallia.transports.BaseTransport.SCHEME", false]], "scheme (gallia.transports.doiptransport attribute)": [[0, "gallia.transports.DoIPTransport.SCHEME", false]], "scheme (gallia.transports.hsfztransport attribute)": [[0, "gallia.transports.HSFZTransport.SCHEME", false]], "scheme (gallia.transports.isotptransport attribute)": [[0, "gallia.transports.ISOTPTransport.SCHEME", false]], "scheme (gallia.transports.rawcantransport attribute)": [[0, "gallia.transports.RawCANTransport.SCHEME", false]], "scheme (gallia.transports.targeturi property)": [[0, "gallia.transports.TargetURI.scheme", false]], "scheme (gallia.transports.tcplinestransport attribute)": [[0, "gallia.transports.TCPLinesTransport.SCHEME", false]], "scheme (gallia.transports.tcptransport attribute)": [[0, "gallia.transports.TCPTransport.SCHEME", false]], "scheme (gallia.transports.unixlinestransport attribute)": [[0, "gallia.transports.UnixLinesTransport.SCHEME", false]], "scheme (gallia.transports.unixtransport attribute)": [[0, "gallia.transports.UnixTransport.SCHEME", false]], "script (class in gallia.command)": [[0, "gallia.command.Script", false]], "set_session_post() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.set_session_post", false]], "set_session_pre() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.set_session_pre", false]], "setup_logging() (in module gallia.log)": [[0, "gallia.log.setup_logging", false]], "short_help (gallia.command.basecommand attribute)": [[0, "gallia.command.BaseCommand.SHORT_HELP", false]], "subfunctionrequest (class in gallia.services.uds)": [[0, "gallia.services.uds.SubFunctionRequest", false]], "subfunctionresponse (class in gallia.services.uds)": [[0, "gallia.services.uds.SubFunctionResponse", false]], "targeturi (class in gallia.transports)": [[0, "gallia.transports.TargetURI", false]], "tcplinestransport (class in gallia.transports)": [[0, "gallia.transports.TCPLinesTransport", false]], "tcptransport (class in gallia.transports)": [[0, "gallia.transports.TCPTransport", false]], "to_level() (gallia.log.penlogpriority method)": [[0, "gallia.log.PenlogPriority.to_level", false]], "transmit_data() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.transmit_data", false]], "transportscheme (class in gallia.transports)": [[0, "gallia.transports.TransportScheme", false]], "udsdiscoveryscanner (class in gallia.command)": [[0, "gallia.command.UDSDiscoveryScanner", false]], "udserrorcodes (class in gallia.services.uds)": [[0, "gallia.services.uds.UDSErrorCodes", false]], "udsisoservices (class in gallia.services.uds)": [[0, "gallia.services.uds.UDSIsoServices", false]], "udsrequest (class in gallia.services.uds)": [[0, "gallia.services.uds.UDSRequest", false]], "udsrequestconfig (class in gallia.services.uds)": [[0, "gallia.services.uds.UDSRequestConfig", false]], "udsresponse (class in gallia.services.uds)": [[0, "gallia.services.uds.UDSResponse", false]], "udsscanner (class in gallia.command)": [[0, "gallia.command.UDSScanner", false]], "unixlinestransport (class in gallia.transports)": [[0, "gallia.transports.UnixLinesTransport", false]], "unixtransport (class in gallia.transports)": [[0, "gallia.transports.UnixTransport", false]], "wait_for_ecu() (gallia.services.uds.ecu method)": [[0, "gallia.services.uds.ECU.wait_for_ecu", false]], "write() (gallia.transports.basetransport method)": [[0, "gallia.transports.BaseTransport.write", false]], "write() (gallia.transports.doiptransport method)": [[0, "gallia.transports.DoIPTransport.write", false]], "write() (gallia.transports.hsfztransport method)": [[0, "gallia.transports.HSFZTransport.write", false]], "write() (gallia.transports.isotptransport method)": [[0, "gallia.transports.ISOTPTransport.write", false]], "write() (gallia.transports.rawcantransport method)": [[0, "gallia.transports.RawCANTransport.write", false]], "write() (gallia.transports.tcptransport method)": [[0, "gallia.transports.TCPTransport.write", false]], "write() (gallia.transports.unixtransport method)": [[0, "gallia.transports.UnixTransport.write", false]]}, "objects": {"gallia": [[0, 0, 0, "-", "command"], [0, 0, 0, "-", "log"], [0, 0, 0, "-", "plugins"], [0, 0, 0, "-", "transports"]], "gallia.command": [[0, 1, 1, "", "AsyncScript"], [0, 1, 1, "", "BaseCommand"], [0, 1, 1, "", "Scanner"], [0, 1, 1, "", "Script"], [0, 1, 1, "", "UDSDiscoveryScanner"], [0, 1, 1, "", "UDSScanner"]], "gallia.command.BaseCommand": [[0, 2, 1, "", "CATCHED_EXCEPTIONS"], [0, 2, 1, "", "CONFIG_TYPE"], [0, 2, 1, "", "EPILOG"], [0, 2, 1, "", "HAS_ARTIFACTS_DIR"], [0, 2, 1, "", "SHORT_HELP"]], "gallia.command.Scanner": [[0, 2, 1, "", "CATCHED_EXCEPTIONS"], [0, 2, 1, "", "HAS_ARTIFACTS_DIR"]], "gallia.log": [[0, 1, 1, "", "ColorMode"], [0, 1, 1, "", "Logger"], [0, 1, 1, "", "Loglevel"], [0, 1, 1, "", "PenlogPriority"], [0, 1, 1, "", "PenlogRecord"], [0, 4, 1, "", "resolve_color_mode"], [0, 4, 1, "", "setup_logging"]], "gallia.log.ColorMode": [[0, 2, 1, "", "ALWAYS"], [0, 2, 1, "", "AUTO"], [0, 2, 1, "", "NEVER"]], "gallia.log.PenlogPriority": [[0, 3, 1, "", "from_level"], [0, 3, 1, "", "from_str"], [0, 3, 1, "", "to_level"]], "gallia.services": [[0, 0, 0, "-", "uds"]], "gallia.services.uds": [[0, 1, 1, "", "ECU"], [0, 1, 1, "", "NegativeResponse"], [0, 1, 1, "", "PositiveResponse"], [0, 1, 1, "", "SubFunctionRequest"], [0, 1, 1, "", "SubFunctionResponse"], [0, 1, 1, "", "UDSErrorCodes"], [0, 1, 1, "", "UDSIsoServices"], [0, 1, 1, "", "UDSRequest"], [0, 1, 1, "", "UDSRequestConfig"], [0, 1, 1, "", "UDSResponse"]], "gallia.services.uds.ECU": [[0, 3, 1, "", "check_and_set_session"], [0, 3, 1, "", "clear_dtc"], [0, 3, 1, "", "leave_session"], [0, 3, 1, "", "ping"], [0, 3, 1, "", "read_dtc"], [0, 3, 1, "", "read_session"], [0, 3, 1, "", "read_vin"], [0, 3, 1, "", "refresh_state"], [0, 3, 1, "", "set_session_post"], [0, 3, 1, "", "set_session_pre"], [0, 3, 1, "", "transmit_data"], [0, 3, 1, "", "wait_for_ecu"]], "gallia.services.uds.SubFunctionRequest": [[0, 2, 1, "", "RESPONSE_TYPE"]], "gallia.transports": [[0, 1, 1, "", "BaseTransport"], [0, 1, 1, "", "DoIPTransport"], [0, 1, 1, "", "HSFZTransport"], [0, 1, 1, "", "ISOTPTransport"], [0, 1, 1, "", "RawCANTransport"], [0, 1, 1, "", "TCPLinesTransport"], [0, 1, 1, "", "TCPTransport"], [0, 1, 1, "", "TargetURI"], [0, 1, 1, "", "TransportScheme"], [0, 1, 1, "", "UnixLinesTransport"], [0, 1, 1, "", "UnixTransport"]], "gallia.transports.BaseTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "check_scheme"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "reconnect"], [0, 3, 1, "", "request"], [0, 3, 1, "", "request_unsafe"], [0, 3, 1, "", "write"]], "gallia.transports.DoIPTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "reconnect"], [0, 3, 1, "", "write"]], "gallia.transports.HSFZTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "write"]], "gallia.transports.ISOTPTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "write"]], "gallia.transports.RawCANTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "get_idle_traffic"], [0, 3, 1, "", "read"], [0, 3, 1, "", "write"]], "gallia.transports.TCPLinesTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"]], "gallia.transports.TCPTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "write"]], "gallia.transports.TargetURI": [[0, 3, 1, "", "from_parts"], [0, 5, 1, "", "hostname"], [0, 5, 1, "", "location"], [0, 5, 1, "", "netloc"], [0, 5, 1, "", "path"], [0, 5, 1, "", "port"], [0, 5, 1, "", "qs_flat"], [0, 5, 1, "", "scheme"]], "gallia.transports.UnixLinesTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"]], "gallia.transports.UnixTransport": [[0, 2, 1, "", "BUFSIZE"], [0, 2, 1, "", "SCHEME"], [0, 3, 1, "", "close"], [0, 3, 1, "", "connect"], [0, 3, 1, "", "read"], [0, 3, 1, "", "write"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"], "3": ["py", "method", "Python method"], "4": ["py", "function", "Python function"], "5": ["py", "property", "Python property"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute", "3": "py:method", "4": "py:function", "5": "py:property"}, "terms": {"": [0, 5, 6, 7, 10], "0": [0, 1, 3, 8, 11], "0x": 8, "0x01": [8, 10, 11], "0x02": 11, "0x03": [10, 11], "0x04": 11, "0x05": 10, "0x07": 11, "0x09": 11, "0x0e00": [8, 10], "0x10": [8, 11], "0x14": 11, "0x1d": [0, 8], "0x22": 11, "0x27": 11, "0x29": 11, "0x2a": 11, "0x31": 11, "0x3d": 11, "0x3f": 11, "0x40": 11, "0x46": 11, "0x54": 8, "0x600": 10, "0x63": 11, "0x64": 11, "0x654": 8, "0x6aa": 11, "0x6bb": 11, "0x6f1": 10, "0x6f4": [8, 11], "0xaa": 11, "0xbb": 11, "0xf1": 10, "0xf4": [0, 8, 11], "1": [0, 7, 8, 10, 11], "10": [0, 7], "100": [0, 8], "1001": 10, "1003": 10, "11": 7, "1234": 8, "127": [8, 11], "13400": [0, 8], "15765": 8, "1623483675214623782": 11, "169": 8, "192": [0, 1, 8], "2": [0, 1, 8, 10, 11], "20162": 11, "222579011926250596": 11, "254": 8, "3": [0, 7, 11], "3e00": 10, "4095": 0, "5": [0, 1, 7, 8, 11], "5025": 1, "6801": 8, "7": 3, "8": 0, "8192": 0, "A": [0, 6, 8, 10], "As": [3, 10, 11], "By": [0, 11], "For": [0, 1, 2, 3, 5, 7, 10, 11], "If": [0, 1, 3, 5, 6, 7, 10, 11], "In": [0, 5, 6, 10, 11], "It": [0, 4, 5, 10, 11], "No": 0, "On": [1, 10], "The": [0, 1, 2, 3, 4, 5, 7, 8, 10, 11], "There": [1, 11], "These": [0, 2, 6], "To": [0, 10, 11], "__name__": 5, "_io": 0, "_python_func_nam": 0, "_python_level_nam": 0, "_python_level_no": 0, "abc": 0, "abl": 11, "about": [2, 10], "abstract": 0, "abstractmethod": 0, "access": [0, 1, 2], "accord": 10, "ack_timeout": 8, "acknowledg": [8, 10], "act": [0, 4, 10], "activ": 7, "ad": [0, 5, 11], "adapt": 10, "add": [0, 6, 7, 10, 11], "addit": [0, 1, 10, 11], "addition": 11, "address": [1, 8, 10, 11], "advantag": 10, "after": [0, 2, 6, 11], "again": 0, "against": 11, "aisec": 7, "alia": 0, "aliv": 0, "all": [0, 1, 2, 7, 8, 10, 11], "alloc": 0, "allow": 11, "alreadi": 11, "also": [8, 10, 11], "alter": 11, "altern": [2, 3], "alwai": [0, 2, 10], "an": [0, 1, 3, 4, 6, 7, 8, 10, 11], "ani": [0, 4, 10, 11], "anoth": 10, "ansi": 0, "answer": [10, 11], "api": 6, "appear": 0, "append": 10, "appli": 10, "applic": [0, 10, 11], "approach": [10, 11], "appropri": 10, "apt": 7, "ar": [0, 1, 2, 3, 4, 6, 7, 8, 10, 11], "arbitrari": 10, "arg": [0, 6, 8], "argcomplet": 7, "argpars": 6, "argument": [0, 1, 3, 5, 8, 10, 11], "argumentpars": 6, "argv": 2, "around": 0, "artifact": 11, "artifacts_bas": 5, "artifacts_dir": [0, 5], "artifactsdir": 2, "ascii": 8, "assum": [6, 10], "assupt": 10, "async": [0, 8], "asynchron": 0, "asyncio": 8, "asyncscript": 0, "attempt": 0, "attribut": 0, "authent": 10, "auto": 0, "autodiscoveri": 3, "autom": 4, "automat": [1, 7, 10, 11], "automot": [4, 10], "avail": [0, 2, 7, 8, 10, 11], "avoid": [0, 10], "await": 8, "back": 0, "background": 0, "bar": 8, "base": [0, 8, 11], "baseclass": 0, "basecommand": [0, 6], "basecommandconfig": 0, "basepowersuppli": 1, "basetransport": [0, 6, 8], "bash_complet": 7, "basic": [0, 8, 10], "been": [5, 10, 11], "befor": [0, 2, 8], "begin": 3, "behav": 0, "behavior": 11, "being": 11, "below": [6, 10], "better": 0, "between": 11, "bin": 7, "block": 0, "block_length": 0, "bool": [0, 8], "bottom": 0, "boundari": 0, "box": 11, "broadcast": 10, "bu": [0, 10, 11], "buffers": 0, "bufsiz": 0, "builtin": 1, "byte": [0, 8, 10], "c": 10, "call": [0, 2, 3, 5, 6, 10], "callabl": 6, "caller": 0, "can": [0, 1, 2, 3, 5, 6, 10, 11], "can0": 8, "can1": 8, "can_id": 10, "candid": 10, "cannot": 10, "car": 4, "carri": 0, "case": [0, 3, 10, 11], "catched_except": 0, "categori": 11, "caus": 4, "certain": 0, "chain": 0, "chang": [7, 10, 11], "channel": [1, 10], "chargon": 10, "check": [0, 2, 8], "check_and_set_sess": 0, "check_schem": 0, "choic": [6, 7], "chosen": [1, 11], "class": [0, 6], "classmethod": 0, "clean": 0, "cleanup": [0, 2], "clear": 0, "clear_dtc": 0, "cleardiagnosticinform": 11, "cleardiagnosticinformationrespons": 0, "cleardiagnostictroublecodesandstoredvalu": 11, "cli": [0, 1, 2, 3, 6], "client": [0, 11], "close": 0, "code": [0, 3, 6, 10], "colon": 0, "color": [0, 3], "color_mod": 0, "colormod": 0, "com": 7, "combin": 11, "come": 10, "command": [2, 6, 7, 11], "commandlin": [1, 2], "commun": [10, 11], "compens": 11, "compon": 8, "comprehens": 4, "compress": 5, "comput": 11, "concept": [4, 10], "conclud": 0, "concurr": 0, "condit": 11, "conduct": 4, "config": [0, 2, 3, 7], "config_typ": 0, "configfil": 2, "configur": [0, 3, 4, 7, 10], "conform": [0, 11], "confus": 10, "conn": 0, "connect": [0, 1, 8, 10, 11], "connectionerror": 0, "connector": 10, "consecut": 8, "consid": 10, "consist": [0, 8, 10], "consol": 0, "constant": [0, 10], "construct": 0, "constructor": 0, "contain": [0, 2, 5, 7, 10, 11], "content": 2, "contrast": [0, 10, 11], "control": [0, 1, 10, 11], "convers": 11, "convert": 0, "core": 0, "correct": [0, 11], "correspond": [0, 11], "could": [0, 1], "cover": 10, "craft": 10, "creat": [0, 5, 8, 11], "creation": 0, "critic": [0, 3], "current": [0, 1, 2, 4, 10, 11], "custom": [5, 10, 11], "cycl": [1, 10], "cyclic": 10, "d": 7, "damag": 4, "data": [0, 4, 10], "databas": [4, 11], "date": 2, "datetim": 0, "debug": [0, 3, 5], "def": [6, 8], "default": [0, 2, 3, 10, 11], "default_buffer_s": 0, "defaultsess": 10, "defin": [7, 10], "deni": 10, "depend": 1, "dependend": 10, "depth": 10, "deriv": 0, "describ": [0, 9, 10], "design": 4, "desir": [0, 6], "destin": [8, 10], "detail": 10, "detect": 10, "determin": 11, "dev": 11, "develop": 4, "devic": [1, 4, 8], "dhcp": 10, "diagnost": [0, 10], "diagnosticmessag": 10, "diagnosticsessioncontrol": [10, 11], "dict": 0, "differ": [0, 2, 7, 10, 11], "directli": 1, "directori": 2, "disabl": [0, 3, 11], "discov": 10, "discoveri": 11, "displai": 5, "distinguish": 10, "do": [0, 4, 8], "document": [0, 2, 7, 9, 10], "doe": [3, 10], "doip": 0, "doipconfig": 0, "doipconnect": 0, "doiptransport": [0, 8], "domain": 4, "driver": 1, "dst_addr": [0, 8, 11], "dtc": 0, "dumpcap": [0, 7], "dure": [0, 6], "e": [0, 1, 2, 7, 8, 10], "each": [4, 5, 10, 11], "earli": [0, 5], "easier": 0, "easiest": 11, "ecosystem": 0, "ecu": [0, 4, 6, 10], "editor": 7, "educ": 10, "either": [1, 2, 11], "embed": 0, "emploi": 10, "enabl": [0, 4, 5, 7, 10, 11], "encod": [0, 2, 8], "encourag": 2, "endless": 10, "endlessli": 0, "endpoint": 10, "entry_point": [0, 6], "enum": 0, "env": [0, 2], "environ": [0, 2, 4], "epilog": 0, "error": [0, 3, 10], "escap": 0, "establish": [0, 8, 10], "etc": 7, "everi": 0, "exampl": [0, 1, 5, 8, 10, 11], "except": 0, "execut": 2, "exist": 11, "exit": 6, "exit_cod": 2, "expect": [0, 8, 10], "expected_sess": 0, "expens": 10, "experiment": 1, "explain": 11, "explan": 10, "explicitli": [0, 11], "expos": [0, 1, 2], "ext_address": [8, 10, 11], "extend": [8, 10], "extendeddiagnosticsess": 10, "f": 10, "facil": 0, "fail": 2, "fall": 0, "fals": [0, 8, 11], "fd": [8, 10], "featur": 10, "few": [0, 10], "field": 10, "file": [0, 1, 2, 3], "file_level": [0, 5], "fill": [0, 11], "filter": 10, "find": 10, "finish": 10, "first": [0, 2, 10], "fix": 10, "flag": [6, 7], "float": 0, "flockmixin": 0, "focu": 4, "follow": [0, 1, 6, 7, 8, 10, 11], "foo": 8, "forget": 0, "form": 1, "found": [0, 10], "frame": 8, "frame_txtim": 8, "fraunhof": 7, "free": 10, "frequent": [0, 2], "from": [0, 2, 3, 4, 5, 6, 7, 8, 10, 11], "from_level": 0, "from_part": 0, "from_str": 0, "fuction": 0, "function": [0, 4, 5, 10, 11], "further": [1, 10, 11], "furthermor": 10, "fuzz": 10, "g": [0, 1, 2, 7, 8, 10], "gallia": [1, 3, 5, 6, 7, 8, 9, 10, 11], "gallia_": 3, "gallia_artifacts_dir": 2, "gallia_cli_init": 6, "gallia_command": [2, 6], "gallia_config": [2, 3], "gallia_exit_cod": 2, "gallia_group": 2, "gallia_hook": 2, "gallia_invoc": 2, "gallia_loglevel": [0, 3], "gallia_meta": 2, "gallia_subgroup": 2, "gallia_transport": 6, "gallia_uds_ecu": 6, "gatewai": [8, 10], "gener": [0, 4, 5, 11], "get": 6, "get_idle_traff": 0, "get_logg": 5, "git": [2, 7], "github": 7, "given": 0, "good": [0, 10], "group": [0, 6], "guess": 10, "h": 2, "ha": [0, 1, 7, 10, 11], "handl": [10, 11], "handler": 0, "hardwar": 10, "has_artifacts_dir": 0, "have": [0, 2, 5, 11], "hello": [5, 6], "hello_gallia": 6, "hello_world_command": 6, "helloworld": 6, "help": [0, 2, 7], "here": 5, "hex": 8, "high": [0, 10], "higher": 0, "hmc804": 1, "hold": 0, "home": 7, "hook": 0, "host": [0, 1, 8], "hostnam": 0, "how": 11, "howev": [10, 11], "hr": 5, "hsfz": 0, "hsfzconfig": 0, "hsfzconnect": 0, "hsfztransport": 0, "http": [1, 3, 7], "i": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11], "id": [0, 10], "idea": [4, 10], "identifi": [0, 2, 8, 11], "idl": 10, "imag": 7, "immedi": 11, "implement": [0, 1, 4, 5, 10], "import": [5, 6, 8, 10], "inappropri": 4, "includ": [0, 1, 5, 7, 11], "inclus": 0, "incorrectmessagelengthorinvalidformat": 10, "independ": 11, "indic": 10, "info": [3, 5], "inform": 0, "inherit": 2, "initi": [0, 6, 11], "input": [0, 10], "insensit": 0, "insid": 7, "instal": 6, "instanc": [0, 2, 10], "instead": 11, "instruct": 10, "int": [0, 3, 8], "integr": 0, "intend": [2, 4, 11], "intenum": 0, "interact": 10, "interfac": [0, 1, 2, 4, 5, 8, 10, 11], "intern": 10, "internet": 10, "invoc": 2, "invok": 7, "io": 0, "ip": [1, 10, 11], "ipv4": 11, "ipv6": 11, "irrevers": 4, "is_extend": 8, "is_fd": [8, 11], "iso": 8, "isotp": [0, 11], "isotpconfig": 0, "isotptransport": 0, "issu": 6, "iter": 10, "its": [0, 9, 11], "itself": 10, "json": [0, 2, 5], "just": [7, 10], "k": 10, "kei": 0, "kernel": [8, 10], "keyword": 5, "kind": 0, "know": 0, "knowledg": 10, "known": [6, 10], "languag": 7, "last": 2, "latest": 7, "law": 10, "layer": [10, 11], "layout": 9, "least": 10, "leave_sess": 0, "length": 10, "let": 6, "level": [0, 5, 7, 10], "li": 4, "like": [1, 6, 10, 11], "limit": [1, 10], "line": [0, 5], "linebas": 8, "linestransportmixin": 0, "link": 11, "linux": [8, 10], "list": [0, 6, 7, 10], "listen": [0, 11], "live": 6, "load": [0, 2], "local": 1, "locat": [0, 2, 8], "log": [3, 4, 8, 11], "logfil": [0, 5], "logger": [0, 5], "logger_nam": 0, "loglevel": [0, 3, 5], "logrecord": 5, "look": [10, 11], "lot": 4, "lsp": 7, "m": [0, 8], "mai": 11, "main": [0, 2, 4, 6, 8], "mainli": 8, "maintain": 2, "make": [10, 11], "manag": 7, "mandatori": 11, "mandatory_servic": 11, "manual": [2, 7, 11], "master": 1, "match": [0, 10, 11], "matrix": 2, "max": [0, 8], "max_block_length": 0, "max_retri": 0, "maximum": 10, "mean": 2, "meant": 0, "mechan": [6, 11], "medium": 10, "mention": [10, 11], "messag": [0, 8, 10], "meta": 2, "method": [0, 2, 8, 10], "middlewar": 0, "might": [0, 1, 4, 7, 10, 11], "millisecond": 8, "mimic": 11, "minim": [0, 5], "mkdir": 7, "mode": [0, 4], "modern": 10, "modul": [0, 4, 5, 6], "moment": 0, "more": [0, 10, 11], "most": [0, 1, 7, 10, 11], "mostli": [1, 3], "multipl": [0, 10, 11], "must": [0, 7, 10], "mutex": 0, "nack": 10, "name": [0, 11], "namespac": 6, "need": [0, 5, 6, 7, 10], "neg": 10, "negativerespons": 0, "negativeresponsebas": 0, "neovim": 7, "netloc": 0, "network": [0, 10], "netzteil": 1, "never": 0, "new": [0, 6, 10], "newlin": 0, "nix": 7, "nixpgk": 7, "no_color": 3, "no_volatile_info": 0, "nocheck_dst_addr": 8, "nocheck_src_addr": 8, "none": [0, 6], "normal": 10, "note": 11, "notic": [0, 3, 5], "notif": 2, "ntfy": 2, "null": 11, "number": [0, 11], "numer": 0, "o": 2, "obd": 10, "object": 0, "observ": 10, "obsolet": 0, "obtain": 2, "oem": [0, 6], "off": 0, "offer": [10, 11], "offset": 10, "often": 10, "old": 0, "onc": 0, "one": [0, 7, 11], "onli": [0, 2, 4, 7, 10, 11], "onlin": 10, "opennetzteil": 0, "oper": [10, 11], "option": [0, 2, 7, 8, 10, 11], "optional_servic": 11, "order": [2, 4, 10], "org": [3, 7], "other": [0, 2, 11], "otherwis": 10, "out": [0, 10, 11], "output": [0, 2, 11], "over": 10, "overview": [7, 11], "overwritten": [0, 2], "own": [1, 3, 4, 5, 8, 10], "p": 7, "p_servic": 11, "packag": [6, 7], "packet": 10, "pad": 8, "pair": [0, 11], "param1": 8, "param2": 8, "paramet": [0, 8, 10], "parse_arg": 6, "parse_q": 0, "parser": 6, "part": [2, 7, 10], "particip": 10, "particular": [0, 2, 11], "particularli": [0, 11], "paru": 7, "pass": 0, "path": [0, 1, 2, 3, 5, 11], "payload": [8, 10], "pcap": 0, "pdu": 0, "penetr": 4, "penlogprior": 0, "penlogread": 5, "penlogrecord": [0, 5], "per": 2, "perform": 10, "period": 0, "persist": 7, "perspect": 10, "phase": [6, 10], "physic": 11, "ping": 0, "pipx": 7, "plai": 10, "pleas": [0, 7], "plugin": 4, "poetri": 6, "point": 2, "port": [0, 1, 8, 11], "portnumb": 0, "posit": [0, 10], "positiverespons": 0, "possibl": [0, 4, 5, 11], "post": [2, 4], "postprocess": 2, "power_suppli": [0, 1], "powersuppli": 0, "pre": 2, "precondit": [0, 10], "prefer": 7, "prefix": 8, "prepar": [0, 2], "presenc": 2, "present": 10, "previou": 10, "previous": [10, 11], "print": [5, 6], "prioriti": 0, "probabl": 11, "procedur": [10, 11], "process": [4, 5, 10], "produc": 4, "product_id": 1, "program": 0, "progress": 9, "project": [4, 7], "properli": 10, "properti": [0, 11], "protect": 0, "protocol": [0, 1, 4, 7, 8, 10, 11], "provid": [0, 4, 7, 8, 10], "public": 4, "publish": 7, "purpos": [5, 11], "put": [2, 10], "py": 6, "pyproject": [6, 7], "python": [0, 4, 6, 7], "q": 0, "qs_flat": 0, "qualnam": 0, "queri": [0, 1], "queuehandl": 0, "random": 11, "raw": [0, 2], "rawcanconfig": 0, "rawcantransport": 0, "re": 0, "reachabl": 10, "read": [0, 3, 5, 10], "read_dtc": 0, "read_sess": 0, "read_vin": 0, "readdatabyidentifi": [10, 11], "readdatabyidentifierrespons": 0, "reader": [0, 5], "receiv": [8, 10, 11], "reconnect": 0, "record": [0, 5], "recreat": 11, "recurs": 10, "refer": [0, 7, 10], "referenc": 11, "refresh": 0, "refresh_st": 0, "regist": [6, 7], "registri": 7, "regularli": 0, "relat": 0, "relev": [0, 1, 2, 8, 10], "remain": 11, "render": 0, "repli": 10, "repo": 7, "repologi": 7, "reportdtcbystatusmaskrespons": 0, "repositori": 2, "repres": [0, 5], "represent": 0, "reproduc": [4, 11], "request": [0, 10, 11], "request_service_id": 0, "request_unsaf": 0, "requestoutofrang": 10, "requesttransferexit": 0, "requestvehicleinform": 11, "requir": [0, 7, 8, 10, 11], "research": 4, "reset": 0, "reset_st": 0, "resolve_color_mod": 0, "respect": 2, "respond": 10, "respons": [0, 4, 10, 11], "response_cod": 0, "response_typ": 0, "ressourc": [0, 10], "result": [9, 10, 11], "retri": 0, "retriev": 0, "return": [0, 11], "revers": 10, "rfc3164": 0, "rfc3986": 0, "root": 2, "rout": 10, "routin": 0, "routinecontrol": [10, 11], "routingactivationrequest": 10, "routingactivationtyp": 10, "run": [8, 10, 11], "rx": 8, "rx_ext_address": [8, 11], "rx_pad": 8, "safe": 0, "sai": 6, "same": [7, 8, 10, 11], "satisfi": 11, "scan": [0, 2, 4, 11], "scanner": [0, 5, 8, 10, 11], "scannerconfig": 0, "scheme": [0, 1, 6, 8, 10, 11], "scope": 4, "script": [0, 2, 3, 5, 6, 8, 11], "search": 10, "second": [2, 10], "section": [10, 11], "securityaccess": 11, "see": [0, 2, 3, 5], "seed": 11, "seen": [0, 10], "select": [10, 11], "self": [0, 6], "semant": 0, "send": [0, 2, 8, 10], "sent": [0, 8, 10], "separ": [0, 5, 11], "server": [7, 10, 11], "servic": [6, 11], "servicenotsupport": 10, "servicenotsupportedinactivesess": 10, "session": [0, 11], "set": [0, 1, 2, 3, 5, 8, 10, 11], "set_color_mod": 0, "set_filt": 0, "set_session_post": 0, "set_session_pr": 0, "setup": [0, 4, 10, 11], "setup_log": [0, 3, 5, 8], "sever": [0, 4, 10, 11], "sh": 2, "share": 8, "shell": 2, "short_help": [0, 6], "should": 10, "show": [6, 11], "showcurrentdata": 11, "showfreezeframedata": 11, "shown": 0, "showpendingdiagnostictroublecod": 11, "side": [10, 11], "simpl": 8, "simul": 11, "sinc": [0, 10], "singl": [4, 7, 11], "size": [0, 8], "skip_hook": 0, "sleep": 0, "sniff_tim": 0, "snippet": 6, "so": 10, "sock": 0, "socket": [0, 8], "softwar": 7, "software_vers": 11, "some": [0, 1, 2, 3, 10], "somehow": 10, "sourc": [0, 7, 8, 10], "sourceaddress": 10, "spawn": 11, "special": [0, 10], "specif": [0, 3, 10], "specifi": [0, 1, 2, 6, 8, 11], "split": 0, "src_addr": [0, 8, 11], "stack": 10, "stacktrac": 0, "stage": 10, "standalon": [0, 4, 6, 8], "standard": [10, 11], "start": [0, 2, 10, 11], "startup": 11, "state": [0, 10, 11], "static": 10, "stderr": 0, "stem": 2, "step": 10, "store": [9, 11], "str": 0, "stream": 0, "streamread": 0, "streamwrit": 0, "strenum": 0, "string": [0, 1, 8], "structur": 5, "sub": [10, 11], "subclass": [0, 6, 8], "subcommand": 2, "subfunct": 10, "subfunctionnotsupport": 10, "subfunctionnotsupportedinactivesess": 10, "subfunctionrequest": 0, "subfunctionrespons": 0, "subgroup": 0, "subsequ": [0, 10], "success": 0, "sudo": 7, "suitabl": 11, "super": 0, "support": [0, 2, 3, 7, 8, 11], "suppress": 0, "suppress_respons": 0, "suppressposrespmsgindicationbit": 11, "sy": [2, 6], "symbol": 10, "sync": 7, "synchron": 0, "system": [0, 7, 10], "systempackag": 7, "tag": 0, "take": [1, 4, 8], "talk": 11, "target": [0, 6, 8], "target_addr": 8, "targetaddress": 10, "targeturi": 0, "task": [0, 2, 4], "tcp": [0, 1, 10], "tcplinestransport": 0, "tcptransport": 0, "teardown": 0, "techniqu": 10, "templat": 2, "ten": 11, "termin": [0, 2], "test": [1, 3, 4, 8, 11], "tester": 10, "testerpres": [0, 10], "testerpresentrespons": 0, "testrun": 2, "text": 7, "textio": 0, "textiowrapp": 0, "thei": [0, 2, 11], "them": [0, 2, 11], "therebi": 11, "therefor": [10, 11], "thi": [0, 1, 2, 3, 4, 6, 7, 9, 10, 11], "those": [10, 11], "three": [0, 10], "thu": [0, 10], "time": [0, 7, 8, 10], "timeout": [0, 8, 10], "to_level": 0, "todo": 10, "togeth": 11, "toml": [3, 6, 7], "tool": [1, 4, 5, 6, 7, 11], "toolchain": 4, "top": [0, 10], "tp": 8, "trace": [0, 3, 5], "trace_log": 5, "traceback": 0, "traffic": [0, 10], "transfer": 0, "transit": 10, "transmiss": 0, "transmit_data": 0, "transport": [4, 6, 10], "transportschem": 0, "tri": [0, 10], "true": [0, 8], "tty": 0, "tunnel": 10, "turn": 0, "two": [0, 10, 11], "tx_dl": 8, "tx_pad": 8, "type": [0, 11], "typic": [0, 10], "ud": [2, 6, 8, 10, 11], "udsclient": 0, "udsdiscoveryscann": 0, "udsdiscoveryscannerconfig": 0, "udserrorcod": 0, "udsexcept": 0, "udsisoservic": 0, "udsrequest": 0, "udsrequestconfig": 0, "udsrespons": 0, "udsscann": 0, "udsscannerconfig": 0, "und": 0, "undefin": 0, "under": [1, 4, 5, 7, 8], "underli": 10, "unfilt": 10, "unimpl": 10, "unix": 0, "unixlinestransport": 0, "unixtransport": 0, "unknown": 10, "until": 0, "up": [0, 2, 4, 11], "updat": 0, "uri": [0, 1, 6], "url": 0, "urlpars": 0, "us": [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11], "usag": 0, "user": 10, "userdata": 10, "userfriendli": 0, "usual": [0, 2, 3, 10], "utf": 0, "util": 4, "uv": 7, "uvx": 7, "valid": 10, "valu": [0, 3, 8, 10], "vari": 10, "variabl": [0, 2, 4], "variant": 0, "vcan": 11, "vcan0": 11, "vecicl": 10, "vecu": 11, "vehicl": 0, "vendor": [0, 10], "venv": 7, "veri": [10, 11], "version": 7, "via": [0, 1, 2, 7, 11], "vin": 0, "virtual": 4, "voltag": 1, "w": 0, "wa": [0, 10, 11], "wai": 0, "wait": [0, 8, 10], "wait_for_ecu": 0, "want": 7, "warn": 3, "we": [4, 10], "well": [1, 2, 10, 11], "what": [0, 10], "when": [0, 3, 7, 10, 11], "where": [0, 1, 4, 7, 11], "whether": 10, "which": [0, 2, 5, 6, 8, 10, 11], "while": 11, "whole": [4, 11], "wire": 0, "wireshark": 7, "within": [6, 7], "without": [0, 3, 10], "word": [0, 2], "work": [9, 10], "world": [5, 6], "would": 11, "wrap": 0, "wrapper": 0, "write": [0, 8], "writememorybyaddress": 11, "writer": 0, "written": [0, 2], "wwh_ob": 10, "x00": 10, "xdg_config_hom": 2, "xyz": 11, "yield": [0, 10], "you": [0, 7], "your": 7, "zero": 2, "zst": 5, "zstd": 5}, "titles": ["Public API", "Automation", "Configuration", "Environment Variables", "Gallia \u2013 Extendable Pentesting Framework", "Logging", "Plugins", "Setup", "Transports", "Database", "Scan Modes", "Virtual ECUs"], "titleterms": {"": 1, "api": [0, 4, 5, 8], "arch": 7, "autom": 1, "bash": 7, "can": 8, "clone": 7, "command": 0, "complet": 7, "concept": 5, "configur": 2, "databas": 9, "db": 11, "debian": 7, "depend": 7, "develop": 7, "discoveri": 10, "docker": 7, "doip": [8, 10], "ecu": 11, "entri": 6, "environ": [3, 7], "exampl": 6, "extend": 4, "fish": 7, "framework": 4, "gallia": [0, 2, 4], "gener": 7, "hmc804x": 1, "hook": 2, "hsfz": 8, "id": 7, "identifi": 10, "instal": 7, "integr": 7, "iso": [10, 11], "isotp": 8, "line": [8, 11], "linux": 7, "log": [0, 5], "memori": 10, "mode": 10, "model": 11, "nixo": 7, "pentest": 4, "plugin": [0, 6, 7], "point": 6, "power": 1, "public": 0, "r": 1, "raw": 8, "repositori": 7, "rng": 11, "run": 7, "scan": 10, "servic": [0, 10], "session": 10, "setup": 7, "shell": 7, "suppli": 1, "support": 1, "tcp": [8, 11], "toml": 2, "tp": [10, 11], "transport": [0, 8, 11], "ubuntu": 7, "ud": [0, 4], "uri": 8, "usag": 4, "variabl": 3, "virtual": 11, "without": 7}}) \ No newline at end of file diff --git a/setup.html b/setup.html new file mode 100644 index 000000000..fd96780d6 --- /dev/null +++ b/setup.html @@ -0,0 +1,277 @@ + + + + + + + + + Setup — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Setup

+
+

Dependencies

+

This project has the following system level dependencies:

+ +

Python dependencies are listed in pyproject.toml.

+
+
+

Install

+

An overview of software repos where gallia is available is provided by repology.org.

+
+

Docker

+

Docker images are published via the Github Container registry.

+
+
+

Arch Linux

+
$ paru -S gallia
+
+
+
+
+

Debian/Ubuntu

+
$ sudo apt install pipx
+$ pipx install gallia
+
+
+
+
+

NixOS

+
$ nix shell nixpgks#gallia
+
+
+

For persistance add gallia to your environment.systemPackages, or when you use home-manager to home.packages.

+
+
+

Generic

+
$ pipx install gallia
+
+
+
+
+

Without Install

+

The uvx tool is provided by uv.

+
$ uvx gallia
+
+
+
+
+
+

Development

+

uv is used to manage dependencies.

+
+

Clone repository

+
$ git clone https://github.com/Fraunhofer-AISEC/gallia.git
+
+
+
+
+

Environment

+

uv manages the project environment, including the python version. +All uv commands must be invoked within the gallia repository.

+
$ pipx install uv
+$ uv sync
+
+
+

If you want to use a different Python version from the one defined in .python-version, the flags --python-preference only-system or --python for uv sync might be helpful; e.g. to use your system provided Python 3.11:

+
$ uv sync --python-preference only-system --python 3.11
+
+
+
+

shell

+

Enable the venv under .venv manually by sourcing:

+
$ source .venv/bin/activate
+$ source .venv/bin/activate.fish
+
+
+
+
+

run

+

Run a single command inside the venv without changing the shell environment:

+
$ uv run gallia
+
+
+
+
+
+
+

Development with Plugins

+

If you want to develop gallia and plugins at the same time, then you need to add gallia as a dependency to your plugin package.

+
+

Shell Completion

+
+

bash

+
# register-python-argcomplete gallia > /etc/bash_completion.d/gallia
+
+
+
+
+

fish

+
$ mkdir -p ~/.config/fish/completions
+$ register-python-argcomplete --shell fish gallia > ~/.config/fish/completions/gallia.fish
+
+
+
+
+
+

IDE Integration

+

Just use LSP. +Most editors (e.g. neovim) support the Language Server Protocol. +The required tools are listed as development dependencies in pyproject.toml and are automatically managed by uv. +Please refer to the documentation of your text editor of choice for configuring LSP support.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/transports.html b/transports.html new file mode 100644 index 000000000..262adeb03 --- /dev/null +++ b/transports.html @@ -0,0 +1,262 @@ + + + + + + + + + Transports — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Transports

+

All scanner share the same basic connection args. +Transports are subclasses of gallia.transports.BaseTransport.

+
+

URIs

+

The argument --target specifies all parameters which are required to establish a connection to the device under test. +The argument to --target is specified as a URI. +An URI consists of these components:

+
scheme://host:port?param1=foo&param2=bar
+        |location |
+
+
+

The parameters support: string, int (use 0x prefix for hex values) and bool (true/false) values. +The relevant transport protocol is specified in the scheme.

+
+

isotp

+

ISO-TP (ISO 15765-2) as provided by the Linux socket API.

+

The can interface is specified as a host, e.g. can0. +The following parameters are available (these are ISOTP transport settings):

+
+
src_addr (required)

The ISOTP source address as int.

+
+
dst_addr (required)

The ISOTP destination address as int.

+
+
is_extended (optional)

Use extended CAN identifiers.

+
+
is_fd (optional)

Use CAN-FD frames.

+
+
frame_txtime (optional)

The time in milliseconds the kernel waits before sending a ISOTP consecutive frame.

+
+
ext_address (optional)

The extended ISOTP address as int.

+
+
rx_ext_address (optional)

The extended ISOTP rx address.

+
+
tx_padding (optional)

Use padding in sent frames.

+
+
rx_padding (optional)

Expect padding in received frames.

+
+
tx_dl (optional)

CAN-FD max payload size.

+
+
+

Example:

+
isotp://can0?src_addr=0x6f4&dst_addr=0x654&rx_ext_address=0xf4&ext_address=0x54&is_fd=false
+
+
+
+
+

can-raw

+
+
src_addr (required)

The ISOTP source address as int.

+
+
dst_addr (required)

The ISOTP destination address as int.

+
+
is_extended (optional)

Use extended CAN identifiers.

+
+
is_fd (optional)

Use CAN-FD frames.

+
+
+

Example:

+
can-raw://can1?is_fd=true
+
+
+
+
+

doip

+

The DoIP gateway address is specified in the location.

+
+
src_addr (required)

The source address as int.

+
+
target_addr (required)

The target address as int.

+
+
+

Example:

+
doip://169.254.100.100:13400?src_addr=0x0e00&target_addr=0x1d
+
+
+
+
+

hsfz

+

The gateway address is specified in the location.

+
    +
  • src_addr (required): The source address as int.

  • +
  • dst_addr (required): The destination address as int.

  • +
  • ack_timeout: Specify the HSFZ acknowledge timeout in ms.

  • +
  • nocheck_src_addr: Do not check the source address in received HSFZ frames.

  • +
  • nocheck_dst_addr: Do not check the destination address in received HSFZ frames.

  • +
+

Example:

+
hsfz://169.254.100.100:6801?src_addr=0xf4&dst_addr=0x1d
+
+
+
+
+

tcp-lines

+

A simple tcp based transport. +UDS messages are sent linebased in ascii hex encoding. +Mainly useful for testing.

+

Example:

+
tcp-lines://127.0.0.1:1234
+
+
+
+
+
+

API

+

Transports can also be used in own standalone scripts; transports are created with the .connect() method which takes a URI.

+
import asyncio
+
+from gallia.log import setup_logging
+from gallia.transports import DOiPTransport
+
+
+async def main():
+    transport = await DOiPTransport.connect("doip://192.0.2.5:13400?src_addr=0xf4&dst_addr=0x1d")
+    await transport.write(bytes([0x10, 0x01]))
+
+
+setup_logging()
+asyncio.run(main())
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/uds/database.html b/uds/database.html new file mode 100644 index 000000000..60e3b1b10 --- /dev/null +++ b/uds/database.html @@ -0,0 +1,132 @@ + + + + + + + + + Database — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Database

+

This documentation describes the database layout gallia uses for storing its results.

+

Work in progress

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/uds/scan_modes.html b/uds/scan_modes.html new file mode 100644 index 000000000..4fd53d70b --- /dev/null +++ b/uds/scan_modes.html @@ -0,0 +1,284 @@ + + + + + + + + + Scan Modes — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Scan Modes

+

A UDS scan usually covers multiple phases:

+
    +
  1. Searching for ECUs on the relevant transport: Discovery Scan

  2. +
  3. Searching for UDS sessions on the found ECUs: Session Scan

  4. +
  5. Searching for UDS services on the found ECUs: Service Scan

  6. +
  7. Searching for UDS identifiers in discovered UDS services: Identifier Scan

  8. +
  9. Additional service specific scans, such as Memory Scan, Fuzzing, …

  10. +
+
+

Discovery Scan

+

Discovery scans are specific for the underlying transport, such as DoIP or ISO-TP. +The idea is crafting a valid UDS payload which is valid and at least some answer is expected. +A well working payload is 1001 which is a request to the DiagnosticSessionControl service. +This request instructs an ECU to change to the so called DefaultSession. +The DiagnosticSessionControl service and the DefaultSession should be always available, thus this payload is a good candidate for a discovery scan. +Payloads different from 1001 can be used as well; for instance 1003 to enable the ExtendedDiagnosticSession (session id 0x03). +Another well working example is 3E00, the TesterPresent service.

+

The addressing of the ECU is provided by the underlying transport protocol. +Most of the time there are two addresses: the tester address and the ECU address. +The basic idea of a discovery scan is sending a valid UDS payload to all valid ECU addresses with a fixed tester address. +When a valid answer is received an ECU has been found.

+
+

DoIP

+

The Diagnostics Over Internet Protocol (DoIP) is a application level protocol on top of TCP enabling tunneling UDS messages. +As an advantage all features of modern operating systems can be used. +Furthermore, no custom hardware, such as expensive CAN bus adapters are required.

+
+

Note

+

The user needs to provide a DHCP server enabling the communication stack on the DoIP gateway’s side.

+
+

DoIP has three parameters which are required to establish a communication channel to an ECU:

+
    +
  1. SourceAddress: In the automotive chargon also called tester address. It is often static and set to 0x0e00.

  2. +
  3. TargetAddress: This is the address of a present ECU. This parameter needs to be discovered.

  4. +
  5. RoutingActivationType: DoIP provides further optional layers of authentication which can e.g. be vendor specific. Since gallia needs UDS endpoints this is most likely WWH_OB, which is a symbolic identifier for the constant 0x01. Optionally, the RoutingActivationType can be scanned in order to find vendor specific routing methods.

  6. +
+

Assuming DHCP and the networking setup is running properly, the DoIP connection establishment looks like the following:

+
    +
  1. TCP connect to the DoIP gateway. The IP address of the gateway is set by the mentioned user controlled DHCP server.

  2. +
  3. Sending a RoutingActivationRequest; the gateway must acknowledge this.

  4. +
  5. UDS requests can be tunneled to arbitrary ECUs using appropriate DoIP DiagnosticMessage packets.

  6. +
+

The mentioned DiagnosticMessage packets contain a SourceAddress, a TargetAddress, and a UserData field. +For the discovery scan on DoIP first step 1. and 2. from the described connection establishment process are performed. +Subsequently, a valid UDS message (c.f. previous section) is the put in the UserData field and the TargetAddress field is iterated. +When a valid answer is received an ECU has been found; when the gateway sends a NACK or just timeouts, no ECU is available on the tried TargetAddress.

+
+
+

ISO-TP

+

ISO-TP is a standard for a transport protocol on top of the CAN bus system. +The CAN bus is a field bus which acts as a broadcast medium; any connected participant can read all messages. +On the CAN bus there is no concept of a connection. +Typically, there are cyclic messages on the CAN bus which are important for selected participants. +However, in order to implement a connection channel for the UDS protocol (which is required by law to be present in vecicles) the ISO-TP standard comes into play. +In contrast to DoIP special CAN hardware is required. +The ISO-TP protocol and the interaction with CAN interfaces is handled by the networking stack of the Linux kernel.

+

For a discovery scan it is important to distinguish whether the tester is connected to a filtered interface (e.g. the OBD connector) or to an unfiltered interface (e.g. an internal CAN bus). +In order to not confuse the discovery scanner, the so called idle traffic needs to be observed. +The idle traffic consists of the mentioned cyclic messages of the can bus. +Since there is no concept of a connection on the CAN bus itself and the parameters for the ISO-TP connection are unknown at the very first stage, an educated guess for a deny list is required. +Typically, gallia waits for a few seconds and observes the CAN bus traffic. +Subsequently, a deny filter is configured which filters out all CAN IDs seen in the idle traffic.

+

ISO-TP provides multiple different addressing methods:

+
    +
  • normal addressing with normal CAN IDs,

  • +
  • normal addressing with extended CAN IDs,

  • +
  • extended addressing with normal CAN IDs,

  • +
  • extended addressing with extended CAN IDs,

  • +
  • the mentioned schemes but with CAN-FD below,

  • +
  • +
+
+

Note

+

For the detailed explanation of all these addressing modes we refer to the relevant ISO standard documents or further documents or presentations which are available online.

+
+

The tester needs to make assuptions about what addressing scheme is used; otherwise the scan does not yield any results. +ISO-TP provides the following parameters:

+
    +
  • source can_id:

    +
      +
    • Without extended addressing: Often set to an address with a static offset to the destination can_id.

    • +
    • Extended addressing: Often set to a static value, e.g. 0x6f1; a.k.a. tester address.

    • +
    +
  • +
  • destination can_id:

    +
      +
    • Without extended addressing: Address of the ECU

    • +
    • Extended addressing: Somehow part of the ECU address, e.g. 0x600 | ext_address

    • +
    +
  • +
  • extended source address: When extended addressing is in use, often set to a static value, e.g. 0xf1.

  • +
  • extended destination address: When extended addressing is in use, it is the address of the ECU.

  • +
+

The discovery procedure is dependend on the used addressing scheme. +From a high level perspective, the destination id is iterated and a valid payload is sent. +If a valid answer is received, an ECU has been found.

+
+
+
+

Session Scan

+

UDS has the concept of sessions. +Different sessions can for example offer different services. +A session is identified by a 1 byte session ID. +The UDS standard defines a set of well known session IDs, but vendors are free to add their own sessions. +Some sessions might only be available from a specific ECU state (e.g. current session, enabled/configured ECU features, coding, …). +Most of those preconditions cannot be detected automatically and might require vendor specific knowledge.

+

The session scan tries to find all available session transitions. +Starting from the default session (0x01), all session IDs are iterated and enabling the relevant session is tried. +If the ECU replies with a positive response, the session is available. +In case of a negative response, the session is considered not available from the current state. +To detect sessions, which are only reachable from a session different to the default session, a recursive approach is used. +The scan for new sessions starts at each previously identified session. +The maximum depth is limited to avoid endless scans in case of transition cycles, such as 0x01 -> 0x03 -> 0x05 -> 0x03. +The scan is finished, if no new session transition is found.

+
+
+

Service Scan

+

The service scan operates at the UDS protocol level. +UDS provides several endpoints called services. +Each service has an identifier and a specific list of arguments or sub-functions.

+

In order to identify available services, a reverse matching is applied. +According to the UDS standard, ECUs reply with the error codes serviceNotSupported or serviceNotSupportedInActiveSession when an unimplemented service is requested. +Therefore, each service which responds with a different error code is considered available. +To address the different services and their varying length of arguments and sub-functions the scanner automatically appends \x00 bytes if the received response was incorrectMessageLengthOrInvalidFormat.

+
+
+

Identifier Scan

+

The identifier scan operates at the UDS protocol level; to be more specific it operates at the level of a specific UDS service. +Most UDS services need identifiers is input arguments. +For instance, the ReadDataByIdentifier service requires a data identifier input for the requested ressource. +In order to find out the available identifiers for a specific service the Identifier Scan is employed.

+

In order to identify available data identifiers, a reverse matching is applied. +According to the UDS standard, ECUs reply with the error codes serviceNotSupported or serviceNotSupportedInActiveSession when an unimplemented service is requested. +If the ECU responds with any of serviceNotSupported, serviceNotSupportedInActiveSession,subFunctionNotSupported, subFunctionNotSupportedInActiveSession, or requestOutOfRange the identifier is considered not available.

+

A few services such as RoutineControl offer a subFunction as well. +SubFunction arguments can be discovered with the same technique but the error codes for the reverse matching are different. +For discovering available subFunctions the following error codes indicate the subFunction is not available: serviceNotSupported, serviceNotSupportedInActiveSession, subFunctionNotSupported, or subFunctionNotSupportedInActiveSession.

+

Each identifier or subFunction which responds with a different error code is considered available.

+
+
+

Memory Scan

+

TODO

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/uds/virtual_ecu.html b/uds/virtual_ecu.html new file mode 100644 index 000000000..ccb47aa4a --- /dev/null +++ b/uds/virtual_ecu.html @@ -0,0 +1,285 @@ + + + + + + + + + Virtual ECUs — gallia documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+

Virtual ECUs

+

For testing purposes, there exists the possibility to spawn virtual ECUs, against which the scanners can be run. +The virtual ECUs can however also be used independently of the remaining Gallia tools.

+

The generic command to create a virtual ECU is as follows:

+
$ gallia script vecu [vecu-arguments] <transport> <model> [model-arguments]
+
+
+

The virtual ECUs support different transport schemes and answering models, +which are explained in the following sections.

+
+

transport

+

The virtual ECU model is separated from the transport layer that is used for communication. +Currently, two different transport types are supported. +For each of them, a corresponding transport scheme exists on the scanner side, +which has to be used to enable communication between the scanner and the virtual ECU.

+
+

tcp-lines

+

The transport scheme which is the easiest to use is the tcp-lines scheme. +It requires no additional setup and can be used immediately. +When using this transport scheme, the virtual ECU can handle requests from multiple UDS clients, +but conversely a scanner can only talk to a single virtual ECU. +For most scanners this is the intended behavior. +For discovery scanners instead, the tcp-lines transport is not suitable.

+

For example, a random virtual ECU, which uses the tcp-lines protocol for communication +and listens for IPv4 connections on port 20162 can be started with the following command:

+
$ gallia script vecu "tcp-lines://127.0.0.1:20162" rng
+
+
+

For IPv6, the command would look as follows:

+
$ gallia script vecu "tcp-lines://[::1]:20162" rng
+
+
+
+
+

iso-tp

+

The iso-tp scheme operates on an existing can interface, which can be a physical interface or a virtual interface. +The following commands can be used to set up a virtual CAN interface with the name vcan0:

+
# ip link add dev vcan0 type vcan
+# ip link set up vcan0
+
+
+

In contrast to the tcp-lines approach, +using the iso-tp transport scheme allows to simulate a whole bus of several virtual ECUs, +and is therefore also suitable for testing discovery scanners.

+

For example, two random virtual ECUs, which uses the iso-tp protocol for communication +and use the vcan0 interface can be started with the following commands:

+
$ gallia script vecu "isotp://vcan0?src_addr=0x6aa&dst_addr=0x6f4&rx_ext_address=0xaa&ext_address=0xf4&is_fd=false" rng
+$ gallia script vecu "isotp://vcan0?src_addr=0x6bb&dst_addr=0x6f4&rx_ext_address=0xbb&ext_address=0xf4&is_fd=false" rng
+
+
+
+
+
+

model

+

There are currently two different types of ECU models, which can be used together with any of the supported transports.

+
+

rng

+

This type of model, creates an ECU, with a random set of supported sessions, services, sub-functions and identifiers, +where applicable. +By default, this model makes use of all available default behaviors, +thereby offering a very standard conformant behavior out of the box. +As with any model, these default mechanisms can be disabled via the vecu-options. +It can be further customized by specifying mandatory as well as optional sessions and services. +Additionally, the probabilities which are used to compute the previously mentioned set of available +functionality can be altered.

+

For example, a random virtual ECU with no customization can be created with the following command:

+
$ gallia vecu "tcp-lines://127.0.0.1:20162" rng
+Storing artifacts at ...
+Starting ...
+Initialized random UDS server with seed 1623483675214623782
+    "0x01": {
+        "0x01 (ShowCurrentData)": null,
+        "0x02 (ShowFreezeFrameData)": null,
+        "0x04 (ClearDiagnosticTroubleCodesAndStoredValues)": null,
+        "0x07 (ShowPendingDiagnosticTroubleCodes)": null,
+        "0x09 (RequestVehicleInformation)": null,
+        "0x10 (DiagnosticSessionControl)": "['0x01']",
+        "0x14 (ClearDiagnosticInformation)": null,
+        "0x27 (SecurityAccess)": "['0x29', '0x2a', '0x3f', '0x40', '0x63', '0x64']",
+        "0x31 (RoutineControl)": "['0x01', '0x02', '0x03']",
+        "0x3d (WriteMemoryByAddress)": null
+    }
+}
+
+
+

After the startup, the output shows an overview of the supported sessions, services and sub-functions. +In this case only one session with ten services is offered.

+

To enable reproducibility, at the startup, the output shows the seed, which has been used to initialize the random +number generator. +Using the same seed in combination with the same arguments, one can recreate the same virtual ECU:

+
gallia vecu "tcp-lines://127.0.0.1:20162" rng --seed <seed>
+
+
+

The following command shows how to control the selection of services, by altering the mandatory and optional services, +as well as the probability that determines, how likely one of the optional services is included. +The same is possible for services. +For other categories, only the probabilities can be changed.

+
$ gallia vecu "tcp-lines://127.0.0.1:20162" rng --mandatory_services "[DiagnosticSessionControl, ReadDataByIdentifier]" --optional_services "[RoutineControl]" --p_service 0.5
+Storing artifacts at ...
+Starting ...
+Initialized random UDS server with seed 222579011926250596
+{
+    "0x01": {
+        "0x10 (DiagnosticSessionControl)": "['0x01', '0x46']",
+        "0x22 (ReadDataByIdentifier)": null,
+        "0x31 (RoutineControl)": "['0x01', '0x02', '0x03']"
+    },
+    "0x46": {
+        "0x10 (DiagnosticSessionControl)": "['0x01']",
+        "0x22 (ReadDataByIdentifier)": null
+    }
+}
+
+
+
+
+

db

+

This type of model creates a virtual ECU that mimics the behavior of an already scanned ECU. +This functionality is based on the database logging of the scanners. +For each request that the virtual ECU receives, it looks up if there is a corresponding request and response pair in +the database, which also satisfies other conditions, such as a matching ECU state. +By default, it will return either the corresponding response, or no response at all, if no such pair exists. +As with any model, default ECU mechanisms, such as a default request or correct handling of the +suppressPosRespMsgIndicationBit are supported, but are disabled by default. +They might be particularly useful to compensate for behavior, that is not intended to be handled explicitly, +while being able to include the corresponding ECU in the testing procedure.

+

When using the db model, the virtual ECU requires the path of the database to which scan results have been +logged for the corresponding ECU. +For example, a virtual ECU that mimics the behavior of an ECU, +that was logged in /path/to/db can be created with the following command:

+
$ gallia vecu "tcp-lines://127.0.0.1:20162" db /path/to/db
+
+
+

If the database contains logs of multiple ECUs, one particular ECU can be chosen by its name. +Note, that the name is not filled in automatically +and may have to be manually added and referenced in one or more addresses. +Additionally, it is possible to include only those scan runs, for which the ECU satisfied a set of properties. +For example, the following command creates a virtual ECU that mimics the behavior of the ECU XYZ based on the logs +in /path/to/db where the software_version was 1.2.3:

+
$ gallia vecu "tcp-lines://127.0.0.1:20162" db /path/to/db --ecu "XYZ" --properties '{"software_version": "1.2.3"}'
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file