-
Notifications
You must be signed in to change notification settings - Fork 0
Added deployment view parsing #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <DeploymentView version="1.2" UiFile="deploymentview.ui.xml" creatorHash="383508e" modifierHash="383508e"> | ||
| <Node id="{d90e674a-8471-4e59-8bc6-c79e82c1067b}" name="SAM V71 RTEMS N7S_1" type="ocarina_processors_arm::samv71.rtems" node_label="Node_2" namespace="ocarina_processors_arm"> | ||
| <Partition id="{6d924f84-5366-47d0-8a89-56a2614f6813}" name="ASW"> | ||
| <Function id="{81aa583e-d1d0-47bb-ae8b-de3323dac654}" name="Frontend" path="Frontend"/> | ||
| <Function id="{4893b901-a505-42da-bad7-e3fa914fda5c}" name="Backend" path="Backend"/> | ||
| </Partition> | ||
| <Device id="{3d4974d8-cd66-4f5b-a8c7-813e8284d726}" name="uart0" requirement_ids="r10" requires_bus_access="ocarina_buses::serial.ccsds" port="uart0" asn1file="/home/taste/tool-inst/include/TASTE-SAMV71-RTEMS-Drivers/configurations/samv71-rtems-serial-driver.asn" asn1type="Serial-SamV71-Rtems-Conf-T" asn1module="SAMV71-RTEMS-SERIAL-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.samv71_rtems" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{429de864-ffda-4d9a-846b-6851debc08d0}" name="uart1" requires_bus_access="ocarina_buses::serial.ccsds" port="uart1" asn1file="/home/taste/tool-inst/include/TASTE-SAMV71-RTEMS-Drivers/configurations/samv71-rtems-serial-driver.asn" asn1type="Serial-SamV71-Rtems-Conf-T" asn1module="SAMV71-RTEMS-SERIAL-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.samv71_rtems" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{3b9b9f0e-4376-441c-9fb4-20a82d9adfaf}" name="uart2" requires_bus_access="ocarina_buses::serial.ccsds" port="uart2" asn1file="/home/taste/tool-inst/include/TASTE-SAMV71-RTEMS-Drivers/configurations/samv71-rtems-serial-driver.asn" asn1type="Serial-SamV71-Rtems-Conf-T" asn1module="SAMV71-RTEMS-SERIAL-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.samv71_rtems" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{46e38140-b204-4360-bdb2-766800b08c13}" name="uart3" requires_bus_access="ocarina_buses::serial.ccsds" port="uart3" asn1file="/home/taste/tool-inst/include/TASTE-SAMV71-RTEMS-Drivers/configurations/samv71-rtems-serial-driver.asn" asn1type="Serial-SamV71-Rtems-Conf-T" asn1module="SAMV71-RTEMS-SERIAL-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.samv71_rtems" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{179bb7d9-a647-401f-b8fc-8398825c82aa}" name="uart4" requires_bus_access="ocarina_buses::serial.ccsds" port="uart4" asn1file="/home/taste/tool-inst/include/TASTE-SAMV71-RTEMS-Drivers/configurations/samv71-rtems-serial-driver.asn" asn1type="Serial-SamV71-Rtems-Conf-T" asn1module="SAMV71-RTEMS-SERIAL-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.samv71_rtems" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| </Node> | ||
| <Node id="{d2ed099d-9b64-446a-828c-639641de9cf7}" name="x86 Linux C++_1" type="ocarina_processors_x86::x86.generic_linux" requirement_ids="r21,r20" node_label="Node_1" namespace="ocarina_processors_x86"> | ||
| <Partition id="{3306c70d-08de-4beb-a6b2-e8b3d4861243}" name="Ground"> | ||
| <Function id="{0042ca70-4823-4460-88fe-d873823b9e73}" name="EGSE" path="EGSE"/> | ||
| </Partition> | ||
| <Device id="{a294e422-1f35-4716-88f2-c89c5621e71c}" name="tcp1" requires_bus_access="ocarina_buses::ip.generic" port="tcp1" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::ip_socket" impl_extends="ocarina_drivers::ip_socket.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{143518ef-a9fa-4c50-a310-7dfd56a92e64}" name="tcp2" requires_bus_access="ocarina_buses::ip.generic" port="tcp2" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::ip_socket" impl_extends="ocarina_drivers::ip_socket.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{4d3b0112-4110-4f88-a0c4-5cbb5c301911}" name="tcp3" requires_bus_access="ocarina_buses::ip.generic" port="tcp3" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::ip_socket" impl_extends="ocarina_drivers::ip_socket.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{486034e5-9983-4c09-add7-d33615bad4ab}" name="tcp4" requires_bus_access="ocarina_buses::ip.generic" port="tcp4" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::ip_socket" impl_extends="ocarina_drivers::ip_socket.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{461825c1-d258-4142-87f3-aa1cf7e4ec58}" name="udp1" requires_bus_access="ocarina_buses::ip.generic" port="udp1" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::udp" impl_extends="ocarina_drivers::udp.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{e4adae5b-da7a-4116-ab07-cca5836ae956}" name="udp2" requires_bus_access="ocarina_buses::ip.generic" port="udp2" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::udp" impl_extends="ocarina_drivers::udp.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{2babb575-5420-4079-a675-10024db8d065}" name="udp3" requires_bus_access="ocarina_buses::ip.generic" port="udp3" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::udp" impl_extends="ocarina_drivers::udp.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{95e09c1c-a585-48ba-989e-05876c88e4b6}" name="udp4" requires_bus_access="ocarina_buses::ip.generic" port="udp4" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-socket-ip-driver.asn" asn1type="Socket-IP-Conf-T" asn1module="LINUX-SOCKET-IP-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::udp" impl_extends="ocarina_drivers::udp.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| <Device id="{e18013d2-4879-47f6-8fd9-a6eefb7d1c38}" name="uart0" requires_bus_access="ocarina_buses::serial.ccsds" port="uart0" asn1file="/home/taste/tool-inst/include/TASTE-Linux-Drivers/configurations/linux-serial-ccsds-driver.asn" asn1type="Serial-CCSDS-Linux-Conf-T" asn1module="LINUX-SERIAL-CCSDS-DRIVER" namespace="ocarina_drivers" extends="ocarina_drivers::serial_ccsds" impl_extends="ocarina_drivers::serial_ccsds.linux" bus_namespace="ocarina_buses"> | ||
| </Device> | ||
| </Node> | ||
| <Connection id="{22fe1747-61e5-4c8c-be5b-64a520be8244}" name="Connection_1" from_node="x86 Linux C++_1" from_port="uart0" to_bus="ocarina_buses::serial.ccsds" to_node="SAM V71 RTEMS N7S_1" to_port="uart0"> | ||
| <Message id="{8db75af6-2fc4-458c-9e30-7fd7e44479eb}" name="Message_1" from_function="EGSE" from_interface="tc" to_function="Frontend" to_interface="tc"/> | ||
| <Message id="{7cd12471-ac99-4dfc-8c7d-a62280efc46b}" name="Message_2" from_function="Frontend" from_interface="tm" to_function="EGSE" to_interface="tm"/> | ||
| </Connection> | ||
| </DeploymentView> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| """ | ||
| TASTE Deployment View (DV) data model classes. | ||
|
|
||
| This module provides Python classes that reflect the schema/structure of | ||
| TASTE Deployment View XML files, allowing for parsing, manipulation, and | ||
| generation of DV data. | ||
| """ | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from typing import List | ||
|
|
||
|
|
||
| @dataclass | ||
| class DeploymentFunction: | ||
| """Function deployed to a partition.""" | ||
|
|
||
| id: str | ||
| name: str | ||
| path: str | ||
|
|
||
|
|
||
| @dataclass | ||
| class Partition: | ||
| """ | ||
| Partition within a node. | ||
|
|
||
| A partition represents an execution context that can host one or more | ||
| functions (software components). | ||
| """ | ||
|
|
||
| id: str | ||
| name: str | ||
| functions: List[DeploymentFunction] = field(default_factory=list) | ||
|
|
||
|
|
||
| @dataclass | ||
| class Device: | ||
| """ | ||
| Hardware device attached to a node. | ||
|
|
||
| Devices represent connection hardpoints (e.g., UART, TCP/UDP ports) that | ||
| provide bus access for communication. | ||
| """ | ||
|
|
||
| id: str | ||
| name: str | ||
| requires_bus_access: str | ||
| port: str | ||
| asn1file: str | ||
| asn1type: str | ||
| asn1module: str | ||
| namespace: str | ||
| extends: str | ||
| impl_extends: str | ||
| bus_namespace: str | ||
| requirement_ids: List[str] = field(default_factory=list) | ||
|
|
||
|
|
||
| @dataclass | ||
| class Node: | ||
| """ | ||
| Deployment node (processor/platform). | ||
|
|
||
| A node represents a physical or virtual hardware platform that can host | ||
| partitions and devices. Examples include embedded processors, Linux systems, | ||
| or other execution platforms. | ||
| """ | ||
|
|
||
| id: str | ||
| name: str | ||
| type: str | ||
| node_label: str | ||
| namespace: str | ||
| partitions: List[Partition] = field(default_factory=list) | ||
| devices: List[Device] = field(default_factory=list) | ||
| requirement_ids: List[str] = field(default_factory=list) | ||
|
|
||
|
|
||
| @dataclass | ||
| class Message: | ||
| """ | ||
| Message routed through a connection. | ||
|
|
||
| Represents data flow from one function's interface to another function's | ||
| interface over a physical connection. | ||
| """ | ||
|
|
||
| id: str | ||
| name: str | ||
| from_function: str | ||
| from_interface: str | ||
| to_function: str | ||
| to_interface: str | ||
|
|
||
|
|
||
| @dataclass | ||
| class Connection: | ||
| """ | ||
| Physical connection between nodes. | ||
|
|
||
| Represents a communication link between devices on different nodes, | ||
| potentially through a bus. Contains the messages that are routed | ||
| through this connection. | ||
| """ | ||
|
|
||
| id: str | ||
| name: str | ||
| from_node: str | ||
| from_port: str | ||
| to_bus: str | ||
| to_node: str | ||
| to_port: str | ||
| messages: List[Message] = field(default_factory=list) | ||
|
|
||
|
|
||
| @dataclass | ||
| class DeploymentView: | ||
| """ | ||
| Root element representing a TASTE Deployment View. | ||
|
|
||
| This is the main data structure that contains all nodes, connections, | ||
| and other elements that define how a TASTE system is deployed to | ||
| physical/virtual hardware. | ||
| """ | ||
|
|
||
| version: str = "" | ||
| ui_file: str = "" | ||
| creator_hash: str = "" | ||
| modifier_hash: str = "" | ||
| nodes: List[Node] = field(default_factory=list) | ||
| connections: List[Connection] = field(default_factory=list) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| """ | ||
| TASTE Deployment View XML Reader. | ||
|
|
||
| This module provides functionality to parse TASTE Deployment View XML files | ||
| and construct DeploymentView data model instances. | ||
| """ | ||
|
|
||
| import xml.etree.ElementTree as ET | ||
| from pathlib import Path | ||
| from typing import Union | ||
|
|
||
| from templateprocessor.dv import ( | ||
| DeploymentView, | ||
| Node, | ||
| Partition, | ||
| DeploymentFunction, | ||
| Device, | ||
| Connection, | ||
| Message, | ||
| ) | ||
|
|
||
|
|
||
| class DVReader: | ||
| """ | ||
| Reader for TASTE Deployment View XML files. | ||
|
|
||
| Parses XML files conforming to the TASTE Deployment View schema and | ||
| constructs corresponding DeploymentView objects. | ||
|
|
||
| Example: | ||
| reader = DVReader() | ||
| deployment_view = reader.read("deploymentview.dv.xml") | ||
| """ | ||
|
|
||
| def read(self, file_path: Union[str, Path]) -> DeploymentView: | ||
| """ | ||
| Read and parse a TASTE Deployment View XML file. | ||
|
|
||
| Args: | ||
| file_path: Path to the DV XML file | ||
|
|
||
| Returns: | ||
| DeploymentView object populated with parsed data | ||
|
|
||
| Raises: | ||
| FileNotFoundError: If the file does not exist | ||
| xml.etree.ElementTree.ParseError: If XML is malformed | ||
| """ | ||
| file_path = Path(file_path) | ||
| if not file_path.exists(): | ||
| raise FileNotFoundError(f"Deployment View file not found: {file_path}") | ||
|
|
||
| tree = ET.parse(file_path) | ||
| root = tree.getroot() | ||
|
|
||
| return self._parse_deployment_view(root) | ||
|
|
||
| def read_string(self, xml_content: str) -> DeploymentView: | ||
| """ | ||
| Read and parse TASTE Deployment View XML from a string. | ||
|
|
||
| Args: | ||
| xml_content: XML content as string | ||
|
|
||
| Returns: | ||
| DeploymentView object populated with parsed data | ||
|
|
||
| Raises: | ||
| xml.etree.ElementTree.ParseError: If XML is malformed | ||
| """ | ||
| root = ET.fromstring(xml_content) | ||
| return self._parse_deployment_view(root) | ||
|
|
||
| def _parse_deployment_view(self, root: ET.Element) -> DeploymentView: | ||
| """Parse the root DeploymentView element.""" | ||
| dv = DeploymentView( | ||
| version=root.get("version", ""), | ||
| ui_file=root.get("UiFile", ""), | ||
| creator_hash=root.get("creatorHash", ""), | ||
| modifier_hash=root.get("modifierHash", ""), | ||
| ) | ||
|
|
||
| # Parse all Node elements | ||
| for node_elem in root.findall("Node"): | ||
| node = self._parse_node(node_elem) | ||
| dv.nodes.append(node) | ||
|
|
||
| # Parse all Connection elements | ||
| for conn_elem in root.findall("Connection"): | ||
| connection = self._parse_connection(conn_elem) | ||
| dv.connections.append(connection) | ||
|
|
||
| return dv | ||
|
|
||
| def _parse_node(self, elem: ET.Element) -> Node: | ||
| """Parse a Node element.""" | ||
| # Parse requirement_ids if present | ||
| requirement_ids = [] | ||
| req_ids_str = elem.get("requirement_ids", "") | ||
| if req_ids_str: | ||
| requirement_ids = [ | ||
| rid.strip() for rid in req_ids_str.split(",") if rid.strip() | ||
| ] | ||
|
|
||
| node = Node( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| type=elem.get("type", ""), | ||
| node_label=elem.get("node_label", ""), | ||
| namespace=elem.get("namespace", ""), | ||
| requirement_ids=requirement_ids, | ||
| ) | ||
|
|
||
| # Parse partitions | ||
| for partition_elem in elem.findall("Partition"): | ||
| partition = self._parse_partition(partition_elem) | ||
| node.partitions.append(partition) | ||
|
|
||
| # Parse devices | ||
| for device_elem in elem.findall("Device"): | ||
| device = self._parse_device(device_elem) | ||
| node.devices.append(device) | ||
|
|
||
| return node | ||
|
|
||
| def _parse_partition(self, elem: ET.Element) -> Partition: | ||
| """Parse a Partition element.""" | ||
| partition = Partition( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| ) | ||
|
|
||
| # Parse functions | ||
| for func_elem in elem.findall("Function"): | ||
| function = self._parse_deployment_function(func_elem) | ||
| partition.functions.append(function) | ||
|
|
||
| return partition | ||
|
|
||
| def _parse_deployment_function(self, elem: ET.Element) -> DeploymentFunction: | ||
| """Parse a Function element within a Partition.""" | ||
| return DeploymentFunction( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| path=elem.get("path", ""), | ||
| ) | ||
|
|
||
| def _parse_device(self, elem: ET.Element) -> Device: | ||
| """Parse a Device element.""" | ||
| # Parse requirement_ids if present | ||
| requirement_ids = [] | ||
| req_ids_str = elem.get("requirement_ids", "") | ||
| if req_ids_str: | ||
| requirement_ids = [ | ||
| rid.strip() for rid in req_ids_str.split(",") if rid.strip() | ||
| ] | ||
|
|
||
| return Device( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| requires_bus_access=elem.get("requires_bus_access", ""), | ||
| port=elem.get("port", ""), | ||
| asn1file=elem.get("asn1file", ""), | ||
| asn1type=elem.get("asn1type", ""), | ||
| asn1module=elem.get("asn1module", ""), | ||
| namespace=elem.get("namespace", ""), | ||
| extends=elem.get("extends", ""), | ||
| impl_extends=elem.get("impl_extends", ""), | ||
| bus_namespace=elem.get("bus_namespace", ""), | ||
| requirement_ids=requirement_ids, | ||
| ) | ||
|
|
||
| def _parse_connection(self, elem: ET.Element) -> Connection: | ||
| """Parse a Connection element.""" | ||
| connection = Connection( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| from_node=elem.get("from_node", ""), | ||
| from_port=elem.get("from_port", ""), | ||
| to_bus=elem.get("to_bus", ""), | ||
| to_node=elem.get("to_node", ""), | ||
| to_port=elem.get("to_port", ""), | ||
| ) | ||
|
|
||
| # Parse messages | ||
| for msg_elem in elem.findall("Message"): | ||
| message = self._parse_message(msg_elem) | ||
| connection.messages.append(message) | ||
|
|
||
| return connection | ||
|
|
||
| def _parse_message(self, elem: ET.Element) -> Message: | ||
| """Parse a Message element.""" | ||
| return Message( | ||
| id=elem.get("id", ""), | ||
| name=elem.get("name", ""), | ||
| from_function=elem.get("from_function", ""), | ||
| from_interface=elem.get("from_interface", ""), | ||
| to_function=elem.get("to_function", ""), | ||
| to_interface=elem.get("to_interface", ""), | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ PYTHON ?= python3 | |
|
|
||
| TESTS = \ | ||
| test_ivreader.py \ | ||
| test_dvreader.py \ | ||
| test_soreader.py | ||
|
|
||
| .PHONY: \ | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.