summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--PKGBUILD2
-rw-r--r--kvmd/__init__.py2
-rw-r--r--kvmd/apps/__init__.py4
-rw-r--r--kvmd/apps/kvmd/api/export.py10
-rw-r--r--kvmd/apps/kvmd/api/ugpio.py3
-rw-r--r--kvmd/apps/kvmd/server.py5
-rw-r--r--kvmd/apps/kvmd/ugpio.py56
-rwxr-xr-xsetup.py2
-rw-r--r--testenv/v2-hdmi-rpi4.override.yaml71
-rw-r--r--web/kvm/index.html3
-rw-r--r--web/kvm/navbar-gpio.pug4
-rw-r--r--web/kvm/navbar.pug1
-rw-r--r--web/share/css/main.css7
-rw-r--r--web/share/js/index/main.js2
-rw-r--r--web/share/js/kvm/gpio.js163
-rw-r--r--web/share/js/kvm/session.js5
-rw-r--r--web/share/svg/led-circle.svg128
18 files changed, 447 insertions, 23 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 0592f4fa..7a68dbc7 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
-current_version = 1.98
+current_version = 1.99
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
serialize =
{major}.{minor}
diff --git a/PKGBUILD b/PKGBUILD
index 1a8bb157..5866525b 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -26,7 +26,7 @@ for _variant in "${_variants[@]}"; do
pkgname+=(kvmd-platform-$_platform-$_board)
done
pkgbase=kvmd
-pkgver=1.98
+pkgver=1.99
pkgrel=1
pkgdesc="The main Pi-KVM daemon"
url="https://github.com/pikvm/kvmd"
diff --git a/kvmd/__init__.py b/kvmd/__init__.py
index 3e3c37d4..b570eafa 100644
--- a/kvmd/__init__.py
+++ b/kvmd/__init__.py
@@ -20,4 +20,4 @@
# ========================================================================== #
-__version__ = "1.98"
+__version__ = "1.99"
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py
index 28fbfc75..7202cfbc 100644
--- a/kvmd/apps/__init__.py
+++ b/kvmd/apps/__init__.py
@@ -187,6 +187,7 @@ def _patch_dynamic( # pylint: disable=too-many-locals
}
if mode == "output":
ch_scheme.update({
+ "busy_delay": Option(0.2, type=valid_float_f01),
"initial": Option(False, type=valid_bool),
"switch": Option(True, type=valid_bool),
"pulse": {
@@ -328,8 +329,7 @@ def _get_config_scheme() -> Dict:
"scheme": {}, # Dymanic content
"view": {
"header": {
- "title": Option("Switches"),
- "leds": Option([], type=valid_string_list),
+ "title": Option("GPIO"),
},
"table": Option([], type=valid_ugpio_view_table),
},
diff --git a/kvmd/apps/kvmd/api/export.py b/kvmd/apps/kvmd/api/export.py
index c37feb57..6dae0ce1 100644
--- a/kvmd/apps/kvmd/api/export.py
+++ b/kvmd/apps/kvmd/api/export.py
@@ -32,27 +32,33 @@ from aiohttp.web import Response
from ....plugins.atx import BaseAtx
from ..info import InfoManager
+from ..ugpio import UserGpio
from ..http import exposed_http
# =====
class ExportApi:
- def __init__(self, info_manager: InfoManager, atx: BaseAtx) -> None:
+ def __init__(self, info_manager: InfoManager, atx: BaseAtx, user_gpio: UserGpio) -> None:
self.__info_manager = info_manager
self.__atx = atx
+ self.__user_gpio = user_gpio
# =====
@exposed_http("GET", "/export/prometheus/metrics")
async def __prometheus_metrics_handler(self, _: Request) -> Response:
- (atx_state, hw_state) = await asyncio.gather(*[
+ (atx_state, hw_state, gpio_state) = await asyncio.gather(*[
self.__atx.get_state(),
self.__info_manager.get_submanager("hw").get_state(),
+ self.__user_gpio.get_state(),
])
rows: List[str] = []
self.__append_prometheus_rows(rows, atx_state["enabled"], "pikvm_atx_enabled")
self.__append_prometheus_rows(rows, atx_state["leds"]["power"], "pikvm_atx_power")
+ for mode in ["input", "output"]:
+ for (channel, gch) in gpio_state[f"{mode}s"].items():
+ self.__append_prometheus_rows(rows, gch["state"], f"pikvm_gpio_input_{channel}")
if hw_state is not None:
self.__append_prometheus_rows(rows, hw_state["health"], "pikvm_hw")
return Response(text="\n".join(rows))
diff --git a/kvmd/apps/kvmd/api/ugpio.py b/kvmd/apps/kvmd/api/ugpio.py
index 69ac6646..66f7b358 100644
--- a/kvmd/apps/kvmd/api/ugpio.py
+++ b/kvmd/apps/kvmd/api/ugpio.py
@@ -44,8 +44,7 @@ class UserGpioApi:
@exposed_http("GET", "/gpio")
async def __state_handler(self, _: Request) -> Response:
return make_json_response({
- "scheme": (await self.__user_gpio.get_scheme()),
- "view": (await self.__user_gpio.get_view()),
+ "model": (await self.__user_gpio.get_model()),
"state": (await self.__user_gpio.get_state()),
})
diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index 30d5c035..57d8c3e9 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -191,7 +191,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
AtxApi(atx),
MsdApi(msd, sync_chunk_size),
StreamerApi(streamer),
- ExportApi(info_manager, atx),
+ ExportApi(info_manager, atx, user_gpio),
]
self.__ws_handlers: Dict[str, Callable] = {}
@@ -244,8 +244,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
await client.ws.prepare(request)
await self.__register_ws_client(client)
try:
- await self.__broadcast_event("gpio_scheme_state", await self.__user_gpio.get_scheme())
- await self.__broadcast_event("gpio_view_state", await self.__user_gpio.get_view())
+ await self.__broadcast_event("gpio_model_state", await self.__user_gpio.get_model())
await asyncio.gather(*[
self.__broadcast_event(component.event_type, await component.get_state())
for component in self.__components
diff --git a/kvmd/apps/kvmd/ugpio.py b/kvmd/apps/kvmd/ugpio.py
index 97556aba..c6cfc121 100644
--- a/kvmd/apps/kvmd/ugpio.py
+++ b/kvmd/apps/kvmd/ugpio.py
@@ -23,6 +23,7 @@
import asyncio
import operator
+from typing import List
from typing import Dict
from typing import AsyncGenerator
from typing import Optional
@@ -88,6 +89,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__pulse_delay: float = config.pulse.delay
self.__min_pulse_delay: float = config.pulse.min_delay
self.__max_pulse_delay: float = config.pulse.max_delay
+ self.__busy_delay: float = config.busy_delay
self.__state = config.initial
self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier)
@@ -97,8 +99,8 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
"switch": self.__switch,
"pulse": {
"delay": self.__pulse_delay,
- "min_delay": self.__min_pulse_delay,
- "max_delay": self.__max_pulse_delay,
+ "min_delay": (self.__min_pulse_delay if self.__pulse_delay else 0),
+ "max_delay": (self.__max_pulse_delay if self.__pulse_delay else 0),
},
}
@@ -125,8 +127,10 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__write(state)
self.__state = state
get_logger(0).info("Switched GPIO %s to %d", self, state)
+ await asyncio.sleep(self.__busy_delay)
return True
self.__state = real_state
+ await asyncio.sleep(self.__busy_delay)
return False
@aiotools.atomic
@@ -146,7 +150,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
await asyncio.sleep(delay)
finally:
self.__write(False)
- await asyncio.sleep(1)
+ await asyncio.sleep(self.__busy_delay)
get_logger(0).info("Pulsed GPIO %s", self)
def __read(self) -> bool:
@@ -185,15 +189,15 @@ class UserGpio:
else: # output:
self.__outputs[channel] = _GpioOutput(channel, ch_config, self.__state_notifier)
- async def get_scheme(self) -> Dict:
+ async def get_model(self) -> Dict:
return {
- "inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()},
- "outputs": {channel: gout.get_scheme() for (channel, gout) in self.__outputs.items()},
+ "scheme": {
+ "inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()},
+ "outputs": {channel: gout.get_scheme() for (channel, gout) in self.__outputs.items()},
+ },
+ "view": self.__make_view(),
}
- async def get_view(self) -> Dict:
- return self.__view
-
async def get_state(self) -> Dict:
return {
"inputs": {channel: gin.get_state() for (channel, gin) in self.__inputs.items()},
@@ -240,3 +244,37 @@ class UserGpio:
if gout is None:
raise GpioChannelNotFoundError()
await gout.pulse(delay)
+
+ # =====
+
+ def __make_view(self) -> Dict:
+ table: List[Optional[List[Dict]]] = []
+ for row in self.__view["table"]:
+ if len(row) == 0:
+ table.append(None)
+ continue
+
+ items: List[Dict] = []
+ for item in map(str.strip, row):
+ if item.startswith("#") or len(item) == 0:
+ items.append({
+ "type": "label",
+ "text": item[1:].strip(),
+ })
+ elif (parts := list(map(str.strip, item.split(",", 1)))):
+ if parts[0] in self.__inputs:
+ items.append({
+ "type": "input",
+ "channel": parts[0],
+ })
+ elif parts[0] in self.__outputs:
+ items.append({
+ "type": "output",
+ "channel": parts[0],
+ "text": (parts[1] if len(parts) > 1 else "Click"),
+ })
+ table.append(items)
+ return {
+ "header": self.__view["header"],
+ "table": table,
+ }
diff --git a/setup.py b/setup.py
index 256b52da..7f50050a 100755
--- a/setup.py
+++ b/setup.py
@@ -67,7 +67,7 @@ def main() -> None:
setup(
name="kvmd",
- version="1.98",
+ version="1.99",
url="https://github.com/pikvm/kvmd",
license="GPLv3",
author="Maxim Devaev",
diff --git a/testenv/v2-hdmi-rpi4.override.yaml b/testenv/v2-hdmi-rpi4.override.yaml
index a297e78b..1977393a 100644
--- a/testenv/v2-hdmi-rpi4.override.yaml
+++ b/testenv/v2-hdmi-rpi4.override.yaml
@@ -37,6 +37,77 @@ kvmd:
- "--notify-parent"
- "--no-log-colors"
+ gpio:
+ scheme:
+ host1: # any name like foo_bar_baz
+ pin: 1
+ mode: input
+ host2:
+ pin: 2
+ mode: input
+ host3:
+ pin: 3
+ mode: input
+ host4:
+ pin: 4
+ mode: input
+ change_host:
+ pin: 5
+ mode: output
+ switch: false
+
+ host1_pwr:
+ pin: 11
+ mode: input
+ host2_pwr:
+ pin: 12
+ mode: input
+ host3_pwr:
+ pin: 13
+ mode: input
+ host4_pwr:
+ pin: 14
+ mode: input
+
+ host1_pwr_btn:
+ pin: 21
+ mode: output
+ switch: false
+ host2_pwr_btn:
+ pin: 22
+ mode: output
+ switch: false
+ host3_pwr_btn:
+ pin: 23
+ mode: output
+ switch: false
+ host4_pwr_btn:
+ pin: 24
+ mode: output
+ switch: false
+
+ lamp:
+ pin: 50
+ mode: output
+ pulse:
+ delay: 0
+
+ view:
+ header:
+ title: Switch
+ table:
+ - ["#Multihost controller"]
+ - []
+ - ["", "#Current", "#Power"]
+ - ["#host1.localdomain:", host1, host1_pwr, "host1_pwr_btn,Pwr"]
+ - ["#host2.localdomain:", host2, host2_pwr, "host2_pwr_btn,Pwr"]
+ - ["#host3.localdomain:", host3, host3_pwr, "host3_pwr_btn,Pwr"]
+ - ["#host4.localdomain:", host4, host4_pwr, "host4_pwr_btn,Pwr"]
+ - []
+ - ["change_host,Change host"]
+ - []
+ - ["#Lamp in the rack", lamp]
+
vnc:
keymap: /usr/share/kvmd/keymaps/ru
diff --git a/web/kvm/index.html b/web/kvm/index.html
index 727888ae..0c0b3ff0 100644
--- a/web/kvm/index.html
+++ b/web/kvm/index.html
@@ -328,6 +328,9 @@
</div>
</div>
</li>
+ <li class="right feature-disabled" id="gpio-dropdown"><a class="menu-button" id="gpio-menu-button" href="#">GPIO &#8628;</a>
+ <div class="menu" data-dont-hide-menu id="gpio-menu"></div>
+ </li>
<li class="right"><a class="menu-button" href="#"><img class="led-gray" data-dont-hide-menu id="hid-recorder-led" src="/share/svg/led-gear.svg">Macro &#8628;</a>
<div class="menu" data-dont-hide-menu>
<div class="text"><b>Record and play keyboard &amp; mouse actions<br></b><sub>For security reasons, the record will not saved on Pi-KVM</sub></div>
diff --git a/web/kvm/navbar-gpio.pug b/web/kvm/navbar-gpio.pug
new file mode 100644
index 00000000..280ba3fd
--- /dev/null
+++ b/web/kvm/navbar-gpio.pug
@@ -0,0 +1,4 @@
+li(id="gpio-dropdown" class="right feature-disabled")
+ a(class="menu-button" id="gpio-menu-button" href="#")
+ | GPIO &#8628;
+ div(data-dont-hide-menu id="gpio-menu" class="menu")
diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug
index d7743160..422cf236 100644
--- a/web/kvm/navbar.pug
+++ b/web/kvm/navbar.pug
@@ -23,5 +23,6 @@ ul(id="navbar")
include navbar-system.pug
include navbar-atx.pug
include navbar-msd.pug
+ include navbar-gpio.pug
include navbar-macro.pug
include navbar-shortcuts.pug
diff --git a/web/share/css/main.css b/web/share/css/main.css
index 443b637a..b41ea15a 100644
--- a/web/share/css/main.css
+++ b/web/share/css/main.css
@@ -94,6 +94,13 @@ img.inline-lamp {
margin-right: 2px;
}
+img.inline-lamp-big {
+ vertical-align: middle;
+ height: 20px;
+ margin-left: 2px;
+ margin-right: 2px;
+}
+
button,
select {
border: none;
diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js
index 24a694b8..ac6cf3fc 100644
--- a/web/share/js/index/main.js
+++ b/web/share/js/index/main.js
@@ -40,7 +40,7 @@ export function main() {
function __setAppText() {
$("app-text").innerHTML = `
<span class="code-comment"># On Linux using Chromium/Chrome via any terminal:<br>
- $</span> \`which chromium 2>/dev/null || which chrome 2>/dev/null\` --app="${window.location.href}"<br>
+ $</span> \`which chromium 2>/dev/null || which chrome 2>/dev/null || which google-chrome\` --app="${window.location.href}"<br>
<br>
<span class="code-comment"># On MacOS using Terminal application:<br>
$</span> /Applications/Google&bsol; Chrome.app/Contents/MacOS/Google&bsol; Chrome --app="${window.location.href}"<br>
diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js
new file mode 100644
index 00000000..56bd5d14
--- /dev/null
+++ b/web/share/js/kvm/gpio.js
@@ -0,0 +1,163 @@
+/*****************************************************************************
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
+# #
+*****************************************************************************/
+
+
+"use strict";
+
+
+import {tools, $, $$$} from "../tools.js";
+import {wm} from "../wm.js";
+
+
+export function Gpio() {
+ var self = this;
+
+ /************************************************************************/
+
+ var __state = null;
+
+ /************************************************************************/
+
+ self.setState = function(state) {
+ if (state) {
+ for (let channel in state.inputs) {
+ let el = $(`gpio-led-${channel}`);
+ if (el) {
+ __setLedState(el, state.inputs[channel].state);
+ }
+ }
+ for (let channel in state.outputs) {
+ for (let type of ["switch", "button"]) {
+ let el = $(`gpio-${type}-${channel}`);
+ if (el) {
+ wm.switchEnabled(el, !state.outputs[channel].busy);
+ }
+ }
+ }
+ } else {
+ for (let el of $$$(".gpio-led")) {
+ __setLedState(el, false);
+ }
+ for (let selector of [".gpio-switch", ".gpio-button"]) {
+ for (let el of $$$(selector)) {
+ wm.switchEnabled(el, false);
+ }
+ }
+ }
+ __state = state;
+ };
+
+ self.setModel = function(model) {
+ tools.featureSetEnabled($("gpio-dropdown"), model.view.table.length);
+ if (model.view.table.length) {
+ $("gpio-menu-button").innerHTML = `${model.view.header.title} &#8628;`;
+ }
+
+ let switches = [];
+ let buttons = [];
+ let content = "<table class=\"kv\">";
+ for (let row of model.view.table) {
+ if (row === null) {
+ content += "</table><hr><table class=\"kv\">";
+ } else {
+ content += "<tr>";
+ for (let item of row) {
+ if (item.type === "output") {
+ item.scheme = model.scheme.outputs[item.channel];
+ }
+ content += `<td align="center">${__createItem(item, switches, buttons)}</td>`;
+ }
+ content += "</tr>";
+ }
+ }
+ content += "</table>";
+ $("gpio-menu").innerHTML = content;
+
+ for (let channel of switches) {
+ tools.setOnClick($(`gpio-switch-${channel}`), () => __switchChannel(channel));
+ }
+ for (let channel of buttons) {
+ tools.setOnClick($(`gpio-button-${channel}`), () => __pulseChannel(channel));
+ }
+
+ self.setState(__state);
+ };
+
+ var __createItem = function(item, switches, buttons) {
+ if (item.type === "label") {
+ return item.text;
+ } else if (item.type === "input") {
+ return `<img id="gpio-led-${item.channel}" class="gpio-led inline-lamp-big led-gray" src="/share/svg/led-circle.svg" />`;
+ } else if (item.type === "output") {
+ let controls = [];
+ if (item.scheme["switch"]) {
+ switches.push(item.channel);
+ controls.push(`
+ <td><div class="switch-box">
+ <input disabled type="checkbox" id="gpio-switch-${item.channel}" class="gpio-switch" />
+ <label for="gpio-switch-${item.channel}">
+ <span class="switch-inner"></span>
+ <span class="switch"></span>
+ </label>
+ </div></td>
+ `);
+ }
+ if (item.scheme.pulse.delay) {
+ buttons.push(item.channel);
+ controls.push(`<td><button disabled id="gpio-button-${item.channel}" class="gpio-button">${item.text}</button></td>`);
+ }
+ return `<table><tr>${controls.join("<td>&nbsp;&nbsp;&nbsp;</td>")}</tr></table>`;
+ } else {
+ return "";
+ }
+ };
+
+ var __setLedState = function(el, state) {
+ if (state) {
+ el.classList.add("led-green");
+ el.classList.remove("led-gray");
+ } else {
+ el.classList.add("led-gray");
+ el.classList.remove("led-green");
+ }
+ };
+
+ var __switchChannel = function(channel) {
+ let to = ($(`gpio-switch-${channel}`).checked ? "1" : "0");
+ __sendPost(`/api/gpio/switch?channel=${channel}&state=${to}`);
+ };
+
+ var __pulseChannel = function(channel) {
+ __sendPost(`/api/gpio/pulse?channel=${channel}`);
+ };
+
+ var __sendPost = function(url) {
+ let http = tools.makeRequest("POST", url, function() {
+ if (http.readyState === 4) {
+ if (http.status === 409) {
+ wm.error("Performing another operation for this GPIO channel.<br>Please try again later");
+ } else if (http.status !== 200) {
+ wm.error("GPIO error:<br>", http.responseText);
+ }
+ }
+ });
+ };
+}
diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js
index 06ffcd95..7112d51c 100644
--- a/web/share/js/kvm/session.js
+++ b/web/share/js/kvm/session.js
@@ -31,6 +31,7 @@ import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
import {WakeOnLan} from "./wol.js";
+import {Gpio} from "./gpio.js";
export function Session() {
@@ -48,6 +49,7 @@ export function Session() {
var __msd = new Msd();
var __streamer = new Streamer();
var __wol = new WakeOnLan();
+ var __gpio = new Gpio();
var __init__ = function() {
__startSession();
@@ -211,6 +213,8 @@ export function Session() {
case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break;
case "wol_state": __wol.setState(data.event); break;
+ case "gpio_model_state": __gpio.setModel(data.event); break;
+ case "gpio_state": __gpio.setState(data.event); break;
case "hid_state": __hid.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.setState(data.event); break;
@@ -237,6 +241,7 @@ export function Session() {
__ping_timer = null;
}
+ __gpio.setState(null);
__hid.setSocket(null);
__atx.setState(null);
__msd.setState(null);
diff --git a/web/share/svg/led-circle.svg b/web/share/svg/led-circle.svg
new file mode 100644
index 00000000..761b9dcf
--- /dev/null
+++ b/web/share/svg/led-circle.svg
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="481.1192"
+ width="481.1192"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="led-circle.svg"
+ xml:space="preserve"
+ viewBox="0 0 481.02523 481.1192"
+ y="0px"
+ x="0px"
+ id="Layer_1"
+ version="1.1"><metadata
+ id="metadata85"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs83" /><sodipodi:namedview
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ lock-margins="false"
+ inkscape:document-rotation="0"
+ inkscape:current-layer="Layer_1"
+ inkscape:window-maximized="1"
+ inkscape:window-y="28"
+ inkscape:window-x="0"
+ inkscape:cy="226.75611"
+ inkscape:cx="207.75746"
+ inkscape:zoom="0.921875"
+ showgrid="false"
+ id="namedview81"
+ inkscape:window-height="714"
+ inkscape:window-width="1366"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff" />
+
+
+
+
+
+
+
+
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g50">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g52">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g54">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g56">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g58">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g60">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g62">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g64">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g66">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g68">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g70">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g72">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g74">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g76">
+</g>
+<g
+ transform="translate(-15.683731,-12.674362)"
+ id="g78">
+</g>
+<rect
+ y="45.901909"
+ x="10.350166"
+ height="351.45764"
+ width="478.37289"
+ id="rect4830"
+ style="opacity:1;fill:none;fill-opacity:1;paint-order:normal" /><circle
+ r="240.5596"
+ cy="240.5596"
+ cx="240.51262"
+ id="path857"
+ style="fill:#000000" /></svg>