From 7b84594043a6b9d5001176961f0e58b6843bcb0a Mon Sep 17 00:00:00 2001
From: yasufumi <52214705+yasufumi-nakata@users.noreply.github.com>
Date: Mon, 23 Feb 2026 06:03:40 +0900
Subject: [PATCH 1/5] Improve cross-platform startup and GUI compatibility
---
PyQt4/Qt.py | 235 ++
PyQt4/QtCore.py | 40 +
PyQt4/QtGui.py | 5 +
PyQt4/Qwt5.py | 221 +
PyQt4/__init__.py | 25 +
PyQt4/uic.py | 11 +
README.md | 27 +
actichamp_w.py | 2608 ++++++------
amplifier.py | 3166 +++++++-------
custom_modules/dc_offset.py | 524 +--
devices/devbase.py | 525 ++-
devices/devcontainer.py | 588 +--
devices/epp.py | 481 ++-
display.py | 1931 ++++-----
filter.py | 1349 +++---
headless.py | 63 +
impedance.py | 936 ++---
loadlibs.py | 271 +-
main.py | 2797 +++++++------
modbase.py | 1419 +++----
montage.py | 1199 +++---
rda_client.py | 1452 +++----
rda_server.py | 1099 ++---
remote.py | 558 +--
requirements.txt | 5 +
res/frmActiChampOnline.py | 210 +-
res/frmMain.py | 225 +-
res/frmMainConfiguration.py | 89 +-
res/frmRdaClientOnline.py | 184 +-
res/frmStorageVisionOnline.py | 352 +-
res/resources_rc.py | 7407 +++++++++++++++++----------------
run_pycorder.bat | 4 +
run_pycorder.ps1 | 58 +
run_pycorder.sh | 32 +
storage.py | 1998 +++++----
syscheck.py | 642 +--
tools/modview.py | 1117 ++---
tutorial/tut_2.py | 252 +-
tutorial/tut_3.py | 1197 +++---
tutorial/tut_4.py | 474 +--
40 files changed, 18389 insertions(+), 17387 deletions(-)
create mode 100644 PyQt4/Qt.py
create mode 100644 PyQt4/QtCore.py
create mode 100644 PyQt4/QtGui.py
create mode 100644 PyQt4/Qwt5.py
create mode 100644 PyQt4/__init__.py
create mode 100644 PyQt4/uic.py
create mode 100644 headless.py
create mode 100644 requirements.txt
create mode 100644 run_pycorder.bat
create mode 100644 run_pycorder.ps1
create mode 100755 run_pycorder.sh
diff --git a/PyQt4/Qt.py b/PyQt4/Qt.py
new file mode 100644
index 0000000..bd09ad6
--- /dev/null
+++ b/PyQt4/Qt.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+"""
+Qt shim to let legacy PyQt4-style imports run on top of PySide6.
+This provides commonly used classes and functions with minimal coverage
+for this project (signals via old-style strings, QMessageBox, QFileDialog,
+QApplication, QWidget, QFrame, QPen, QFont, QColor, QRect, QPoint, etc.).
+"""
+from PySide6 import QtCore, QtGui, QtWidgets
+
+# Expose common namespaces under a single module like PyQt4.Qt
+Qt = QtCore.Qt
+
+# Widgets commonly referenced
+QApplication = QtWidgets.QApplication
+QApplication.UnicodeUTF8 = object()
+
+
+def _qt4_translate(context, text, disambiguation=None, encoding=None):
+ # PyQt4 used a four-argument translate that accepted an encoding flag.
+ # Qt6 dropped the encoding parameter, so ignore it here for compatibility.
+ if disambiguation is None:
+ return QtCore.QCoreApplication.translate(context, text)
+ return QtCore.QCoreApplication.translate(context, text, disambiguation)
+
+
+QApplication.translate = staticmethod(_qt4_translate)
+QWidget = QtWidgets.QWidget
+QDialog = QtWidgets.QDialog
+QMainWindow = QtWidgets.QMainWindow
+QMessageBox = QtWidgets.QMessageBox
+QFileDialog = QtWidgets.QFileDialog
+QFrame = QtWidgets.QFrame
+QProgressBar = QtWidgets.QProgressBar
+QLabel = QtWidgets.QLabel
+QPen = QtGui.QPen
+QFont = QtGui.QFont
+QColor = QtGui.QColor
+QRect = QtCore.QRect
+QPoint = QtCore.QPoint
+QSize = QtCore.QSize
+QDir = QtCore.QDir
+QTableView = QtWidgets.QTableView
+QHeaderView = QtWidgets.QHeaderView
+QAbstractItemView = QtWidgets.QAbstractItemView
+QStyledItemDelegate = QtWidgets.QStyledItemDelegate
+QComboBox = QtWidgets.QComboBox
+QPlainTextEdit = QtWidgets.QPlainTextEdit
+QSpinBox = QtWidgets.QSpinBox
+QDoubleSpinBox = QtWidgets.QDoubleSpinBox
+QAbstractTableModel = QtCore.QAbstractTableModel
+QModelIndex = QtCore.QModelIndex
+
+
+class QMetaType:
+ Bool = 1
+ QString = 2
+
+
+class QVariant:
+ def __init__(self, value=None):
+ self._value = value
+
+ def isValid(self):
+ return self._value is not None
+
+ def type(self):
+ v = self._value
+ if isinstance(v, bool):
+ return QMetaType.Bool
+ # treat everything else as non-bool (string/number)
+ return QMetaType.QString
+
+ def toBool(self):
+ return bool(self._value)
+
+ def toInt(self):
+ try:
+ return int(self._value), True
+ except Exception:
+ return 0, False
+
+ def toDouble(self):
+ try:
+ return float(self._value), True
+ except Exception:
+ return 0.0, False
+
+ def toString(self):
+ return str(self._value)
+
+ def toStringList(self):
+ if isinstance(self._value, (list, tuple)):
+ return [str(x) for x in self._value]
+ return []
+
+
+class _QStringHelper(str):
+ @staticmethod
+ def number(n):
+ return str(n)
+
+ def toFloat(self):
+ try:
+ return float(self), True
+ except Exception:
+ return 0.0, False
+
+ def toDouble(self):
+ return self.toFloat()
+
+ def toInt(self):
+ try:
+ return int(float(self)), True
+ except Exception:
+ return 0, False
+
+QString = _QStringHelper # legacy alias
+
+
+def _wrap_qstring_return(func):
+ def wrapper(*args, **kwargs):
+ result = func(*args, **kwargs)
+ if isinstance(result, str) and not isinstance(result, _QStringHelper):
+ return _QStringHelper(result)
+ return result
+ return wrapper
+
+
+for _cls, _methods in (
+ (QtWidgets.QComboBox, ('currentText', 'itemText')),
+ (QtWidgets.QLineEdit, ('text', 'displayText')),
+):
+ for _name in _methods:
+ if hasattr(_cls, _name):
+ setattr(_cls, _name, _wrap_qstring_return(getattr(_cls, _name)))
+
+# Version string expected by legacy code
+try:
+ QT_VERSION_STR = QtCore.qVersion()
+except Exception:
+ QT_VERSION_STR = "6"
+
+# Old-style SIGNAL/SLOT compatibility
+class _SignalProxy(object):
+ def __init__(self, obj):
+ self._obj = obj
+
+ def __call__(self, signature):
+ # Return a wrapper that emits via dynamic signal map
+ # We create/lookup a QtCore.Signal dynamically on the instance
+ name = signature.split('(')[0]
+ # Maintain a map on the object
+ if not hasattr(self._obj, "__legacy_signals__"):
+ self._obj.__legacy_signals__ = {}
+ signals = self._obj.__legacy_signals__
+ if name not in signals:
+ # Create a Python signal using QObject's meta-object is non-trivial at runtime.
+ # Instead, we emulate emit/connect using a list of python callables.
+ signals[name] = []
+
+ class _Emitter(object):
+ def __init__(self, obj, name):
+ self._obj = obj
+ self._name = name
+
+ def connect(self, slot):
+ self._obj.__legacy_signals__[self._name].append(slot)
+
+ def emit(self, *args, **kwargs):
+ for cb in list(self._obj.__legacy_signals__.get(self._name, [])):
+ cb(*args, **kwargs)
+
+ return _Emitter(self._obj, name)
+
+
+def SIGNAL(signature):
+ try:
+ return QtCore.SIGNAL(signature)
+ except AttributeError:
+ return signature
+
+
+def SLOT(signature):
+ try:
+ return QtCore.SLOT(signature)
+ except AttributeError:
+ return signature
+
+
+class QObject(QtCore.QObject):
+ # Provide legacy helpers only if needed; native PySide6 connect/emit usually works
+ def emit(self, signal, *args):
+ # best-effort legacy emit when used with string signatures
+ if not hasattr(self, 'SIGNAL'):
+ self.SIGNAL = _SignalProxy(self)
+ self.SIGNAL(signal).emit(*args)
+
+ def connect(self, obj, signal, slot): # noqa: A003 - keep legacy name
+ if isinstance(signal, str):
+ if not hasattr(obj, 'SIGNAL'):
+ obj.SIGNAL = _SignalProxy(obj)
+ obj.SIGNAL(signal).connect(slot)
+ return True
+ if hasattr(signal, 'connect'):
+ signal.connect(slot)
+ return True
+ raise AttributeError("Unsupported signal type: %r" % (signal,))
+
+
+# Patch PySide6 QObject with legacy helpers
+QtCore.QObject.emit = QObject.emit
+QtCore.QObject.connect = QObject.connect
+
+
+# Inject mixin behavior into key widgets so .connect/.emit is available
+# Avoid altering Qt class hierarchies to prevent instability
+
+# Namespace shortcuts expected by code
+class QStringList(list):
+ def __init__(self, *args, **kwargs):
+ if len(args) == 1 and isinstance(args[0], str):
+ super(QStringList, self).__init__([args[0]])
+ else:
+ super(QStringList, self).__init__(*args, **kwargs)
+
+QDir = QtCore.QDir
+
+# Re-export submodules for import style from PyQt4 import Qt; then Qt.Qt etc.
+__all__ = [
+ 'Qt', 'QApplication', 'QWidget', 'QDialog', 'QMainWindow', 'QMessageBox', 'QFileDialog',
+ 'QFrame', 'QProgressBar', 'QLabel', 'QPen', 'QFont', 'QColor', 'QRect', 'QPoint', 'QDir',
+ 'QTableView', 'QHeaderView', 'QAbstractItemView', 'QStyledItemDelegate', 'QComboBox',
+ 'QPlainTextEdit', 'QSpinBox', 'QDoubleSpinBox', 'QAbstractTableModel', 'QModelIndex',
+ 'QObject', 'SIGNAL', 'SLOT', 'QT_VERSION_STR', 'QString', 'QStringList', 'QMetaType', 'QVariant'
+]
diff --git a/PyQt4/QtCore.py b/PyQt4/QtCore.py
new file mode 100644
index 0000000..1cbe11a
--- /dev/null
+++ b/PyQt4/QtCore.py
@@ -0,0 +1,40 @@
+from PySide6.QtCore import * # noqa: F401,F403
+
+
+class QVariant(object): # minimal shim for Qt4 API
+ def __init__(self, value=None):
+ self._value = value
+
+ def isValid(self):
+ return self._value is not None
+
+ def type(self):
+ value = self._value
+ if isinstance(value, bool):
+ return 1
+ return 2
+
+ def toBool(self):
+ return bool(self._value)
+
+ def toInt(self):
+ try:
+ return int(self._value), True
+ except Exception:
+ return 0, False
+
+ def toDouble(self):
+ try:
+ return float(self._value), True
+ except Exception:
+ return 0.0, False
+
+ def toString(self):
+ return str(self._value)
+
+ def toStringList(self):
+ if isinstance(self._value, (list, tuple)):
+ return [str(x) for x in self._value]
+ return []
+
+
diff --git a/PyQt4/QtGui.py b/PyQt4/QtGui.py
new file mode 100644
index 0000000..7bb443e
--- /dev/null
+++ b/PyQt4/QtGui.py
@@ -0,0 +1,5 @@
+from PySide6.QtWidgets import * # noqa: F401,F403
+from PySide6.QtGui import * # noqa: F401,F403
+
+
+
diff --git a/PyQt4/Qwt5.py b/PyQt4/Qwt5.py
new file mode 100644
index 0000000..6f17099
--- /dev/null
+++ b/PyQt4/Qwt5.py
@@ -0,0 +1,221 @@
+# Very small Qwt5 compatibility shim using pyqtgraph for plotting.
+# Only covers what display.py/impedance.py need.
+
+import pyqtgraph as pg
+from PySide6 import QtWidgets, QtGui, QtCore
+
+QWT_VERSION_STR = "5.99-shim"
+
+class QwtLegend(QtWidgets.QListWidget):
+ ClickableItem = 1
+ def __init__(self, *args):
+ super(QwtLegend, self).__init__(*args)
+ self._items = []
+ def setItemMode(self, mode):
+ pass
+ def legendItems(self):
+ return self._items
+ def itemCount(self):
+ return len(self._items)
+ def contentsWidget(self):
+ return self
+ def verticalScrollBar(self):
+ return self
+ def sizeHint(self):
+ return super().sizeHint()
+
+
+class QwtText:
+ PaintUsingTextFont = 1
+ def __init__(self, text=''):
+ self._text = str(text)
+ self._color = QtGui.QColor('black')
+ self._font = QtGui.QFont()
+ def setFont(self, f):
+ self._font = f
+ def setColor(self, c):
+ self._color = c
+ def setPaintAttribute(self, attr):
+ pass
+ def text(self):
+ return self._text
+
+
+class QwtPlot(pg.PlotWidget):
+ xBottom = 0
+ yLeft = 1
+ LeftLegend = 0
+ def __init__(self, *args, **kwargs):
+ super(QwtPlot, self).__init__(*args, **kwargs)
+ self._legend = QwtLegend()
+ self._grid = None
+ def setCanvasBackground(self, color):
+ if isinstance(color, (tuple, list)) and color:
+ color = color[0]
+ if not isinstance(color, QtGui.QColor):
+ color = QtGui.QColor(color)
+ self.setBackground(color)
+ def insertLegend(self, legend, where):
+ self._legend = legend
+ def legend(self):
+ return self._legend
+ def setAxisTitle(self, axis, title):
+ pass
+ def setAxisMaxMajor(self, axis, val):
+ pass
+ def setAxisMaxMinor(self, axis, val):
+ pass
+ def setAxisFont(self, axis, font):
+ pass
+ def setAxisScaleDraw(self, axis, draw):
+ pass
+ def enableAxis(self, axis, on):
+ pass
+ def setAxisScale(self, axis, vmin, vmax, step=None):
+ if axis == self.xBottom:
+ self.setXRange(vmin, vmax)
+ else:
+ self.setYRange(vmin, vmax)
+ def plotLayout(self):
+ return self
+ def canvasMargin(self, which):
+ return 0
+ def replot(self):
+ self.repaint()
+ def axisScaleDiv(self, axis):
+ class _R:
+ def range(self):
+ return 10.0
+ return _R()
+
+
+class QwtPlotGrid:
+ def __init__(self):
+ pass
+ def enableY(self, on):
+ pass
+ def enableX(self, on):
+ pass
+ def enableXMin(self, on):
+ pass
+ def setMajPen(self, pen):
+ pass
+ def setMinPen(self, pen):
+ pass
+ def attach(self, plot):
+ plot._grid = self
+
+
+class QwtPlotCurve(pg.PlotDataItem):
+ PaintFiltered = 1
+ Lines = 0
+ Sticks = 1
+ NoCurve = 2
+ def __init__(self, title=None):
+ if title is None:
+ title = QwtText('')
+ super(QwtPlotCurve, self).__init__(x=[], y=[])
+ self._title = title
+ self._style = self.Lines
+ def setPen(self, pen):
+ color = pen.color()
+ super(QwtPlotCurve, self).setPen(pg.mkPen(color))
+ def setYAxis(self, axis):
+ pass
+ def setPaintAttribute(self, attr):
+ pass
+ def attach(self, plot):
+ plot.addItem(self)
+ def detach(self):
+ self.setVisible(False)
+ def title(self):
+ return self._title
+ def setTitle(self, t):
+ self._title = t
+ def setData(self, x, y):
+ super().setData(x, y)
+ def setStyle(self, style):
+ self._style = style
+ if style == self.Sticks:
+ try:
+ self.setFillLevel(0)
+ except Exception:
+ pass
+ else:
+ try:
+ self.setFillLevel(None)
+ except Exception:
+ pass
+
+
+class QwtSymbol:
+ VLine = 1
+ def __init__(self):
+ self._style = None
+ self._size = 10
+ def setStyle(self, s):
+ self._style = s
+ def setSize(self, s):
+ self._size = s
+
+
+class QwtPlotMarker(pg.InfiniteLine):
+ NoLine = 0
+ def __init__(self):
+ super(QwtPlotMarker, self).__init__(angle=90)
+ self.sampleCounter = 0
+ def setLabel(self, txt):
+ pass
+ def setLabelAlignment(self, align):
+ pass
+ def setLineStyle(self, style):
+ pass
+ def setXValue(self, x):
+ self.setPos(x)
+ def setYValue(self, y):
+ pass
+ def setSymbol(self, sym):
+ pass
+ def attach(self, plot):
+ plot.addItem(self)
+ def detach(self):
+ self.setVisible(False)
+
+
+class QwtScaleDraw:
+ RightScale = 0
+ def __init__(self, *args):
+ pass
+ @staticmethod
+ def invalidateCache(obj):
+ pass
+
+
+class QwtScaleDiv:
+ def __init__(self, vmin, vmax, a, ticks, b):
+ self._vmin = vmin
+ self._vmax = vmax
+ self._ticks = ticks
+
+
+class QwtPlotScaleItem:
+ def __init__(self, where):
+ self._div = None
+ def setBorderDistance(self, d):
+ pass
+ def attach(self, plot):
+ pass
+ def setScaleDiv(self, div):
+ self._div = div
+
+
+QwtText = QwtText
+QwtPlot = QwtPlot
+QwtPlotGrid = QwtPlotGrid
+QwtPlotCurve = QwtPlotCurve
+QwtSymbol = QwtSymbol
+QwtPlotMarker = QwtPlotMarker
+QwtScaleDraw = QwtScaleDraw
+QwtScaleDiv = QwtScaleDiv
+QwtPlotScaleItem = QwtPlotScaleItem
+QwtLegend = QwtLegend
diff --git a/PyQt4/__init__.py b/PyQt4/__init__.py
new file mode 100644
index 0000000..6bec800
--- /dev/null
+++ b/PyQt4/__init__.py
@@ -0,0 +1,25 @@
+# Minimal PyQt4 compatibility layer backed by PySide6
+
+from . import Qt as Qt # re-export Qt shim
+
+# Optional submodules for legacy imports
+from . import QtCore as QtCore # noqa: F401
+from . import QtGui as QtGui # noqa: F401
+
+# Provide uic compatibility
+from . import uic as uic # noqa: F401
+
+# Qwt5 shim (very minimal)
+from . import Qwt5 as Qwt5 # noqa: F401
+
+__all__ = [
+ "Qt",
+ "QtCore",
+ "QtGui",
+ "uic",
+ "Qwt5",
+]
+
+
+
+
diff --git a/PyQt4/uic.py b/PyQt4/uic.py
new file mode 100644
index 0000000..e09a5ec
--- /dev/null
+++ b/PyQt4/uic.py
@@ -0,0 +1,11 @@
+from PySide6 import QtUiTools
+
+def compileUi(ui_filename, fp):
+ loader = QtUiTools.QUiLoader()
+ # We cannot compile to python; emulate by writing a minimal stub or raising.
+ # For this project, .py files already exist in res/. If needed, we fallback.
+ raise NotImplementedError("uic.compileUi is not supported in this shim. Use generated .py files.")
+
+
+
+
diff --git a/README.md b/README.md
index 8ac891a..123ad66 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,33 @@
# PyCorder
Modified PyCorder Code for EIT Experiments
+Compatibility updates (2025):
+- Python 3.x compatible (print, queue, configparser, perf_counter)
+- macOS (Apple Silicon/Intel) and Windows 11: runs in GUI with fallback simulation when ActiChamp DLL is absent
+- File I/O uses Python standard API (no libc dependency)
+- Legacy PyQt4/PyQwt imports are shimmed to PySide6/pyqtgraph (`PyQt4/` directory)
+
+Requirements:
+- Python 3.9+
+- numpy, scipy, lxml
+- PySide6, pyqtgraph
+
+Quick start:
+- Windows with hardware: install vendor DLLs in the working directory (`ActiChamp_x64.dll`/`ActiChamp_x86.dll`)
+- macOS/No hardware: app starts in simulation mode automatically
+- First run on macOS/Linux (or when `.venv` is broken): `bash ./run_pycorder.sh`
+- First run on Windows PowerShell: `.\run_pycorder.ps1`
+- First run on Windows CMD: `run_pycorder.bat`
+- Manual run after setup:
+ - macOS/Linux:
+ - `source .venv/bin/activate`
+ - `python main.py`
+ - Windows:
+ - `.venv\Scripts\activate`
+ - `python main.py`
+- Automated smoke test (headless, no window popup):
+ - `bash ./run_pycorder.sh --smoketest`
+
Current additions:
DC Offset pane shows the DC voltage on each electrode
diff --git a/actichamp_w.py b/actichamp_w.py
index 83f7e60..e940cfe 100644
--- a/actichamp_w.py
+++ b/actichamp_w.py
@@ -1,1295 +1,1313 @@
-# -*- coding: utf-8 -*-
-'''
-Python wrapper for ActiChamp Windows library
-
-ActiChamp_x86.dll (32-Bit) and ActiChamp_x64.dll (64-Bit)
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@version: 1.0
-'''
-
-import ctypes
-import ctypes.wintypes
-import _ctypes
-import numpy as np
-import time
-import ConfigParser
-import platform
-
-# enable or disable the Python Signal Generator for simulation mode
-PYSIGGEN = False
-#PYSIGGEN = True
-
-
-# max integer
-INT32_MAX = 2**31-1
-
-ADC_MAX = 0x7FFFFF
-
-# required hardware DLL version
-#CHAMP_VERSION = 0x080B0519 # 08.11.05.25 DLL
-#CHAMP_VERSION = 0x090B0B07 # 09.11.11.07 DLL
-#CHAMP_VERSION = 0x0A0B0C02 # 10.11.12.02 DLL
-#CHAMP_VERSION = 0x0A0B0C17 # 10.11.12.23 DLL
-#CHAMP_VERSION = 0x0A0B0C1D # 10.11.12.29 DLL
-#CHAMP_VERSION = 0x0B0C0A02 # 11.12.10.02 DLL
-#CHAMP_VERSION = 0x110C0B10 # 17.12.11.16 DLL
-#CHAMP_VERSION = 0x120D0710 # 18.13.07.16 DLL
-#CHAMP_VERSION = 0x160D0B0E # 22.13.11.14 DLL
-#CHAMP_VERSION = 0x170E040F # 23.14.04.15 DLL
-CHAMP_VERSION = 0x190E0804 # 25.14.08.04 DLL
-
-# required firmware versions (board revision 4)
-CHAMP_4_VERSION_CTRL = 0x040B041C # 04.11.04.28 FX2 USB controller
-CHAMP_4_VERSION_FPGA = 0x2C000000 # 44.00.00.00 FPGA
-CHAMP_4_VERSION_DSP = 0x060B0519 # 06.11.05.25 MSP430
-
-# required firmware versions (board revision 6)
-CHAMP_6_VERSION_CTRL = 0x660F0609 # 102.15.06.09 FX2 USB controller
-CHAMP_6_VERSION_FPGAM = 0x30000000 # 48.00.00.00 FPGA media controller
-CHAMP_6_VERSION_FPGAC = 0x2D000000 # 45.00.00.00 FPGA carrier board
-CHAMP_6_VERSION_DSP = 0x690E0A07 # 105.14.10.07 MSP430
-
-# compensate constant trigger delay
-CHAMP_COMPTRIGGER = False
-
-# C error numbers
-CHAMP_ERR_OK = 0 # Success (no errors)
-CHAMP_ERR_HANDLE = -1 # Invalid handle (such handle not present now)
-CHAMP_ERR_PARAM = -2 # Invalid function parameter(s)
-CHAMP_ERR_FAIL = -3 # Function fail (internal error)
-CHAMP_ERR_MONITORING = -4 # data rate monitoring failed
-CHAMP_ERR_SUPPORT = -5 # function not supported
-
-# ADC data filter enum
-CHAMP_ADC_NATIVE = 0 # no ADC data filter
-CHAMP_ADC_AVERAGING_2 = 1 # ADC data moving average filter by 2 samples
-
-# ADC data decimation
-CHAMP_DECIMATION_0 = 0 # no decimation
-CHAMP_DECIMATION_2 = 2 # decimation by 2
-CHAMP_DECIMATION_5 = 5 # decimation by 5
-CHAMP_DECIMATION_10 = 10 # decimation by 10
-CHAMP_DECIMATION_20 = 20 # decimation by 20
-CHAMP_DECIMATION_50 = 50 # decimation by 50
-
-# Mode enum
-CHAMP_MODE_NORMAL = 0 # normal data acquisition
-CHAMP_MODE_ACTIVE_SHIELD = 1 # data acquisition with ActiveShield
-CHAMP_MODE_IMPEDANCE = 2 # impedance measure
-CHAMP_MODE_TEST = 3 # test signal (square wave 200 uV, 1 Hz)
-CHAMP_MODE_LED_TEST = 99 # active electrode LED test mode
-
-# Mode text
-CHAMP_Modes = {CHAMP_MODE_NORMAL:"acquisition",
- CHAMP_MODE_ACTIVE_SHIELD:"acquisition with shield",
- CHAMP_MODE_IMPEDANCE:"impedance measurement",
- CHAMP_MODE_TEST:"test signal",
- CHAMP_MODE_LED_TEST:"active electrode LED test" }
-
-# actiChamp base sample rate enum
-CHAMP_RATE_10KHZ = 0 # 10 kHz, all channels (default mode)
-CHAMP_RATE_50KHZ = 1 # 50 kHz
-CHAMP_RATE_100KHZ = 2 # 100 kHz, max 64 channels
-# actiChamp base sample rate for extended settings enum
-CHAMP_RATE_25KHZ = 10 # 25 kHz
-CHAMP_RATE_5KHZ = 11 # 5 kHz
-CHAMP_RATE_2KHZ = 12 # 2 kHz
-CHAMP_RATE_1KHZ = 13 # 1 kHz
-CHAMP_RATE_500HZ = 14 # 500 Hz
-CHAMP_RATE_200HZ = 15 # 200 Hz
-
-# sample rate frequency dictionary (amplifier DLL base frequencies available for the application)
-# if you want to do the decimation and filtering in Python (amplifier.py) then
-# set this value to True:
-PythonDecimation = False
-if PythonDecimation:
- sample_rate = {
- CHAMP_RATE_10KHZ:10000.0,
- CHAMP_RATE_50KHZ:50000.0,
- CHAMP_RATE_100KHZ:100000.0
- }
-else:
- sample_rate = {
- CHAMP_RATE_200HZ:200.0, CHAMP_RATE_500HZ:500.0, CHAMP_RATE_1KHZ:1000.0,
- CHAMP_RATE_2KHZ:2000.0, CHAMP_RATE_5KHZ:5000.0,
- CHAMP_RATE_10KHZ:10000.0, CHAMP_RATE_25KHZ:25000.0,
- CHAMP_RATE_50KHZ:50000.0, CHAMP_RATE_100KHZ:100000.0
- }
-
-# trigger delay dictionary (for constant trigger delay compensation)
-trigger_delay = {
- CHAMP_RATE_200HZ:1, CHAMP_RATE_500HZ:1, CHAMP_RATE_1KHZ:1,
- CHAMP_RATE_2KHZ:1, CHAMP_RATE_5KHZ:1,
- CHAMP_RATE_10KHZ:1, CHAMP_RATE_25KHZ:1,
- CHAMP_RATE_50KHZ:1, CHAMP_RATE_100KHZ:1 }
-
-# sample rate extended settings dictionary
-# translate application base frequency to amplifier physical frequency
-# 0=10kHz, 1=50kHz, 2=100kHz
-sample_rate_settings = {
- CHAMP_RATE_200HZ:0, CHAMP_RATE_500HZ:0, CHAMP_RATE_1KHZ:0,
- CHAMP_RATE_2KHZ:0, CHAMP_RATE_5KHZ:0,
- CHAMP_RATE_10KHZ:0, CHAMP_RATE_25KHZ:1,
- CHAMP_RATE_50KHZ:1, CHAMP_RATE_100KHZ:2 }
-# decimation values (rate = physical / decimation)
-sample_rate_decimation = {
- CHAMP_RATE_200HZ:CHAMP_DECIMATION_50, CHAMP_RATE_500HZ:CHAMP_DECIMATION_20, CHAMP_RATE_1KHZ:CHAMP_DECIMATION_10,
- CHAMP_RATE_2KHZ:CHAMP_DECIMATION_5, CHAMP_RATE_5KHZ:CHAMP_DECIMATION_2,
- CHAMP_RATE_10KHZ:CHAMP_DECIMATION_0, CHAMP_RATE_25KHZ:CHAMP_DECIMATION_2,
- CHAMP_RATE_50KHZ:CHAMP_DECIMATION_0, CHAMP_RATE_100KHZ:CHAMP_DECIMATION_0 }
-
-
-
-
-class CHAMP_SETTINGS(ctypes.Structure):
- ''' C amplifier settings
- '''
- _pack_ = 1
- _fields_ = [("Mode", ctypes.c_int), # mode of acquisition
- ("Rate", ctypes.c_int)] # sample rate
-
-class CHAMP_SETTINGS_EX(ctypes.Structure):
- ''' C extended amplifier settings
- '''
- _pack_ = 1
- _fields_ = [("Mode", ctypes.c_int), # mode of acquisition
- ("Rate", ctypes.c_int), # sample rate
- ("AdcFilter", ctypes.c_int), # ADC data filter
- ("Decimation", ctypes.c_int)] # ADC data decimation
-
-class CHAMP_PROPERTIES(ctypes.Structure):
- ''' C amplifier properties
- '''
- _pack_ = 1
- _fields_ = [("CountEeg", ctypes.c_uint), # number of Eeg channels
- ("CountAux", ctypes.c_uint), # number of Aux channels
- ("TriggersIn", ctypes.c_uint), # numbers of input triggers
- ("TriggersOut", ctypes.c_uint), # numbers of output triggers
- ("Rate", ctypes.c_float), # sampling rate, Hz
- ("ResolutionEeg", ctypes.c_float), # EEG amplitude scale coefficients, V/bit
- ("ResolutionAux", ctypes.c_float), # AUX amplitude scale coefficients, V/bit
- ("RangeEeg", ctypes.c_float), # EEG input range peak-peak, V
- ("RangeAux", ctypes.c_float)] # AUX input range peak-peak, V
-
-class CHAMP_IMPEDANCE_SETUP(ctypes.Structure):
- ''' C impedance settings
- '''
- _pack_ = 1
- _fields_ = [("Good", ctypes.c_uint), # Good level (green led indication), Ohm
- ("Bad", ctypes.c_uint), # Bad level (red led indication), Ohm
- ("LedsDisable", ctypes.c_uint), # Disable electrode's leds, if not zero
- ("TimeOut", ctypes.c_uint)] # Impedance mode time-out (0 - 65535), sec
-
-class CHAMP_DATA_STATUS(ctypes.Structure):
- ''' C device data status
- '''
- _pack_ = 1
- _fields_ = [("Samples", ctypes.c_uint), # Total samples
- ("Errors", ctypes.c_uint), # Total errors
- ("Rate", ctypes.c_float), # Data rate, Hz
- ("Speed", ctypes.c_float)] # Data speed, MB/s
-
-class CHAMP_SYSTEMTIME(ctypes.Structure):
- ''' C system time struct
- '''
- _pack_ = 1
- _fields_ = [( 'wYear', ctypes.wintypes.WORD ),
- ( 'wMonth', ctypes.wintypes.WORD ),
- ( 'wDayOfWeek', ctypes.wintypes.WORD ),
- ( 'wDay', ctypes.wintypes.WORD ),
- ( 'wHour', ctypes.wintypes.WORD ),
- ( 'wMinute', ctypes.wintypes.WORD ),
- ( 'wSecond', ctypes.wintypes.WORD ),
- ( 'wMilliseconds', ctypes.wintypes.WORD )]
-
-class CHAMP_MODULE_INFO(ctypes.Structure):
- ''' C device and module info
- '''
- _pack_ = 1
- _fields_ = [( 'Model', ctypes.c_uint ), # Model ID
- ( 'SerialNumber', ctypes.c_uint ), # Serial Number
- ( 'Date', CHAMP_SYSTEMTIME )] # Production Date and Time
-
-CHAMP_DEVICE_INFO = CHAMP_MODULE_INFO * 6 # index 0=device, index 1-5=modules
-
-class CHAMP_VERSION_INFO(ctypes.Structure):
- ''' C DLL, USB driver and firmware versions
- '''
- _pack_ = 1
- _fields_ = [( 'DLL', ctypes.wintypes.DWORD ), # DLL version
- ( 'USBDRV', ctypes.wintypes.DWORD ), # USB driver version
- ( 'USBCTRL', ctypes.wintypes.DWORD ), # USB controller firmware version
- ( 'FPGA', ctypes.wintypes.DWORD ), # FPGA firmware version
- ( 'DSP', ctypes.wintypes.DWORD )] # MSP430 firmware version
-
-
-class CHAMP_VERSION_INFO_EXT(ctypes.Structure):
- ''' C DLL, USB driver and firmware versions for board revision 6
- '''
- _pack_ = 1
- _fields_ = [( 'DLL', ctypes.wintypes.DWORD ), # DLL version
- ( 'USBDRV', ctypes.wintypes.DWORD ), # USB driver version
- ( 'USBCTRL', ctypes.wintypes.DWORD ), # USB controller firmware version
- ( 'FPGAM', ctypes.wintypes.DWORD ), # Media converter FPGA firmware version
- ( 'DSP', ctypes.wintypes.DWORD ), # MSP430 firmware version
- ( 'FPGAC', ctypes.wintypes.DWORD )] # Carrier board FPGA firmware version
-
-
-class CHAMP_VOLTAGES(ctypes.Structure):
- ''' C Amplifier voltages and temperature
- The voltages DVDD3, AVDD3, AVDD5 and REF are valid only during data acquisition
- '''
- _pack_ = 1
- _fields_ = [( 'VDC', ctypes.c_float ), # Power supply, [V]
- ( 'V3', ctypes.c_float ), # Internal 3.3, [V]
- ( 'TEMP', ctypes.c_float ), # Temperature, degree Celsius
- ( 'DVDD3', ctypes.c_float ), # Digital 3.3, [V]
- ( 'AVDD3', ctypes.c_float ), # Analog 3.3, [V]
- ( 'AVDD5', ctypes.c_float ), # Analog 5.0, [V]
- ( 'REF', ctypes.c_float )] # Reference 2.048, [V]
-
-
-class CHAMP_MODULES(ctypes.Structure):
- ''' C Module control structure
- Bits:
- 0 - AUX module
- 1 - 5 - Main EEG modules (1 - 5)
- 6 - 31 - Reserved
- '''
- _pack_ = 1
- _fields_ = [( 'Present', ctypes.c_uint ), # Bits indicate that the module is present in hardware
- ( 'Enabled', ctypes.c_uint )] # Bits indicate that the module is enabled for use
-
-class CHAMP_PLL(ctypes.Structure):
- ''' C PLL Parameters
- '''
- _pack_ = 1
- _fields_ = [( 'PllExternal', ctypes.c_uint ), # if 1 - use External clock for PLL, if 0 - use Internal 48 MHz
- ( 'AdcExternal', ctypes.c_uint ), # if 1 - out External clock to ADC, if 0 - use PLL output
- ( 'PllFrequency', ctypes.c_uint ), # PLL frequency 10 MHz - 27 MHz (needs set if AdcExternal = 0), Hz
- ( 'PllPhase', ctypes.c_uint ), # Phase shift (hardware step 360 / 10 = 36), degrees
- ( 'Status', ctypes.c_uint )] # PLL status (read only)
-
-
-
-class AmpError(Exception):
- ''' Generic amplifier exception
- '''
- def __init__(self, value, errornr = 0):
- errortext = ""
- if errornr == CHAMP_ERR_HANDLE:
- errortext = "Invalid handle (device disconnected)"
- elif errornr == CHAMP_ERR_PARAM:
- errortext = "Invalid function parameter(s)"
- elif errornr == CHAMP_ERR_FAIL:
- errortext = "Function fail (internal error)"
- elif errornr == CHAMP_ERR_MONITORING:
- errortext = "Data rate mismatch"
- elif errornr == CHAMP_ERR_SUPPORT:
- errortext = "Function is not supported"
- errortext = errortext + " :%i"%(errornr)
- if errornr != 0:
- self.value = "actiChamp: " + str(value) + " -> " + errortext
- else:
- self.value = "actiChamp: " + str(value)
- def __str__(self):
- return self.value
-
-
-
-class AmpVersion(object):
- def __init__(self):
- self.version = CHAMP_VERSION_INFO()
- self.versionext = CHAMP_VERSION_INFO_EXT()
- self.boardRevision = 4
-
- def read(self, lib, device):
- ''' read board dependent version infos from amplifier
- attention: the carrier board FPGA version (FPGAC) is only available if the acquisition is running.
- @param lib: DLL handle
- @param device: device handle
- @return: DLL result value
- '''
- res = lib.champGetVersion(device, ctypes.byref(self.version))
- DSP_MajorVersion = self.version.DSP >> 24
- if DSP_MajorVersion >= 100:
- # board revision 6
- self.boardRevision = 6
- elif DSP_MajorVersion > 0:
- self.boardRevision = 4
- else:
- self.boardRevision = 0
- return res
-
- def readext(self, lib, device):
- ''' read version info for amplifier board revision 6
- attention: the carrier board FPGA version (FPGAC) is only available if the acquisition is running.
- @param lib: DLL handle
- @param device: device handle
- @return: DLL result value
- '''
- res = lib.champGetVersion(device, ctypes.byref(self.version))
- if self.boardRevision == 6:
- res = lib.champGetVersionExt(device, ctypes.byref(self.versionext))
- return res
-
- def isFpgaProgrammed(self):
- if self.boardRevision and self.version.FPGA == 0:
- return False
- return True
-
- def isValid(self):
- ''' Validate major firmware versions
- @return: True if valid (or emulated), False if not
- '''
- if self.boardRevision == 4:
- if self.version.USBCTRL != 0 and self.version.USBCTRL & 0xFF000000 != CHAMP_4_VERSION_CTRL & 0xFF000000:
- return False
- if self.version.FPGA !=0 and self.version.FPGA & 0xFF000000 != CHAMP_4_VERSION_FPGA & 0xFF000000:
- return False
- if self.version.DSP != 0 and self.version.DSP & 0xFF000000 != CHAMP_4_VERSION_DSP & 0xFF000000:
- return False
- if self.boardRevision == 6:
- if self.versionext.USBCTRL != 0 and self.versionext.USBCTRL & 0xFF000000 != CHAMP_6_VERSION_CTRL & 0xFF000000:
- return False
- if self.versionext.FPGAM !=0 and self.versionext.FPGAM & 0xFF000000 != CHAMP_6_VERSION_FPGAM & 0xFF000000:
- return False
- if self.versionext.FPGAC !=0 and self.versionext.FPGAC & 0xFF000000 != CHAMP_6_VERSION_FPGAC & 0xFF000000:
- return False
- if self.versionext.DSP != 0 and self.versionext.DSP & 0xFF000000 != CHAMP_6_VERSION_DSP & 0xFF000000:
- return False
- return True
-
- def _getVersionString(self, rawversion):
- ''' get readable version string from DWORD
- @param rawversion: raw version number from DLL
- @return: version string
- '''
- # split version number
- version = ""
- for i in reversed(range(4)):
- version += "%02i"%((rawversion >> i*8) & 0xFF)
- if i:
- version +="."
- return version
-
-
- def info(self):
- ''' get all amplifier firmware versions as string
- '''
- if self.boardRevision == 4:
- # create version string for board revision 4
- version = "Version: DLL_%s, DRV_%s, CTRL_%s, FPGA_%s, DSP_%s"%(self._getVersionString(self.version.DLL),
- self._getVersionString(self.version.USBDRV),
- self._getVersionString(self.version.USBCTRL),
- self._getVersionString(self.version.FPGA),
- self._getVersionString(self.version.DSP))
- # required firmware versions
- req_version = "Firmware Version MISMATCH, required: CTRL_%s, FPGA_%s, DSP_%s"%(self._getVersionString(CHAMP_4_VERSION_CTRL),
- self._getVersionString(CHAMP_4_VERSION_FPGA),
- self._getVersionString(CHAMP_4_VERSION_DSP))
-
- elif self.boardRevision == 6:
- # create version string for board revision 6
- version = "Version: DLL_%s, DRV_%s, CTRL_%s, FPGAM_%s, FPGAC_%s, DSP_%s"%(self._getVersionString(self.versionext.DLL),
- self._getVersionString(self.versionext.USBDRV),
- self._getVersionString(self.versionext.USBCTRL),
- self._getVersionString(self.versionext.FPGAM),
- self._getVersionString(self.versionext.FPGAC),
- self._getVersionString(self.versionext.DSP))
- # required firmware versions
- req_version = "Firmware Version MISMATCH, required: CTRL_%s, FPGAM_%s, FPGAC_%s DSP_%s"%(self._getVersionString(CHAMP_6_VERSION_CTRL),
- self._getVersionString(CHAMP_6_VERSION_FPGAM),
- self._getVersionString(CHAMP_6_VERSION_FPGAC),
- self._getVersionString(CHAMP_6_VERSION_DSP))
- else:
- version = ""
- req_version = ""
-
- '''
- if self.isValid():
- return version
- else:
- return version +"\n" + req_version
- '''
- return version
-
- def DLL(self):
- ''' get the DLL version
- '''
- return self.version.DLL
-
- def revision(self):
- ''' get the amplifier revision, depending on board revision
- '''
- if self.boardRevision > 4:
- return 3
- return 2
-
-
-
-class ActiChamp(object):
- ''' ActiChamp hardware object (Python wrapper for actiCHamp Windows DLL)
- '''
-
- def __init__(self):
- ''' Constructor
- '''
- # get OS architecture (32/64-bit)
- self.x64 = ("64" in platform.architecture()[0])
-
- # set default values
- self.devicehandle = 0
- self.ampversion = AmpVersion() #: actiCHamp version info structure
- self.deviceinfo = CHAMP_DEVICE_INFO() #: actiCHamp device info structure
- self.modulestate = CHAMP_MODULES() #: actiCHamp module connection state structure
- self.properties = CHAMP_PROPERTIES() #: actiCHamp property structure
- self.settings = CHAMP_SETTINGS() #: actiCHamp settings structure
- self.settings.Rate = CHAMP_RATE_10KHZ #: sampling rate
- self.settings.Mode = CHAMP_MODE_NORMAL #: acquisition mode
- self.running = False #: data acquisition running
- self.buffer = ctypes.create_string_buffer(10000*1024) #: raw data transfer buffer
- self.impbuffer = ctypes.create_string_buffer(1000) #: impedance raw data transfer buffer
- self.readError = False #: an error occurred during data acquisition
- self.activeShieldGain = 5 #: default active shield gain
- self.enablePllConfiguration = False #: enable the PLL configuration option
- self.PllExternal = 0 #: use external input for the PLL
-
- # binning buffer for max. 100 samples with 170 channels with a datasize of int32 (4 bytes)
- self.binning_buffer = ctypes.create_string_buffer(100*170*4) #: binning buffer
- self.binning = 1 #: binning size for buffer alignment
- self.binning_offset = 0 #: raw data buffer offset in bytes for binning
-
- self.sampleCounterAdjust = 0 #: sample counter wrap around, HW counter is 32bit value but we need 64bit
- self.BlockingMode = True #: read data in blocking mode
- self.EmulationMode = False #: emulate hardware
-
- # set default properties
- self.properties.CountEeg = 32
- self.properties.CountAux = 8
- self.properties.TriggersIn = 8
- self.properties.TriggersOut = 8
- self.properties.Rate = 10000.0
- self.properties.ResolutionEeg = 4.88e-08
- self.properties.ResolutionAux = 2.98e-07
- self.properties.RangeEeg = 0.819
- self.properties.RangeAux = 5.0
-
- # load ActiChamp 32 or 64 bit windows library
- self.lib = None
- self.loadLib()
-
- # get and check DLL version
- self.ampversion.read(self.lib, self.devicehandle)
- if self.ampversion.DLL() != CHAMP_VERSION:
- raise AmpError("wrong ActiChamp DLL version (%X / %X)"%(self.ampversion.DLL(),
- CHAMP_VERSION))
-
-
- # try to open device and get device properties
- try:
- # get hardware properties
- self.open()
- self.getDeviceInfo()
- except:
- pass
-
- try:
- self.close()
- except:
- pass
-
- def _resetDeviceProperties(self):
- ''' Set channel count to zero
- '''
- self.properties.CountEeg = 0
- self.properties.CountAux = 0
- self.properties.TriggersIn = 0
- self.properties.TriggersOut = 0
-
-
- def loadLib(self):
- ''' Load windows library
- '''
- # load ActiChamp 32 or 64 bit windows library
- try:
- # unload existing library
- if self.lib != None:
- _ctypes.FreeLibrary(self.lib._handle)
- # load/reload library
- if self.x64:
- self.lib = ctypes.windll.LoadLibrary("ActiChamp_x64.dll")
- self.lib.champOpen.restype = ctypes.c_uint64
- else:
- self.lib = ctypes.windll.LoadLibrary("ActiChamp_x86.dll")
- except:
- self.lib = None
- if self.x64:
- raise AmpError("failed to open library (ActiChamp_x64.dll)")
- else:
- raise AmpError("failed to open library (ActiChamp_x86.dll)")
-
-
- def open(self):
- ''' Open the hardware device and get a device handle and device properties
- '''
- if self.running:
- return
- if self.lib == None:
- raise AmpError("library ActiChamp_x86.dll not available")
-
- # check if device hardware is available
- self._resetDeviceProperties()
- if self.lib.champGetCount() == 0:
- raise AmpError("hardware not available")
-
- retry = 3
- while retry > 0:
- # open the first available device
- if self.x64:
- self.devicehandle = ctypes.c_uint64(self.lib.champOpen(0))
- else:
- self.devicehandle = ctypes.c_int32(self.lib.champOpen(0))
- if self.devicehandle.value == 0:
- self.devicehandle = 0
- raise AmpError("failed to open device")
-
- # get device version info
- err = self.ampversion.read(self.lib, self.devicehandle)
- if err != CHAMP_ERR_OK:
- self.close()
- raise AmpError("failed to get device version info", err)
-
- # check if fpga loaded successfully
- if not self.ampversion.isFpgaProgrammed():
- self.lib.champClose(self.devicehandle)
- self.devicehandle = 0
- retry -= 1
- if retry == 0:
- raise AmpError("failed to open device")
- else:
- retry = 0
-
- # get device module connection info
- self.modulestate.Enabled = 0
- self.modulestate.Present = 0
- self.lib.champGetModules(self.devicehandle, ctypes.byref(self.modulestate))
-
- # get device properties
- self.lib.champGetProperty(self.devicehandle, ctypes.byref(self.properties))
-
- def close(self):
- ''' Close hardware device
- '''
- if self.lib == None:
- raise AmpError("library ActiChamp_x86.dll not available")
- if self.devicehandle != 0:
- if self.running:
- try:
- self.stop()
- except:
- pass
- self.lib.champClose(self.devicehandle)
- self.devicehandle = 0
-
- def _get_settings_ex(self, settings):
- ''' Prepare extended settings (rate, decimation and filter)
- @param settings: amplifier base settings
- @return: extended settings
- '''
- csext = CHAMP_SETTINGS_EX()
- csext.Mode = settings.Mode
- csext.Rate = sample_rate_settings[settings.Rate]
- csext.Decimation = sample_rate_decimation[settings.Rate]
- csext.AdcFilter = CHAMP_ADC_AVERAGING_2
- return csext
-
- def setup(self, mode, rate, binning):
- ''' Prepare device for acquisition
- @param mode: device mode, one of CHAMP_MODE_ values
- @param rate: device sampling rate, one of CHAMP_RATE_ values
- @param binning: sampling rate divider to align read buffer to requested binning size
- '''
- # LED test is done in normal recording mode
- if mode == CHAMP_MODE_LED_TEST:
- self.settings.Mode = CHAMP_MODE_NORMAL
- else:
- self.settings.Mode = mode
- self.settings.Rate = rate
- self.binning = int(binning)
- self.binning_offset = 0
- if self.devicehandle == 0:
- raise AmpError("device not open")
-
- # setup amplifier
- ex_settings = self._get_settings_ex(self.settings)
-
- # limit the number of modules (1xEEG + AUX) if sampling rate is 100KHz
- if self.settings.Rate == CHAMP_RATE_100KHZ:
- self.modulestate.Enabled = self.modulestate.Present & 0x03
- # limit the number of modules (2xEEG + AUX) if sampling rate is 50KHz
- elif self.settings.Rate == CHAMP_RATE_50KHZ:
- self.modulestate.Enabled = self.modulestate.Present & 0x07
- # limit the number of modules (4xEEG + AUX) if sampling rate is 25KHz
- elif self.settings.Rate == CHAMP_RATE_25KHZ:
- self.modulestate.Enabled = self.modulestate.Present & 0x1F
- # enable all present modules if sampling rate is below 25KHz
- else:
- self.modulestate.Enabled = self.modulestate.Present
-
- # start impedance measurement always with 10KHz
- if ex_settings.Mode == CHAMP_MODE_IMPEDANCE:
- ex_settings.Rate = CHAMP_RATE_10KHZ
- ex_settings.Decimation = CHAMP_DECIMATION_0
-
- # enable modules
- err = self.lib.champSetModules(self.devicehandle, ctypes.byref(self.modulestate))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to setup module selection", err)
-
- # setup device
- err = self.lib.champSetSettingsEx(self.devicehandle, ctypes.byref(ex_settings))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to setup device", err)
-
- # set active shield gain
- gain = ctypes.c_uint(self.activeShieldGain) # 1-100, default = 100
- err = self.lib.champSetActiveShieldGain(self.devicehandle, gain)
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set active shield gain", err)
-
- # get device properties
- self._resetDeviceProperties()
- self.lib.champGetProperty(self.devicehandle, ctypes.byref(self.properties))
-
- # create constant trigger delay compensation buffer
- trgdelay = trigger_delay[self.settings.Rate]
- self.trgdelaybuf = np.zeros(trgdelay, np.uint32) + 0xFFFF
-
-
- def start(self):
- ''' Start data acquisition
- '''
- if self.running:
- return
- if self.devicehandle == 0:
- raise AmpError("device not open")
-
- # start amplifier
- err = self.lib.champStart(self.devicehandle)
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to start device", err)
-
- # read the amplifier extended versions to get the carrier board FPGA version also
- self.ampversion.readext(self.lib, self.devicehandle)
-
- # get infos from device
- self.deviceinfo = CHAMP_DEVICE_INFO() # reset the info structure
- module = -1
- for info in self.deviceinfo:
- # get device info
- if module == -1:
- self.lib.champFactoryDeviceProductionGet(self.devicehandle, ctypes.byref(info))
- else:
- self.lib.champFactoryModuleProductionGet(self.devicehandle, module, ctypes.byref(info))
- module += 1
-
- self.running = True
- self.readError = False
- self.sampleCounterAdjust = 0
- self.BlockTimer = time.clock()
-
- # try to set the PLL input
- self.setPllInput()
-
- # reset signal generator
- self.DummySignals = []
-
- def stop(self):
- ''' Stop data acquisition
- '''
- if not self.running:
- return
- self.running = False
- if self.devicehandle == 0:
- raise AmpError("device not open")
- err = self.lib.champStop(self.devicehandle)
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to stop device", err)
-
- def read(self, indices, eegcount, auxcount):
- ''' Read data from device
- @param indices: to select the requested channels from raw data stream
- @param eegcount: number of requested EEG channels
- @param auxcount: number of requested AUX channels
- @return: list of np arrays for channel data, trigger channel and sample counter,
- indices of disconnected channels
- '''
- if not self.running or (self.devicehandle == 0) or self.readError:
- return None, None
-
- # calculate data amount for an interval of
- interval = 0.05 # interval in [s]
- bytes_per_sample = (self.properties.CountEeg + self.properties.CountAux + 1 + 1) *\
- np.dtype(np.int32).itemsize
- requestedbytes = int(bytes_per_sample * sample_rate[self.settings.Rate] * interval)
-
- t = time.clock()
-
- # read data from device
- if not self.BlockingMode:
- bytesread = self.lib.champGetData(self.devicehandle,
- ctypes.byref(self.buffer, self.binning_offset),
- len(self.buffer) - self.binning_offset)
- else:
- bytesread = self.lib.champGetDataBlocking(self.devicehandle,
- ctypes.byref(self.buffer, self.binning_offset),
- requestedbytes)
-
- blocktime = (time.clock() - self.BlockTimer)
- self.BlockTimer = time.clock()
- #print str(blocktime) + " : " + str(bytesread)
-
- #print str(t-self.lastt) + " : " + str(bytesread)
- #self.lastt = t
-
- # check for device error
- if bytesread < 0:
- if bytesread == CHAMP_ERR_MONITORING:
- return None, CHAMP_ERR_MONITORING
- self.readError = True # block next read access, until acquisition is restarted
- raise AmpError("failed to read data from device", bytesread)
-
- # data available?
- if bytesread == 0:
- return None, None
-
- if self.binning > 1:
- # align buffer to requested binning size
- total_bytes = bytesread + self.binning_offset
- # copy remainder from last read back to sample buffer
- ctypes.memmove(self.buffer, self.binning_buffer, self.binning_offset)
- # new remainder size
- remainder = ((total_bytes / bytes_per_sample) % self.binning) * bytes_per_sample
- # number of binning aligned samples
- binning_samples = total_bytes / bytes_per_sample / self.binning * self.binning
- src_offset = binning_samples * bytes_per_sample
- # copy new remainder to binning buffer
- ctypes.memmove(self.binning_buffer, ctypes.byref(self.buffer, src_offset), remainder)
- self.binning_offset = remainder
-
- # there must be at least one binning sample
- if binning_samples == 0:
- return None, None
- items = binning_samples * bytes_per_sample / np.dtype(np.int32).itemsize
- else:
- items = bytesread / np.dtype(np.int32).itemsize
-
- # channel order in buffer is S1CH1,S1CH2..S1CHn, S2CH1,S2CH2,..S2nCHn, ...
- x = np.fromstring(self.buffer, np.int32, items)
- # shape and transpose to 1st axis is channel and 2nd axis is sample
- samplesize = self.properties.CountEeg + self.properties.CountAux + 1 + 1
- x.shape = (-1, samplesize)
- y = x.transpose()
-
- # extract the different channel types
- index = 0
- eeg = np.array(y[indices], np.float)
-
- # get indices of disconnected electrodes (all values == ADC_MAX)
- # disconnected = np.nonzero(np.all(eeg == ADC_MAX, axis=1))
- disconnected = None # not possible yet
-
- # extract and scale the different channel types
- eegscale = self.properties.ResolutionEeg * 1e6 # convert to µV
- eeg[index:eegcount] = eeg[index:eegcount] * eegscale
- index += eegcount
- auxscale = self.properties.ResolutionAux * 1e6 # convert to µV
- eeg[index:index+auxcount] = eeg[index:index+auxcount] * auxscale
-
- # extract trigger channel
- index = self.properties.CountEeg + self.properties.CountAux
- trg = np.array(y[index:index + 1], np.uint32)
-
- # compensate constant trigger delay
- if CHAMP_COMPTRIGGER:
- dsize = len(trg[0])
- temp = np.append(self.trgdelaybuf, trg[0], 0)
- trg[0] = temp[:dsize]
- self.trgdelaybuf = temp[dsize:]
-
- # extract sample counter channel
- index += 1
- sctTemp = np.array(y[index:index + 1], np.uint32)
-
- # search for sample counter wrap around and adjust counter
- sct = np.array(sctTemp, np.uint64) + self.sampleCounterAdjust
- wrap = np.nonzero(sctTemp == 0)
- if (wrap[1].size > 0) and sct[0][0]:
- wrapIndex = wrap[1][0]
- adjust = np.iinfo(np.uint32).max + 1
- self.sampleCounterAdjust += adjust
- sct[:,wrapIndex:] += adjust
-
-
- # Test Signal Generator
- # use internal signal generator?
- if PYSIGGEN and self.EmulationMode:
- if not len(self.DummySignals):
- # create dummy signals at the first read
- sg = SignalGenerator(np.float)
- sr = sample_rate[self.settings.Rate]
- numchannels = eegcount+auxcount
- '''
- t, self.DummySignals = sg.GetSineWaveBuffers(numchannels,
- 5.0, sr/40/numchannels ,
- 100.0, 10.0,
- sr)
- '''
- t, self.DummySignals = sg.GetSineWaveBuffers(numchannels,
- [1.0, 2.0, 3.7, 5.0, 10.0, 17.2, 20.0, 50.0, 100.0, 200.0], 1.0,
- 100.0, 0.0,
- sr)
- # replace eeg with generated signals
- sc32 = np.array(sct[0], dtype=np.int)
- for c in range(len(eeg)):
- eeg[c] = np.take(self.DummySignals[c], sc32, mode="wrap")
-
- # write trigger every 10s
- tr = sample_rate[self.settings.Rate] * 10
- trIdx = np.nonzero((sc32 % tr) < 3)[0]
- trg[0] = 0
- if trIdx.size:
- trg[0,trIdx] = 1
-
- d = []
- d.append(eeg)
- d.append(trg)
- d.append(sct)
- return d, disconnected
-
- def readImpedances(self):
- ''' Get the electrode impedance values
- @return: list of impedance values for all EEG channels plus ground electrode in Ohm.
- '''
- if not self.running or (self.devicehandle == 0):
- return None, None
-
- disconnected = None
- # read impedance data from device
- err = self.lib.champImpedanceGetData(self.devicehandle,
- ctypes.byref(self.impbuffer),
- len(self.impbuffer))
-
- # dummy read data from device
- err2 = self.lib.champGetData(self.devicehandle,
- ctypes.byref(self.buffer),
- len(self.buffer))
-
- if err2 == CHAMP_ERR_MONITORING:
- disconnected = CHAMP_ERR_MONITORING
-
- if err == CHAMP_ERR_FAIL:
- return None, None
-
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to read impedance values", err)
-
- # channel order in buffer is CH1,CH2..CHn, GND
- items = self.properties.CountEeg + 1
- return np.fromstring(self.impbuffer, np.uint32, items), disconnected
-
- def setImpedanceRange(self, good, bad):
- ''' set ActiCap impedance range
- @param good: impedance value for green LED in Ohm
- @param bad: impedance value for red LED in Ohm
- '''
- if self.devicehandle == 0:
- return
- imp_settings = CHAMP_IMPEDANCE_SETUP()
- imp_settings.Good = int(good)
- imp_settings.Bad = int(bad)
- imp_settings.LedsDisable = 0
- imp_settings.TimeOut = 5
- err = self.lib.champImpedanceSetSetup(self.devicehandle, ctypes.byref(imp_settings))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set LED impedance range", err)
-
- def setTrigger(self, trigger):
- ''' set trigger output
- @param trigger: trigger values to set 8-bit outputs (bits 0 - 7).
- '''
- if self.devicehandle == 0:
- return
-
- # 8-bit inputs (bits 0 - 7) + 8-bit outputs (bits 8 - 15) + 16 MSB reserved bits.
- trigger = (trigger & 0xFF) << 8
- ct_trigger = ctypes.c_uint(trigger)
- err = self.lib.champSetTriggers(self.devicehandle, ct_trigger)
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set trigger output", err)
-
- def getEmulationMode(self):
- ''' Lookup emulation and PLL configuration flag in INI file
- @return: number of modules if in emulation mode, else 0
- '''
- emulation = 0
- modules = 0
- try:
- ini = ConfigParser.ConfigParser()
- if self.x64:
- filename = "ActiChamp_x64.dll.ini"
- else:
- filename = "ActiChamp_x86.dll.ini"
-
- if len(ini.read(filename)) > 0:
- emulation = ini.getint("Main", "Emulation")
- if emulation != 0:
- modules = ini.getint("Emulation", "Model") / 32
- try:
- self.enablePllConfiguration = (ini.getint("Main", "EnablePllConfiguration") != 0)
- except:
- self.enablePllConfiguration = False
- except:
- modules = 0
- self.EmulationMode = (modules > 0)
- return modules
-
- def setEmulationMode(self, modules):
- ''' Set/Reset emulation flag in INI file
- @param modules: number of modules to emulate, 0= no emulation
- '''
- # not possible if device is already open
- if self.devicehandle != 0:
- return
-
- # write new settings to INI file
- ini = ConfigParser.ConfigParser()
- if self.x64:
- filename = "ActiChamp_x64.dll.ini"
- else:
- filename = "ActiChamp_x86.dll.ini"
-
- if len(ini.read(filename)) > 0:
- if modules > 0:
- channels = modules * 32
- ini.set("Main", "Emulation", "1")
- ini.set("Emulation", "Model", repr(channels))
- else:
- ini.set("Main", "Emulation", "0")
- fp = open(filename, "w")
- ini.write(fp)
- fp.close()
- else:
- raise AmpError("INI file %s not found"%(filename))
-
- # reload the DLL
- self.loadLib()
- # get new configuration
- try:
- self.open()
- self.getDeviceInfo()
- self.setup(self.settings.Mode, self.settings.Rate, self.binning)
- except:
- pass
-
- try:
- self.close()
- except:
- pass
-
-
- def readConfiguration(self, rate, force=False):
- ''' Update device sampling rate and get new configuration
- @param rate: device base sampling rate
- '''
- # not possible if device is already open or not necessary if rate has not changed
- if (self.devicehandle != 0 or rate == self.settings.Rate) and not force:
- return
- # update sampling rate and get new configuration
- try:
- self.open()
- self.setup(self.settings.Mode, rate, self.binning)
- except:
- pass
-
- try:
- self.close()
- except:
- pass
-
-
- def getDeviceStatus(self):
- ''' Read status values from device
- @return: total samples, total errors, data rate and data speed as tuple
- '''
- if self.devicehandle == 0:
- return 0, 0, 0, 0
- status = CHAMP_DATA_STATUS()
- err = self.lib.champGetDataStatus(self.devicehandle, ctypes.byref(status))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to read device status", err)
- return status.Samples, status.Errors, status.Rate, status.Speed
-
-
- def getSamplingRateBase(self, samplingrate):
- ''' Get base sampling rate ID and divider for the requested sampling rate
- @param samplingrate: requested sampling rate in Hz
- @return: base rate ID (-1 if not possible) and base rate divider
- '''
- mindiv = 100000
- base = -1
- div = 1
- for sr in sample_rate:
- div = sample_rate[sr] / samplingrate
- if int(div) == div:
- if div < mindiv:
- mindiv = div
- base = sr
- if base >= 0:
- div = int(sample_rate[base] / samplingrate)
- return base, div
-
- def getDeviceInfo(self):
- ''' Read ID, serial number and production date from device and all connected modules
- '''
- if self.devicehandle == 0 or self.running or self.getEmulationMode() != 0:
- return
-
- # reset the info structure
- self.deviceinfo = CHAMP_DEVICE_INFO()
-
- # power up device
- self.setup(CHAMP_MODE_NORMAL, CHAMP_RATE_10KHZ, 1)
- self.start()
-
- # read the amplifier extended versions to get the carrier board FPGA version also
- self.ampversion.readext(self.lib, self.devicehandle)
-
- # get infos from device
- module = -1
- for info in self.deviceinfo:
- # get device info
- if module == -1:
- self.lib.champFactoryDeviceProductionGet(self.devicehandle, ctypes.byref(info))
- else:
- self.lib.champFactoryModuleProductionGet(self.devicehandle, module, ctypes.byref(info))
- module += 1
-
- # power down device
- self.stop()
-
- def getDeviceInfoString(self):
- ''' Return device info as string
- '''
- emulation = self.getEmulationMode()
- if emulation != 0:
- info = "actiCHamp Simulation Mode, %i Module(s)\n"%(emulation)
- else:
- info = ""
- for n in range(0, len(self.deviceinfo)):
- if n == 0:
- info += "actiCHamp "
- else:
- info += "Module %i "%(n)
- if self.deviceinfo[n].Date.wYear == 0:
- info += "n.a."
- else:
- info += "(%i) SN: %08i"%(self.deviceinfo[n].Model,
- self.deviceinfo[n].SerialNumber)
- if n == 0:
- info += " Rev. %i"%self.ampversion.revision()
- info += "\n"
- # get firmware versions
- info += self.ampversion.info() + "\n"
- return info
-
- def getBatteryVoltage(self):
- ''' Read the amplifier battery voltages
- @return: state (0=ok, 1=critical, 2=bad) and voltage
- '''
- faultyVoltages = []
- voltages = CHAMP_VOLTAGES()
- #voltages.VDC = 0.0
- if self.devicehandle == 0:
- return 0, voltages, faultyVoltages
-
- # get amplifier voltages
- err = self.lib.champGetVoltages(self.devicehandle, ctypes.byref(voltages))
- if err != CHAMP_ERR_OK:
- time.sleep(0.005)
- err = self.lib.champGetVoltages(self.devicehandle, ctypes.byref(voltages))
- if err != CHAMP_ERR_OK:
- if self.running:
- return 2, voltages, faultyVoltages
- else:
- return 0, voltages, faultyVoltages
-
- # check battery voltage
- state = 0
- if voltages.VDC < 5.6:
- state = 1
- if voltages.VDC < 5.3:
- state = 2
- # check other voltages
- # 'V3' Internal 3.3, [V]
- # 'DVDD3' Digital 3.3, [V]
- # 'AVDD3' Analog 3.3, [V]
- # 'AVDD5' Analog 5.0, [V]
- # 'REF' Reference 2.048, [V]
- if self.running:
- targets = [("V3", 3.3), ("DVDD3", 3.3), ("AVDD3", 3.3), ("AVDD5", 5.0), ("REF", 2.048)]
- if self.deviceinfo[0].SerialNumber == 11020001:
- targets = [("V3", 3.3), ("DVDD3", 2.2), ("AVDD3", 3.3), ("AVDD5", 5.0), ("REF", 2.048)]
- for idx, target in targets:
- u = getattr(voltages,idx)
- if u < 0.9 * target or u > 1.1 * target:
- faultyVoltages.append("%s=%.1fV"%(idx, u))
-
- return state, voltages, faultyVoltages
-
- def setButtonLed(self, period, dutyCycle):
- ''' Control MyButton LED via pulse-width modulation
- @param period: cycle period in [ms]
- @param dutyCycle: duty cycle in [%], 0%=always off, 100%=always on
- '''
- if self.devicehandle == 0:
- return
- dutyCycle = max(min(dutyCycle,100),0) # limit to 0-100%
- period = max(min(period,10000),1) # limit to 1-10000ms
- # use a fixed period for on/off
- if dutyCycle == 0 or dutyCycle == 100:
- period = 10
- # convert to C variables
- cPeriod = ctypes.c_uint(period)
- cDutyCycle = ctypes.c_uint(dutyCycle)
- # set LED
- err = self.lib.champSetMyButtonLed(self.devicehandle, cPeriod, cDutyCycle)
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set MyButton LED", err)
-
- def LedTest(self, step):
- ''' Toggle active electrode LEDs
- @param step: 0 = switch off all electrode LEDs, reset index
- 1 = set next electrode to green
- 2 = set next electrode to red
- 11 = set all electrodes to green
- 12 = set all electrodes to red
- @return: TRUE if last electrode index reached
- '''
- ledcount = self.properties.CountEeg + 1
- led_array = (ctypes.c_int * ledcount)()
- led_array[:] = [0]*len(led_array)
- if step == 0:
- self.LED_index = 0
- self.lib.champSetElectrodes(self.devicehandle, None, 0)
- return True
- elif step == 1:
- led_array[self.LED_index] = 1
- self.LED_index += 1
- elif step == 2:
- led_array[self.LED_index] = 2
- self.LED_index += 1
- elif step == 11:
- led_array[:] = [1]*len(led_array)
- self.LED_index = 0
- elif step == 12:
- led_array[:] = [2]*len(led_array)
- self.LED_index = 0
- err = self.lib.champSetElectrodes(self.devicehandle, led_array, ctypes.sizeof(led_array))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set electrode LEDs", err)
- if self.LED_index >= len(led_array):
- self.LED_index = 0
- return self.LED_index == 0
-
-
- def hasPllOption(self):
- ''' The PLL option is available for Rev. 3 amplifiers only and has to be enabled in the INI file
- '''
- return self.enablePllConfiguration and (self.ampversion.revision() >= 3)
-
-
- def setPllInput(self):
- ''' Set the PLL input either to external or internal
- '''
- if self.devicehandle == 0 or not self.hasPllOption() or self.getEmulationMode() != 0:
- return
-
- PllParamters = CHAMP_PLL()
- PllParamters.PllExternal = self.PllExternal
- PllParamters.AdcExternal = 0
- PllParamters.PllFrequency = 25600000
- PllParamters.PllPhase = 0
-
- err = self.lib.champSetPll(self.devicehandle, ctypes.byref(PllParamters))
- if err != CHAMP_ERR_OK:
- raise AmpError("failed to set PLL parameters\nPLL frequency: %d, Status: %d"%(PllParamters.PllFrequency, PllParamters.Status), err)
-
-
-
-'''
-------------------------------------------------------------
-Signal Generator for simulation mode
-------------------------------------------------------------
-'''
-
-class SignalGenerator():
- def __init__(self, dtype=np.int16):
- self.dtype = dtype
- self.lasttime = 0
-
- def GetSineWave(self, freq, samplerate, amplitude, time):
- w = 2.0 * np.pi * freq
- t = np.linspace(0, time, samplerate * time)
- return t, np.asarray(np.sin(w*t) * amplitude, dtype=self.dtype)
-
- def GetTriangleWave(self, freq, samplerate, amplitude, time):
- a = 1.0/freq
- t = np.linspace(0, time, samplerate)
- trig = (np.abs(2*(t/a - np.floor(t/a + 0.5))) - 0.5) * amplitude * 2
- return t, np.asarray(trig, dtype=self.dtype)
-
- def GetSineWaveBuffers(self, NumChannels, StartFrequency, DeltaFrequency, StartAmplitude, DeltaAmplitude, SampleRate):
- # calculate buffer sizes for n*2*PI
- cycles = 40.0
- if type(StartFrequency) == list:
- fl = StartFrequency*NumChannels
- fSin = np.array(fl[:NumChannels+1])
- else:
- fSin = np.arange(StartFrequency, StartFrequency + NumChannels * DeltaFrequency, DeltaFrequency)
- if DeltaAmplitude != 0:
- aSin = np.arange(StartAmplitude, StartAmplitude + NumChannels * DeltaAmplitude, DeltaAmplitude, dtype=self.dtype)
- else:
- aSin = np.zeros(NumChannels)
- aSin[:] = StartAmplitude
-
- Tsin = 1.0/fSin
- NumSamples = (Tsin * SampleRate)*cycles
- tl = list(np.linspace(0, 2.0 * np.pi * cycles, s)[:-1] for s in NumSamples)
- signals = list(np.asarray(np.sin(t) * a, dtype=self.dtype) for t,a in zip(tl,aSin))
- return tl, signals
-
+# -*- coding: utf-8 -*-
+'''
+Python wrapper for ActiChamp Windows library
+
+ActiChamp_x86.dll (32-Bit) and ActiChamp_x64.dll (64-Bit)
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@version: 1.0
+'''
+
+import ctypes
+import ctypes.wintypes
+import _ctypes
+import numpy as np
+import time
+try:
+ import ConfigParser as configparser
+except ImportError:
+ import configparser
+import platform
+
+# enable or disable the Python Signal Generator for simulation mode
+PYSIGGEN = False
+#PYSIGGEN = True
+# 非Windows環境では常にシミュレーションを許可
+if platform.system() != 'Windows':
+ PYSIGGEN = True
+
+
+# max integer
+INT32_MAX = 2**31-1
+
+ADC_MAX = 0x7FFFFF
+
+# required hardware DLL version
+#CHAMP_VERSION = 0x080B0519 # 08.11.05.25 DLL
+#CHAMP_VERSION = 0x090B0B07 # 09.11.11.07 DLL
+#CHAMP_VERSION = 0x0A0B0C02 # 10.11.12.02 DLL
+#CHAMP_VERSION = 0x0A0B0C17 # 10.11.12.23 DLL
+#CHAMP_VERSION = 0x0A0B0C1D # 10.11.12.29 DLL
+#CHAMP_VERSION = 0x0B0C0A02 # 11.12.10.02 DLL
+#CHAMP_VERSION = 0x110C0B10 # 17.12.11.16 DLL
+#CHAMP_VERSION = 0x120D0710 # 18.13.07.16 DLL
+#CHAMP_VERSION = 0x160D0B0E # 22.13.11.14 DLL
+#CHAMP_VERSION = 0x170E040F # 23.14.04.15 DLL
+CHAMP_VERSION = 0x190E0804 # 25.14.08.04 DLL
+
+# required firmware versions (board revision 4)
+CHAMP_4_VERSION_CTRL = 0x040B041C # 04.11.04.28 FX2 USB controller
+CHAMP_4_VERSION_FPGA = 0x2C000000 # 44.00.00.00 FPGA
+CHAMP_4_VERSION_DSP = 0x060B0519 # 06.11.05.25 MSP430
+
+# required firmware versions (board revision 6)
+CHAMP_6_VERSION_CTRL = 0x660F0609 # 102.15.06.09 FX2 USB controller
+CHAMP_6_VERSION_FPGAM = 0x30000000 # 48.00.00.00 FPGA media controller
+CHAMP_6_VERSION_FPGAC = 0x2D000000 # 45.00.00.00 FPGA carrier board
+CHAMP_6_VERSION_DSP = 0x690E0A07 # 105.14.10.07 MSP430
+
+# compensate constant trigger delay
+CHAMP_COMPTRIGGER = False
+
+# C error numbers
+CHAMP_ERR_OK = 0 # Success (no errors)
+CHAMP_ERR_HANDLE = -1 # Invalid handle (such handle not present now)
+CHAMP_ERR_PARAM = -2 # Invalid function parameter(s)
+CHAMP_ERR_FAIL = -3 # Function fail (internal error)
+CHAMP_ERR_MONITORING = -4 # data rate monitoring failed
+CHAMP_ERR_SUPPORT = -5 # function not supported
+
+# ADC data filter enum
+CHAMP_ADC_NATIVE = 0 # no ADC data filter
+CHAMP_ADC_AVERAGING_2 = 1 # ADC data moving average filter by 2 samples
+
+# ADC data decimation
+CHAMP_DECIMATION_0 = 0 # no decimation
+CHAMP_DECIMATION_2 = 2 # decimation by 2
+CHAMP_DECIMATION_5 = 5 # decimation by 5
+CHAMP_DECIMATION_10 = 10 # decimation by 10
+CHAMP_DECIMATION_20 = 20 # decimation by 20
+CHAMP_DECIMATION_50 = 50 # decimation by 50
+
+# Mode enum
+CHAMP_MODE_NORMAL = 0 # normal data acquisition
+CHAMP_MODE_ACTIVE_SHIELD = 1 # data acquisition with ActiveShield
+CHAMP_MODE_IMPEDANCE = 2 # impedance measure
+CHAMP_MODE_TEST = 3 # test signal (square wave 200 uV, 1 Hz)
+CHAMP_MODE_LED_TEST = 99 # active electrode LED test mode
+
+# Mode text
+CHAMP_Modes = {CHAMP_MODE_NORMAL:"acquisition",
+ CHAMP_MODE_ACTIVE_SHIELD:"acquisition with shield",
+ CHAMP_MODE_IMPEDANCE:"impedance measurement",
+ CHAMP_MODE_TEST:"test signal",
+ CHAMP_MODE_LED_TEST:"active electrode LED test" }
+
+# actiChamp base sample rate enum
+CHAMP_RATE_10KHZ = 0 # 10 kHz, all channels (default mode)
+CHAMP_RATE_50KHZ = 1 # 50 kHz
+CHAMP_RATE_100KHZ = 2 # 100 kHz, max 64 channels
+# actiChamp base sample rate for extended settings enum
+CHAMP_RATE_25KHZ = 10 # 25 kHz
+CHAMP_RATE_5KHZ = 11 # 5 kHz
+CHAMP_RATE_2KHZ = 12 # 2 kHz
+CHAMP_RATE_1KHZ = 13 # 1 kHz
+CHAMP_RATE_500HZ = 14 # 500 Hz
+CHAMP_RATE_200HZ = 15 # 200 Hz
+
+# sample rate frequency dictionary (amplifier DLL base frequencies available for the application)
+# if you want to do the decimation and filtering in Python (amplifier.py) then
+# set this value to True:
+PythonDecimation = False
+if PythonDecimation:
+ sample_rate = {
+ CHAMP_RATE_10KHZ:10000.0,
+ CHAMP_RATE_50KHZ:50000.0,
+ CHAMP_RATE_100KHZ:100000.0
+ }
+else:
+ sample_rate = {
+ CHAMP_RATE_200HZ:200.0, CHAMP_RATE_500HZ:500.0, CHAMP_RATE_1KHZ:1000.0,
+ CHAMP_RATE_2KHZ:2000.0, CHAMP_RATE_5KHZ:5000.0,
+ CHAMP_RATE_10KHZ:10000.0, CHAMP_RATE_25KHZ:25000.0,
+ CHAMP_RATE_50KHZ:50000.0, CHAMP_RATE_100KHZ:100000.0
+ }
+
+# trigger delay dictionary (for constant trigger delay compensation)
+trigger_delay = {
+ CHAMP_RATE_200HZ:1, CHAMP_RATE_500HZ:1, CHAMP_RATE_1KHZ:1,
+ CHAMP_RATE_2KHZ:1, CHAMP_RATE_5KHZ:1,
+ CHAMP_RATE_10KHZ:1, CHAMP_RATE_25KHZ:1,
+ CHAMP_RATE_50KHZ:1, CHAMP_RATE_100KHZ:1 }
+
+# sample rate extended settings dictionary
+# translate application base frequency to amplifier physical frequency
+# 0=10kHz, 1=50kHz, 2=100kHz
+sample_rate_settings = {
+ CHAMP_RATE_200HZ:0, CHAMP_RATE_500HZ:0, CHAMP_RATE_1KHZ:0,
+ CHAMP_RATE_2KHZ:0, CHAMP_RATE_5KHZ:0,
+ CHAMP_RATE_10KHZ:0, CHAMP_RATE_25KHZ:1,
+ CHAMP_RATE_50KHZ:1, CHAMP_RATE_100KHZ:2 }
+# decimation values (rate = physical / decimation)
+sample_rate_decimation = {
+ CHAMP_RATE_200HZ:CHAMP_DECIMATION_50, CHAMP_RATE_500HZ:CHAMP_DECIMATION_20, CHAMP_RATE_1KHZ:CHAMP_DECIMATION_10,
+ CHAMP_RATE_2KHZ:CHAMP_DECIMATION_5, CHAMP_RATE_5KHZ:CHAMP_DECIMATION_2,
+ CHAMP_RATE_10KHZ:CHAMP_DECIMATION_0, CHAMP_RATE_25KHZ:CHAMP_DECIMATION_2,
+ CHAMP_RATE_50KHZ:CHAMP_DECIMATION_0, CHAMP_RATE_100KHZ:CHAMP_DECIMATION_0 }
+
+
+
+
+class CHAMP_SETTINGS(ctypes.Structure):
+ ''' C amplifier settings
+ '''
+ _pack_ = 1
+ _fields_ = [("Mode", ctypes.c_int), # mode of acquisition
+ ("Rate", ctypes.c_int)] # sample rate
+
+class CHAMP_SETTINGS_EX(ctypes.Structure):
+ ''' C extended amplifier settings
+ '''
+ _pack_ = 1
+ _fields_ = [("Mode", ctypes.c_int), # mode of acquisition
+ ("Rate", ctypes.c_int), # sample rate
+ ("AdcFilter", ctypes.c_int), # ADC data filter
+ ("Decimation", ctypes.c_int)] # ADC data decimation
+
+class CHAMP_PROPERTIES(ctypes.Structure):
+ ''' C amplifier properties
+ '''
+ _pack_ = 1
+ _fields_ = [("CountEeg", ctypes.c_uint), # number of Eeg channels
+ ("CountAux", ctypes.c_uint), # number of Aux channels
+ ("TriggersIn", ctypes.c_uint), # numbers of input triggers
+ ("TriggersOut", ctypes.c_uint), # numbers of output triggers
+ ("Rate", ctypes.c_float), # sampling rate, Hz
+ ("ResolutionEeg", ctypes.c_float), # EEG amplitude scale coefficients, V/bit
+ ("ResolutionAux", ctypes.c_float), # AUX amplitude scale coefficients, V/bit
+ ("RangeEeg", ctypes.c_float), # EEG input range peak-peak, V
+ ("RangeAux", ctypes.c_float)] # AUX input range peak-peak, V
+
+class CHAMP_IMPEDANCE_SETUP(ctypes.Structure):
+ ''' C impedance settings
+ '''
+ _pack_ = 1
+ _fields_ = [("Good", ctypes.c_uint), # Good level (green led indication), Ohm
+ ("Bad", ctypes.c_uint), # Bad level (red led indication), Ohm
+ ("LedsDisable", ctypes.c_uint), # Disable electrode's leds, if not zero
+ ("TimeOut", ctypes.c_uint)] # Impedance mode time-out (0 - 65535), sec
+
+class CHAMP_DATA_STATUS(ctypes.Structure):
+ ''' C device data status
+ '''
+ _pack_ = 1
+ _fields_ = [("Samples", ctypes.c_uint), # Total samples
+ ("Errors", ctypes.c_uint), # Total errors
+ ("Rate", ctypes.c_float), # Data rate, Hz
+ ("Speed", ctypes.c_float)] # Data speed, MB/s
+
+class CHAMP_SYSTEMTIME(ctypes.Structure):
+ ''' C system time struct
+ '''
+ _pack_ = 1
+ _fields_ = [( 'wYear', ctypes.wintypes.WORD ),
+ ( 'wMonth', ctypes.wintypes.WORD ),
+ ( 'wDayOfWeek', ctypes.wintypes.WORD ),
+ ( 'wDay', ctypes.wintypes.WORD ),
+ ( 'wHour', ctypes.wintypes.WORD ),
+ ( 'wMinute', ctypes.wintypes.WORD ),
+ ( 'wSecond', ctypes.wintypes.WORD ),
+ ( 'wMilliseconds', ctypes.wintypes.WORD )]
+
+class CHAMP_MODULE_INFO(ctypes.Structure):
+ ''' C device and module info
+ '''
+ _pack_ = 1
+ _fields_ = [( 'Model', ctypes.c_uint ), # Model ID
+ ( 'SerialNumber', ctypes.c_uint ), # Serial Number
+ ( 'Date', CHAMP_SYSTEMTIME )] # Production Date and Time
+
+CHAMP_DEVICE_INFO = CHAMP_MODULE_INFO * 6 # index 0=device, index 1-5=modules
+
+class CHAMP_VERSION_INFO(ctypes.Structure):
+ ''' C DLL, USB driver and firmware versions
+ '''
+ _pack_ = 1
+ _fields_ = [( 'DLL', ctypes.wintypes.DWORD ), # DLL version
+ ( 'USBDRV', ctypes.wintypes.DWORD ), # USB driver version
+ ( 'USBCTRL', ctypes.wintypes.DWORD ), # USB controller firmware version
+ ( 'FPGA', ctypes.wintypes.DWORD ), # FPGA firmware version
+ ( 'DSP', ctypes.wintypes.DWORD )] # MSP430 firmware version
+
+
+class CHAMP_VERSION_INFO_EXT(ctypes.Structure):
+ ''' C DLL, USB driver and firmware versions for board revision 6
+ '''
+ _pack_ = 1
+ _fields_ = [( 'DLL', ctypes.wintypes.DWORD ), # DLL version
+ ( 'USBDRV', ctypes.wintypes.DWORD ), # USB driver version
+ ( 'USBCTRL', ctypes.wintypes.DWORD ), # USB controller firmware version
+ ( 'FPGAM', ctypes.wintypes.DWORD ), # Media converter FPGA firmware version
+ ( 'DSP', ctypes.wintypes.DWORD ), # MSP430 firmware version
+ ( 'FPGAC', ctypes.wintypes.DWORD )] # Carrier board FPGA firmware version
+
+
+class CHAMP_VOLTAGES(ctypes.Structure):
+ ''' C Amplifier voltages and temperature
+ The voltages DVDD3, AVDD3, AVDD5 and REF are valid only during data acquisition
+ '''
+ _pack_ = 1
+ _fields_ = [( 'VDC', ctypes.c_float ), # Power supply, [V]
+ ( 'V3', ctypes.c_float ), # Internal 3.3, [V]
+ ( 'TEMP', ctypes.c_float ), # Temperature, degree Celsius
+ ( 'DVDD3', ctypes.c_float ), # Digital 3.3, [V]
+ ( 'AVDD3', ctypes.c_float ), # Analog 3.3, [V]
+ ( 'AVDD5', ctypes.c_float ), # Analog 5.0, [V]
+ ( 'REF', ctypes.c_float )] # Reference 2.048, [V]
+
+
+class CHAMP_MODULES(ctypes.Structure):
+ ''' C Module control structure
+ Bits:
+ 0 - AUX module
+ 1 - 5 - Main EEG modules (1 - 5)
+ 6 - 31 - Reserved
+ '''
+ _pack_ = 1
+ _fields_ = [( 'Present', ctypes.c_uint ), # Bits indicate that the module is present in hardware
+ ( 'Enabled', ctypes.c_uint )] # Bits indicate that the module is enabled for use
+
+class CHAMP_PLL(ctypes.Structure):
+ ''' C PLL Parameters
+ '''
+ _pack_ = 1
+ _fields_ = [( 'PllExternal', ctypes.c_uint ), # if 1 - use External clock for PLL, if 0 - use Internal 48 MHz
+ ( 'AdcExternal', ctypes.c_uint ), # if 1 - out External clock to ADC, if 0 - use PLL output
+ ( 'PllFrequency', ctypes.c_uint ), # PLL frequency 10 MHz - 27 MHz (needs set if AdcExternal = 0), Hz
+ ( 'PllPhase', ctypes.c_uint ), # Phase shift (hardware step 360 / 10 = 36), degrees
+ ( 'Status', ctypes.c_uint )] # PLL status (read only)
+
+
+
+class AmpError(Exception):
+ ''' Generic amplifier exception
+ '''
+ def __init__(self, value, errornr = 0):
+ errortext = ""
+ if errornr == CHAMP_ERR_HANDLE:
+ errortext = "Invalid handle (device disconnected)"
+ elif errornr == CHAMP_ERR_PARAM:
+ errortext = "Invalid function parameter(s)"
+ elif errornr == CHAMP_ERR_FAIL:
+ errortext = "Function fail (internal error)"
+ elif errornr == CHAMP_ERR_MONITORING:
+ errortext = "Data rate mismatch"
+ elif errornr == CHAMP_ERR_SUPPORT:
+ errortext = "Function is not supported"
+ errortext = errortext + " :%i"%(errornr)
+ if errornr != 0:
+ self.value = "actiChamp: " + str(value) + " -> " + errortext
+ else:
+ self.value = "actiChamp: " + str(value)
+ def __str__(self):
+ return self.value
+
+
+
+class AmpVersion(object):
+ def __init__(self):
+ self.version = CHAMP_VERSION_INFO()
+ self.versionext = CHAMP_VERSION_INFO_EXT()
+ self.boardRevision = 4
+
+ def read(self, lib, device):
+ ''' read board dependent version infos from amplifier
+ attention: the carrier board FPGA version (FPGAC) is only available if the acquisition is running.
+ @param lib: DLL handle
+ @param device: device handle
+ @return: DLL result value
+ '''
+ res = lib.champGetVersion(device, ctypes.byref(self.version))
+ DSP_MajorVersion = self.version.DSP >> 24
+ if DSP_MajorVersion >= 100:
+ # board revision 6
+ self.boardRevision = 6
+ elif DSP_MajorVersion > 0:
+ self.boardRevision = 4
+ else:
+ self.boardRevision = 0
+ return res
+
+ def readext(self, lib, device):
+ ''' read version info for amplifier board revision 6
+ attention: the carrier board FPGA version (FPGAC) is only available if the acquisition is running.
+ @param lib: DLL handle
+ @param device: device handle
+ @return: DLL result value
+ '''
+ res = lib.champGetVersion(device, ctypes.byref(self.version))
+ if self.boardRevision == 6:
+ res = lib.champGetVersionExt(device, ctypes.byref(self.versionext))
+ return res
+
+ def isFpgaProgrammed(self):
+ if self.boardRevision and self.version.FPGA == 0:
+ return False
+ return True
+
+ def isValid(self):
+ ''' Validate major firmware versions
+ @return: True if valid (or emulated), False if not
+ '''
+ if self.boardRevision == 4:
+ if self.version.USBCTRL != 0 and self.version.USBCTRL & 0xFF000000 != CHAMP_4_VERSION_CTRL & 0xFF000000:
+ return False
+ if self.version.FPGA !=0 and self.version.FPGA & 0xFF000000 != CHAMP_4_VERSION_FPGA & 0xFF000000:
+ return False
+ if self.version.DSP != 0 and self.version.DSP & 0xFF000000 != CHAMP_4_VERSION_DSP & 0xFF000000:
+ return False
+ if self.boardRevision == 6:
+ if self.versionext.USBCTRL != 0 and self.versionext.USBCTRL & 0xFF000000 != CHAMP_6_VERSION_CTRL & 0xFF000000:
+ return False
+ if self.versionext.FPGAM !=0 and self.versionext.FPGAM & 0xFF000000 != CHAMP_6_VERSION_FPGAM & 0xFF000000:
+ return False
+ if self.versionext.FPGAC !=0 and self.versionext.FPGAC & 0xFF000000 != CHAMP_6_VERSION_FPGAC & 0xFF000000:
+ return False
+ if self.versionext.DSP != 0 and self.versionext.DSP & 0xFF000000 != CHAMP_6_VERSION_DSP & 0xFF000000:
+ return False
+ return True
+
+ def _getVersionString(self, rawversion):
+ ''' get readable version string from DWORD
+ @param rawversion: raw version number from DLL
+ @return: version string
+ '''
+ # split version number
+ version = ""
+ for i in reversed(range(4)):
+ version += "%02i"%((rawversion >> i*8) & 0xFF)
+ if i:
+ version +="."
+ return version
+
+
+ def info(self):
+ ''' get all amplifier firmware versions as string
+ '''
+ if self.boardRevision == 4:
+ # create version string for board revision 4
+ version = "Version: DLL_%s, DRV_%s, CTRL_%s, FPGA_%s, DSP_%s"%(self._getVersionString(self.version.DLL),
+ self._getVersionString(self.version.USBDRV),
+ self._getVersionString(self.version.USBCTRL),
+ self._getVersionString(self.version.FPGA),
+ self._getVersionString(self.version.DSP))
+ # required firmware versions
+ req_version = "Firmware Version MISMATCH, required: CTRL_%s, FPGA_%s, DSP_%s"%(self._getVersionString(CHAMP_4_VERSION_CTRL),
+ self._getVersionString(CHAMP_4_VERSION_FPGA),
+ self._getVersionString(CHAMP_4_VERSION_DSP))
+
+ elif self.boardRevision == 6:
+ # create version string for board revision 6
+ version = "Version: DLL_%s, DRV_%s, CTRL_%s, FPGAM_%s, FPGAC_%s, DSP_%s"%(self._getVersionString(self.versionext.DLL),
+ self._getVersionString(self.versionext.USBDRV),
+ self._getVersionString(self.versionext.USBCTRL),
+ self._getVersionString(self.versionext.FPGAM),
+ self._getVersionString(self.versionext.FPGAC),
+ self._getVersionString(self.versionext.DSP))
+ # required firmware versions
+ req_version = "Firmware Version MISMATCH, required: CTRL_%s, FPGAM_%s, FPGAC_%s DSP_%s"%(self._getVersionString(CHAMP_6_VERSION_CTRL),
+ self._getVersionString(CHAMP_6_VERSION_FPGAM),
+ self._getVersionString(CHAMP_6_VERSION_FPGAC),
+ self._getVersionString(CHAMP_6_VERSION_DSP))
+ else:
+ version = ""
+ req_version = ""
+
+ '''
+ if self.isValid():
+ return version
+ else:
+ return version +"\n" + req_version
+ '''
+ return version
+
+ def DLL(self):
+ ''' get the DLL version
+ '''
+ return self.version.DLL
+
+ def revision(self):
+ ''' get the amplifier revision, depending on board revision
+ '''
+ if self.boardRevision > 4:
+ return 3
+ return 2
+
+
+
+class ActiChamp(object):
+ ''' ActiChamp hardware object (Python wrapper for actiCHamp Windows DLL)
+ '''
+
+ def __init__(self):
+ ''' Constructor
+ '''
+ # get OS architecture (32/64-bit)
+ self.x64 = ("64" in platform.architecture()[0])
+
+ # set default values
+ self.devicehandle = 0
+ self.ampversion = AmpVersion() #: actiCHamp version info structure
+ self.deviceinfo = CHAMP_DEVICE_INFO() #: actiCHamp device info structure
+ self.modulestate = CHAMP_MODULES() #: actiCHamp module connection state structure
+ self.properties = CHAMP_PROPERTIES() #: actiCHamp property structure
+ self.settings = CHAMP_SETTINGS() #: actiCHamp settings structure
+ self.settings.Rate = CHAMP_RATE_10KHZ #: sampling rate
+ self.settings.Mode = CHAMP_MODE_NORMAL #: acquisition mode
+ self.running = False #: data acquisition running
+ self.buffer = ctypes.create_string_buffer(10000*1024) #: raw data transfer buffer
+ self.impbuffer = ctypes.create_string_buffer(1000) #: impedance raw data transfer buffer
+ self.readError = False #: an error occurred during data acquisition
+ self.activeShieldGain = 5 #: default active shield gain
+ self.enablePllConfiguration = False #: enable the PLL configuration option
+ self.PllExternal = 0 #: use external input for the PLL
+
+ # binning buffer for max. 100 samples with 170 channels with a datasize of int32 (4 bytes)
+ self.binning_buffer = ctypes.create_string_buffer(100*170*4) #: binning buffer
+ self.binning = 1 #: binning size for buffer alignment
+ self.binning_offset = 0 #: raw data buffer offset in bytes for binning
+
+ self.sampleCounterAdjust = 0 #: sample counter wrap around, HW counter is 32bit value but we need 64bit
+ self.BlockingMode = True #: read data in blocking mode
+ self.EmulationMode = False #: emulate hardware
+
+ # set default properties
+ self.properties.CountEeg = 32
+ self.properties.CountAux = 8
+ self.properties.TriggersIn = 8
+ self.properties.TriggersOut = 8
+ self.properties.Rate = 10000.0
+ self.properties.ResolutionEeg = 4.88e-08
+ self.properties.ResolutionAux = 2.98e-07
+ self.properties.RangeEeg = 0.819
+ self.properties.RangeAux = 5.0
+
+ # load ActiChamp 32 or 64 bit windows library (Windowsのみ)
+ self.lib = None
+ try:
+ self.loadLib()
+ except Exception:
+ # 非WindowsやDLL未検出時はエミュレーション許容
+ self.EmulationMode = True
+ self.lib = None
+
+ # get and check DLL version (ライブラリがある場合のみ)
+ if self.lib is not None:
+ self.ampversion.read(self.lib, self.devicehandle)
+ if self.ampversion.DLL() != CHAMP_VERSION:
+ raise AmpError("wrong ActiChamp DLL version (%X / %X)"%(self.ampversion.DLL(),
+ CHAMP_VERSION))
+
+
+ # try to open device and get device properties
+ try:
+ # get hardware properties
+ self.open()
+ self.getDeviceInfo()
+ except:
+ pass
+
+ try:
+ self.close()
+ except:
+ pass
+
+ def _resetDeviceProperties(self):
+ ''' Set channel count to zero
+ '''
+ self.properties.CountEeg = 0
+ self.properties.CountAux = 0
+ self.properties.TriggersIn = 0
+ self.properties.TriggersOut = 0
+
+
+ def loadLib(self):
+ ''' Load windows library
+ '''
+ # load ActiChamp 32 or 64 bit windows library
+ try:
+ # unload existing library
+ if self.lib != None:
+ _ctypes.FreeLibrary(self.lib._handle)
+ # load/reload library
+ if self.x64:
+ self.lib = ctypes.windll.LoadLibrary("ActiChamp_x64.dll")
+ self.lib.champOpen.restype = ctypes.c_uint64
+ else:
+ self.lib = ctypes.windll.LoadLibrary("ActiChamp_x86.dll")
+ except:
+ self.lib = None
+ if self.x64:
+ raise AmpError("failed to open library (ActiChamp_x64.dll)")
+ else:
+ raise AmpError("failed to open library (ActiChamp_x86.dll)")
+
+
+ def open(self):
+ ''' Open the hardware device and get a device handle and device properties
+ '''
+ if self.running:
+ return
+ if self.lib == None:
+ # 非Windows・未検出時はエミュレーションにフォールバック
+ if platform.system() != 'Windows':
+ self.EmulationMode = True
+ return
+ raise AmpError("library ActiChamp_x86.dll not available")
+
+ # check if device hardware is available
+ self._resetDeviceProperties()
+ if self.lib.champGetCount() == 0:
+ raise AmpError("hardware not available")
+
+ retry = 3
+ while retry > 0:
+ # open the first available device
+ if self.x64:
+ self.devicehandle = ctypes.c_uint64(self.lib.champOpen(0))
+ else:
+ self.devicehandle = ctypes.c_int32(self.lib.champOpen(0))
+ if self.devicehandle.value == 0:
+ self.devicehandle = 0
+ raise AmpError("failed to open device")
+
+ # get device version info
+ err = self.ampversion.read(self.lib, self.devicehandle)
+ if err != CHAMP_ERR_OK:
+ self.close()
+ raise AmpError("failed to get device version info", err)
+
+ # check if fpga loaded successfully
+ if not self.ampversion.isFpgaProgrammed():
+ self.lib.champClose(self.devicehandle)
+ self.devicehandle = 0
+ retry -= 1
+ if retry == 0:
+ raise AmpError("failed to open device")
+ else:
+ retry = 0
+
+ # get device module connection info
+ self.modulestate.Enabled = 0
+ self.modulestate.Present = 0
+ self.lib.champGetModules(self.devicehandle, ctypes.byref(self.modulestate))
+
+ # get device properties
+ self.lib.champGetProperty(self.devicehandle, ctypes.byref(self.properties))
+
+ def close(self):
+ ''' Close hardware device
+ '''
+ if self.lib == None:
+ if platform.system() != 'Windows':
+ return
+ raise AmpError("library ActiChamp_x86.dll not available")
+ if self.devicehandle != 0:
+ if self.running:
+ try:
+ self.stop()
+ except:
+ pass
+ self.lib.champClose(self.devicehandle)
+ self.devicehandle = 0
+
+ def _get_settings_ex(self, settings):
+ ''' Prepare extended settings (rate, decimation and filter)
+ @param settings: amplifier base settings
+ @return: extended settings
+ '''
+ csext = CHAMP_SETTINGS_EX()
+ csext.Mode = settings.Mode
+ csext.Rate = sample_rate_settings[settings.Rate]
+ csext.Decimation = sample_rate_decimation[settings.Rate]
+ csext.AdcFilter = CHAMP_ADC_AVERAGING_2
+ return csext
+
+ def setup(self, mode, rate, binning):
+ ''' Prepare device for acquisition
+ @param mode: device mode, one of CHAMP_MODE_ values
+ @param rate: device sampling rate, one of CHAMP_RATE_ values
+ @param binning: sampling rate divider to align read buffer to requested binning size
+ '''
+ # LED test is done in normal recording mode
+ if mode == CHAMP_MODE_LED_TEST:
+ self.settings.Mode = CHAMP_MODE_NORMAL
+ else:
+ self.settings.Mode = mode
+ self.settings.Rate = rate
+ self.binning = int(binning)
+ self.binning_offset = 0
+ if self.devicehandle == 0:
+ raise AmpError("device not open")
+
+ # setup amplifier
+ ex_settings = self._get_settings_ex(self.settings)
+
+ # limit the number of modules (1xEEG + AUX) if sampling rate is 100KHz
+ if self.settings.Rate == CHAMP_RATE_100KHZ:
+ self.modulestate.Enabled = self.modulestate.Present & 0x03
+ # limit the number of modules (2xEEG + AUX) if sampling rate is 50KHz
+ elif self.settings.Rate == CHAMP_RATE_50KHZ:
+ self.modulestate.Enabled = self.modulestate.Present & 0x07
+ # limit the number of modules (4xEEG + AUX) if sampling rate is 25KHz
+ elif self.settings.Rate == CHAMP_RATE_25KHZ:
+ self.modulestate.Enabled = self.modulestate.Present & 0x1F
+ # enable all present modules if sampling rate is below 25KHz
+ else:
+ self.modulestate.Enabled = self.modulestate.Present
+
+ # start impedance measurement always with 10KHz
+ if ex_settings.Mode == CHAMP_MODE_IMPEDANCE:
+ ex_settings.Rate = CHAMP_RATE_10KHZ
+ ex_settings.Decimation = CHAMP_DECIMATION_0
+
+ # enable modules
+ err = self.lib.champSetModules(self.devicehandle, ctypes.byref(self.modulestate))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to setup module selection", err)
+
+ # setup device
+ err = self.lib.champSetSettingsEx(self.devicehandle, ctypes.byref(ex_settings))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to setup device", err)
+
+ # set active shield gain
+ gain = ctypes.c_uint(self.activeShieldGain) # 1-100, default = 100
+ err = self.lib.champSetActiveShieldGain(self.devicehandle, gain)
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set active shield gain", err)
+
+ # get device properties
+ self._resetDeviceProperties()
+ self.lib.champGetProperty(self.devicehandle, ctypes.byref(self.properties))
+
+ # create constant trigger delay compensation buffer
+ trgdelay = trigger_delay[self.settings.Rate]
+ self.trgdelaybuf = np.zeros(trgdelay, np.uint32) + 0xFFFF
+
+
+ def start(self):
+ ''' Start data acquisition
+ '''
+ if self.running:
+ return
+ if self.devicehandle == 0:
+ raise AmpError("device not open")
+
+ # start amplifier
+ err = self.lib.champStart(self.devicehandle)
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to start device", err)
+
+ # read the amplifier extended versions to get the carrier board FPGA version also
+ self.ampversion.readext(self.lib, self.devicehandle)
+
+ # get infos from device
+ self.deviceinfo = CHAMP_DEVICE_INFO() # reset the info structure
+ module = -1
+ for info in self.deviceinfo:
+ # get device info
+ if module == -1:
+ self.lib.champFactoryDeviceProductionGet(self.devicehandle, ctypes.byref(info))
+ else:
+ self.lib.champFactoryModuleProductionGet(self.devicehandle, module, ctypes.byref(info))
+ module += 1
+
+ self.running = True
+ self.readError = False
+ self.sampleCounterAdjust = 0
+ self.BlockTimer = time.perf_counter()
+
+ # try to set the PLL input
+ self.setPllInput()
+
+ # reset signal generator
+ self.DummySignals = []
+
+ def stop(self):
+ ''' Stop data acquisition
+ '''
+ if not self.running:
+ return
+ self.running = False
+ if self.devicehandle == 0:
+ raise AmpError("device not open")
+ err = self.lib.champStop(self.devicehandle)
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to stop device", err)
+
+ def read(self, indices, eegcount, auxcount):
+ ''' Read data from device
+ @param indices: to select the requested channels from raw data stream
+ @param eegcount: number of requested EEG channels
+ @param auxcount: number of requested AUX channels
+ @return: list of np arrays for channel data, trigger channel and sample counter,
+ indices of disconnected channels
+ '''
+ if not self.running or (self.devicehandle == 0) or self.readError:
+ return None, None
+
+ # calculate data amount for an interval of
+ interval = 0.05 # interval in [s]
+ bytes_per_sample = (self.properties.CountEeg + self.properties.CountAux + 1 + 1) *\
+ np.dtype(np.int32).itemsize
+ requestedbytes = int(bytes_per_sample * sample_rate[self.settings.Rate] * interval)
+
+ t = time.perf_counter()
+
+ # read data from device
+ if not self.BlockingMode:
+ bytesread = self.lib.champGetData(self.devicehandle,
+ ctypes.byref(self.buffer, self.binning_offset),
+ len(self.buffer) - self.binning_offset)
+ else:
+ bytesread = self.lib.champGetDataBlocking(self.devicehandle,
+ ctypes.byref(self.buffer, self.binning_offset),
+ requestedbytes)
+
+ blocktime = (time.perf_counter() - self.BlockTimer)
+ self.BlockTimer = time.perf_counter()
+ #print str(blocktime) + " : " + str(bytesread)
+
+ #print str(t-self.lastt) + " : " + str(bytesread)
+ #self.lastt = t
+
+ # check for device error
+ if bytesread < 0:
+ if bytesread == CHAMP_ERR_MONITORING:
+ return None, CHAMP_ERR_MONITORING
+ self.readError = True # block next read access, until acquisition is restarted
+ raise AmpError("failed to read data from device", bytesread)
+
+ # data available?
+ if bytesread == 0:
+ return None, None
+
+ if self.binning > 1:
+ # align buffer to requested binning size
+ total_bytes = bytesread + self.binning_offset
+ # copy remainder from last read back to sample buffer
+ ctypes.memmove(self.buffer, self.binning_buffer, self.binning_offset)
+ # new remainder size
+ remainder = ((total_bytes / bytes_per_sample) % self.binning) * bytes_per_sample
+ # number of binning aligned samples
+ binning_samples = total_bytes / bytes_per_sample / self.binning * self.binning
+ src_offset = binning_samples * bytes_per_sample
+ # copy new remainder to binning buffer
+ ctypes.memmove(self.binning_buffer, ctypes.byref(self.buffer, src_offset), remainder)
+ self.binning_offset = remainder
+
+ # there must be at least one binning sample
+ if binning_samples == 0:
+ return None, None
+ items = binning_samples * bytes_per_sample / np.dtype(np.int32).itemsize
+ else:
+ items = bytesread / np.dtype(np.int32).itemsize
+
+ # channel order in buffer is S1CH1,S1CH2..S1CHn, S2CH1,S2CH2,..S2nCHn, ...
+ x = np.fromstring(self.buffer, np.int32, items)
+ # shape and transpose to 1st axis is channel and 2nd axis is sample
+ samplesize = self.properties.CountEeg + self.properties.CountAux + 1 + 1
+ x.shape = (-1, samplesize)
+ y = x.transpose()
+
+ # extract the different channel types
+ index = 0
+ eeg = np.array(y[indices], float)
+
+ # get indices of disconnected electrodes (all values == ADC_MAX)
+ # disconnected = np.nonzero(np.all(eeg == ADC_MAX, axis=1))
+ disconnected = None # not possible yet
+
+ # extract and scale the different channel types
+ eegscale = self.properties.ResolutionEeg * 1e6 # convert to µV
+ eeg[index:eegcount] = eeg[index:eegcount] * eegscale
+ index += eegcount
+ auxscale = self.properties.ResolutionAux * 1e6 # convert to µV
+ eeg[index:index+auxcount] = eeg[index:index+auxcount] * auxscale
+
+ # extract trigger channel
+ index = self.properties.CountEeg + self.properties.CountAux
+ trg = np.array(y[index:index + 1], np.uint32)
+
+ # compensate constant trigger delay
+ if CHAMP_COMPTRIGGER:
+ dsize = len(trg[0])
+ temp = np.append(self.trgdelaybuf, trg[0], 0)
+ trg[0] = temp[:dsize]
+ self.trgdelaybuf = temp[dsize:]
+
+ # extract sample counter channel
+ index += 1
+ sctTemp = np.array(y[index:index + 1], np.uint32)
+
+ # search for sample counter wrap around and adjust counter
+ sct = np.array(sctTemp, np.uint64) + self.sampleCounterAdjust
+ wrap = np.nonzero(sctTemp == 0)
+ if (wrap[1].size > 0) and sct[0][0]:
+ wrapIndex = wrap[1][0]
+ adjust = np.iinfo(np.uint32).max + 1
+ self.sampleCounterAdjust += adjust
+ sct[:,wrapIndex:] += adjust
+
+
+ # Test Signal Generator
+ # use internal signal generator?
+ if PYSIGGEN and self.EmulationMode:
+ if not len(self.DummySignals):
+ # create dummy signals at the first read
+ sg = SignalGenerator(float)
+ sr = sample_rate[self.settings.Rate]
+ numchannels = eegcount+auxcount
+ '''
+ t, self.DummySignals = sg.GetSineWaveBuffers(numchannels,
+ 5.0, sr/40/numchannels ,
+ 100.0, 10.0,
+ sr)
+ '''
+ t, self.DummySignals = sg.GetSineWaveBuffers(numchannels,
+ [1.0, 2.0, 3.7, 5.0, 10.0, 17.2, 20.0, 50.0, 100.0, 200.0], 1.0,
+ 100.0, 0.0,
+ sr)
+ # replace eeg with generated signals
+ sc32 = np.array(sct[0], dtype=int)
+ for c in range(len(eeg)):
+ eeg[c] = np.take(self.DummySignals[c], sc32, mode="wrap")
+
+ # write trigger every 10s
+ tr = sample_rate[self.settings.Rate] * 10
+ trIdx = np.nonzero((sc32 % tr) < 3)[0]
+ trg[0] = 0
+ if trIdx.size:
+ trg[0,trIdx] = 1
+
+ d = []
+ d.append(eeg)
+ d.append(trg)
+ d.append(sct)
+ return d, disconnected
+
+ def readImpedances(self):
+ ''' Get the electrode impedance values
+ @return: list of impedance values for all EEG channels plus ground electrode in Ohm.
+ '''
+ if not self.running or (self.devicehandle == 0):
+ return None, None
+
+ disconnected = None
+ # read impedance data from device
+ err = self.lib.champImpedanceGetData(self.devicehandle,
+ ctypes.byref(self.impbuffer),
+ len(self.impbuffer))
+
+ # dummy read data from device
+ err2 = self.lib.champGetData(self.devicehandle,
+ ctypes.byref(self.buffer),
+ len(self.buffer))
+
+ if err2 == CHAMP_ERR_MONITORING:
+ disconnected = CHAMP_ERR_MONITORING
+
+ if err == CHAMP_ERR_FAIL:
+ return None, None
+
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to read impedance values", err)
+
+ # channel order in buffer is CH1,CH2..CHn, GND
+ items = self.properties.CountEeg + 1
+ return np.fromstring(self.impbuffer, np.uint32, items), disconnected
+
+ def setImpedanceRange(self, good, bad):
+ ''' set ActiCap impedance range
+ @param good: impedance value for green LED in Ohm
+ @param bad: impedance value for red LED in Ohm
+ '''
+ if self.devicehandle == 0:
+ return
+ imp_settings = CHAMP_IMPEDANCE_SETUP()
+ imp_settings.Good = int(good)
+ imp_settings.Bad = int(bad)
+ imp_settings.LedsDisable = 0
+ imp_settings.TimeOut = 5
+ err = self.lib.champImpedanceSetSetup(self.devicehandle, ctypes.byref(imp_settings))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set LED impedance range", err)
+
+ def setTrigger(self, trigger):
+ ''' set trigger output
+ @param trigger: trigger values to set 8-bit outputs (bits 0 - 7).
+ '''
+ if self.devicehandle == 0:
+ return
+
+ # 8-bit inputs (bits 0 - 7) + 8-bit outputs (bits 8 - 15) + 16 MSB reserved bits.
+ trigger = (trigger & 0xFF) << 8
+ ct_trigger = ctypes.c_uint(trigger)
+ err = self.lib.champSetTriggers(self.devicehandle, ct_trigger)
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set trigger output", err)
+
+ def getEmulationMode(self):
+ ''' Lookup emulation and PLL configuration flag in INI file
+ @return: number of modules if in emulation mode, else 0
+ '''
+ emulation = 0
+ modules = 0
+ try:
+ ini = configparser.ConfigParser()
+ if self.x64:
+ filename = "ActiChamp_x64.dll.ini"
+ else:
+ filename = "ActiChamp_x86.dll.ini"
+
+ if len(ini.read(filename)) > 0:
+ emulation = ini.getint("Main", "Emulation")
+ if emulation != 0:
+ modules = ini.getint("Emulation", "Model") / 32
+ try:
+ self.enablePllConfiguration = (ini.getint("Main", "EnablePllConfiguration") != 0)
+ except:
+ self.enablePllConfiguration = False
+ except:
+ modules = 0
+ self.EmulationMode = (modules > 0)
+ return modules
+
+ def setEmulationMode(self, modules):
+ ''' Set/Reset emulation flag in INI file
+ @param modules: number of modules to emulate, 0= no emulation
+ '''
+ # not possible if device is already open
+ if self.devicehandle != 0:
+ return
+
+ # write new settings to INI file
+ ini = configparser.ConfigParser()
+ if self.x64:
+ filename = "ActiChamp_x64.dll.ini"
+ else:
+ filename = "ActiChamp_x86.dll.ini"
+
+ if len(ini.read(filename)) > 0:
+ if modules > 0:
+ channels = modules * 32
+ ini.set("Main", "Emulation", "1")
+ ini.set("Emulation", "Model", repr(channels))
+ else:
+ ini.set("Main", "Emulation", "0")
+ fp = open(filename, "w")
+ ini.write(fp)
+ fp.close()
+ else:
+ raise AmpError("INI file %s not found"%(filename))
+
+ # reload the DLL
+ self.loadLib()
+ # get new configuration
+ try:
+ self.open()
+ self.getDeviceInfo()
+ self.setup(self.settings.Mode, self.settings.Rate, self.binning)
+ except:
+ pass
+
+ try:
+ self.close()
+ except:
+ pass
+
+
+ def readConfiguration(self, rate, force=False):
+ ''' Update device sampling rate and get new configuration
+ @param rate: device base sampling rate
+ '''
+ # not possible if device is already open or not necessary if rate has not changed
+ if (self.devicehandle != 0 or rate == self.settings.Rate) and not force:
+ return
+ # update sampling rate and get new configuration
+ try:
+ self.open()
+ self.setup(self.settings.Mode, rate, self.binning)
+ except:
+ pass
+
+ try:
+ self.close()
+ except:
+ pass
+
+
+ def getDeviceStatus(self):
+ ''' Read status values from device
+ @return: total samples, total errors, data rate and data speed as tuple
+ '''
+ if self.devicehandle == 0:
+ return 0, 0, 0, 0
+ status = CHAMP_DATA_STATUS()
+ err = self.lib.champGetDataStatus(self.devicehandle, ctypes.byref(status))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to read device status", err)
+ return status.Samples, status.Errors, status.Rate, status.Speed
+
+
+ def getSamplingRateBase(self, samplingrate):
+ ''' Get base sampling rate ID and divider for the requested sampling rate
+ @param samplingrate: requested sampling rate in Hz
+ @return: base rate ID (-1 if not possible) and base rate divider
+ '''
+ mindiv = 100000
+ base = -1
+ div = 1
+ for sr in sample_rate:
+ div = sample_rate[sr] / samplingrate
+ if int(div) == div:
+ if div < mindiv:
+ mindiv = div
+ base = sr
+ if base >= 0:
+ div = int(sample_rate[base] / samplingrate)
+ return base, div
+
+ def getDeviceInfo(self):
+ ''' Read ID, serial number and production date from device and all connected modules
+ '''
+ if self.devicehandle == 0 or self.running or self.getEmulationMode() != 0:
+ return
+
+ # reset the info structure
+ self.deviceinfo = CHAMP_DEVICE_INFO()
+
+ # power up device
+ self.setup(CHAMP_MODE_NORMAL, CHAMP_RATE_10KHZ, 1)
+ self.start()
+
+ # read the amplifier extended versions to get the carrier board FPGA version also
+ self.ampversion.readext(self.lib, self.devicehandle)
+
+ # get infos from device
+ module = -1
+ for info in self.deviceinfo:
+ # get device info
+ if module == -1:
+ self.lib.champFactoryDeviceProductionGet(self.devicehandle, ctypes.byref(info))
+ else:
+ self.lib.champFactoryModuleProductionGet(self.devicehandle, module, ctypes.byref(info))
+ module += 1
+
+ # power down device
+ self.stop()
+
+ def getDeviceInfoString(self):
+ ''' Return device info as string
+ '''
+ emulation = self.getEmulationMode()
+ if emulation != 0:
+ info = "actiCHamp Simulation Mode, %i Module(s)\n"%(emulation)
+ else:
+ info = ""
+ for n in range(0, len(self.deviceinfo)):
+ if n == 0:
+ info += "actiCHamp "
+ else:
+ info += "Module %i "%(n)
+ if self.deviceinfo[n].Date.wYear == 0:
+ info += "n.a."
+ else:
+ info += "(%i) SN: %08i"%(self.deviceinfo[n].Model,
+ self.deviceinfo[n].SerialNumber)
+ if n == 0:
+ info += " Rev. %i"%self.ampversion.revision()
+ info += "\n"
+ # get firmware versions
+ info += self.ampversion.info() + "\n"
+ return info
+
+ def getBatteryVoltage(self):
+ ''' Read the amplifier battery voltages
+ @return: state (0=ok, 1=critical, 2=bad) and voltage
+ '''
+ faultyVoltages = []
+ voltages = CHAMP_VOLTAGES()
+ #voltages.VDC = 0.0
+ if self.devicehandle == 0:
+ return 0, voltages, faultyVoltages
+
+ # get amplifier voltages
+ err = self.lib.champGetVoltages(self.devicehandle, ctypes.byref(voltages))
+ if err != CHAMP_ERR_OK:
+ time.sleep(0.005)
+ err = self.lib.champGetVoltages(self.devicehandle, ctypes.byref(voltages))
+ if err != CHAMP_ERR_OK:
+ if self.running:
+ return 2, voltages, faultyVoltages
+ else:
+ return 0, voltages, faultyVoltages
+
+ # check battery voltage
+ state = 0
+ if voltages.VDC < 5.6:
+ state = 1
+ if voltages.VDC < 5.3:
+ state = 2
+ # check other voltages
+ # 'V3' Internal 3.3, [V]
+ # 'DVDD3' Digital 3.3, [V]
+ # 'AVDD3' Analog 3.3, [V]
+ # 'AVDD5' Analog 5.0, [V]
+ # 'REF' Reference 2.048, [V]
+ if self.running:
+ targets = [("V3", 3.3), ("DVDD3", 3.3), ("AVDD3", 3.3), ("AVDD5", 5.0), ("REF", 2.048)]
+ if self.deviceinfo[0].SerialNumber == 11020001:
+ targets = [("V3", 3.3), ("DVDD3", 2.2), ("AVDD3", 3.3), ("AVDD5", 5.0), ("REF", 2.048)]
+ for idx, target in targets:
+ u = getattr(voltages,idx)
+ if u < 0.9 * target or u > 1.1 * target:
+ faultyVoltages.append("%s=%.1fV"%(idx, u))
+
+ return state, voltages, faultyVoltages
+
+ def setButtonLed(self, period, dutyCycle):
+ ''' Control MyButton LED via pulse-width modulation
+ @param period: cycle period in [ms]
+ @param dutyCycle: duty cycle in [%], 0%=always off, 100%=always on
+ '''
+ if self.devicehandle == 0:
+ return
+ dutyCycle = max(min(dutyCycle,100),0) # limit to 0-100%
+ period = max(min(period,10000),1) # limit to 1-10000ms
+ # use a fixed period for on/off
+ if dutyCycle == 0 or dutyCycle == 100:
+ period = 10
+ # convert to C variables
+ cPeriod = ctypes.c_uint(period)
+ cDutyCycle = ctypes.c_uint(dutyCycle)
+ # set LED
+ err = self.lib.champSetMyButtonLed(self.devicehandle, cPeriod, cDutyCycle)
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set MyButton LED", err)
+
+ def LedTest(self, step):
+ ''' Toggle active electrode LEDs
+ @param step: 0 = switch off all electrode LEDs, reset index
+ 1 = set next electrode to green
+ 2 = set next electrode to red
+ 11 = set all electrodes to green
+ 12 = set all electrodes to red
+ @return: TRUE if last electrode index reached
+ '''
+ ledcount = self.properties.CountEeg + 1
+ led_array = (ctypes.c_int * ledcount)()
+ led_array[:] = [0]*len(led_array)
+ if step == 0:
+ self.LED_index = 0
+ self.lib.champSetElectrodes(self.devicehandle, None, 0)
+ return True
+ elif step == 1:
+ led_array[self.LED_index] = 1
+ self.LED_index += 1
+ elif step == 2:
+ led_array[self.LED_index] = 2
+ self.LED_index += 1
+ elif step == 11:
+ led_array[:] = [1]*len(led_array)
+ self.LED_index = 0
+ elif step == 12:
+ led_array[:] = [2]*len(led_array)
+ self.LED_index = 0
+ err = self.lib.champSetElectrodes(self.devicehandle, led_array, ctypes.sizeof(led_array))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set electrode LEDs", err)
+ if self.LED_index >= len(led_array):
+ self.LED_index = 0
+ return self.LED_index == 0
+
+
+ def hasPllOption(self):
+ ''' The PLL option is available for Rev. 3 amplifiers only and has to be enabled in the INI file
+ '''
+ return self.enablePllConfiguration and (self.ampversion.revision() >= 3)
+
+
+ def setPllInput(self):
+ ''' Set the PLL input either to external or internal
+ '''
+ if self.devicehandle == 0 or not self.hasPllOption() or self.getEmulationMode() != 0:
+ return
+
+ PllParamters = CHAMP_PLL()
+ PllParamters.PllExternal = self.PllExternal
+ PllParamters.AdcExternal = 0
+ PllParamters.PllFrequency = 25600000
+ PllParamters.PllPhase = 0
+
+ err = self.lib.champSetPll(self.devicehandle, ctypes.byref(PllParamters))
+ if err != CHAMP_ERR_OK:
+ raise AmpError("failed to set PLL parameters\nPLL frequency: %d, Status: %d"%(PllParamters.PllFrequency, PllParamters.Status), err)
+
+
+
+'''
+------------------------------------------------------------
+Signal Generator for simulation mode
+------------------------------------------------------------
+'''
+
+class SignalGenerator():
+ def __init__(self, dtype=np.int16):
+ self.dtype = dtype
+ self.lasttime = 0
+
+ def GetSineWave(self, freq, samplerate, amplitude, time):
+ w = 2.0 * np.pi * freq
+ t = np.linspace(0, time, samplerate * time)
+ return t, np.asarray(np.sin(w*t) * amplitude, dtype=self.dtype)
+
+ def GetTriangleWave(self, freq, samplerate, amplitude, time):
+ a = 1.0/freq
+ t = np.linspace(0, time, samplerate)
+ trig = (np.abs(2*(t/a - np.floor(t/a + 0.5))) - 0.5) * amplitude * 2
+ return t, np.asarray(trig, dtype=self.dtype)
+
+ def GetSineWaveBuffers(self, NumChannels, StartFrequency, DeltaFrequency, StartAmplitude, DeltaAmplitude, SampleRate):
+ # calculate buffer sizes for n*2*PI
+ cycles = 40.0
+ if type(StartFrequency) == list:
+ fl = StartFrequency*NumChannels
+ fSin = np.array(fl[:NumChannels+1])
+ else:
+ fSin = np.arange(StartFrequency, StartFrequency + NumChannels * DeltaFrequency, DeltaFrequency)
+ if DeltaAmplitude != 0:
+ aSin = np.arange(StartAmplitude, StartAmplitude + NumChannels * DeltaAmplitude, DeltaAmplitude, dtype=self.dtype)
+ else:
+ aSin = np.zeros(NumChannels)
+ aSin[:] = StartAmplitude
+
+ Tsin = 1.0/fSin
+ NumSamples = (Tsin * SampleRate)*cycles
+ tl = list(np.linspace(0, 2.0 * np.pi * cycles, s)[:-1] for s in NumSamples)
+ signals = list(np.asarray(np.sin(t) * a, dtype=self.dtype) for t,a in zip(tl,aSin))
+ return tl, signals
+
diff --git a/amplifier.py b/amplifier.py
index 007b640..829298d 100644
--- a/amplifier.py
+++ b/amplifier.py
@@ -1,1585 +1,1581 @@
-# -*- coding: utf-8 -*-
-'''
-Acquisition Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@version: 1.0
-'''
-
-from scipy import signal
-from PyQt4 import Qt
-from modbase import *
-from actichamp_w import *
-from res import frmActiChampOnline
-from res import frmActiChampConfig
-from operator import itemgetter
-import textwrap
-from devices.devcontainer import DeviceContainer
-
-# enable active shielding mode
-AMP_SHIELD_MODE = False
-
-# allow multiple reference channels
-AMP_MULTIPLE_REF = True
-
-# hide the reference channel(s), works only without separate montage module
-AMP_HIDE_REF = True
-
-# no channel selection within amplifier module, for use with an separate montage module.
-AMP_MONTAGE = False
-
-'''
-------------------------------------------------------------
-AMPLIFIER MODULE
-------------------------------------------------------------
-'''
-
-class AMP_ActiChamp(ModuleBase):
- ''' ActiChamp EEG amplifier module
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- ModuleBase.__init__(self, name="Amplifier", **keys)
-
- # XML parameter version
- # 1: initial version
- # 2: input device container added
- # 3: PLL external input
- self.xmlVersion = 3
-
- # create hardware object
- self.amp = ActiChamp() #: amplifier hardware object
-
- # set default channel configuration
- self.max_eeg_channels = 160 #: number of EEG channels for max. HW configuration
- self.max_aux_channels = 8 #: number of AUX channels for max. HW configuration
- self.channel_config = EEG_DataBlock.get_default_properties(self.max_eeg_channels, self.max_aux_channels)
- self.recording_mode = CHAMP_MODE_NORMAL
-
- # create dictionary of possible sampling rates
- self.sample_rates = []
- for rate in [100000.0, 50000.0, 25000.0, 10000.0, 5000.0, 2000.0, 1000.0, 500.0, 200.0]:
- base, div = self.amp.getSamplingRateBase(rate)
- if base >= 0:
- self.sample_rates.append({'rate':str(int(rate)), 'base':base, 'div':div, 'value':rate})
-
- self.sample_rate = self.sample_rates[7]
- self.binning = self.sample_rate['div']
- self.binningoffset = 0
-
- # set default data block
- if AMP_MONTAGE:
- self._create_channel_selection()
- else:
- self._create_all_channel_selection()
-
- # create the input device container
- self.inputDevices = DeviceContainer()
-
- # date and time of acquisition start
- self.start_time = datetime.datetime.now()
-
- # create online configuration pane
- self.online_cfg = _OnlineCfgPane(self)
- self.connect(self.online_cfg, Qt.SIGNAL("modeChanged(int)"), self._online_mode_changed)
-
- # impedance interval timer
- self.impedance_timer = time.clock()
-
- # batter check interval timer and last voltage warning string
- self.battery_timer = time.clock()
- self.voltage_warning = ""
-
- # skip the first received data blocks
- self.skip_counter = 5
- self.blocking_counter = 0
-
- # reset hardware error counter and acquisition time out
- self.initialErrorCount = -1
- self.acquisitionTimeoutCounter = 0
- self.test_counter = 0
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- '''
- return self.online_cfg
-
- def get_configuration_pane(self):
- ''' Get the configuration pane if available.
- Qt widgets are not reusable, so we have to create it every time
- '''
- Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
- # read amplifier configuration
- self.amp.readConfiguration(self.sample_rate['base'], force=True)
- self.update_receivers()
- Qt.QApplication.restoreOverrideCursor()
- # create configuration pane
- if AMP_MONTAGE:
- config = _ConfigurationPane(self)
- else:
- config = _DeviceConfigurationPane(self)
- self.connect(config, Qt.SIGNAL("dataChanged()"), self._configuration_changed)
- self.connect(config, Qt.SIGNAL("emulationChanged(int)"), self._emulation_changed)
- self.connect(config, Qt.SIGNAL("rateChanged(int)"), self._samplerate_changed)
- return config
-
- def get_module_info(self):
- ''' Get information about this module for the about dialog
- @return: Serial numbers of amplifier and modules
- '''
- return self.amp.getDeviceInfoString()
-
-
- def _emulation_changed(self, index):
- ''' SIGNAL from configuration pane if emulation mode has changed
- '''
- try:
- self.amp.setEmulationMode(index)
- except Exception as e:
- self.send_exception(e)
- self.update_receivers()
-
- def _samplerate_changed(self, index):
- ''' SIGNAL from configuration pane if sample rate has changed
- '''
- Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
- self.sample_rate = self.sample_rates[index]
- self.update_receivers()
- Qt.QApplication.restoreOverrideCursor()
-
- def _configuration_changed(self):
- ''' SIGNAL from configuration pane if values has changed
- '''
- self.update_receivers()
-
- def _online_mode_changed(self, new_mode):
- ''' SIGNAL from online configuration pane if recording mode has changed
- '''
- if self.amp.running:
- if not self.stop():
- self.online_cfg.updateUI(self.recording_mode)
- return
-
- if new_mode >= 0:
- Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
- self.recording_mode = new_mode
- self.start()
- Qt.QApplication.restoreOverrideCursor()
-
- def _set_default_filter(self):
- ''' set all filter properties to HW filter values
- '''
- for channel in self.channel_config:
- channel.highpass = 0.0 # high pass off
- channel.lowpass = 0.0 # low pass off
- channel.notchfilter = False # notch filter off
-
- def _check_reference(self):
- ''' check if selected reference channels are consistent with the global flag
- '''
- # nothing to do if multiple channels are allowed
- if AMP_MULTIPLE_REF:
- return
- # else keep the first reference channel only
- eeg_ref = np.array(map(lambda x: x.isReference, self.channel_config))
- ref_index = np.nonzero(eeg_ref)[0] # indices of reference channel(s)
- for ch in self.channel_config[ref_index[1:]]:
- ch.isReference = False
-
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- emulation_mode = self.amp.getEmulationMode() > 0
- self.sample_rate = self.sample_rates[7] # 500Hz sample rate
- for channel in self.channel_config:
- channel.isReference = False
- if channel.group == ChannelGroup.EEG:
- channel.enable = True # enable all EEG channels
- if (channel.input == 1) and not emulation_mode:
- channel.isReference = True # use first channel as reference
- else:
- channel.enable = False # disable all AUX channels
- self._set_default_filter()
- self.inputDevices.reset()
- self.update_receivers()
-
- def stop(self, force=False):
- ''' Stop data acquisition
- @param force: force stop without query
- @return: True, if stop was accepted by attached modules
- '''
- # ask attached modules for acceptance
- if not force:
- if not self.query("Stop"):
- return False
- # stop it
- ModuleBase.stop(self)
- return True
-
-
- def process_event(self, event):
- ''' Handle events from attached receivers
- @param event: ModuleEvent
- '''
- # Command events
- if event.type == EventType.COMMAND:
- # check for new impedance color range values
- if event.info == "ImpColorRange":
- good, bad = event.cmd_value
-
- if self.recording_mode == CHAMP_MODE_IMPEDANCE:
- self._thLock.acquire()
- try:
- self.amp.setImpedanceRange(good * 1000, bad * 1000)
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- # check for stop command
- if event.info == "Stop":
- if event.cmd_value == "force":
- self.stop(force=True)
- else:
- self.stop()
-
- # check for recording start command
- if event.info == "StartRecording":
- self._online_mode_changed(CHAMP_MODE_NORMAL)
-
- # check for impedance start command
- if event.info == "StartImpedance":
- self._online_mode_changed(CHAMP_MODE_IMPEDANCE)
-
- # check for trigger out command
- if event.info == "TriggerOut":
- self._thLock.acquire()
- try:
- self.amp.setTrigger(event.cmd_value)
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- # check for button LED command
- # cmd_value is a tuple with period and duty cycle
- if event.info == "SetLED":
- self._thLock.acquire()
- try:
- self.amp.setButtonLed(event.cmd_value[0], event.cmd_value[1])
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- # check for acitve shield gain command
- # cmd_value is the gain from 1 to 100
- if event.info == "SetShieldGain":
- self._thLock.acquire()
- self.amp.activeShieldGain = event.cmd_value
- self._thLock.release()
-
- # Error events
- if event.type == EventType.ERROR or event.type == EventType.LOG:
- # add device status info to "sample missing" events
- if "samples missing" in event.info:
- self._thLock.acquire()
- try:
- errors = self.amp.getDeviceStatus()[1] - self.initialErrorCount
- event.info += " (device errors = %d)"%errors
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- event.info += " (%s)"%(str(e))
-
-
- def process_start(self):
- ''' Open amplifier hardware and start data acquisition
- '''
- # reset variables
- self.eeg_data.sample_counter = 0
- self.acquisitionTimeoutCounter = 0
- self.battery_timer = 0
- self.test_counter = 0
-
- # open and setup hardware
- self.amp.open()
-
- # check battery
- ok,voltage = self._check_battery()
- if not ok:
- raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
-
- self.amp.setup(self.recording_mode, self.sample_rate['base'], self.sample_rate['div'])
- self.update_receivers()
- if len(self.channel_indices) == 0:
- raise ModuleError(self._object_name, "no input channels selected!")
-
- # check battery again
- ok,voltage = self._check_battery()
- if not ok:
- raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
-
- # start hardware
- self.amp.start()
-
- # set start time on first call
- self.start_time = datetime.datetime.now()
-
- # send status info
- if AMP_MONTAGE:
- info = "Start %s at %.0fHz with %d channels"%(CHAMP_Modes[self.recording_mode],\
- self.eeg_data.sample_rate,\
- len(self.channel_indices))
- else:
- if self.amp.hasPllOption() and self.amp.PllExternal:
- info = "Start %s at %.0fHz (ext. PLL)"%(CHAMP_Modes[self.recording_mode],\
- self.eeg_data.sample_rate)
- else:
- info = "Start %s at %.0fHz"%(CHAMP_Modes[self.recording_mode],\
- self.eeg_data.sample_rate)
-
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE, info))
- # send recording mode
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = self.recording_mode,
- status_field="Mode"))
- # update button state
- self.online_cfg.updateUI(self.recording_mode)
-
- # skip the first received data blocks
- self.skip_counter = 5
- self.blocking_counter = 0
- self.initialErrorCount = -1
-
-
- def process_stop(self):
- ''' Stop data acquisition and close hardware object
- '''
- errors = 999
- try:
- errors = self.amp.getDeviceStatus()[1] - self.initialErrorCount # get number of device errors
- except:
- pass
- try:
- if self.recording_mode == CHAMP_MODE_LED_TEST:
- self.amp.LedTest(0)
- self.amp.stop()
- except:
- pass
- try:
- self.amp.close()
- except:
- pass
-
- # send status info
- info = "Stop %s"%(CHAMP_Modes[self.recording_mode])
- if (errors > 0) and (self.recording_mode != CHAMP_MODE_IMPEDANCE) and (self.recording_mode != CHAMP_MODE_LED_TEST):
- info += " (device errors = %d)"%(errors)
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE, info))
- # send recording mode
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = -1, # stop
- status_field="Mode"))
- # update button state
- self.online_cfg.updateUI(-1)
-
-
- def _create_channel_selection(self):
- ''' Create index arrays of selected channels and prepare EEG_DataBlock
- '''
- # get all active eeg channel indices (including reference channel)
- mask = lambda x: (x.group == ChannelGroup.EEG) and (x.enable | x.isReference) and (x.input <= self.amp.properties.CountEeg)
- eeg_map = np.array(map(mask, self.channel_config))
- self.eeg_indices = np.nonzero(eeg_map)[0] # indices of all eeg channels
-
- # get all active aux channel indices
- mask = lambda x: (x.group == ChannelGroup.AUX) and x.enable and (x.input <= self.amp.properties.CountAux)
- eeg_map = np.array(map(mask, self.channel_config))
- self.aux_indices = np.nonzero(eeg_map)[0] # indices of all aux channels
- self.property_indices = np.append(self.eeg_indices, self.aux_indices)
-
- # adjust AUX indices to the actual available EEG channels
- self.aux_indices -= (self.max_eeg_channels - self.amp.properties.CountEeg)
- self.channel_indices = np.append(self.eeg_indices, self.aux_indices)
-
- # create a new data block based on channel selection
- self.eeg_data = EEG_DataBlock(len(self.eeg_indices), len(self.aux_indices))
- self.eeg_data.channel_properties = copy.deepcopy(self.channel_config[self.property_indices])
- self.eeg_data.sample_rate = self.sample_rate['value']
-
- # get the reference channel indices
- #mask = lambda x: (x.group == ChannelGroup.EEG) and x.isReference and (x.input <= self.amp.properties.CountEeg)
- eeg_ref = np.array(map(lambda x: x.isReference, self.eeg_data.channel_properties))
- self.ref_index = np.nonzero(eeg_ref)[0] # indices of reference channel(s)
- if len(self.ref_index) and not AMP_MULTIPLE_REF:
- # use only the first reference channel
- self.ref_index = self.ref_index[0:1]
- idx = np.nonzero(map(lambda x: x not in self.ref_index,
- range(0, len(self.eeg_indices))
- )
- )[0]
- for prop in self.eeg_data.channel_properties[idx]:
- prop.isReference = False
-
- # append "REF" to the reference channel name and create the combined reference channel name
- refnames = []
- for prop in self.eeg_data.channel_properties[self.ref_index]:
- refnames.append(str(prop.name))
- prop.name = "REF_" + prop.name
- prop.refname = "REF"
- # global hide for all reference channels?
- if AMP_HIDE_REF:
- prop.enable = False
- if len(refnames) > 1:
- self.eeg_data.ref_channel_name = "AVG(" + "+".join(refnames) + ")"
- else:
- self.eeg_data.ref_channel_name = "".join(refnames)
-
- # remove reference channel if not in impedance mode
- self.ref_remove_index = self.ref_index
- if (self.recording_mode != CHAMP_MODE_IMPEDANCE) and len(self.ref_index):
- # set reference channel names for all other electrodes
- idx = np.nonzero(map(lambda x: x not in self.ref_index,
- range(0, len(self.eeg_indices))
- )
- )[0]
- for prop in self.eeg_data.channel_properties[idx]:
- prop.refname = "REF"
-
- '''
- # remove single reference channel
- if AMP_HIDE_REF or not self.eeg_data.channel_properties[self.ref_index[0]].enable:
- self.eeg_data.channel_properties = np.delete(self.eeg_data.channel_properties, self.ref_index, 0)
- self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_index, 0)
- '''
- # remove all disabled reference channels
- ref_dis = np.array(map(lambda x: x.isReference and not x.enable,
- self.eeg_data.channel_properties))
- self.ref_remove_index = np.nonzero(ref_dis)[0] # indices of disabled reference channels
- self.eeg_data.channel_properties = np.delete(self.eeg_data.channel_properties, self.ref_remove_index, 0)
- self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_remove_index, 0)
-
- # prepare recording mode and anti aliasing filters
- self._prepare_mode_and_filters()
-
-
- def _create_all_channel_selection(self):
- ''' Create index arrays of all available channels and prepare EEG_DataBlock
- '''
- # get all eeg channel indices
- mask = lambda x: (x.group == ChannelGroup.EEG) and (x.input <= self.amp.properties.CountEeg)
- eeg_map = np.array(map(mask, self.channel_config))
- self.eeg_indices = np.nonzero(eeg_map)[0] # indices of all eeg channels
-
- # get all aux channel indices
- mask = lambda x: (x.group == ChannelGroup.AUX) and (x.input <= self.amp.properties.CountAux)
- eeg_map = np.array(map(mask, self.channel_config))
- self.aux_indices = np.nonzero(eeg_map)[0] # indices of all aux channels
- self.property_indices = np.append(self.eeg_indices, self.aux_indices)
-
- # adjust AUX indices to the actual available EEG channels
- self.aux_indices -= (self.max_eeg_channels - self.amp.properties.CountEeg)
- self.channel_indices = np.append(self.eeg_indices, self.aux_indices)
-
- # create a new data block based on channel selection
- self.eeg_data = EEG_DataBlock(len(self.eeg_indices), len(self.aux_indices))
- self.eeg_data.channel_properties = copy.deepcopy(self.channel_config[self.property_indices])
- self.eeg_data.sample_rate = self.sample_rate['value']
-
- # reset the reference channel indices
- self.ref_index = np.array([]) # indices of reference channel(s)
- self.eeg_data.ref_channel_name = ""
- self.ref_remove_index = self.ref_index
-
- # prepare recording mode and anti aliasing filters
- self._prepare_mode_and_filters()
-
-
-
-
- def _prepare_mode_and_filters(self):
- # translate recording modes
- if (self.recording_mode == CHAMP_MODE_NORMAL) or (self.recording_mode == CHAMP_MODE_ACTIVE_SHIELD):
- self.eeg_data.recording_mode = RecordingMode.NORMAL
- elif self.recording_mode == CHAMP_MODE_IMPEDANCE:
- self.eeg_data.recording_mode = RecordingMode.IMPEDANCE
- elif self.recording_mode == CHAMP_MODE_TEST:
- self.eeg_data.recording_mode = RecordingMode.TEST
-
- # down sampling
- self.binning = self.sample_rate['div']
- self.binningoffset = 0
-
- # design anti-aliasing filter for down sampling
- # it's an Nth order lowpass Butterworth filter from scipy
- # signal.filter_design.butter(N, Wn, btype='low')
- # N = filter order, Wn = cut-off frequency / nyquist frequency
- # f_nyquist = f_in / 2
- # f_cutoff = f_in / rate_divider * filter_factor
- # Wn = f_cutoff / f_nyquist = f_in / rate_divider * filter_factor / f_in * 2
- # Wn = 1 / rate_divider * 2 * filter_factor
- filter_order = 4
- filter_factor = 0.333
- rate_divider = self.binning
- Wn = 1.0 / rate_divider * 2.0 * filter_factor
- self.aliasing_b,self.aliasing_a = signal.filter_design.butter(filter_order, Wn, btype='low')
- zi = signal.lfiltic(self.aliasing_b, self.aliasing_a, (0.0,))
- self.aliasing_zi = np.resize(zi, (len(self.channel_indices),len(zi)))
-
- # define which channels contains which impedance values
- self.eeg_data.eeg_channels[:,:] = 0
- if self.eeg_data.recording_mode == RecordingMode.IMPEDANCE:
- self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.DATA] = 1
- self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.GND] = 1
-
-
- def _check_battery(self):
- ''' Check amplifier battery voltages
- @return: state (ok=True, bad=False) and voltage
- '''
- # read battery state and internal voltages from amplifier
- state, voltages, faultyVoltages = self.amp.getBatteryVoltage()
- severe = ErrorSeverity.IGNORE
- if state == 1:
- severe = ErrorSeverity.NOTIFY
- elif state == 2:
- severe = ErrorSeverity.STOP
-
- # create and send faulty voltages warning message
- v_warning = ""
- if len(faultyVoltages) > 0:
- severe = ErrorSeverity.NOTIFY
- v_warning = "Faulty internal voltage(s): "
- for u in faultyVoltages:
- v_warning += " %s"%(u)
- # warning already sent?
- if v_warning != self.voltage_warning:
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- info = v_warning,
- severity = severe))
- self.voltage_warning = v_warning
-
- # create and send status message
- voltage_info = "%.2fV"%(voltages.VDC) # battery voltage
- for u in faultyVoltages:
- voltage_info += "\n%s"%(u)
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = voltage_info,
- severity = severe,
- status_field="Battery"))
- return state < 2, voltages.VDC
-
- def process_update(self, params):
- ''' Prepare channel properties and propagate update to all connected receivers
- '''
- # update device sampling rate and get new configuration
- try:
- self.amp.readConfiguration(self.sample_rate['base'])
- except Exception as e:
- self.send_exception(e)
- # indicate amplifier simulation
- if self.amp.getEmulationMode() > 0:
- self.online_cfg.groupBoxMode.setTitle("Amplifier SIMULATION")
- else:
- self.online_cfg.groupBoxMode.setTitle("Amplifier")
-
- # create channel selection maps
- if AMP_MONTAGE:
- self._create_channel_selection()
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = "%d ch"%(len(self.channel_indices)),
- status_field="Channels"))
- else:
- self._create_all_channel_selection()
- try:
- self.inputDevices.process_update(self.eeg_data)
- except Exception as e:
- self.send_exception(e)
-
- # send current status as event
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = "%.0f Hz"%(self.eeg_data.sample_rate),
- status_field = "Rate"))
- return copy.copy(self.eeg_data)
-
- def process_impedance(self):
- ''' Get the impedance values from amplifier
- and return the eeg data block
- '''
- # send values only once per second
- t = time.clock()
- if (t - self.impedance_timer) < 1.0:
- return None
- self.impedance_timer = t
-
- # get impedance values from device
- #imp = (np.arange(len(self.channel_indices))+1)*1000.0 # TODO: for testing only
- imp, disconnected = self.amp.readImpedances()
- # check data rate mismatch messages
- ''' suppress warning because it has no influence on the impedance measurement
- if disconnected == CHAMP_ERR_MONITORING:
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- info = "USB data rate mismatch",
- severity = ErrorSeverity.NOTIFY))
- '''
- if imp == None:
- return None
-
- eeg_imp = imp[self.eeg_indices]
- gnd_imp = imp[-1]
- self.eeg_data.impedances = eeg_imp.tolist()
- self.eeg_data.impedances.append(gnd_imp)
-
- # invalidate the old impedance data list
- self.eeg_data.impedances = []
-
- # copy impedance values to data array
- self.eeg_data.eeg_channels = np.zeros((len(self.channel_indices), 10), 'd')
- self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.DATA] = eeg_imp
- self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.GND] = gnd_imp
-
- # dummy values for trigger and sample counter
- self.eeg_data.trigger_channel = np.zeros((1, 10), np.uint32)
- self.eeg_data.sample_channel = np.zeros((1, 10), np.uint32)
-
- # process connected input devices
- if not AMP_MONTAGE:
- self.inputDevices.process_input(self.eeg_data)
-
- # set recording time
- self.eeg_data.block_time = datetime.datetime.now()
-
- # put it into the receiver queues
- eeg = copy.copy(self.eeg_data)
- return eeg
-
- def process_led_test(self):
- ''' toggle LEDs on active electrodes
- and return no eeg data
- '''
- # dummy read data
- d, disconnected = self.amp.read(self.channel_indices, len(self.eeg_indices), len(self.aux_indices))
-
- # toggle LEDs twice per second
- t = time.clock()
- if (t - self.impedance_timer) < 0.5:
- return None
- self.impedance_timer = t
-
- # toggle all LEDs between off, green and red
- if self.test_counter % 3 == 0:
- self.amp.LedTest(0)
- elif self.test_counter % 3 == 1:
- self.amp.LedTest(11)
- else:
- self.amp.LedTest(12)
-
- self.test_counter += 1
- return None
-
- def process_output(self):
- ''' Get data from amplifier
- and return the eeg data block
- '''
- t = time.clock()
- self.eeg_data.performance_timer = 0
- self.eeg_data.performance_timer_max = 0
- self.recordtime = 0.0
-
- # check battery voltage every 5s
- if (t - self.battery_timer) > 5.0 or self.battery_timer == 0:
- ok,voltage = self._check_battery()
- if not ok:
- raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
- self.battery_timer = t
-
- if self.recording_mode == CHAMP_MODE_IMPEDANCE:
- return self.process_impedance()
-
- if self.recording_mode == CHAMP_MODE_LED_TEST:
- return self.process_led_test()
-
- if self.amp.BlockingMode:
- self._thLock.release()
- try:
- d, disconnected = self.amp.read(self.channel_indices,
- len(self.eeg_indices), len(self.aux_indices))
- finally:
- self._thLock.acquire()
- self.output_timer = time.clock()
- else:
- d, disconnected = self.amp.read(self.channel_indices,
- len(self.eeg_indices), len(self.aux_indices))
-
- if d == None:
- self.acquisitionTimeoutCounter += 1
- # about 5s timeout
- if self.acquisitionTimeoutCounter > 100:
- self.acquisitionTimeoutCounter = 0
- raise ModuleError(self._object_name, "connection to hardware is broken!")
- # check data rate mismatch messages
- if disconnected == CHAMP_ERR_MONITORING:
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- info = "USB data rate mismatch",
- severity = ErrorSeverity.NOTIFY))
- return None
- else:
- self.acquisitionTimeoutCounter = 0
-
-
- # skip the first received data blocks
- if self.skip_counter > 0:
- self.skip_counter -= 1
- return None
- # get the initial error counter
- if self.initialErrorCount < 0:
- self.initialErrorCount = self.amp.getDeviceStatus()[1]
-
-
- # down sample required?
- if self.binning > 1:
- # anti-aliasing filter
- filtered ,self.aliasing_zi = \
- signal.lfilter(self.aliasing_b, self.aliasing_a, d[0], zi=self.aliasing_zi)
- # reduce reslution to avoid limit cycle
- # self.aliasing_zi = np.asfarray(self.aliasing_zi, np.float32)
-
- self.eeg_data.eeg_channels = filtered[:, self.binningoffset::self.binning]
- self.eeg_data.trigger_channel = np.bitwise_or.reduce(d[1][:].reshape(-1, self.binning), axis=1).reshape(1,-1)
- self.eeg_data.sample_channel = d[2][:, self.binningoffset::self.binning] / self.binning
- self.eeg_data.sample_counter += self.eeg_data.sample_channel.shape[1]
- else:
- self.eeg_data.eeg_channels = d[0]
- self.eeg_data.trigger_channel = d[1]
- self.eeg_data.sample_channel = d[2]
- self.eeg_data.sample_counter += self.eeg_data.sample_channel.shape[1]
-
- # average, subtract and remove the reference channels
- if len(self.ref_index):
- '''
- # subtract
- for ref_channel in self.eeg_data.eeg_channels[self.ref_index]:
- self.eeg_data.eeg_channels[:len(self.eeg_indices)] -= ref_channel
- # restore reference channel
- self.eeg_data.eeg_channels[self.ref_index[0]] = ref_channel
-
- # remove single reference channel if not enabled
- if not (len(self.eeg_data.channel_properties) > self.ref_index[0] and
- self.eeg_data.channel_properties[self.ref_index[0]].isReference ):
- self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_index, 0)
- '''
- # average reference channels
- reference = np.mean(self.eeg_data.eeg_channels[self.ref_index], 0)
-
- # subtract reference
- self.eeg_data.eeg_channels[:len(self.eeg_indices)] -= reference
-
- # remove all disabled reference channels
- if len(self.ref_remove_index) > 0:
- self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_remove_index, 0)
-
-
- # calculate date and time for the first sample of this block in s
- sampletime = self.eeg_data.sample_channel[0][0] / self.eeg_data.sample_rate
- self.eeg_data.block_time = self.start_time + datetime.timedelta(seconds=sampletime)
-
- # process connected input devices
- if not AMP_MONTAGE:
- self.inputDevices.process_input(self.eeg_data)
-
- # put it into the receiver queues
- eeg = copy.copy(self.eeg_data)
-
- self.recordtime = time.clock() - t
-
- return eeg
-
- def process_idle(self):
- ''' Check if record time exceeds 200ms over a period of 10 blocks
- and adjust idle time to record time
- '''
- if self.recordtime > 0.2:
- self.blocking_counter += 1
- # drop blocks if exceeded
- if self.blocking_counter > 10:
- self.skip_counter = 10
- self.blocking_counter = 0
- else:
- self.blocking_counter = 0
-
- # adjust idle time to record time
- idletime = max(0.06-self.recordtime, 0.02)
-
- if self.amp.BlockingMode:
- time.sleep(0.001)
- else:
- time.sleep(idletime) # suspend the worker thread for 60ms
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
-
-
- ...
-
- 1000
-
- '''
- E = objectify.E
-
- channels = E.channels()
- if AMP_MONTAGE:
- for channel in self.channel_config:
- channels.append(channel.getXML())
-
- # input device container
- devices = E.InputDeviceContainer(self.inputDevices.getXML())
-
- amplifier = E.AMP_ActiChamp(E.samplerate(self.sample_rate['value']),
- E.pllexternal(self.amp.PllExternal),
- channels,
- devices,
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="amplifier")
- return amplifier
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # set default values in case we get no configuration data
- self.inputDevices.reset()
-
- # search my configuration data
- amps = xml.xpath("//AMP_ActiChamp[@module='amplifier' and @instance='%i']"%(self._instance) )
- if len(amps) == 0:
- return # configuration data not found, leave everything unchanged
-
- cfg = amps[0] # we should have only one amplifier instance from this type
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- # setup channel configuration from xml
- for idx, channel in enumerate(cfg.channels.iterchildren()):
- self.channel_config[idx].setXML(channel)
- # reset filter properties to default values (because configuration has been moved to filter module)
- self._set_default_filter()
- # validate reference channel selection
- self._check_reference()
- # set closest matching sample rate
- sr = cfg.samplerate.pyval
- for rate in sorted(self.sample_rates, key=itemgetter('value')):
- if rate["value"] >= sr:
- self.sample_rate = rate
- break
- if version >= 2:
- # setup the input device configuration
- self.inputDevices.setXML(cfg.InputDeviceContainer)
- else:
- self.inputDevices.reset()
- if version >= 3:
- self.amp.PllExternal = cfg.pllexternal.pyval
- else:
- self.amp.PllExternal = 0
-
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
-
-
-
-'''
-------------------------------------------------------------
-AMPLIFIER MODULE ONLINE GUI
-------------------------------------------------------------
-'''
-
-class _OnlineCfgPane(Qt.QFrame, frmActiChampOnline.Ui_frmActiChampOnline):
- ''' ActiChamp online configuration pane
- '''
- def __init__(self, amp, *args):
- ''' Constructor
- @param amp: parent module object
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
- self.amp = amp
-
- # set default values
- self.pushButtonStop.setChecked(True)
-
- # re-assign the shielding button
- if not AMP_SHIELD_MODE:
- self.pushButtonStartShielding.setText("Electrode LED\nTest")
-
- # actions
- self.connect(self.pushButtonStartDefault, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
- self.connect(self.pushButtonStartImpedance, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
- self.connect(self.pushButtonStartShielding, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
- self.connect(self.pushButtonStartTest, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
- self.connect(self.pushButtonStop, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
-
- def _button_toggle(self, checked):
- ''' SIGNAL if one of the push buttons is clicked
- '''
- if checked:
- mode = -1 #stop
- if self.pushButtonStartDefault.isChecked():
- mode = CHAMP_MODE_NORMAL
- elif self.pushButtonStartShielding.isChecked():
- if AMP_SHIELD_MODE:
- mode = CHAMP_MODE_ACTIVE_SHIELD
- else:
- mode = CHAMP_MODE_LED_TEST
- elif self.pushButtonStartImpedance.isChecked():
- mode = CHAMP_MODE_IMPEDANCE
- elif self.pushButtonStartTest.isChecked():
- mode = CHAMP_MODE_TEST
- self.emit(Qt.SIGNAL('modeChanged(int)'), mode)
-
- def updateUI(self, mode):
- ''' Update user interface according to recording mode
- '''
- if mode == CHAMP_MODE_NORMAL:
- self.pushButtonStartDefault.setChecked(True)
- elif mode == CHAMP_MODE_ACTIVE_SHIELD or mode == CHAMP_MODE_LED_TEST:
- self.pushButtonStartShielding.setChecked(True)
- elif mode == CHAMP_MODE_IMPEDANCE:
- self.pushButtonStartImpedance.setChecked(True)
- elif mode == CHAMP_MODE_TEST:
- self.pushButtonStartTest.setChecked(True)
- else:
- self.pushButtonStop.setChecked(True)
-
-
-'''
------------------------------------------------------------------
-AMPLIFIER MODULE CONFIGURATION GUI (with input device selection)
------------------------------------------------------------------
-'''
-
-class _DeviceConfigurationPane(Qt.QFrame):
- ''' ActiChamp configuration pane
- '''
- def __init__(self, amplifier, *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # reference to our parent module
- self.amplifier = amplifier
-
- # Set tab name
- self.setWindowTitle("Amplifier")
-
- # make it nice
- self.setFrameShape(Qt.QFrame.StyledPanel)
- self.setFrameShadow(Qt.QFrame.Raised)
-
- # base layout
- self.gridLayout = Qt.QGridLayout(self)
-
- # spacers
- self.vspacer_1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
- self.vspacer_2 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
- self.vspacer_3 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
- self.hspacer_1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Minimum)
-
- # create the amplifier GUI elements
- self.comboBoxSampleRate = Qt.QComboBox()
- self.comboBoxEmulation = Qt.QComboBox()
- self.label_Simulated = Qt.QLabel()
-
- self.labelPLL = Qt.QLabel("PLL Input")
- self.radioPllInternal = Qt.QRadioButton("Internal")
- self.radioPllExternal = Qt.QRadioButton("External")
-
- self.label_AvailableChannels = Qt.QLabel("Available channels: 32 EEG and 5 AUX")
- self.label_AvailableChannels.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Minimum)
- #self.label_AvailableChannels.setIndent(20)
- font = Qt.QFont("Ms Shell Dlg 2", 10)
- self.label_AvailableChannels.setFont(font)
-
- self.label_1 = Qt.QLabel("Sampling Rate")
- self.label_2 = Qt.QLabel("[Hz]")
- self.label_3 = Qt.QLabel("Simulation")
- self.label_4 = Qt.QLabel("Module(s)")
-
- # group amplifier elements
- self.groupAmplifier = Qt.QGroupBox("Amplifier Configuration")
-
- self.gridLayoutAmp = Qt.QGridLayout()
- self.gridLayoutAmp.addWidget(self.label_1, 0, 0)
- self.gridLayoutAmp.addWidget(self.comboBoxSampleRate, 0, 1)
- self.gridLayoutAmp.addWidget(self.label_2, 0, 2)
- self.gridLayoutAmp.addWidget(self.label_3, 1, 0)
- self.gridLayoutAmp.addWidget(self.comboBoxEmulation, 1, 1)
- self.gridLayoutAmp.addWidget(self.label_4, 1, 2)
- self.gridLayoutAmp.addWidget(self.label_Simulated, 2, 1, 1, 2)
-
- self.gridLayoutAmp.addWidget(self.labelPLL, 3, 0)
- self.gridLayoutAmp.addWidget(self.radioPllInternal, 3, 1)
- self.gridLayoutAmp.addWidget(self.radioPllExternal, 4, 1)
- self.gridLayoutAmp.addItem(self.vspacer_1, 5, 0, 1, 3)
-
- self.gridLayoutAmpGroup = Qt.QGridLayout()
- self.gridLayoutAmpGroup.addLayout(self.gridLayoutAmp, 0, 0, 2, 1)
- self.gridLayoutAmpGroup.addItem(self.hspacer_1, 0, 1)
- self.gridLayoutAmpGroup.addWidget(self.label_AvailableChannels, 0, 2)
- self.gridLayoutAmpGroup.addItem(self.vspacer_2, 1, 1)
-
- self.groupAmplifier.setLayout(self.gridLayoutAmpGroup)
-
- # get the device configuration widget
- self.device_cfg = self.amplifier.inputDevices.get_configuration_widget()
-
- # group device elements
- self.groupDevices = Qt.QGroupBox("Optional Input Devices")
- self.gridLayoutDeviceGroup = Qt.QGridLayout()
- self.gridLayoutDeviceGroup.addWidget(self.device_cfg, 0, 0)
- self.groupDevices.setLayout(self.gridLayoutDeviceGroup)
-
- # add all items to the main layout
- self.gridLayout.addWidget(self.groupAmplifier, 0, 0)
- self.gridLayout.addWidget(self.groupDevices, 1, 0)
-
- # actions
- self.connect(self.comboBoxSampleRate, Qt.SIGNAL("currentIndexChanged(int)"), self._samplerate_changed)
- self.connect(self.comboBoxEmulation, Qt.SIGNAL("currentIndexChanged(int)"), self._emulationChanged)
- self.connect(self.amplifier.inputDevices, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
-
- # emulation combobox
- self.comboBoxEmulation.addItems(["off", "1", "2", "3", "4", "5"])
- self.comboBoxEmulation.setCurrentIndex(self.amplifier.amp.getEmulationMode())
-
- # sample rate combobox
- sr_index = -1
- for sr in self.amplifier.sample_rates:
- self.comboBoxSampleRate.addItem(sr['rate'])
- if sr == self.amplifier.sample_rate:
- sr_index = self.comboBoxSampleRate.count()-1
- self.comboBoxSampleRate.setCurrentIndex(sr_index)
-
- # available channels display
- self._updateAvailableChannels()
-
- # PLL configuration
- self.radioPllExternal.setChecked(self.amplifier.amp.PllExternal != 0)
- self.radioPllInternal.setChecked(self.amplifier.amp.PllExternal == 0)
- self.showPllParams(self.amplifier.amp.hasPllOption())
- self.connect(self.radioPllExternal, Qt.SIGNAL("toggled(bool)"), self._pllExternalToggled)
-
- def _samplerate_changed(self, index):
- ''' SIGNAL sample rate combobox value has changed
- '''
- if index >= 0:
- # notify parent about changes
- self.emit(Qt.SIGNAL('rateChanged(int)'), index)
- self._updateAvailableChannels()
-
- def _emulationChanged(self, index):
- ''' SIGNAL emulation mode combobox value has changed
- '''
- if index >= 0:
- # notify parent about changes
- self.emit(Qt.SIGNAL('emulationChanged(int)'),index)
- # simulated channels
- if index > 0:
- self.label_Simulated.setText("simulating %i + 8 channels"%(index * 32))
- else:
- self.label_Simulated.setText("")
- self._updateAvailableChannels()
-
- def _configurationDataChanged(self):
- self.emit(Qt.SIGNAL('dataChanged()'))
-
- def _updateAvailableChannels(self):
- eeg = self.amplifier.amp.properties.CountEeg
- aux = self.amplifier.amp.properties.CountAux
- if self.amplifier.amp.getEmulationMode() == 0:
- amp = "actiCHamp"
- else:
- amp = "Simulation"
- self.label_AvailableChannels.setText("Amplifier: %s\n\nAvailable channels: %d EEG and %d AUX"%(amp, eeg, aux))
-
-
- def showEvent(self, event):
- pass
-
- def showPllParams(self, show):
- self.labelPLL.setVisible(show)
- self.radioPllExternal.setVisible(show)
- self.radioPllInternal.setVisible(show)
-
- def _pllExternalToggled(self, checked):
- if checked:
- self.amplifier.amp.PllExternal = 1
- else:
- self.amplifier.amp.PllExternal = 0
-
-
-
-
-'''
-------------------------------------------------------------
-AMPLIFIER MODULE CONFIGURATION GUI (with channel selection)
-------------------------------------------------------------
-'''
-
-class _ConfigurationPane(Qt.QFrame, frmActiChampConfig.Ui_frmActiChampConfig):
- ''' ActiChamp configuration pane
- '''
- def __init__(self, amplifier, *args):
- ''' Constructor
- @param amplifier: parent module object
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
- self.tableViewChannels.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
- self.tableViewAux.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
-
- # setup content
- self.amplifier = amplifier
-
- # emulation combobox
- self.comboBoxEmulation.setCurrentIndex(self.amplifier.amp.getEmulationMode())
-
- # channel tables
- self._fillChannelTables()
-
- # sample rate combobox
- sr_index = -1
- for sr in self.amplifier.sample_rates:
- self.comboBoxSampleRate.addItem(sr['rate'])
- if sr == self.amplifier.sample_rate:
- sr_index = self.comboBoxSampleRate.count()-1
- self.comboBoxSampleRate.setCurrentIndex(sr_index)
-
- # reference channel display
- self.show_reference()
-
- # actions
- self.connect(self.comboBoxSampleRate, Qt.SIGNAL("currentIndexChanged(int)"), self._samplerate_changed)
- self.connect(self.tableViewAux.selectionModel(), Qt.SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self._selectionChanged)
- self.connect(self.comboBoxEmulation, Qt.SIGNAL("currentIndexChanged(int)"), self._emulationChanged)
-
- def _fillChannelTables(self):
- ''' Create and fill channel tables
- '''
- # EEG channel table, show available channels only
- mask = lambda x: (x.group == ChannelGroup.EEG) & (x.input <= self.amplifier.amp.properties.CountEeg)
- ch_map = np.array(map(mask, self.amplifier.channel_config))
- ch_indices = np.nonzero(ch_map)[0]
- self.eeg_model = _ConfigTableModel(self.amplifier.channel_config[ch_indices])
- self.tableViewChannels.setModel(self.eeg_model)
- self.tableViewChannels.setItemDelegate(_ConfigItemDelegate())
- self.tableViewChannels.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
-
- # AUX channel table, show available channels only
- mask = lambda x: (x.group == ChannelGroup.AUX) & (x.input <= self.amplifier.amp.properties.CountAux)
- #mask = lambda x: x.group == ChannelGroup.AUX
- ch_map = np.array(map(mask, self.amplifier.channel_config))
- ch_indices = np.nonzero(ch_map)[0]
- self.aux_model = _ConfigTableModel(self.amplifier.channel_config[ch_indices])
- self.tableViewAux.setModel(self.aux_model)
- self.tableViewAux.setItemDelegate(_ConfigItemDelegate())
- self.tableViewAux.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
-
- # simulated channels
- simulated_channels = self.comboBoxEmulation.currentIndex()
- if simulated_channels > 0:
- self.label_Simulated.setText("(simulating %i + 8 channels)"%(simulated_channels * 32))
- else:
- self.label_Simulated.setText("")
-
- # actions
- self.connect(self.eeg_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
- self.connect(self.aux_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
-
- def _channeltable_changed(self, topLeft, bottomRight):
- ''' SIGNAL data in channel table has changed
- '''
- # update reference channel display
- self.show_reference()
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
-
- def _samplerate_changed(self, index):
- ''' SIGNAL sample rate combobox value has changed
- '''
- if index >= 0:
- # notify parent about changes
- self.emit(Qt.SIGNAL('rateChanged(int)'), index)
- self._fillChannelTables()
- self.show_reference()
-
- def _emulationChanged(self, index):
- ''' SIGNAL emulation mode combobox value has changed
- '''
- if index >= 0:
- # notify parent about changes
- self.emit(Qt.SIGNAL('emulationChanged(int)'),index)
- self._fillChannelTables()
- self.show_reference()
-
- def _selectionChanged(self, selected, deselected):
- #print selected.indexes()
- pass
-
- def show_reference(self):
- ''' Display selected reference channel
- '''
- # get the selected reference channel index
- mask = lambda x: x.isReference & (x.group == ChannelGroup.EEG) & (x.input <= self.amplifier.amp.properties.CountEeg)
- ref = np.array(map(mask, self.amplifier.channel_config))
- ref_index = np.nonzero(ref)[0]
- # update display
- '''
- if len(ref_index) == 1:
- self.label_Reference.setText("Selected Reference Channel\r\nCh%d -> %s"%
- (self.amplifier.channel_config[ref_index[0]].input,
- self.amplifier.channel_config[ref_index[0]].name))
- '''
- if len(ref_index) > 0:
- labelText = "Selected Reference Channel(s)\r\n"
- chText = []
- for idx in ref_index[:10]:
- chText.append(u"%s"%(self.amplifier.channel_config[idx].name))
- if len(ref_index) > 10:
- chText.append("...")
- labelText += textwrap.fill(" + ".join(chText), 30)
- self.label_Reference.setText(labelText)
- else:
- self.label_Reference.setText("Selected Reference Channel\r\nNone")
-
-
-
-
-
-
-class _ConfigTableModel(Qt.QAbstractTableModel):
- ''' EEG and AUX table data model for the configuration pane
- '''
- def __init__(self, data, parent=None, *args):
- ''' Constructor
- @param data: array of EEG_ChannelProperties objects
- '''
- Qt.QAbstractTableModel.__init__(self, parent, *args)
- self.arraydata = data
- # column description
- self.columns = [{'property':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
- {'property':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
- #{'property':'lowpass', 'header':'High Cutoff', 'edit':False, 'editor':'combobox'},
- #{'property':'highpass', 'header':'Low Cutoff', 'edit':False, 'editor':'combobox'},
- #{'property':'notchfilter', 'header':'Notch', 'edit':False, 'editor':'default'},
- {'property':'name', 'header':'Name', 'edit':True, 'editor':'default'},
- ]
-
- # insert reference channel selection to column description for EEG channels
- if (len(data) > 0) and (data[0].group == ChannelGroup.EEG):
- self.columns.insert(2,
- {'property':'isReference', 'header':'Reference', 'edit':True, 'editor':'default'}
- )
-
- # combo box list contents
- self.lowpasslist = ['off', '10', '20', '50', '100', '200', '500', '1000', '2000']
- self.highpasslist = ['off','0.01', '0.02', '0.05', '0.1', '0.2', '0.5', '1', '2', '5', '10']
-
- def _getitem(self, row, column):
- ''' Get amplifier property item based on table row and column
- @param row: row number
- @param column: column number
- @return: QVariant property value
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return Qt.QVariant()
-
- # get channel properties
- property = self.arraydata[row]
- # get property name from column description
- property_name = self.columns[column]['property']
- # get property value
- if property_name == 'input':
- d = Qt.QVariant(property.input)
- elif property_name == 'enable':
- d = Qt.QVariant(property.enable)
- elif property_name == 'name':
- d = Qt.QVariant(property.name)
- elif property_name == 'lowpass':
- if property.lowpass == 0.0:
- d = Qt.QVariant('off')
- else:
- d = Qt.QVariant(property.lowpass)
- elif property_name == 'highpass':
- if property.highpass == 0.0:
- d = Qt.QVariant('off')
- else:
- d = Qt.QVariant(property.highpass)
- elif property_name == 'notchfilter':
- d = Qt.QVariant(property.notchfilter)
- elif property_name == 'isReference':
- d = Qt.QVariant(property.isReference)
- else:
- d = Qt.QVariant()
- return d
-
- def _setitem(self, row, column, value):
- ''' Set amplifier property item based on table row and column
- @param row: row number
- @param column: column number
- @param value: QVariant value object
- @return: True if property value was set, False if not
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return False
- # get channel properties
- property = self.arraydata[row]
- # get property name from column description
- property_name = self.columns[column]['property']
- # set channel property
- if property_name == 'enable':
- property.enable = value.toBool()
- return True
- elif property_name == 'name':
- n = value.toString()
- if n.isEmpty() or n.trimmed().isEmpty():
- return False
- property.name = value.toString()
- return True
- elif property_name == 'lowpass':
- property.lowpass,ok = value.toDouble()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.lowpass = property.lowpass
- return True
- elif property_name == 'highpass':
- property.highpass,ok = value.toDouble()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.highpass = property.highpass
- return True
- elif property_name == 'notchfilter':
- property.notchfilter = value.toBool()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.notchfilter = property.notchfilter
- self.reset()
- return True
- elif property_name == 'isReference':
- # available for EEG channels only
- if property.group == ChannelGroup.EEG:
- # remove previously selected reference channel in single channel mode
- if not AMP_MULTIPLE_REF:
- if value.toBool() == True:
- for prop in self.arraydata:
- prop.isReference = False
- property.isReference = value.toBool()
- self.reset()
- return True
- return False
-
- def editorType(self, column):
- ''' Get the columns editor type from column description
- @param column: table column number
- @return: editor type as QVariant (string)
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- return Qt.QVariant(self.columns[column]['editor'])
-
- def comboBoxList(self, column):
- ''' Get combo box item list for column 'highpass' or 'lowpass'
- @param column: table column number
- @return: combo box item list as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column]['property'] == 'lowpass':
- return Qt.QVariant(self.lowpasslist)
- elif self.columns[column]['property'] == 'highpass':
- return Qt.QVariant(self.highpasslist)
- else:
- return Qt.QVariant()
-
- def rowCount(self, parent):
- ''' Get the number of required table rows
- @return: number of rows
- '''
- if parent.isValid():
- return 0
- return len(self.arraydata)
-
- def columnCount(self, parent):
- ''' Get the number of required table columns
- @return: number of columns
- '''
- if parent.isValid():
- return 0
- return len(self.columns)
-
- def data(self, index, role):
- ''' Abstract method from QAbstactItemModel to get cell data based on role
- @param index: QModelIndex table cell reference
- @param role: given role for the item referred to by the index
- @return: the data stored under the given role for the item referred to by the index
- '''
- if not index.isValid():
- return Qt.QVariant()
-
- # get the underlying data
- value = self._getitem(index.row(), index.column())
-
- if role == Qt.Qt.CheckStateRole:
- # hide/disable the reference channel
- if AMP_HIDE_REF:
- properties = self.arraydata[index.row()]
- property_name = self.columns[index.column()]['property']
- if property_name == "enable" and properties.isReference:
- return Qt.Qt.Unchecked
-
- # set check state
- if value.type() == Qt.QMetaType.Bool:
- if value.toBool():
- return Qt.Qt.Checked
- else:
- return Qt.Qt.Unchecked
-
- elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
- if value.type() != Qt.QMetaType.Bool:
- return value
-
- elif role == Qt.Qt.BackgroundRole:
- # change background color for reference channel
- property = self.arraydata[index.row()]
- #if (property.isReference) and (index.column() == 0):
- if (property.isReference):
- return Qt.QVariant( Qt.QColor(0, 0, 255))
-
- return Qt.QVariant()
-
- def flags(self, index):
- ''' Abstract method from QAbstactItemModel
- @param index: QModelIndex table cell reference
- @return: the item flags for the given index
- '''
- if not index.isValid():
- return Qt.Qt.ItemIsEnabled
- if not self.columns[index.column()]['edit']:
- return Qt.Qt.ItemIsEnabled
- value = self._getitem(index.row(), index.column())
- if value.type() == Qt.QMetaType.Bool:
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
-
- def setData(self, index, value, role):
- ''' Abstract method from QAbstactItemModel to set cell data based on role
- @param index: QModelIndex table cell reference
- @param value: QVariant new cell data
- @param role: given role for the item referred to by the index
- @return: true if successful; otherwise returns false.
- '''
- if index.isValid():
- if role == Qt.Qt.EditRole:
- if not self._setitem(index.row(), index.column(), value):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
- return True
- elif role == Qt.Qt.CheckStateRole:
- if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
- return True
- return False
-
- def headerData(self, col, orientation, role):
- ''' Abstract method from QAbstactItemModel to get the column header
- @param col: column number
- @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
- @param role: given role for the item referred to by the index
- @return: header
- '''
- if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
- return Qt.QVariant(self.columns[col]['header'])
- return Qt.QVariant()
-
-
-class _ConfigItemDelegate(Qt.QStyledItemDelegate):
- ''' Combobox item editor
- '''
- def __init__(self, parent=None):
- super(_ConfigItemDelegate, self).__init__(parent)
-
- def createEditor(self, parent, option, index):
- if index.model().editorType(index.column()) == 'combobox':
- combobox = Qt.QComboBox(parent)
- combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
- combobox.setEditable(False)
- self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
- return combobox
- return Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
-
- def setEditorData(self, editor, index):
- if index.model().columns[index.column()]['editor'] == 'combobox':
- text = index.model().data(index, Qt.Qt.DisplayRole).toString()
- i = editor.findText(text)
- if i == -1:
- i = 0
- editor.setCurrentIndex(i)
- Qt.QStyledItemDelegate.setEditorData(self, editor, index)
-
-
- def setModelData(self, editor, model, index):
- if model.columns[index.column()]['editor'] == 'combobox':
- model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
- model.reset()
- Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
-
- def emitCommitData(self):
- self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Acquisition Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@version: 1.0
+'''
+
+from scipy import signal
+from PyQt4 import Qt
+from modbase import *
+from actichamp_w import *
+from res import frmActiChampOnline
+from res import frmActiChampConfig
+from operator import itemgetter
+import textwrap
+from devices.devcontainer import DeviceContainer
+
+# enable active shielding mode
+AMP_SHIELD_MODE = False
+
+# allow multiple reference channels
+AMP_MULTIPLE_REF = True
+
+# hide the reference channel(s), works only without separate montage module
+AMP_HIDE_REF = True
+
+# no channel selection within amplifier module, for use with an separate montage module.
+AMP_MONTAGE = False
+
+'''
+------------------------------------------------------------
+AMPLIFIER MODULE
+------------------------------------------------------------
+'''
+
+class AMP_ActiChamp(ModuleBase):
+ ''' ActiChamp EEG amplifier module
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ ModuleBase.__init__(self, name="Amplifier", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ # 2: input device container added
+ # 3: PLL external input
+ self.xmlVersion = 3
+
+ # create hardware object
+ self.amp = ActiChamp() #: amplifier hardware object
+
+ # set default channel configuration
+ self.max_eeg_channels = 160 #: number of EEG channels for max. HW configuration
+ self.max_aux_channels = 8 #: number of AUX channels for max. HW configuration
+ self.channel_config = EEG_DataBlock.get_default_properties(self.max_eeg_channels, self.max_aux_channels)
+ self.recording_mode = CHAMP_MODE_NORMAL
+
+ # create dictionary of possible sampling rates
+ self.sample_rates = []
+ for rate in [100000.0, 50000.0, 25000.0, 10000.0, 5000.0, 2000.0, 1000.0, 500.0, 200.0]:
+ base, div = self.amp.getSamplingRateBase(rate)
+ if base >= 0:
+ self.sample_rates.append({'rate':str(int(rate)), 'base':base, 'div':div, 'value':rate})
+
+ self.sample_rate = self.sample_rates[7]
+ self.binning = self.sample_rate['div']
+ self.binningoffset = 0
+
+ # set default data block
+ if AMP_MONTAGE:
+ self._create_channel_selection()
+ else:
+ self._create_all_channel_selection()
+
+ # create the input device container
+ self.inputDevices = DeviceContainer()
+
+ # date and time of acquisition start
+ self.start_time = datetime.datetime.now()
+
+ # create online configuration pane
+ self.online_cfg = _OnlineCfgPane(self)
+ self.connect(self.online_cfg, Qt.SIGNAL("modeChanged(int)"), self._online_mode_changed)
+
+ # impedance interval timer
+ self.impedance_timer = time.perf_counter()
+
+ # batter check interval timer and last voltage warning string
+ self.battery_timer = time.perf_counter()
+ self.voltage_warning = ""
+
+ # skip the first received data blocks
+ self.skip_counter = 5
+ self.blocking_counter = 0
+
+ # reset hardware error counter and acquisition time out
+ self.initialErrorCount = -1
+ self.acquisitionTimeoutCounter = 0
+ self.test_counter = 0
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ '''
+ return self.online_cfg
+
+ def get_configuration_pane(self):
+ ''' Get the configuration pane if available.
+ Qt widgets are not reusable, so we have to create it every time
+ '''
+ Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
+ # read amplifier configuration
+ self.amp.readConfiguration(self.sample_rate['base'], force=True)
+ self.update_receivers()
+ Qt.QApplication.restoreOverrideCursor()
+ # create configuration pane
+ if AMP_MONTAGE:
+ config = _ConfigurationPane(self)
+ else:
+ config = _DeviceConfigurationPane(self)
+ self.connect(config, Qt.SIGNAL("dataChanged()"), self._configuration_changed)
+ self.connect(config, Qt.SIGNAL("emulationChanged(int)"), self._emulation_changed)
+ self.connect(config, Qt.SIGNAL("rateChanged(int)"), self._samplerate_changed)
+ return config
+
+ def get_module_info(self):
+ ''' Get information about this module for the about dialog
+ @return: Serial numbers of amplifier and modules
+ '''
+ return self.amp.getDeviceInfoString()
+
+
+ def _emulation_changed(self, index):
+ ''' SIGNAL from configuration pane if emulation mode has changed
+ '''
+ try:
+ self.amp.setEmulationMode(index)
+ except Exception as e:
+ self.send_exception(e)
+ self.update_receivers()
+
+ def _samplerate_changed(self, index):
+ ''' SIGNAL from configuration pane if sample rate has changed
+ '''
+ Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
+ self.sample_rate = self.sample_rates[index]
+ self.update_receivers()
+ Qt.QApplication.restoreOverrideCursor()
+
+ def _configuration_changed(self):
+ ''' SIGNAL from configuration pane if values has changed
+ '''
+ self.update_receivers()
+
+ def _online_mode_changed(self, new_mode):
+ ''' SIGNAL from online configuration pane if recording mode has changed
+ '''
+ if self.amp.running:
+ if not self.stop():
+ self.online_cfg.updateUI(self.recording_mode)
+ return
+
+ if new_mode >= 0:
+ Qt.QApplication.setOverrideCursor(Qt.Qt.WaitCursor)
+ self.recording_mode = new_mode
+ self.start()
+ Qt.QApplication.restoreOverrideCursor()
+
+ def _set_default_filter(self):
+ ''' set all filter properties to HW filter values
+ '''
+ for channel in self.channel_config:
+ channel.highpass = 0.0 # high pass off
+ channel.lowpass = 0.0 # low pass off
+ channel.notchfilter = False # notch filter off
+
+ def _check_reference(self):
+ ''' check if selected reference channels are consistent with the global flag
+ '''
+ # nothing to do if multiple channels are allowed
+ if AMP_MULTIPLE_REF:
+ return
+ # else keep the first reference channel only
+ eeg_ref = np.array([x.isReference for x in self.channel_config], dtype=bool)
+ ref_index = np.nonzero(eeg_ref)[0] # indices of reference channel(s)
+ for ch in self.channel_config[ref_index[1:]]:
+ ch.isReference = False
+
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ emulation_mode = self.amp.getEmulationMode() > 0
+ self.sample_rate = self.sample_rates[7] # 500Hz sample rate
+ for channel in self.channel_config:
+ channel.isReference = False
+ if channel.group == ChannelGroup.EEG:
+ channel.enable = True # enable all EEG channels
+ if (channel.input == 1) and not emulation_mode:
+ channel.isReference = True # use first channel as reference
+ else:
+ channel.enable = False # disable all AUX channels
+ self._set_default_filter()
+ self.inputDevices.reset()
+ self.update_receivers()
+
+ def stop(self, force=False):
+ ''' Stop data acquisition
+ @param force: force stop without query
+ @return: True, if stop was accepted by attached modules
+ '''
+ # ask attached modules for acceptance
+ if not force:
+ if not self.query("Stop"):
+ return False
+ # stop it
+ ModuleBase.stop(self)
+ return True
+
+
+ def process_event(self, event):
+ ''' Handle events from attached receivers
+ @param event: ModuleEvent
+ '''
+ # Command events
+ if event.type == EventType.COMMAND:
+ # check for new impedance color range values
+ if event.info == "ImpColorRange":
+ good, bad = event.cmd_value
+
+ if self.recording_mode == CHAMP_MODE_IMPEDANCE:
+ self._thLock.acquire()
+ try:
+ self.amp.setImpedanceRange(good * 1000, bad * 1000)
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ # check for stop command
+ if event.info == "Stop":
+ if event.cmd_value == "force":
+ self.stop(force=True)
+ else:
+ self.stop()
+
+ # check for recording start command
+ if event.info == "StartRecording":
+ self._online_mode_changed(CHAMP_MODE_NORMAL)
+
+ # check for impedance start command
+ if event.info == "StartImpedance":
+ self._online_mode_changed(CHAMP_MODE_IMPEDANCE)
+
+ # check for trigger out command
+ if event.info == "TriggerOut":
+ self._thLock.acquire()
+ try:
+ self.amp.setTrigger(event.cmd_value)
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ # check for button LED command
+ # cmd_value is a tuple with period and duty cycle
+ if event.info == "SetLED":
+ self._thLock.acquire()
+ try:
+ self.amp.setButtonLed(event.cmd_value[0], event.cmd_value[1])
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ # check for acitve shield gain command
+ # cmd_value is the gain from 1 to 100
+ if event.info == "SetShieldGain":
+ self._thLock.acquire()
+ self.amp.activeShieldGain = event.cmd_value
+ self._thLock.release()
+
+ # Error events
+ if event.type == EventType.ERROR or event.type == EventType.LOG:
+ # add device status info to "sample missing" events
+ if "samples missing" in event.info:
+ self._thLock.acquire()
+ try:
+ errors = self.amp.getDeviceStatus()[1] - self.initialErrorCount
+ event.info += " (device errors = %d)"%errors
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ event.info += " (%s)"%(str(e))
+
+
+ def process_start(self):
+ ''' Open amplifier hardware and start data acquisition
+ '''
+ # reset variables
+ self.eeg_data.sample_counter = 0
+ self.acquisitionTimeoutCounter = 0
+ self.battery_timer = 0
+ self.test_counter = 0
+
+ # open and setup hardware
+ self.amp.open()
+
+ # check battery
+ ok,voltage = self._check_battery()
+ if not ok:
+ raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
+
+ self.amp.setup(self.recording_mode, self.sample_rate['base'], self.sample_rate['div'])
+ self.update_receivers()
+ if len(self.channel_indices) == 0:
+ raise ModuleError(self._object_name, "no input channels selected!")
+
+ # check battery again
+ ok,voltage = self._check_battery()
+ if not ok:
+ raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
+
+ # start hardware
+ self.amp.start()
+
+ # set start time on first call
+ self.start_time = datetime.datetime.now()
+
+ # send status info
+ if AMP_MONTAGE:
+ info = "Start %s at %.0fHz with %d channels"%(CHAMP_Modes[self.recording_mode],\
+ self.eeg_data.sample_rate,\
+ len(self.channel_indices))
+ else:
+ if self.amp.hasPllOption() and self.amp.PllExternal:
+ info = "Start %s at %.0fHz (ext. PLL)"%(CHAMP_Modes[self.recording_mode],\
+ self.eeg_data.sample_rate)
+ else:
+ info = "Start %s at %.0fHz"%(CHAMP_Modes[self.recording_mode],\
+ self.eeg_data.sample_rate)
+
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE, info))
+ # send recording mode
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = self.recording_mode,
+ status_field="Mode"))
+ # update button state
+ self.online_cfg.updateUI(self.recording_mode)
+
+ # skip the first received data blocks
+ self.skip_counter = 5
+ self.blocking_counter = 0
+ self.initialErrorCount = -1
+
+
+ def process_stop(self):
+ ''' Stop data acquisition and close hardware object
+ '''
+ errors = 999
+ try:
+ errors = self.amp.getDeviceStatus()[1] - self.initialErrorCount # get number of device errors
+ except:
+ pass
+ try:
+ if self.recording_mode == CHAMP_MODE_LED_TEST:
+ self.amp.LedTest(0)
+ self.amp.stop()
+ except:
+ pass
+ try:
+ self.amp.close()
+ except:
+ pass
+
+ # send status info
+ info = "Stop %s"%(CHAMP_Modes[self.recording_mode])
+ if (errors > 0) and (self.recording_mode != CHAMP_MODE_IMPEDANCE) and (self.recording_mode != CHAMP_MODE_LED_TEST):
+ info += " (device errors = %d)"%(errors)
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE, info))
+ # send recording mode
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = -1, # stop
+ status_field="Mode"))
+ # update button state
+ self.online_cfg.updateUI(-1)
+
+
+ def _create_channel_selection(self):
+ ''' Create index arrays of selected channels and prepare EEG_DataBlock
+ '''
+ # get all active eeg channel indices (including reference channel)
+ mask = lambda x: (x.group == ChannelGroup.EEG) and (x.enable | x.isReference) and (x.input <= self.amp.properties.CountEeg)
+ eeg_map = np.array([mask(ch) for ch in self.channel_config], dtype=bool)
+ self.eeg_indices = np.nonzero(eeg_map)[0] # indices of all eeg channels
+
+ # get all active aux channel indices
+ mask = lambda x: (x.group == ChannelGroup.AUX) and x.enable and (x.input <= self.amp.properties.CountAux)
+ eeg_map = np.array([mask(ch) for ch in self.channel_config], dtype=bool)
+ self.aux_indices = np.nonzero(eeg_map)[0] # indices of all aux channels
+ self.property_indices = np.append(self.eeg_indices, self.aux_indices)
+
+ # adjust AUX indices to the actual available EEG channels
+ self.aux_indices -= (self.max_eeg_channels - self.amp.properties.CountEeg)
+ self.channel_indices = np.append(self.eeg_indices, self.aux_indices)
+
+ # create a new data block based on channel selection
+ self.eeg_data = EEG_DataBlock(len(self.eeg_indices), len(self.aux_indices))
+ self.eeg_data.channel_properties = copy.deepcopy(self.channel_config[self.property_indices])
+ self.eeg_data.sample_rate = self.sample_rate['value']
+
+ # get the reference channel indices
+ #mask = lambda x: (x.group == ChannelGroup.EEG) and x.isReference and (x.input <= self.amp.properties.CountEeg)
+ eeg_ref = np.array([x.isReference for x in self.eeg_data.channel_properties], dtype=bool)
+ self.ref_index = np.nonzero(eeg_ref)[0] # indices of reference channel(s)
+ if len(self.ref_index) and not AMP_MULTIPLE_REF:
+ # use only the first reference channel
+ self.ref_index = self.ref_index[0:1]
+ idx = np.nonzero(map(lambda x: x not in self.ref_index,
+ range(0, len(self.eeg_indices))
+ )
+ )[0]
+ for prop in self.eeg_data.channel_properties[idx]:
+ prop.isReference = False
+
+ # append "REF" to the reference channel name and create the combined reference channel name
+ refnames = []
+ for prop in self.eeg_data.channel_properties[self.ref_index]:
+ refnames.append(str(prop.name))
+ prop.name = "REF_" + prop.name
+ prop.refname = "REF"
+ # global hide for all reference channels?
+ if AMP_HIDE_REF:
+ prop.enable = False
+ if len(refnames) > 1:
+ self.eeg_data.ref_channel_name = "AVG(" + "+".join(refnames) + ")"
+ else:
+ self.eeg_data.ref_channel_name = "".join(refnames)
+
+ # remove reference channel if not in impedance mode
+ self.ref_remove_index = self.ref_index
+ if (self.recording_mode != CHAMP_MODE_IMPEDANCE) and len(self.ref_index):
+ # set reference channel names for all other electrodes
+ idx = np.nonzero([x not in self.ref_index for x in range(len(self.eeg_indices))])[0]
+ for prop in self.eeg_data.channel_properties[idx]:
+ prop.refname = "REF"
+
+ '''
+ # remove single reference channel
+ if AMP_HIDE_REF or not self.eeg_data.channel_properties[self.ref_index[0]].enable:
+ self.eeg_data.channel_properties = np.delete(self.eeg_data.channel_properties, self.ref_index, 0)
+ self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_index, 0)
+ '''
+ # remove all disabled reference channels
+ ref_dis = np.array([x.isReference and not x.enable for x in self.eeg_data.channel_properties], dtype=bool)
+ self.ref_remove_index = np.nonzero(ref_dis)[0] # indices of disabled reference channels
+ self.eeg_data.channel_properties = np.delete(self.eeg_data.channel_properties, self.ref_remove_index, 0)
+ self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_remove_index, 0)
+
+ # prepare recording mode and anti aliasing filters
+ self._prepare_mode_and_filters()
+
+
+ def _create_all_channel_selection(self):
+ ''' Create index arrays of all available channels and prepare EEG_DataBlock
+ '''
+ # get all eeg channel indices
+ mask = lambda x: (x.group == ChannelGroup.EEG) and (x.input <= self.amp.properties.CountEeg)
+ eeg_map = np.array([mask(ch) for ch in self.channel_config], dtype=bool)
+ self.eeg_indices = np.nonzero(eeg_map)[0] # indices of all eeg channels
+
+ # get all aux channel indices
+ mask = lambda x: (x.group == ChannelGroup.AUX) and (x.input <= self.amp.properties.CountAux)
+ eeg_map = np.array([mask(ch) for ch in self.channel_config], dtype=bool)
+ self.aux_indices = np.nonzero(eeg_map)[0] # indices of all aux channels
+ self.property_indices = np.append(self.eeg_indices, self.aux_indices)
+
+ # adjust AUX indices to the actual available EEG channels
+ self.aux_indices -= (self.max_eeg_channels - self.amp.properties.CountEeg)
+ self.channel_indices = np.append(self.eeg_indices, self.aux_indices)
+
+ # create a new data block based on channel selection
+ self.eeg_data = EEG_DataBlock(len(self.eeg_indices), len(self.aux_indices))
+ self.eeg_data.channel_properties = copy.deepcopy(self.channel_config[self.property_indices])
+ self.eeg_data.sample_rate = self.sample_rate['value']
+
+ # reset the reference channel indices
+ self.ref_index = np.array([]) # indices of reference channel(s)
+ self.eeg_data.ref_channel_name = ""
+ self.ref_remove_index = self.ref_index
+
+ # prepare recording mode and anti aliasing filters
+ self._prepare_mode_and_filters()
+
+
+
+
+ def _prepare_mode_and_filters(self):
+ # translate recording modes
+ if (self.recording_mode == CHAMP_MODE_NORMAL) or (self.recording_mode == CHAMP_MODE_ACTIVE_SHIELD):
+ self.eeg_data.recording_mode = RecordingMode.NORMAL
+ elif self.recording_mode == CHAMP_MODE_IMPEDANCE:
+ self.eeg_data.recording_mode = RecordingMode.IMPEDANCE
+ elif self.recording_mode == CHAMP_MODE_TEST:
+ self.eeg_data.recording_mode = RecordingMode.TEST
+
+ # down sampling
+ self.binning = self.sample_rate['div']
+ self.binningoffset = 0
+
+ # design anti-aliasing filter for down sampling
+ # it's an Nth order lowpass Butterworth filter from scipy
+ # signal.filter_design.butter(N, Wn, btype='low')
+ # N = filter order, Wn = cut-off frequency / nyquist frequency
+ # f_nyquist = f_in / 2
+ # f_cutoff = f_in / rate_divider * filter_factor
+ # Wn = f_cutoff / f_nyquist = f_in / rate_divider * filter_factor / f_in * 2
+ # Wn = 1 / rate_divider * 2 * filter_factor
+ filter_order = 4
+ filter_factor = 0.333
+ rate_divider = self.binning
+ Wn = 1.0 / rate_divider * 2.0 * filter_factor
+ self.aliasing_b,self.aliasing_a = signal.filter_design.butter(filter_order, Wn, btype='low')
+ zi = signal.lfiltic(self.aliasing_b, self.aliasing_a, (0.0,))
+ self.aliasing_zi = np.resize(zi, (len(self.channel_indices),len(zi)))
+
+ # define which channels contains which impedance values
+ self.eeg_data.eeg_channels[:,:] = 0
+ if self.eeg_data.recording_mode == RecordingMode.IMPEDANCE:
+ self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.DATA] = 1
+ self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.GND] = 1
+
+
+ def _check_battery(self):
+ ''' Check amplifier battery voltages
+ @return: state (ok=True, bad=False) and voltage
+ '''
+ # read battery state and internal voltages from amplifier
+ state, voltages, faultyVoltages = self.amp.getBatteryVoltage()
+ severe = ErrorSeverity.IGNORE
+ if state == 1:
+ severe = ErrorSeverity.NOTIFY
+ elif state == 2:
+ severe = ErrorSeverity.STOP
+
+ # create and send faulty voltages warning message
+ v_warning = ""
+ if len(faultyVoltages) > 0:
+ severe = ErrorSeverity.NOTIFY
+ v_warning = "Faulty internal voltage(s): "
+ for u in faultyVoltages:
+ v_warning += " %s"%(u)
+ # warning already sent?
+ if v_warning != self.voltage_warning:
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ info = v_warning,
+ severity = severe))
+ self.voltage_warning = v_warning
+
+ # create and send status message
+ voltage_info = "%.2fV"%(voltages.VDC) # battery voltage
+ for u in faultyVoltages:
+ voltage_info += "\n%s"%(u)
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = voltage_info,
+ severity = severe,
+ status_field="Battery"))
+ return state < 2, voltages.VDC
+
+ def process_update(self, params):
+ ''' Prepare channel properties and propagate update to all connected receivers
+ '''
+ # update device sampling rate and get new configuration
+ try:
+ self.amp.readConfiguration(self.sample_rate['base'])
+ except Exception as e:
+ self.send_exception(e)
+ # indicate amplifier simulation
+ if self.amp.getEmulationMode() > 0:
+ self.online_cfg.groupBoxMode.setTitle("Amplifier SIMULATION")
+ else:
+ self.online_cfg.groupBoxMode.setTitle("Amplifier")
+
+ # create channel selection maps
+ if AMP_MONTAGE:
+ self._create_channel_selection()
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = "%d ch"%(len(self.channel_indices)),
+ status_field="Channels"))
+ else:
+ self._create_all_channel_selection()
+ try:
+ self.inputDevices.process_update(self.eeg_data)
+ except Exception as e:
+ self.send_exception(e)
+
+ # send current status as event
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = "%.0f Hz"%(self.eeg_data.sample_rate),
+ status_field = "Rate"))
+ return copy.copy(self.eeg_data)
+
+ def process_impedance(self):
+ ''' Get the impedance values from amplifier
+ and return the eeg data block
+ '''
+ # send values only once per second
+ t = time.perf_counter()
+ if (t - self.impedance_timer) < 1.0:
+ return None
+ self.impedance_timer = t
+
+ # get impedance values from device
+ #imp = (np.arange(len(self.channel_indices))+1)*1000.0 # TODO: for testing only
+ imp, disconnected = self.amp.readImpedances()
+ # check data rate mismatch messages
+ ''' suppress warning because it has no influence on the impedance measurement
+ if disconnected == CHAMP_ERR_MONITORING:
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ info = "USB data rate mismatch",
+ severity = ErrorSeverity.NOTIFY))
+ '''
+ if imp == None:
+ return None
+
+ eeg_imp = imp[self.eeg_indices]
+ gnd_imp = imp[-1]
+ self.eeg_data.impedances = eeg_imp.tolist()
+ self.eeg_data.impedances.append(gnd_imp)
+
+ # invalidate the old impedance data list
+ self.eeg_data.impedances = []
+
+ # copy impedance values to data array
+ self.eeg_data.eeg_channels = np.zeros((len(self.channel_indices), 10), 'd')
+ self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.DATA] = eeg_imp
+ self.eeg_data.eeg_channels[self.eeg_indices,ImpedanceIndex.GND] = gnd_imp
+
+ # dummy values for trigger and sample counter
+ self.eeg_data.trigger_channel = np.zeros((1, 10), np.uint32)
+ self.eeg_data.sample_channel = np.zeros((1, 10), np.uint32)
+
+ # process connected input devices
+ if not AMP_MONTAGE:
+ self.inputDevices.process_input(self.eeg_data)
+
+ # set recording time
+ self.eeg_data.block_time = datetime.datetime.now()
+
+ # put it into the receiver queues
+ eeg = copy.copy(self.eeg_data)
+ return eeg
+
+ def process_led_test(self):
+ ''' toggle LEDs on active electrodes
+ and return no eeg data
+ '''
+ # dummy read data
+ d, disconnected = self.amp.read(self.channel_indices, len(self.eeg_indices), len(self.aux_indices))
+
+ # toggle LEDs twice per second
+ t = time.perf_counter()
+ if (t - self.impedance_timer) < 0.5:
+ return None
+ self.impedance_timer = t
+
+ # toggle all LEDs between off, green and red
+ if self.test_counter % 3 == 0:
+ self.amp.LedTest(0)
+ elif self.test_counter % 3 == 1:
+ self.amp.LedTest(11)
+ else:
+ self.amp.LedTest(12)
+
+ self.test_counter += 1
+ return None
+
+ def process_output(self):
+ ''' Get data from amplifier
+ and return the eeg data block
+ '''
+ t = time.clock()
+ self.eeg_data.performance_timer = 0
+ self.eeg_data.performance_timer_max = 0
+ self.recordtime = 0.0
+
+ # check battery voltage every 5s
+ if (t - self.battery_timer) > 5.0 or self.battery_timer == 0:
+ ok,voltage = self._check_battery()
+ if not ok:
+ raise ModuleError(self._object_name, "battery low (%.1fV)!"%voltage)
+ self.battery_timer = t
+
+ if self.recording_mode == CHAMP_MODE_IMPEDANCE:
+ return self.process_impedance()
+
+ if self.recording_mode == CHAMP_MODE_LED_TEST:
+ return self.process_led_test()
+
+ if self.amp.BlockingMode:
+ self._thLock.release()
+ try:
+ d, disconnected = self.amp.read(self.channel_indices,
+ len(self.eeg_indices), len(self.aux_indices))
+ finally:
+ self._thLock.acquire()
+ self.output_timer = time.perf_counter()
+ else:
+ d, disconnected = self.amp.read(self.channel_indices,
+ len(self.eeg_indices), len(self.aux_indices))
+
+ if d == None:
+ self.acquisitionTimeoutCounter += 1
+ # about 5s timeout
+ if self.acquisitionTimeoutCounter > 100:
+ self.acquisitionTimeoutCounter = 0
+ raise ModuleError(self._object_name, "connection to hardware is broken!")
+ # check data rate mismatch messages
+ if disconnected == CHAMP_ERR_MONITORING:
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ info = "USB data rate mismatch",
+ severity = ErrorSeverity.NOTIFY))
+ return None
+ else:
+ self.acquisitionTimeoutCounter = 0
+
+
+ # skip the first received data blocks
+ if self.skip_counter > 0:
+ self.skip_counter -= 1
+ return None
+ # get the initial error counter
+ if self.initialErrorCount < 0:
+ self.initialErrorCount = self.amp.getDeviceStatus()[1]
+
+
+ # down sample required?
+ if self.binning > 1:
+ # anti-aliasing filter
+ filtered ,self.aliasing_zi = \
+ signal.lfilter(self.aliasing_b, self.aliasing_a, d[0], zi=self.aliasing_zi)
+ # reduce reslution to avoid limit cycle
+ # self.aliasing_zi = np.asfarray(self.aliasing_zi, np.float32)
+
+ self.eeg_data.eeg_channels = filtered[:, self.binningoffset::self.binning]
+ self.eeg_data.trigger_channel = np.bitwise_or.reduce(d[1][:].reshape(-1, self.binning), axis=1).reshape(1,-1)
+ self.eeg_data.sample_channel = d[2][:, self.binningoffset::self.binning] / self.binning
+ self.eeg_data.sample_counter += self.eeg_data.sample_channel.shape[1]
+ else:
+ self.eeg_data.eeg_channels = d[0]
+ self.eeg_data.trigger_channel = d[1]
+ self.eeg_data.sample_channel = d[2]
+ self.eeg_data.sample_counter += self.eeg_data.sample_channel.shape[1]
+
+ # average, subtract and remove the reference channels
+ if len(self.ref_index):
+ '''
+ # subtract
+ for ref_channel in self.eeg_data.eeg_channels[self.ref_index]:
+ self.eeg_data.eeg_channels[:len(self.eeg_indices)] -= ref_channel
+ # restore reference channel
+ self.eeg_data.eeg_channels[self.ref_index[0]] = ref_channel
+
+ # remove single reference channel if not enabled
+ if not (len(self.eeg_data.channel_properties) > self.ref_index[0] and
+ self.eeg_data.channel_properties[self.ref_index[0]].isReference ):
+ self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_index, 0)
+ '''
+ # average reference channels
+ reference = np.mean(self.eeg_data.eeg_channels[self.ref_index], 0)
+
+ # subtract reference
+ self.eeg_data.eeg_channels[:len(self.eeg_indices)] -= reference
+
+ # remove all disabled reference channels
+ if len(self.ref_remove_index) > 0:
+ self.eeg_data.eeg_channels = np.delete(self.eeg_data.eeg_channels, self.ref_remove_index, 0)
+
+
+ # calculate date and time for the first sample of this block in s
+ sampletime = self.eeg_data.sample_channel[0][0] / self.eeg_data.sample_rate
+ self.eeg_data.block_time = self.start_time + datetime.timedelta(seconds=sampletime)
+
+ # process connected input devices
+ if not AMP_MONTAGE:
+ self.inputDevices.process_input(self.eeg_data)
+
+ # put it into the receiver queues
+ eeg = copy.copy(self.eeg_data)
+
+ self.recordtime = time.perf_counter() - t
+
+ return eeg
+
+ def process_idle(self):
+ ''' Check if record time exceeds 200ms over a period of 10 blocks
+ and adjust idle time to record time
+ '''
+ if self.recordtime > 0.2:
+ self.blocking_counter += 1
+ # drop blocks if exceeded
+ if self.blocking_counter > 10:
+ self.skip_counter = 10
+ self.blocking_counter = 0
+ else:
+ self.blocking_counter = 0
+
+ # adjust idle time to record time
+ idletime = max(0.06-self.recordtime, 0.02)
+
+ if self.amp.BlockingMode:
+ time.sleep(0.001)
+ else:
+ time.sleep(idletime) # suspend the worker thread for 60ms
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+
+
+ ...
+
+ 1000
+
+ '''
+ E = objectify.E
+
+ channels = E.channels()
+ if AMP_MONTAGE:
+ for channel in self.channel_config:
+ channels.append(channel.getXML())
+
+ # input device container
+ devices = E.InputDeviceContainer(self.inputDevices.getXML())
+
+ amplifier = E.AMP_ActiChamp(E.samplerate(self.sample_rate['value']),
+ E.pllexternal(self.amp.PllExternal),
+ channels,
+ devices,
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="amplifier")
+ return amplifier
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # set default values in case we get no configuration data
+ self.inputDevices.reset()
+
+ # search my configuration data
+ amps = xml.xpath("//AMP_ActiChamp[@module='amplifier' and @instance='%i']"%(self._instance) )
+ if len(amps) == 0:
+ return # configuration data not found, leave everything unchanged
+
+ cfg = amps[0] # we should have only one amplifier instance from this type
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ # setup channel configuration from xml
+ for idx, channel in enumerate(cfg.channels.iterchildren()):
+ self.channel_config[idx].setXML(channel)
+ # reset filter properties to default values (because configuration has been moved to filter module)
+ self._set_default_filter()
+ # validate reference channel selection
+ self._check_reference()
+ # set closest matching sample rate
+ sr = cfg.samplerate.pyval
+ for rate in sorted(self.sample_rates, key=itemgetter('value')):
+ if rate["value"] >= sr:
+ self.sample_rate = rate
+ break
+ if version >= 2:
+ # setup the input device configuration
+ self.inputDevices.setXML(cfg.InputDeviceContainer)
+ else:
+ self.inputDevices.reset()
+ if version >= 3:
+ self.amp.PllExternal = cfg.pllexternal.pyval
+ else:
+ self.amp.PllExternal = 0
+
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+
+
+
+'''
+------------------------------------------------------------
+AMPLIFIER MODULE ONLINE GUI
+------------------------------------------------------------
+'''
+
+class _OnlineCfgPane(Qt.QFrame, frmActiChampOnline.Ui_frmActiChampOnline):
+ ''' ActiChamp online configuration pane
+ '''
+ def __init__(self, amp, *args):
+ ''' Constructor
+ @param amp: parent module object
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+ self.amp = amp
+
+ # set default values
+ self.pushButtonStop.setChecked(True)
+
+ # re-assign the shielding button
+ if not AMP_SHIELD_MODE:
+ self.pushButtonStartShielding.setText("Electrode LED\nTest")
+
+ # actions
+ self.connect(self.pushButtonStartDefault, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+ self.connect(self.pushButtonStartImpedance, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+ self.connect(self.pushButtonStartShielding, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+ self.connect(self.pushButtonStartTest, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+ self.connect(self.pushButtonStop, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+
+ def _button_toggle(self, checked):
+ ''' SIGNAL if one of the push buttons is clicked
+ '''
+ if checked:
+ mode = -1 #stop
+ if self.pushButtonStartDefault.isChecked():
+ mode = CHAMP_MODE_NORMAL
+ elif self.pushButtonStartShielding.isChecked():
+ if AMP_SHIELD_MODE:
+ mode = CHAMP_MODE_ACTIVE_SHIELD
+ else:
+ mode = CHAMP_MODE_LED_TEST
+ elif self.pushButtonStartImpedance.isChecked():
+ mode = CHAMP_MODE_IMPEDANCE
+ elif self.pushButtonStartTest.isChecked():
+ mode = CHAMP_MODE_TEST
+ self.emit(Qt.SIGNAL('modeChanged(int)'), mode)
+
+ def updateUI(self, mode):
+ ''' Update user interface according to recording mode
+ '''
+ if mode == CHAMP_MODE_NORMAL:
+ self.pushButtonStartDefault.setChecked(True)
+ elif mode == CHAMP_MODE_ACTIVE_SHIELD or mode == CHAMP_MODE_LED_TEST:
+ self.pushButtonStartShielding.setChecked(True)
+ elif mode == CHAMP_MODE_IMPEDANCE:
+ self.pushButtonStartImpedance.setChecked(True)
+ elif mode == CHAMP_MODE_TEST:
+ self.pushButtonStartTest.setChecked(True)
+ else:
+ self.pushButtonStop.setChecked(True)
+
+
+'''
+-----------------------------------------------------------------
+AMPLIFIER MODULE CONFIGURATION GUI (with input device selection)
+-----------------------------------------------------------------
+'''
+
+class _DeviceConfigurationPane(Qt.QFrame):
+ ''' ActiChamp configuration pane
+ '''
+ def __init__(self, amplifier, *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # reference to our parent module
+ self.amplifier = amplifier
+
+ # Set tab name
+ self.setWindowTitle("Amplifier")
+
+ # make it nice
+ self.setFrameShape(Qt.QFrame.StyledPanel)
+ self.setFrameShadow(Qt.QFrame.Raised)
+
+ # base layout
+ self.gridLayout = Qt.QGridLayout(self)
+
+ # spacers
+ self.vspacer_1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
+ self.vspacer_2 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
+ self.vspacer_3 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
+ self.hspacer_1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Minimum)
+
+ # create the amplifier GUI elements
+ self.comboBoxSampleRate = Qt.QComboBox()
+ self.comboBoxEmulation = Qt.QComboBox()
+ self.label_Simulated = Qt.QLabel()
+
+ self.labelPLL = Qt.QLabel("PLL Input")
+ self.radioPllInternal = Qt.QRadioButton("Internal")
+ self.radioPllExternal = Qt.QRadioButton("External")
+
+ self.label_AvailableChannels = Qt.QLabel("Available channels: 32 EEG and 5 AUX")
+ self.label_AvailableChannels.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Minimum)
+ #self.label_AvailableChannels.setIndent(20)
+ font = Qt.QFont("Ms Shell Dlg 2", 10)
+ self.label_AvailableChannels.setFont(font)
+
+ self.label_1 = Qt.QLabel("Sampling Rate")
+ self.label_2 = Qt.QLabel("[Hz]")
+ self.label_3 = Qt.QLabel("Simulation")
+ self.label_4 = Qt.QLabel("Module(s)")
+
+ # group amplifier elements
+ self.groupAmplifier = Qt.QGroupBox("Amplifier Configuration")
+
+ self.gridLayoutAmp = Qt.QGridLayout()
+ self.gridLayoutAmp.addWidget(self.label_1, 0, 0)
+ self.gridLayoutAmp.addWidget(self.comboBoxSampleRate, 0, 1)
+ self.gridLayoutAmp.addWidget(self.label_2, 0, 2)
+ self.gridLayoutAmp.addWidget(self.label_3, 1, 0)
+ self.gridLayoutAmp.addWidget(self.comboBoxEmulation, 1, 1)
+ self.gridLayoutAmp.addWidget(self.label_4, 1, 2)
+ self.gridLayoutAmp.addWidget(self.label_Simulated, 2, 1, 1, 2)
+
+ self.gridLayoutAmp.addWidget(self.labelPLL, 3, 0)
+ self.gridLayoutAmp.addWidget(self.radioPllInternal, 3, 1)
+ self.gridLayoutAmp.addWidget(self.radioPllExternal, 4, 1)
+ self.gridLayoutAmp.addItem(self.vspacer_1, 5, 0, 1, 3)
+
+ self.gridLayoutAmpGroup = Qt.QGridLayout()
+ self.gridLayoutAmpGroup.addLayout(self.gridLayoutAmp, 0, 0, 2, 1)
+ self.gridLayoutAmpGroup.addItem(self.hspacer_1, 0, 1)
+ self.gridLayoutAmpGroup.addWidget(self.label_AvailableChannels, 0, 2)
+ self.gridLayoutAmpGroup.addItem(self.vspacer_2, 1, 1)
+
+ self.groupAmplifier.setLayout(self.gridLayoutAmpGroup)
+
+ # get the device configuration widget
+ self.device_cfg = self.amplifier.inputDevices.get_configuration_widget()
+
+ # group device elements
+ self.groupDevices = Qt.QGroupBox("Optional Input Devices")
+ self.gridLayoutDeviceGroup = Qt.QGridLayout()
+ self.gridLayoutDeviceGroup.addWidget(self.device_cfg, 0, 0)
+ self.groupDevices.setLayout(self.gridLayoutDeviceGroup)
+
+ # add all items to the main layout
+ self.gridLayout.addWidget(self.groupAmplifier, 0, 0)
+ self.gridLayout.addWidget(self.groupDevices, 1, 0)
+
+ # actions
+ self.connect(self.comboBoxSampleRate, Qt.SIGNAL("currentIndexChanged(int)"), self._samplerate_changed)
+ self.connect(self.comboBoxEmulation, Qt.SIGNAL("currentIndexChanged(int)"), self._emulationChanged)
+ self.connect(self.amplifier.inputDevices, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
+
+ # emulation combobox
+ self.comboBoxEmulation.addItems(["off", "1", "2", "3", "4", "5"])
+ self.comboBoxEmulation.setCurrentIndex(self.amplifier.amp.getEmulationMode())
+
+ # sample rate combobox
+ sr_index = -1
+ for sr in self.amplifier.sample_rates:
+ self.comboBoxSampleRate.addItem(sr['rate'])
+ if sr == self.amplifier.sample_rate:
+ sr_index = self.comboBoxSampleRate.count()-1
+ self.comboBoxSampleRate.setCurrentIndex(sr_index)
+
+ # available channels display
+ self._updateAvailableChannels()
+
+ # PLL configuration
+ self.radioPllExternal.setChecked(self.amplifier.amp.PllExternal != 0)
+ self.radioPllInternal.setChecked(self.amplifier.amp.PllExternal == 0)
+ self.showPllParams(self.amplifier.amp.hasPllOption())
+ self.connect(self.radioPllExternal, Qt.SIGNAL("toggled(bool)"), self._pllExternalToggled)
+
+ def _samplerate_changed(self, index):
+ ''' SIGNAL sample rate combobox value has changed
+ '''
+ if index >= 0:
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('rateChanged(int)'), index)
+ self._updateAvailableChannels()
+
+ def _emulationChanged(self, index):
+ ''' SIGNAL emulation mode combobox value has changed
+ '''
+ if index >= 0:
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('emulationChanged(int)'),index)
+ # simulated channels
+ if index > 0:
+ self.label_Simulated.setText("simulating %i + 8 channels"%(index * 32))
+ else:
+ self.label_Simulated.setText("")
+ self._updateAvailableChannels()
+
+ def _configurationDataChanged(self):
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+ def _updateAvailableChannels(self):
+ eeg = self.amplifier.amp.properties.CountEeg
+ aux = self.amplifier.amp.properties.CountAux
+ if self.amplifier.amp.getEmulationMode() == 0:
+ amp = "actiCHamp"
+ else:
+ amp = "Simulation"
+ self.label_AvailableChannels.setText("Amplifier: %s\n\nAvailable channels: %d EEG and %d AUX"%(amp, eeg, aux))
+
+
+ def showEvent(self, event):
+ pass
+
+ def showPllParams(self, show):
+ self.labelPLL.setVisible(show)
+ self.radioPllExternal.setVisible(show)
+ self.radioPllInternal.setVisible(show)
+
+ def _pllExternalToggled(self, checked):
+ if checked:
+ self.amplifier.amp.PllExternal = 1
+ else:
+ self.amplifier.amp.PllExternal = 0
+
+
+
+
+'''
+------------------------------------------------------------
+AMPLIFIER MODULE CONFIGURATION GUI (with channel selection)
+------------------------------------------------------------
+'''
+
+class _ConfigurationPane(Qt.QFrame, frmActiChampConfig.Ui_frmActiChampConfig):
+ ''' ActiChamp configuration pane
+ '''
+ def __init__(self, amplifier, *args):
+ ''' Constructor
+ @param amplifier: parent module object
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+ self.tableViewChannels.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
+ self.tableViewAux.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
+
+ # setup content
+ self.amplifier = amplifier
+
+ # emulation combobox
+ self.comboBoxEmulation.setCurrentIndex(self.amplifier.amp.getEmulationMode())
+
+ # channel tables
+ self._fillChannelTables()
+
+ # sample rate combobox
+ sr_index = -1
+ for sr in self.amplifier.sample_rates:
+ self.comboBoxSampleRate.addItem(sr['rate'])
+ if sr == self.amplifier.sample_rate:
+ sr_index = self.comboBoxSampleRate.count()-1
+ self.comboBoxSampleRate.setCurrentIndex(sr_index)
+
+ # reference channel display
+ self.show_reference()
+
+ # actions
+ self.connect(self.comboBoxSampleRate, Qt.SIGNAL("currentIndexChanged(int)"), self._samplerate_changed)
+ self.connect(self.tableViewAux.selectionModel(), Qt.SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self._selectionChanged)
+ self.connect(self.comboBoxEmulation, Qt.SIGNAL("currentIndexChanged(int)"), self._emulationChanged)
+
+ def _fillChannelTables(self):
+ ''' Create and fill channel tables
+ '''
+ # EEG channel table, show available channels only
+ mask = lambda x: (x.group == ChannelGroup.EEG) & (x.input <= self.amplifier.amp.properties.CountEeg)
+ ch_map = np.array([mask(ch) for ch in self.amplifier.channel_config], dtype=bool)
+ ch_indices = np.nonzero(ch_map)[0]
+ self.eeg_model = _ConfigTableModel(self.amplifier.channel_config[ch_indices])
+ self.tableViewChannels.setModel(self.eeg_model)
+ self.tableViewChannels.setItemDelegate(_ConfigItemDelegate())
+ self.tableViewChannels.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
+
+ # AUX channel table, show available channels only
+ mask = lambda x: (x.group == ChannelGroup.AUX) & (x.input <= self.amplifier.amp.properties.CountAux)
+ #mask = lambda x: x.group == ChannelGroup.AUX
+ ch_map = np.array([mask(ch) for ch in self.amplifier.channel_config], dtype=bool)
+ ch_indices = np.nonzero(ch_map)[0]
+ self.aux_model = _ConfigTableModel(self.amplifier.channel_config[ch_indices])
+ self.tableViewAux.setModel(self.aux_model)
+ self.tableViewAux.setItemDelegate(_ConfigItemDelegate())
+ self.tableViewAux.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
+
+ # simulated channels
+ simulated_channels = self.comboBoxEmulation.currentIndex()
+ if simulated_channels > 0:
+ self.label_Simulated.setText("(simulating %i + 8 channels)"%(simulated_channels * 32))
+ else:
+ self.label_Simulated.setText("")
+
+ # actions
+ self.connect(self.eeg_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
+ self.connect(self.aux_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
+
+ def _channeltable_changed(self, topLeft, bottomRight):
+ ''' SIGNAL data in channel table has changed
+ '''
+ # update reference channel display
+ self.show_reference()
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+ def _samplerate_changed(self, index):
+ ''' SIGNAL sample rate combobox value has changed
+ '''
+ if index >= 0:
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('rateChanged(int)'), index)
+ self._fillChannelTables()
+ self.show_reference()
+
+ def _emulationChanged(self, index):
+ ''' SIGNAL emulation mode combobox value has changed
+ '''
+ if index >= 0:
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('emulationChanged(int)'),index)
+ self._fillChannelTables()
+ self.show_reference()
+
+ def _selectionChanged(self, selected, deselected):
+ #print selected.indexes()
+ pass
+
+ def show_reference(self):
+ ''' Display selected reference channel
+ '''
+ # get the selected reference channel index
+ mask = lambda x: x.isReference & (x.group == ChannelGroup.EEG) & (x.input <= self.amplifier.amp.properties.CountEeg)
+ ref = np.array([mask(ch) for ch in self.amplifier.channel_config], dtype=bool)
+ ref_index = np.nonzero(ref)[0]
+ # update display
+ '''
+ if len(ref_index) == 1:
+ self.label_Reference.setText("Selected Reference Channel\r\nCh%d -> %s"%
+ (self.amplifier.channel_config[ref_index[0]].input,
+ self.amplifier.channel_config[ref_index[0]].name))
+ '''
+ if len(ref_index) > 0:
+ labelText = "Selected Reference Channel(s)\r\n"
+ chText = []
+ for idx in ref_index[:10]:
+ chText.append(u"%s"%(self.amplifier.channel_config[idx].name))
+ if len(ref_index) > 10:
+ chText.append("...")
+ labelText += textwrap.fill(" + ".join(chText), 30)
+ self.label_Reference.setText(labelText)
+ else:
+ self.label_Reference.setText("Selected Reference Channel\r\nNone")
+
+
+
+
+
+
+class _ConfigTableModel(Qt.QAbstractTableModel):
+ ''' EEG and AUX table data model for the configuration pane
+ '''
+ def __init__(self, data, parent=None, *args):
+ ''' Constructor
+ @param data: array of EEG_ChannelProperties objects
+ '''
+ Qt.QAbstractTableModel.__init__(self, parent, *args)
+ self.arraydata = data
+ # column description
+ self.columns = [{'property':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
+ {'property':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
+ #{'property':'lowpass', 'header':'High Cutoff', 'edit':False, 'editor':'combobox'},
+ #{'property':'highpass', 'header':'Low Cutoff', 'edit':False, 'editor':'combobox'},
+ #{'property':'notchfilter', 'header':'Notch', 'edit':False, 'editor':'default'},
+ {'property':'name', 'header':'Name', 'edit':True, 'editor':'default'},
+ ]
+
+ # insert reference channel selection to column description for EEG channels
+ if (len(data) > 0) and (data[0].group == ChannelGroup.EEG):
+ self.columns.insert(2,
+ {'property':'isReference', 'header':'Reference', 'edit':True, 'editor':'default'}
+ )
+
+ # combo box list contents
+ self.lowpasslist = ['off', '10', '20', '50', '100', '200', '500', '1000', '2000']
+ self.highpasslist = ['off','0.01', '0.02', '0.05', '0.1', '0.2', '0.5', '1', '2', '5', '10']
+
+ def _getitem(self, row, column):
+ ''' Get amplifier property item based on table row and column
+ @param row: row number
+ @param column: column number
+ @return: QVariant property value
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return Qt.QVariant()
+
+ # get channel properties
+ property = self.arraydata[row]
+ # get property name from column description
+ property_name = self.columns[column]['property']
+ # get property value
+ if property_name == 'input':
+ d = Qt.QVariant(property.input)
+ elif property_name == 'enable':
+ d = Qt.QVariant(property.enable)
+ elif property_name == 'name':
+ d = Qt.QVariant(property.name)
+ elif property_name == 'lowpass':
+ if property.lowpass == 0.0:
+ d = Qt.QVariant('off')
+ else:
+ d = Qt.QVariant(property.lowpass)
+ elif property_name == 'highpass':
+ if property.highpass == 0.0:
+ d = Qt.QVariant('off')
+ else:
+ d = Qt.QVariant(property.highpass)
+ elif property_name == 'notchfilter':
+ d = Qt.QVariant(property.notchfilter)
+ elif property_name == 'isReference':
+ d = Qt.QVariant(property.isReference)
+ else:
+ d = Qt.QVariant()
+ return d
+
+ def _setitem(self, row, column, value):
+ ''' Set amplifier property item based on table row and column
+ @param row: row number
+ @param column: column number
+ @param value: QVariant value object
+ @return: True if property value was set, False if not
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return False
+ # get channel properties
+ property = self.arraydata[row]
+ # get property name from column description
+ property_name = self.columns[column]['property']
+ # set channel property
+ if property_name == 'enable':
+ property.enable = value.toBool()
+ return True
+ elif property_name == 'name':
+ n = value.toString()
+ if n.isEmpty() or n.trimmed().isEmpty():
+ return False
+ property.name = value.toString()
+ return True
+ elif property_name == 'lowpass':
+ property.lowpass,ok = value.toDouble()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.lowpass = property.lowpass
+ return True
+ elif property_name == 'highpass':
+ property.highpass,ok = value.toDouble()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.highpass = property.highpass
+ return True
+ elif property_name == 'notchfilter':
+ property.notchfilter = value.toBool()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.notchfilter = property.notchfilter
+ self.reset()
+ return True
+ elif property_name == 'isReference':
+ # available for EEG channels only
+ if property.group == ChannelGroup.EEG:
+ # remove previously selected reference channel in single channel mode
+ if not AMP_MULTIPLE_REF:
+ if value.toBool() == True:
+ for prop in self.arraydata:
+ prop.isReference = False
+ property.isReference = value.toBool()
+ self.reset()
+ return True
+ return False
+
+ def editorType(self, column):
+ ''' Get the columns editor type from column description
+ @param column: table column number
+ @return: editor type as QVariant (string)
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ return Qt.QVariant(self.columns[column]['editor'])
+
+ def comboBoxList(self, column):
+ ''' Get combo box item list for column 'highpass' or 'lowpass'
+ @param column: table column number
+ @return: combo box item list as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if self.columns[column]['property'] == 'lowpass':
+ return Qt.QVariant(self.lowpasslist)
+ elif self.columns[column]['property'] == 'highpass':
+ return Qt.QVariant(self.highpasslist)
+ else:
+ return Qt.QVariant()
+
+ def rowCount(self, parent):
+ ''' Get the number of required table rows
+ @return: number of rows
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.arraydata)
+
+ def columnCount(self, parent):
+ ''' Get the number of required table columns
+ @return: number of columns
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.columns)
+
+ def data(self, index, role):
+ ''' Abstract method from QAbstactItemModel to get cell data based on role
+ @param index: QModelIndex table cell reference
+ @param role: given role for the item referred to by the index
+ @return: the data stored under the given role for the item referred to by the index
+ '''
+ if not index.isValid():
+ return Qt.QVariant()
+
+ # get the underlying data
+ value = self._getitem(index.row(), index.column())
+
+ if role == Qt.Qt.CheckStateRole:
+ # hide/disable the reference channel
+ if AMP_HIDE_REF:
+ properties = self.arraydata[index.row()]
+ property_name = self.columns[index.column()]['property']
+ if property_name == "enable" and properties.isReference:
+ return Qt.Qt.Unchecked
+
+ # set check state
+ if value.type() == Qt.QMetaType.Bool:
+ if value.toBool():
+ return Qt.Qt.Checked
+ else:
+ return Qt.Qt.Unchecked
+
+ elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
+ if value.type() != Qt.QMetaType.Bool:
+ return value
+
+ elif role == Qt.Qt.BackgroundRole:
+ # change background color for reference channel
+ property = self.arraydata[index.row()]
+ #if (property.isReference) and (index.column() == 0):
+ if (property.isReference):
+ return Qt.QVariant( Qt.QColor(0, 0, 255))
+
+ return Qt.QVariant()
+
+ def flags(self, index):
+ ''' Abstract method from QAbstactItemModel
+ @param index: QModelIndex table cell reference
+ @return: the item flags for the given index
+ '''
+ if not index.isValid():
+ return Qt.Qt.ItemIsEnabled
+ if not self.columns[index.column()]['edit']:
+ return Qt.Qt.ItemIsEnabled
+ value = self._getitem(index.row(), index.column())
+ if value.type() == Qt.QMetaType.Bool:
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
+
+ def setData(self, index, value, role):
+ ''' Abstract method from QAbstactItemModel to set cell data based on role
+ @param index: QModelIndex table cell reference
+ @param value: QVariant new cell data
+ @param role: given role for the item referred to by the index
+ @return: true if successful; otherwise returns false.
+ '''
+ if index.isValid():
+ if role == Qt.Qt.EditRole:
+ if not self._setitem(index.row(), index.column(), value):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
+ return True
+ elif role == Qt.Qt.CheckStateRole:
+ if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
+ return True
+ return False
+
+ def headerData(self, col, orientation, role):
+ ''' Abstract method from QAbstactItemModel to get the column header
+ @param col: column number
+ @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
+ @param role: given role for the item referred to by the index
+ @return: header
+ '''
+ if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
+ return Qt.QVariant(self.columns[col]['header'])
+ return Qt.QVariant()
+
+
+class _ConfigItemDelegate(Qt.QStyledItemDelegate):
+ ''' Combobox item editor
+ '''
+ def __init__(self, parent=None):
+ super(_ConfigItemDelegate, self).__init__(parent)
+
+ def createEditor(self, parent, option, index):
+ if index.model().editorType(index.column()) == 'combobox':
+ combobox = Qt.QComboBox(parent)
+ combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
+ combobox.setEditable(False)
+ self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
+ return combobox
+ return Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
+
+ def setEditorData(self, editor, index):
+ if index.model().columns[index.column()]['editor'] == 'combobox':
+ text = index.model().data(index, Qt.Qt.DisplayRole).toString()
+ i = editor.findText(text)
+ if i == -1:
+ i = 0
+ editor.setCurrentIndex(i)
+ Qt.QStyledItemDelegate.setEditorData(self, editor, index)
+
+
+ def setModelData(self, editor, model, index):
+ if model.columns[index.column()]['editor'] == 'combobox':
+ model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
+ model.reset()
+ Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
+
+ def emitCommitData(self):
+ self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
+
+
+
+
diff --git a/custom_modules/dc_offset.py b/custom_modules/dc_offset.py
index 84612e7..efc1890 100644
--- a/custom_modules/dc_offset.py
+++ b/custom_modules/dc_offset.py
@@ -1,260 +1,264 @@
- # -*- coding: utf-8 -*-
-'''
-DC Offset Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Thomas Dowrick
-@date: $Date: 04.10.2016 $
-@version: 1.0
-
-'''
-
-from modbase import *
-from PyQt4 import QtGui
-from PyQt4 import Qwt5 as Qwt
-import collections
-from PyQt4 import Qt
-from sqlite3.dbapi2 import paramstyle
-
-
-################################################################
-# The module itself
-
-class dc_offset(ModuleBase):
- ''' DC_Offset Module
-
- Calculate and display the DC offset on all EEG channels.
-
- - GUI elements
- - Create and use configuration panes
- - Create and use a DC offset signal pane
- - Data processing
- - calculate and display the DC offset
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor.
- Initialize instance variables and instantiate GUI objects
- '''
- # initialize the base class, give a descriptive name
- ModuleBase.__init__(self, name="DC_Offset", **keys)
-
-
- # initialize module variables
- self.data = None #: hold the data block we got from previous module
- self.dataavailable = False #: data available for output to next module
-
-
- self.onlinePane = _OnlineCfgPane()
-
- self.signalPane = _SignalPane()
- self.setDCScale()
-
- self.connect(self.onlinePane.btnDCOffset,
- Qt.SIGNAL("clicked()"),
- self.toggleDCPane)
-
- self.connect(self.onlinePane.comboBoxScale,
- Qt.SIGNAL("currentIndexChanged(int)"),
- self.setDCScale)
-
- def toggleDCPane(self):
- if self.signalPane.isVisible():
- self.signalPane.hide()
-
- else:
- self.signalPane.show()
-
- #set the Scale of the DC offset plot
- def setDCScale(self):
- scale = self.onlinePane.getCurrentValues()
- self.signalPane.setScale(scale)
-
- def process_input(self, datablock):
- ''' Get data from previous module.
- Because we need to exchange data between different threads we will use
- a queue object to send data to the display thread to be thread-safe.
- @param datablock: EEG_DataBlock object
- '''
- self.dataavailable = True # signal data availability
- self.data = datablock # get a local reference
-
- #Caluclate the mean dc value on each channel and update plot values
- #but only if the pane is visible
-
- if self.signalPane.isVisible():
- dc_offsets = np.mean(datablock.eeg_channels,1)/1000 #Convert from uV to mV
- chan_indices = [x + 1 for x in range(datablock.channel_properties.shape[0])]
-
- self.signalPane.DCValues.setData(chan_indices,dc_offsets)
-
-
- def process_output(self):
- ''' Send data out to next module
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
- def process_update(self, params):
- if params != None:
- #Set the width of the pen for DC offset plot based on the dimensions of the window
- #and the number of channels
- num_channels = params.channel_properties.shape[0]
- frame_width = self.signalPane.width()
- #Calculate sensible value for line width, so that bars don't overlap
- new_pen_width = 0.9*frame_width/ (2*num_channels)
- self.signalPane.setLineWidth(new_pen_width)
-
- return params
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- @return: a QFrame object or None if you don't need a online configuration pane
- '''
- return self.onlinePane
-
- def get_display_pane(self):
- ''' Get the signal pane
- @return: a QFrame object
- '''
- return self.signalPane
-
-################################################################
-# Online Configuration Pane
-
-class _OnlineCfgPane(Qt.QFrame):
- ''' Online configuration pane
- '''
- def __init__(self , *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # make it nice ;-)
- self.setFrameShape(QtGui.QFrame.Panel)
- self.setFrameShadow(QtGui.QFrame.Raised)
-
- # give us a layout and group box
- self.gridLayout = QtGui.QGridLayout(self)
- self.groupBox = QtGui.QGroupBox(self)
- self.groupBox.setTitle("DC Offset")
-
- # group box layout
- self.gridLayoutGroup = QtGui.QGridLayout(self.groupBox)
- self.gridLayoutGroup.setHorizontalSpacing(10)
- self.gridLayoutGroup.setContentsMargins(20, -1, 20, -1)
-
- # add the chunk size combobox
- self.btnDCOffset = QtGui.QPushButton('Show DC',self.groupBox)
- self.btnDCOffset.setCheckable(True)
- self.btnDCOffset.setObjectName("btnDCOffset")
-
-
- # add the frequency range combobox
- self.comboBoxScale = QtGui.QComboBox(self.groupBox)
- self.comboBoxScale.setObjectName("comboBoxScale")
- self.comboBoxScale.addItem(Qt.QString("10"))
- self.comboBoxScale.addItem(Qt.QString("100"))
- self.comboBoxScale.addItem(Qt.QString("250"))
- self.comboBoxScale.addItem(Qt.QString("500"))
-
- # create unit labels
-
- self.labelFrequency = QtGui.QLabel(self.groupBox)
- self.labelFrequency.setText("Scale [mV]")
-
- # add widgets to layouts
- self.gridLayoutGroup.addWidget(self.comboBoxScale, 0, 0, 1, 1)
- self.gridLayoutGroup.addWidget(self.labelFrequency, 0, 1, 1, 1)
- self.gridLayoutGroup.addWidget(self.btnDCOffset, 0, 2, 1, 1)
-
- self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1)
-
- # set default values
- self.comboBoxScale.setCurrentIndex(1)
-
- def getCurrentValues(self):
- scale,ok = self.comboBoxScale.currentText().toFloat()
- return scale
-
- ################################################################
-# Signal Pane
-
-class _SignalPane(Qt.QFrame):
- ''' FFT display pane
- '''
- def __init__(self , *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- self.setMinimumSize(Qt.QSize(200,50))
-
- #Initialise plot object and set background, labels etc
- self.DCplot = Qwt.QwtPlot(self)
- self.DCplot.setCanvasBackground(Qt.Qt.white)
-
- font = Qt.QFont("arial", 9)
- title = Qwt.QwtText('DC Offset')
- xLabel = Qwt.QwtText('Channel number')
- yLabel = Qwt.QwtText('Voltage (mV)')
-
- title.setFont(font)
- xLabel.setFont(font)
- yLabel.setFont(font)
-
- self.DCplot.setTitle(title)
- self.DCplot.setAxisTitle(Qwt.QwtPlot.xBottom,xLabel)
- self.DCplot.setAxisTitle(Qwt.QwtPlot.yLeft,yLabel)
-
-
- self.verticalLayout = QtGui.QVBoxLayout(self)
- self.verticalLayout.addWidget(self.DCplot)
-
- #Simple hack to get a bar graph by using stick plot and making the pen wider than default
- self.DCValues = Qwt.QwtPlotCurve()
- self.DCValues.setStyle(Qwt.QwtPlotCurve.Sticks)
- #Set the line width
- self.setLineWidth(5) #Set pen width to 5 - too big if lots of channels (e.g. 128) are used
- self.DCValues.attach(self.DCplot)
-
- self.grid = Qwt.QwtPlotGrid()
- self.grid.enableXMin(True)
- self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
- self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DashLine))
- self.grid.attach(self.DCplot)
-
- #Start display timer to update plot
- self.startTimer(100)
-
- #Set the y-axis scale
- def setScale(self, scale):
- self.DCplot.setAxisScale(0,-scale,scale)
-
- def setLineWidth(self,width):
-
- self.DCValues.setPen(Qt.QPen(Qt.Qt.red, width, Qt.Qt.SolidLine))
-
- def timerEvent(self,e):
- ''' Display timer callback.
- Get data from input queue and distribute it to the plot widgets
- '''
- self.DCplot.replot()
+ # -*- coding: utf-8 -*-
+'''
+DC Offset Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Thomas Dowrick
+@date: $Date: 04.10.2016 $
+@version: 1.0
+
+'''
+
+from modbase import *
+from PyQt4 import QtGui
+from PyQt4 import Qwt5 as Qwt
+import collections
+from PyQt4 import Qt
+from sqlite3.dbapi2 import paramstyle
+
+
+################################################################
+# The module itself
+
+class dc_offset(ModuleBase):
+ ''' DC_Offset Module
+
+ Calculate and display the DC offset on all EEG channels.
+
+ - GUI elements
+ - Create and use configuration panes
+ - Create and use a DC offset signal pane
+ - Data processing
+ - calculate and display the DC offset
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor.
+ Initialize instance variables and instantiate GUI objects
+ '''
+ # initialize the base class, give a descriptive name
+ ModuleBase.__init__(self, name="DC_Offset", **keys)
+
+
+ # initialize module variables
+ self.data = None #: hold the data block we got from previous module
+ self.dataavailable = False #: data available for output to next module
+
+
+ self.onlinePane = _OnlineCfgPane()
+
+ self.signalPane = _SignalPane()
+ self.setDCScale()
+
+ self.connect(self.onlinePane.btnDCOffset,
+ Qt.SIGNAL("clicked()"),
+ self.toggleDCPane)
+
+ self.connect(self.onlinePane.comboBoxScale,
+ Qt.SIGNAL("currentIndexChanged(int)"),
+ self.setDCScale)
+
+ def toggleDCPane(self):
+ if self.signalPane.isVisible():
+ self.signalPane.hide()
+
+ else:
+ self.signalPane.show()
+
+ # set the Scale of the DC offset plot
+ def setDCScale(self):
+ scale = self.onlinePane.getCurrentValues()
+ self.signalPane.setScale(scale)
+
+ def process_input(self, datablock):
+ ''' Get data from previous module.
+ Because we need to exchange data between different threads we will use
+ a queue object to send data to the display thread to be thread-safe.
+ @param datablock: EEG_DataBlock object
+ '''
+ self.dataavailable = True # signal data availability
+ self.data = datablock # get a local reference
+
+ #Caluclate the mean dc value on each channel and update plot values
+ #but only if the pane is visible
+
+ if self.signalPane.isVisible():
+ dc_offsets = np.mean(datablock.eeg_channels,1)/1000 #Convert from uV to mV
+ chan_indices = [x + 1 for x in range(datablock.channel_properties.shape[0])]
+ self.signalPane.DCValues.setData(chan_indices,dc_offsets)
+
+
+ def process_output(self):
+ ''' Send data out to next module
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+ def process_update(self, params):
+ if params != None:
+ # Set the width of the pen for DC offset plot based on the dimensions of the window
+ # and the number of channels
+ num_channels = params.channel_properties.shape[0]
+ frame_width = self.signalPane.width()
+ # Calculate sensible value for line width, so that bars don't overlap
+ new_pen_width = 0.9*frame_width/ (2*num_channels)
+ self.signalPane.setLineWidth(new_pen_width)
+
+ return params
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ @return: a QFrame object or None if you don't need a online configuration pane
+ '''
+ return self.onlinePane
+
+ def get_display_pane(self):
+ ''' Get the signal pane
+ @return: a QFrame object
+ '''
+ return self.signalPane
+
+################################################################
+# Online Configuration Pane
+
+class _OnlineCfgPane(Qt.QFrame):
+ ''' Online configuration pane
+ '''
+ def __init__(self , *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # make it nice ;-)
+ self.setFrameShape(QtGui.QFrame.Panel)
+ self.setFrameShadow(QtGui.QFrame.Raised)
+
+ # give us a layout and group box
+ self.gridLayout = QtGui.QGridLayout(self)
+ self.groupBox = QtGui.QGroupBox(self)
+ self.groupBox.setTitle("DC Offset")
+
+ # group box layout
+ self.gridLayoutGroup = QtGui.QGridLayout(self.groupBox)
+ self.gridLayoutGroup.setHorizontalSpacing(10)
+ self.gridLayoutGroup.setContentsMargins(20, -1, 20, -1)
+
+ # add the chunk size combobox
+ self.btnDCOffset = QtGui.QPushButton('Show DC',self.groupBox)
+ self.btnDCOffset.setCheckable(True)
+ self.btnDCOffset.setObjectName("btnDCOffset")
+
+
+ # add the frequency range combobox
+ self.comboBoxScale = QtGui.QComboBox(self.groupBox)
+ self.comboBoxScale.setObjectName("comboBoxScale")
+ self.comboBoxScale.addItem(Qt.QString("10"))
+ self.comboBoxScale.addItem(Qt.QString("100"))
+ self.comboBoxScale.addItem(Qt.QString("250"))
+ self.comboBoxScale.addItem(Qt.QString("500"))
+
+ # create unit labels
+
+ self.labelFrequency = QtGui.QLabel(self.groupBox)
+ self.labelFrequency.setText("Scale [mV]")
+
+ # add widgets to layouts
+ self.gridLayoutGroup.addWidget(self.comboBoxScale, 0, 0, 1, 1)
+ self.gridLayoutGroup.addWidget(self.labelFrequency, 0, 1, 1, 1)
+ self.gridLayoutGroup.addWidget(self.btnDCOffset, 0, 2, 1, 1)
+
+ self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1)
+
+ # set default values
+ self.comboBoxScale.setCurrentIndex(1)
+
+ def getCurrentValues(self):
+ text = self.comboBoxScale.currentText()
+ try:
+ scale = float(text)
+ ok = True
+ except Exception:
+ scale = 1.0
+ ok = False
+ return scale
+
+ ################################################################
+# Signal Pane
+
+class _SignalPane(Qt.QFrame):
+ ''' FFT display pane
+ '''
+ def __init__(self , *args):
+ Qt.QFrame.__init__(self, *args)
+
+ self.setMinimumSize(Qt.QSize(200,50))
+
+ #Initialise plot object and set background, labels etc
+ self.DCplot = Qwt.QwtPlot(self)
+ self.DCplot.setCanvasBackground(Qt.Qt.white)
+
+ font = Qt.QFont("arial", 9)
+ title = Qwt.QwtText('DC Offset')
+ xLabel = Qwt.QwtText('Channel number')
+ yLabel = Qwt.QwtText('Voltage (mV)')
+
+ title.setFont(font)
+ xLabel.setFont(font)
+ yLabel.setFont(font)
+
+ self.DCplot.setTitle(title)
+ self.DCplot.setAxisTitle(Qwt.QwtPlot.xBottom,xLabel)
+ self.DCplot.setAxisTitle(Qwt.QwtPlot.yLeft,yLabel)
+
+
+ self.verticalLayout = QtGui.QVBoxLayout(self)
+ self.verticalLayout.addWidget(self.DCplot)
+
+ #Simple hack to get a bar graph by using stick plot and making the pen wider than default
+ self.DCValues = Qwt.QwtPlotCurve()
+ self.DCValues.setStyle(Qwt.QwtPlotCurve.Sticks)
+ #Set the line width
+ self.setLineWidth(5) #Set pen width to 5 - too big if lots of channels (e.g. 128) are used
+ self.DCValues.attach(self.DCplot)
+
+ self.grid = Qwt.QwtPlotGrid()
+ self.grid.enableXMin(True)
+ self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
+ self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DashLine))
+ self.grid.attach(self.DCplot)
+
+ #Start display timer to update plot
+ self.startTimer(100)
+
+ #Set the y-axis scale
+ def setScale(self, scale):
+ self.DCplot.setAxisScale(0,-scale,scale)
+
+ def setLineWidth(self,width):
+ self.DCValues.setPen(Qt.QPen(Qt.Qt.red, width, Qt.Qt.SolidLine))
+
+ def timerEvent(self,e):
+ ''' Display timer callback.
+ Get data from input queue and distribute it to the plot widgets
+ '''
+ self.DCplot.replot()
diff --git a/devices/devbase.py b/devices/devbase.py
index c2e81b6..a2b32ee 100644
--- a/devices/devbase.py
+++ b/devices/devbase.py
@@ -1,263 +1,262 @@
-# -*- coding: utf-8 -*-
-'''
-Base class for all hardware input devices
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-18 16:31:58 +0200 (Di, 18 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 204 $
-'''
-
-
-from modbase import *
-from tools.modview import GenericTableWidget
-
-
-################################################################
-# Base class for input devices
-
-class HardwareInputDevice():
- deviceName = ""
- def __init__(self):
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # device input configuration
- self.inputGroup = ChannelGroup.AUX # input channel group
- self.inputChannel = 1 # device is attached to this channel
- self.inputImpedances = [] # ImpedanceIndex list with required input impedances
- self.possibleGroups = [ChannelGroup.AUX] # groups to which the device can be connected
- self.possibleChannels = range(1,9) # channels to which the device can be connected
-
- # device output configuration
- self.outputGroup = ChannelGroup.EEG # processed channels will be put into this group
- self.outputChannelName = "x" # default channel name prefix
- self.outputImpedances = [] # ImpedanceIndex list for output impedances
-
- # calculated values
- self.inputChannels = np.array([[],[]]) # input channel numbers for this device, grouped by function
- self.input_channel_indices = np.array([[]]) # input channel indices for this device
- self.outputProperties = np.array([])
- self.description = self.deviceName
- self.hasInputImpedances = False
-
- def hasOverlappingInputChannels(self, device):
- ''' check for overlapping input channels
- @param device: HardwareInputDevice to compare with
- @return: True if overlapping channels detected
- '''
- # compare input groups of both devices
- if device.inputGroup != self.inputGroup:
- return False
- # compare input channels of both devices
- i1 = self.inputChannels.flatten()[None,...]
- i2 = device.inputChannels.flatten()[...,None]
- return (i1==i2).any()
-
- def process_updatechannels(self, params):
- ''' Get the whole input data object, select the affected channels
- and return an output property array and indices of processed channels
- '''
- # get the required channel indices
- mask = lambda x: (x.inputgroup == self.inputGroup) and (x.input in self.inputChannels)
- ch_map = np.array(map(mask, params.channel_properties))
- indices = np.nonzero(ch_map)[0] # indices of required channels
- # dictionary with channel number as key and its index in the input data array as value
- idx = dict((x.input, indices[i]) for i, x in enumerate(params.channel_properties[indices]))
- # indices of required channels
- if len(params.channel_properties) == 0:
- self.input_channel_indices = np.array([[]])
- else:
- self.input_channel_indices = np.empty_like(self.inputChannels)
- for i in range(self.input_channel_indices.shape[0]):
- for n in range(self.input_channel_indices.shape[1]):
- if not idx.has_key(self.inputChannels[i][n]):
- self.input_channel_indices = np.array([[]])
- raise Exception("missing input channels for device: " + self.deviceName)
- self.input_channel_indices[i,n] = idx[self.inputChannels[i,n]]
- # Update and return the channel properties for this device
- # take the parameters from the first channel in each function group, modify group and name and use it
- # as output channel for this device
- if self.input_channel_indices.size > 0:
- #print "Input Channel Indices", self.input_channel_indices
- self.outputProperties = params.channel_properties[self.input_channel_indices[:,0]]
- n = 1
- for ch in self.outputProperties:
- ch.enable = False
- ch.group = self.outputGroup
- ch.name = "%s%d"%(self.outputChannelName, n)
- n += 1
- # add impedance identifiers to channel values
- self.hasInputImpedances = (params.eeg_channels[self.input_channel_indices.flatten()][:,self.inputImpedances] == 1).all()
- outputData = params.eeg_channels[self.input_channel_indices[:,0]]
- outputData[:] = 0
- if params.recording_mode == RecordingMode.IMPEDANCE and self.hasInputImpedances:
- for imp_index in self.outputImpedances:
- outputData[:,imp_index] = 1
- # prepare and use the output mask for enabled channel selection
- self.outputMask = self.prepareOutputMask()
- outputProperties = self.outputProperties[self.outputMask]
- outputData = outputData[self.outputMask,:]
- else:
- self.outputProperties = np.array([])
- self.outputMask = np.array([])
- outputData = np.array([[]])
- outputProperties = self.outputProperties
-
- return outputProperties, outputData, self.input_channel_indices.flatten()
-
- def prepareOutputMask(self):
- ''' Create a mask for enabled output channels. Override this function if you want to hide some of the default output channels.
- @return: channel index array
- '''
- # the default implementation enables all output channels
- mask = lambda x: True
- ch_map = np.array(map(mask, self.outputProperties))
- # indices of enabled output channels
- indices = np.nonzero(ch_map)[0]
- return indices
-
-
- def output_function(self, x):
- ''' This is the modules data process function, override this function and implement your own algorithm
- '''
- raise Exception("function not implemented")
-
- def impedance_function(self, x):
- ''' This is the modules impedance process function, override this function and implement your own algorithm
- '''
- raise Exception("function not implemented")
-
- def process_input(self, data):
- ''' Get the whole input channels data block, select and process the affected channels
- and return the processed output channels array or None
- '''
- # anything todo?
- if self.input_channel_indices.size == 0:
- return None
- # select the device input channels
- x = data.eeg_channels[self.input_channel_indices]
- if data.recording_mode == RecordingMode.IMPEDANCE:
- device_channels = self.impedance_function(x)
- else:
- device_channels = self.output_function(x)
- return device_channels[self.outputMask]
-
- def update_device(self):
- pass
-
- def configure_device(self):
- dlg = DeviceConfigDlg()
- curidx = 0
- for idx, item in enumerate(self.possibleGroups):
- dlg.comboGroup.addItem(ChannelGroup.Name[item])
- if item == self.inputGroup:
- curidx = idx
- dlg.comboGroup.setCurrentIndex(curidx)
- dlg.spinboxChannel.setValue(self.inputChannel)
- dlg.spinboxChannel.setMinimum(self.possibleChannels[0])
- dlg.spinboxChannel.setMaximum(self.possibleChannels[-1])
- dlg.setWindowTitle("%s configuration"%(self.deviceName))
- if dlg.exec_() == Qt.QDialog.Accepted:
- self.inputChannel = dlg.spinboxChannel.value()
- self.inputGroup = self.possibleGroups[dlg.comboGroup.currentIndex()]
- self.update_device()
- return True
- return False
-
- def getXML(self):
- ''' Get device properties as XML for configuration file
- @return: objectify XML element
- '''
- E = objectify.E
- ch = E.device(
- E.classname(self.__class__.__name__),
- E.inputgroup(self.inputGroup),
- E.inputchannel(self.inputChannel),
- )
- ch.attrib["version"] = str(self.xmlVersion)
- return ch
-
- def setXML(self, xml):
- ''' Setup device properties from XML configuration file
- @param xml: objectify XML device configuration
- '''
- # check version, has to be lower or equal than current version
- version = xml.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- raise Exception, "Device %s wrong version > %d"%(self.deviceName, self.xmlVersion)
- version = int(version)
-
- # get the values
- self.inputGroup = xml.inputgroup.pyval
- self.inputChannel = xml.inputchannel.pyval
- self.update_device()
- return version
-
-
-
-
-################################################################
-# Default configuration dialog for input devices
-
-class DeviceConfigDlg(Qt.QDialog):
- def __init__(self, parent=None):
- super(DeviceConfigDlg, self).__init__(parent)
- self.labelConnectedTo = Qt.QLabel("Connected to:")
- self.comboGroup = Qt.QComboBox()
- self.labelChannel = Qt.QLabel("Channel #")
- self.spinboxChannel = Qt.QSpinBox()
- self.spinboxChannel.setMinimum(1)
- self.spinboxChannel.setMaximum(256)
- self.buttonBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel)
-
- layout = Qt.QHBoxLayout()
- layout.addWidget(self.labelConnectedTo)
- layout.addWidget(self.comboGroup)
- layout.addWidget(self.labelChannel)
- layout.addWidget(self.spinboxChannel)
-
- vlayout = Qt.QVBoxLayout()
- vlayout.addLayout(layout)
- vlayout.addWidget(self.buttonBox)
-
- self.setLayout(vlayout)
- self.setWindowTitle("Device")
-
- self.connect(self.buttonBox, Qt.SIGNAL("accepted()"), self.accept)
- self.connect(self.buttonBox, Qt.SIGNAL("rejected()"), self.reject)
-
-
-
-
-
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Base class for all hardware input devices
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-18 16:31:58 +0200 (Di, 18 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 204 $
+'''
+
+
+from modbase import *
+from tools.modview import GenericTableWidget
+
+
+################################################################
+# Base class for input devices
+
+class HardwareInputDevice():
+ deviceName = ""
+ def __init__(self):
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # device input configuration
+ self.inputGroup = ChannelGroup.AUX # input channel group
+ self.inputChannel = 1 # device is attached to this channel
+ self.inputImpedances = [] # ImpedanceIndex list with required input impedances
+ self.possibleGroups = [ChannelGroup.AUX] # groups to which the device can be connected
+ self.possibleChannels = range(1,9) # channels to which the device can be connected
+
+ # device output configuration
+ self.outputGroup = ChannelGroup.EEG # processed channels will be put into this group
+ self.outputChannelName = "x" # default channel name prefix
+ self.outputImpedances = [] # ImpedanceIndex list for output impedances
+
+ # calculated values
+ self.inputChannels = np.array([[],[]]) # input channel numbers for this device, grouped by function
+ self.input_channel_indices = np.array([[]]) # input channel indices for this device
+ self.outputProperties = np.array([])
+ self.description = self.deviceName
+ self.hasInputImpedances = False
+
+ def hasOverlappingInputChannels(self, device):
+ ''' check for overlapping input channels
+ @param device: HardwareInputDevice to compare with
+ @return: True if overlapping channels detected
+ '''
+ # compare input groups of both devices
+ if device.inputGroup != self.inputGroup:
+ return False
+ # compare input channels of both devices
+ i1 = self.inputChannels.flatten()[None,...]
+ i2 = device.inputChannels.flatten()[...,None]
+ return (i1==i2).any()
+
+ def process_updatechannels(self, params):
+ ''' Get the whole input data object, select the affected channels
+ and return an output property array and indices of processed channels
+ '''
+ # get the required channel indices
+ mask = lambda x: (x.inputgroup == self.inputGroup) and (x.input in self.inputChannels)
+ ch_map = np.array([mask(ch) for ch in params.channel_properties], dtype=bool)
+ indices = np.nonzero(ch_map)[0] # indices of required channels
+ # dictionary with channel number as key and its index in the input data array as value
+ idx = dict((x.input, indices[i]) for i, x in enumerate(params.channel_properties[indices]))
+ # indices of required channels
+ if len(params.channel_properties) == 0:
+ self.input_channel_indices = np.array([[]])
+ else:
+ self.input_channel_indices = np.empty_like(self.inputChannels)
+ for i in range(self.input_channel_indices.shape[0]):
+ for n in range(self.input_channel_indices.shape[1]):
+ if not idx.has_key(self.inputChannels[i][n]):
+ self.input_channel_indices = np.array([[]])
+ raise Exception("missing input channels for device: " + self.deviceName)
+ self.input_channel_indices[i,n] = idx[self.inputChannels[i,n]]
+ # Update and return the channel properties for this device
+ # take the parameters from the first channel in each function group, modify group and name and use it
+ # as output channel for this device
+ if self.input_channel_indices.size > 0:
+ #print "Input Channel Indices", self.input_channel_indices
+ self.outputProperties = params.channel_properties[self.input_channel_indices[:,0]]
+ n = 1
+ for ch in self.outputProperties:
+ ch.enable = False
+ ch.group = self.outputGroup
+ ch.name = "%s%d"%(self.outputChannelName, n)
+ n += 1
+ # add impedance identifiers to channel values
+ self.hasInputImpedances = (params.eeg_channels[self.input_channel_indices.flatten()][:,self.inputImpedances] == 1).all()
+ outputData = params.eeg_channels[self.input_channel_indices[:,0]]
+ outputData[:] = 0
+ if params.recording_mode == RecordingMode.IMPEDANCE and self.hasInputImpedances:
+ for imp_index in self.outputImpedances:
+ outputData[:,imp_index] = 1
+ # prepare and use the output mask for enabled channel selection
+ self.outputMask = self.prepareOutputMask()
+ outputProperties = self.outputProperties[self.outputMask]
+ outputData = outputData[self.outputMask,:]
+ else:
+ self.outputProperties = np.array([])
+ self.outputMask = np.array([])
+ outputData = np.array([[]])
+ outputProperties = self.outputProperties
+
+ return outputProperties, outputData, self.input_channel_indices.flatten()
+
+ def prepareOutputMask(self):
+ ''' Create a mask for enabled output channels. Override this function if you want to hide some of the default output channels.
+ @return: channel index array
+ '''
+ # the default implementation enables all output channels
+ mask = lambda x: True
+ ch_map = np.array([mask(ch) for ch in self.outputProperties], dtype=bool)
+ # indices of enabled output channels
+ indices = np.nonzero(ch_map)[0]
+ return indices
+
+
+ def output_function(self, x):
+ ''' This is the modules data process function, override this function and implement your own algorithm
+ '''
+ raise Exception("function not implemented")
+
+ def impedance_function(self, x):
+ ''' This is the modules impedance process function, override this function and implement your own algorithm
+ '''
+ raise Exception("function not implemented")
+
+ def process_input(self, data):
+ ''' Get the whole input channels data block, select and process the affected channels
+ and return the processed output channels array or None
+ '''
+ # anything todo?
+ if self.input_channel_indices.size == 0:
+ return None
+ # select the device input channels
+ x = data.eeg_channels[self.input_channel_indices]
+ if data.recording_mode == RecordingMode.IMPEDANCE:
+ device_channels = self.impedance_function(x)
+ else:
+ device_channels = self.output_function(x)
+ return device_channels[self.outputMask]
+
+ def update_device(self):
+ pass
+
+ def configure_device(self):
+ dlg = DeviceConfigDlg()
+ curidx = 0
+ for idx, item in enumerate(self.possibleGroups):
+ dlg.comboGroup.addItem(ChannelGroup.Name[item])
+ if item == self.inputGroup:
+ curidx = idx
+ dlg.comboGroup.setCurrentIndex(curidx)
+ dlg.spinboxChannel.setValue(self.inputChannel)
+ dlg.spinboxChannel.setMinimum(self.possibleChannels[0])
+ dlg.spinboxChannel.setMaximum(self.possibleChannels[-1])
+ dlg.setWindowTitle("%s configuration"%(self.deviceName))
+ if dlg.exec_() == Qt.QDialog.Accepted:
+ self.inputChannel = dlg.spinboxChannel.value()
+ self.inputGroup = self.possibleGroups[dlg.comboGroup.currentIndex()]
+ self.update_device()
+ return True
+ return False
+
+ def getXML(self):
+ ''' Get device properties as XML for configuration file
+ @return: objectify XML element
+ '''
+ E = objectify.E
+ ch = E.device(
+ E.classname(self.__class__.__name__),
+ E.inputgroup(self.inputGroup),
+ E.inputchannel(self.inputChannel),
+ )
+ ch.attrib["version"] = str(self.xmlVersion)
+ return ch
+
+ def setXML(self, xml):
+ ''' Setup device properties from XML configuration file
+ @param xml: objectify XML device configuration
+ '''
+ # check version, has to be lower or equal than current version
+ version = xml.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ raise Exception("Device %s wrong version > %d"%(self.deviceName, self.xmlVersion))
+ version = int(version)
+
+ # get the values
+ self.inputGroup = xml.inputgroup.pyval
+ self.inputChannel = xml.inputchannel.pyval
+ self.update_device()
+ return version
+
+
+
+
+################################################################
+# Default configuration dialog for input devices
+
+class DeviceConfigDlg(Qt.QDialog):
+ def __init__(self, parent=None):
+ super(DeviceConfigDlg, self).__init__(parent)
+ self.labelConnectedTo = Qt.QLabel("Connected to:")
+ self.comboGroup = Qt.QComboBox()
+ self.labelChannel = Qt.QLabel("Channel #")
+ self.spinboxChannel = Qt.QSpinBox()
+ self.spinboxChannel.setMinimum(1)
+ self.spinboxChannel.setMaximum(256)
+ self.buttonBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel)
+
+ layout = Qt.QHBoxLayout()
+ layout.addWidget(self.labelConnectedTo)
+ layout.addWidget(self.comboGroup)
+ layout.addWidget(self.labelChannel)
+ layout.addWidget(self.spinboxChannel)
+
+ vlayout = Qt.QVBoxLayout()
+ vlayout.addLayout(layout)
+ vlayout.addWidget(self.buttonBox)
+
+ self.setLayout(vlayout)
+ self.setWindowTitle("Device")
+
+ self.connect(self.buttonBox, Qt.SIGNAL("accepted()"), self.accept)
+ self.connect(self.buttonBox, Qt.SIGNAL("rejected()"), self.reject)
+
+
+
+
+
+
+
+
diff --git a/devices/devcontainer.py b/devices/devcontainer.py
index 188296b..393c11e 100644
--- a/devices/devcontainer.py
+++ b/devices/devcontainer.py
@@ -1,294 +1,294 @@
-# -*- coding: utf-8 -*-
-'''
-Container class for input devices
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-10 08:40:11 +0200 (Mo, 10 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 199 $
-'''
-
-# add ourself package path to system path
-import os, sys
-res_path = os.path.abspath('devices')
-sys.path.append(res_path)
-
-
-from modbase import *
-import pkgutil
-from inspect import getmembers, isclass
-from tools.modview import GenericTableWidget
-
-################################################################
-# Input device container and factory
-
-class DeviceContainer(Qt.QObject):
- def __init__(self):
- Qt.QObject.__init__(self)
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # a valid device class needs the attribute deviceName
- def isDeviceClass(obj):
- if not isclass(obj):
- return False
- if not hasattr(obj, "deviceName"):
- return False
- return len(obj.deviceName) > 0
-
- # create a dictionary for all device classes within this package
- self.availableDevices = dict()
- for importer, module, ispgk in pkgutil.iter_modules(["../devices", "devices"]):
- classes = getmembers(__import__(module), isDeviceClass)
- self.availableDevices.update(classes)
-
- # initialize class variables
- self.instantiatedDevices = []
-
- def reset(self):
- ''' Remove all instantiated devices
- '''
- self.instantiatedDevices = []
-
- def get_configuration_widget(self):
- self.CfgWidget = DeviceContainerWidget()
- acolumns = [
- {'variable':'deviceName', 'header':'Devices available', 'edit':False, 'editor':'default'},
- ]
- ccolumns = [
- {'variable':'description', 'header':'Devices connected', 'edit':False, 'editor':'default'},
- ]
- cblist = {}
- self.CfgWidget.availabletable.setData(self.availableDevices.values(), acolumns, cblist)
- self.CfgWidget.instantiatedtable.setData(self.instantiatedDevices, ccolumns, cblist)
- self.connect(self.CfgWidget, Qt.SIGNAL("insertDevice(int)"), self._insertDevice)
- self.connect(self.CfgWidget, Qt.SIGNAL("removeDevice(int)"), self._removeDevice)
- self.connect(self.CfgWidget, Qt.SIGNAL("updateDevice(int)"), self._updateDevice)
- return self.CfgWidget
-
- def _insertDevice(self, idx):
- ''' Signal from configuration widget
- '''
- dev = self.availableDevices.values()
- if idx >= 0 and idx < len(dev):
- new_device = dev[idx]()
- if new_device.configure_device():
- # check for already connected input channels
- overlapping = False
- for d in self.instantiatedDevices:
- overlapping |= new_device.hasOverlappingInputChannels(d)
- if not overlapping:
- self.instantiatedDevices.append(new_device)
- self.CfgWidget.instantiatedtable.model().reset()
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
- else:
- Qt.QMessageBox.critical(None,"Can't connect device","Required input channels are already in use by other devices")
-
- def _removeDevice(self, idx):
- ''' Signal from configuration widget
- '''
- if idx >= 0 and idx < len(self.instantiatedDevices):
- del self.instantiatedDevices[idx]
- self.CfgWidget.instantiatedtable.model().reset()
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
-
- def _updateDevice(self, idx):
- ''' Signal from configuration widget
- '''
- if idx >= 0 and idx < len(self.instantiatedDevices):
- device = self.instantiatedDevices[idx]
- while True:
- # configure device
- device.configure_device()
- # check for already connected input channels
- overlapping = False
- for d in self.instantiatedDevices:
- if d != device:
- overlapping |= device.hasOverlappingInputChannels(d)
- if not overlapping:
- break
- else:
- Qt.QMessageBox.critical(None,"Can't reconnect device","Required input channels are already in use by other devices")
-
- self.CfgWidget.instantiatedtable.model().reset()
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
-
-
- def process_update(self, params):
- ''' Get the whole input data object, select the affected channels
- and replace the output property array
- '''
- new_properties = np.array([])
- new_output = np.array([[]])
- processed_indices = np.array([])
-
- # let all connected devices select their channels
- for device in self.instantiatedDevices:
- properties, output, processed = device.process_updatechannels(params)
- if properties.size > 0:
- new_properties = np.concatenate((new_properties, properties))
- if output.size > 0:
- if new_output.size == 0:
- new_output = output
- else:
- new_output = np.concatenate((new_output, output))
- if processed.size > 0:
- processed_indices = np.concatenate((processed_indices, processed))
-
- # remove all processed channels from the original properties
- self.processed_indices = np.unique(processed_indices)
- remaining_channel_properties = np.delete(params.channel_properties, self.processed_indices, 0)
- if params.eeg_channels.size > 0:
- remaining_channel_output = np.delete(params.eeg_channels, self.processed_indices, 0)
- else:
- remaining_channel_output = params.eeg_channels
-
- # add all new channel definitions to the remaining properties
- self.new_channel_properties = np.concatenate((remaining_channel_properties, new_properties))
- params.channel_properties = self.new_channel_properties
- if new_output.size > 0:
- params.eeg_channels = np.concatenate((remaining_channel_output, new_output))
- else:
- params.eeg_channels = remaining_channel_output
- return params
-
- def process_input(self, data):
- ''' let all connected devices process the input data
- '''
- if len(self.instantiatedDevices) > 0:
- data.channel_properties = self.new_channel_properties
- output_channels = np.delete(data.eeg_channels, self.processed_indices, 0)
- for device in self.instantiatedDevices:
- output = device.process_input(data)
- if output != None:
- output_channels = np.concatenate((output_channels, output))
- data.eeg_channels = output_channels
-
-
- def getXML(self):
- ''' Get input device configuration as XML for configuration file
- @return: objectify XML element
- '''
- E = objectify.E
- devices = E.InputDevices()
- for device in self.instantiatedDevices:
- devices.append(device.getXML())
- devices.attrib["version"] = str(self.xmlVersion)
- return devices
-
- def setXML(self, xml):
- ''' Setup device properties from XML configuration file
- @param xml: objectify XML device configuration
- '''
- # remove existing devices
- self.reset()
-
- # check version, has to be lower or equal than current version
- version = xml.InputDevices.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- raise Exception, "Input Device Configuration: wrong version > %d"%(self.xmlVersion)
- version = int(version)
-
- # get and instantiate the input devices
- for device in xml.InputDevices.iterchildren():
- # instantiate device
- if self.availableDevices.has_key(device.classname.pyval):
- d = self.availableDevices[device.classname.pyval]()
- d.setXML(device)
- # check for already connected input channels
- overlapping = False
- for di in self.instantiatedDevices:
- overlapping |= di.hasOverlappingInputChannels(d)
- if not overlapping:
- self.instantiatedDevices.append(d)
- else:
- raise Exception, "Can't connect %s: "%(d.description)+"Required input channels are already in use by other devices"
-
-
-
-
-
-
-
-################################################################
-# Input device container configuration widget
-
-class DeviceContainerWidget(Qt.QWidget):
- def __init__(self, parent=None):
- super(DeviceContainerWidget, self).__init__(parent)
-
- # base layout
- self.gridLayout = Qt.QGridLayout(self)
-
- # create insert / remove buttons
- self.buttonRemove = Qt.QPushButton("<", self)
- self.buttonInsert = Qt.QPushButton(">", self)
-
- # create available device table view
- self.availabletable = GenericTableWidget(self, RowNumbers=True, SelectionBehavior=Qt.QAbstractItemView.SelectRows)
-
- # create instantiated device table view
- self.instantiatedtable = GenericTableWidget(self, RowNumbers=True, SelectionBehavior=Qt.QAbstractItemView.SelectRows)
-
- # add all items to the layout
- self.gridLayout.addWidget(self.availabletable, 0, 0, 3, 2)
- self.gridLayout.addWidget(self.buttonInsert, 1, 2)
- self.gridLayout.addWidget(self.buttonRemove, 2, 2)
- self.gridLayout.addWidget(self.instantiatedtable, 0, 3, 3, 1)
-
- # actions
- self.connect(self.buttonInsert, Qt.SIGNAL("clicked()"), self._insertDevice)
- self.connect(self.buttonRemove, Qt.SIGNAL("clicked()"), self._removeDevice)
- self.connect(self.instantiatedtable, Qt.SIGNAL("doubleClicked(QModelIndex)"), self._updateDevice)
- self.connect(self.availabletable, Qt.SIGNAL("doubleClicked(QModelIndex)"), self._dcinsertDevice)
-
- def _insertDevice(self):
- idx = self.availabletable.getSelectedRow()
- self.emit(Qt.SIGNAL('insertDevice(int)'), idx)
-
- def _dcinsertDevice(self, modelIndex):
- idx = modelIndex.row()
- self.emit(Qt.SIGNAL('insertDevice(int)'), idx)
-
- def _removeDevice(self):
- idx = self.instantiatedtable.getSelectedRow()
- self.emit(Qt.SIGNAL('removeDevice(int)'), idx)
-
- def _updateDevice(self, modelIndex):
- idx = modelIndex.row()
- self.emit(Qt.SIGNAL('updateDevice(int)'), idx)
-
- def resizeEvent(self, event):
- self.availabletable.resizeRowsToContents()
- self.instantiatedtable.resizeRowsToContents()
-
-
+# -*- coding: utf-8 -*-
+'''
+Container class for input devices
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-10 08:40:11 +0200 (Mo, 10 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 199 $
+'''
+
+# add ourself package path to system path
+import os, sys
+res_path = os.path.abspath('devices')
+sys.path.append(res_path)
+
+
+from modbase import *
+import pkgutil
+from inspect import getmembers, isclass
+from tools.modview import GenericTableWidget
+
+################################################################
+# Input device container and factory
+
+class DeviceContainer(Qt.QObject):
+ def __init__(self):
+ Qt.QObject.__init__(self)
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # a valid device class needs the attribute deviceName
+ def isDeviceClass(obj):
+ if not isclass(obj):
+ return False
+ if not hasattr(obj, "deviceName"):
+ return False
+ return len(obj.deviceName) > 0
+
+ # create a dictionary for all device classes within this package
+ self.availableDevices = dict()
+ for importer, module, ispgk in pkgutil.iter_modules(["../devices", "devices"]):
+ classes = getmembers(__import__(module), isDeviceClass)
+ self.availableDevices.update(classes)
+
+ # initialize class variables
+ self.instantiatedDevices = []
+
+ def reset(self):
+ ''' Remove all instantiated devices
+ '''
+ self.instantiatedDevices = []
+
+ def get_configuration_widget(self):
+ self.CfgWidget = DeviceContainerWidget()
+ acolumns = [
+ {'variable':'deviceName', 'header':'Devices available', 'edit':False, 'editor':'default'},
+ ]
+ ccolumns = [
+ {'variable':'description', 'header':'Devices connected', 'edit':False, 'editor':'default'},
+ ]
+ cblist = {}
+ self.CfgWidget.availabletable.setData(self.availableDevices.values(), acolumns, cblist)
+ self.CfgWidget.instantiatedtable.setData(self.instantiatedDevices, ccolumns, cblist)
+ self.connect(self.CfgWidget, Qt.SIGNAL("insertDevice(int)"), self._insertDevice)
+ self.connect(self.CfgWidget, Qt.SIGNAL("removeDevice(int)"), self._removeDevice)
+ self.connect(self.CfgWidget, Qt.SIGNAL("updateDevice(int)"), self._updateDevice)
+ return self.CfgWidget
+
+ def _insertDevice(self, idx):
+ ''' Signal from configuration widget
+ '''
+ dev = self.availableDevices.values()
+ if idx >= 0 and idx < len(dev):
+ new_device = dev[idx]()
+ if new_device.configure_device():
+ # check for already connected input channels
+ overlapping = False
+ for d in self.instantiatedDevices:
+ overlapping |= new_device.hasOverlappingInputChannels(d)
+ if not overlapping:
+ self.instantiatedDevices.append(new_device)
+ self.CfgWidget.instantiatedtable.model().reset()
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+ else:
+ Qt.QMessageBox.critical(None,"Can't connect device","Required input channels are already in use by other devices")
+
+ def _removeDevice(self, idx):
+ ''' Signal from configuration widget
+ '''
+ if idx >= 0 and idx < len(self.instantiatedDevices):
+ del self.instantiatedDevices[idx]
+ self.CfgWidget.instantiatedtable.model().reset()
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+ def _updateDevice(self, idx):
+ ''' Signal from configuration widget
+ '''
+ if idx >= 0 and idx < len(self.instantiatedDevices):
+ device = self.instantiatedDevices[idx]
+ while True:
+ # configure device
+ device.configure_device()
+ # check for already connected input channels
+ overlapping = False
+ for d in self.instantiatedDevices:
+ if d != device:
+ overlapping |= device.hasOverlappingInputChannels(d)
+ if not overlapping:
+ break
+ else:
+ Qt.QMessageBox.critical(None,"Can't reconnect device","Required input channels are already in use by other devices")
+
+ self.CfgWidget.instantiatedtable.model().reset()
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+
+ def process_update(self, params):
+ ''' Get the whole input data object, select the affected channels
+ and replace the output property array
+ '''
+ new_properties = np.array([])
+ new_output = np.array([[]])
+ processed_indices = np.array([])
+
+ # let all connected devices select their channels
+ for device in self.instantiatedDevices:
+ properties, output, processed = device.process_updatechannels(params)
+ if properties.size > 0:
+ new_properties = np.concatenate((new_properties, properties))
+ if output.size > 0:
+ if new_output.size == 0:
+ new_output = output
+ else:
+ new_output = np.concatenate((new_output, output))
+ if processed.size > 0:
+ processed_indices = np.concatenate((processed_indices, processed))
+
+ # remove all processed channels from the original properties
+ self.processed_indices = np.unique(processed_indices)
+ remaining_channel_properties = np.delete(params.channel_properties, self.processed_indices, 0)
+ if params.eeg_channels.size > 0:
+ remaining_channel_output = np.delete(params.eeg_channels, self.processed_indices, 0)
+ else:
+ remaining_channel_output = params.eeg_channels
+
+ # add all new channel definitions to the remaining properties
+ self.new_channel_properties = np.concatenate((remaining_channel_properties, new_properties))
+ params.channel_properties = self.new_channel_properties
+ if new_output.size > 0:
+ params.eeg_channels = np.concatenate((remaining_channel_output, new_output))
+ else:
+ params.eeg_channels = remaining_channel_output
+ return params
+
+ def process_input(self, data):
+ ''' let all connected devices process the input data
+ '''
+ if len(self.instantiatedDevices) > 0:
+ data.channel_properties = self.new_channel_properties
+ output_channels = np.delete(data.eeg_channels, self.processed_indices, 0)
+ for device in self.instantiatedDevices:
+ output = device.process_input(data)
+ if output != None:
+ output_channels = np.concatenate((output_channels, output))
+ data.eeg_channels = output_channels
+
+
+ def getXML(self):
+ ''' Get input device configuration as XML for configuration file
+ @return: objectify XML element
+ '''
+ E = objectify.E
+ devices = E.InputDevices()
+ for device in self.instantiatedDevices:
+ devices.append(device.getXML())
+ devices.attrib["version"] = str(self.xmlVersion)
+ return devices
+
+ def setXML(self, xml):
+ ''' Setup device properties from XML configuration file
+ @param xml: objectify XML device configuration
+ '''
+ # remove existing devices
+ self.reset()
+
+ # check version, has to be lower or equal than current version
+ version = xml.InputDevices.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ raise Exception("Input Device Configuration: wrong version > %d"%(self.xmlVersion))
+ version = int(version)
+
+ # get and instantiate the input devices
+ for device in xml.InputDevices.iterchildren():
+ # instantiate device
+ if self.availableDevices.has_key(device.classname.pyval):
+ d = self.availableDevices[device.classname.pyval]()
+ d.setXML(device)
+ # check for already connected input channels
+ overlapping = False
+ for di in self.instantiatedDevices:
+ overlapping |= di.hasOverlappingInputChannels(d)
+ if not overlapping:
+ self.instantiatedDevices.append(d)
+ else:
+ raise Exception("Can't connect %s: "%(d.description)+"Required input channels are already in use by other devices")
+
+
+
+
+
+
+
+################################################################
+# Input device container configuration widget
+
+class DeviceContainerWidget(Qt.QWidget):
+ def __init__(self, parent=None):
+ super(DeviceContainerWidget, self).__init__(parent)
+
+ # base layout
+ self.gridLayout = Qt.QGridLayout(self)
+
+ # create insert / remove buttons
+ self.buttonRemove = Qt.QPushButton("<", self)
+ self.buttonInsert = Qt.QPushButton(">", self)
+
+ # create available device table view
+ self.availabletable = GenericTableWidget(self, RowNumbers=True, SelectionBehavior=Qt.QAbstractItemView.SelectRows)
+
+ # create instantiated device table view
+ self.instantiatedtable = GenericTableWidget(self, RowNumbers=True, SelectionBehavior=Qt.QAbstractItemView.SelectRows)
+
+ # add all items to the layout
+ self.gridLayout.addWidget(self.availabletable, 0, 0, 3, 2)
+ self.gridLayout.addWidget(self.buttonInsert, 1, 2)
+ self.gridLayout.addWidget(self.buttonRemove, 2, 2)
+ self.gridLayout.addWidget(self.instantiatedtable, 0, 3, 3, 1)
+
+ # actions
+ self.connect(self.buttonInsert, Qt.SIGNAL("clicked()"), self._insertDevice)
+ self.connect(self.buttonRemove, Qt.SIGNAL("clicked()"), self._removeDevice)
+ self.connect(self.instantiatedtable, Qt.SIGNAL("doubleClicked(QModelIndex)"), self._updateDevice)
+ self.connect(self.availabletable, Qt.SIGNAL("doubleClicked(QModelIndex)"), self._dcinsertDevice)
+
+ def _insertDevice(self):
+ idx = self.availabletable.getSelectedRow()
+ self.emit(Qt.SIGNAL('insertDevice(int)'), idx)
+
+ def _dcinsertDevice(self, modelIndex):
+ idx = modelIndex.row()
+ self.emit(Qt.SIGNAL('insertDevice(int)'), idx)
+
+ def _removeDevice(self):
+ idx = self.instantiatedtable.getSelectedRow()
+ self.emit(Qt.SIGNAL('removeDevice(int)'), idx)
+
+ def _updateDevice(self, modelIndex):
+ idx = modelIndex.row()
+ self.emit(Qt.SIGNAL('updateDevice(int)'), idx)
+
+ def resizeEvent(self, event):
+ self.availabletable.resizeRowsToContents()
+ self.instantiatedtable.resizeRowsToContents()
+
+
diff --git a/devices/epp.py b/devices/epp.py
index 5c309fd..6f1ce41 100644
--- a/devices/epp.py
+++ b/devices/epp.py
@@ -1,241 +1,240 @@
-# -*- coding: utf-8 -*-
-'''
-EP-PreAmp input device
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-10 12:20:40 +0200 (Mo, 10 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 201 $
-'''
-
-from modbase import *
-from devbase import HardwareInputDevice
-from tools.modview import GenericTableWidget
-
-class EppChProperty(object):
- def __init__(self):
- self.setDefault()
- def setDefault(self):
- self.gain = "50"
-
-class DeviceEpPreamp(HardwareInputDevice):
- deviceName = "EP-PreAmp"
- def __init__(self):
- # initialize the base class
- HardwareInputDevice.__init__(self)
-
- # device input configuration
- self.inputGroup = ChannelGroup.EEG # input channel group
- self.inputChannel = 1 # device is attached to this channel
- self.inputImpedances = [ImpedanceIndex.DATA, ImpedanceIndex.GND] # we need impedance values for each input channel
- self.possibleGroups = [ChannelGroup.EEG]
- self.possibleChannels = range(1,161)
- self.possibleGains = ['off','1','50']
-
- # device output configuration
- self.outputGroup = ChannelGroup.EPP
- self.outputImpedances = [ImpedanceIndex.DATA, ImpedanceIndex.REF, ImpedanceIndex.GND]
- #self.outputGroup = ChannelGroup.AUX
- self.outputChannelName = "EP"
-
- # set default input channel gains
- self.channelProperties = []
- for c in range(0, 16):
- prop = EppChProperty()
- if c >= 2:
- prop.gain = "off"
- self.channelProperties.append(prop)
-
- self.update_device()
-
- def prepareOutputMask(self):
- ''' Create a mask for enabled output channels. Hide all channels with gain = "off".
- @return: channel index array
- '''
- mask = lambda x: x.gain != "off"
- ch_map = np.array(map(mask, self.channelProperties))
- # indices of enabled output channels
- indices = np.nonzero(ch_map)[0]
- return indices
-
- def output_function(self, x):
- # subtract reference channels from data channels and scale to gain
- return (x[:,0] - x[:,1]) * self.gain_div
-
- def impedance_function(self, x):
- # put the electrode impedance values from the reference channels into the data channels
- # keep the GND value from data channel
- out = x[:,0]
- ref = x[:,1]
- # set unused values to 0
- mask = (np.zeros(out.shape[1]) == 0)
- mask[self.outputImpedances] = False
- out[:,mask] = 0
- # combine data and reference impedances
- out[:,ImpedanceIndex.REF] = ref[:,ImpedanceIndex.DATA]
- return out
-
- def update_device(self):
- ''' Configure the input channel numbers, based on the actiCHamp EEG channel number
- '''
- # adjust input channel to module boundaries
- self.inputChannel = ((self.inputChannel-1)/32) * 32 + 1
- self.inputChannels = np.arange(self.inputChannel, self.inputChannel+32).reshape(-1,2)
- # create the gain divisor array
- g = []
- for p in self.channelProperties:
- try:
- gdiv = 0.5 / float(p.gain)
- except:
- gdiv = 0.0
- g.append(gdiv)
- self.gain_div = np.array(g, float)[:,np.newaxis]
- self.description = "%s connected to %s channels %i-%i\nGain: %s"%(self.deviceName,
- ChannelGroup.Name[self.inputGroup],
- self.inputChannel,
- self.inputChannel+31,
- ", ".join(p.gain for p in self.channelProperties))
-
- def configure_device(self):
- ''' override default configuration dialog
- '''
- dlg = EPPConfigDlg()
- # gain selection
- columns = [
- {'variable':'gain', 'header':'Input gain', 'edit':True, 'editor':'combobox'},
- ]
- cblist = {'gain':self.possibleGains}
- dlg.gaintable.setData(self.channelProperties, columns, cblist)
-
- # module number from channel
- module = ((self.inputChannel-1)/32) + 1
- dlg.spinboxModule.setValue(module)
- dlg.setWindowTitle("%s configuration"%(self.deviceName))
- if dlg.exec_() == Qt.QDialog.Accepted:
- # adjust input channel to module boundaries
- self.inputChannel = (dlg.spinboxModule.value()-1) * 32 + 1
- self.inputGroup = self.possibleGroups[0]
- self.update_device()
- return True
- return False
-
-
- def getXML(self):
- ''' Get device properties as XML for configuration file
- @return: objectify XML element
- '''
- E = objectify.E
- properties = E.properties()
- for c in range(len(self.channelProperties)):
- chproperty = E.property(
- E.channel(c),
- E.gain(self.channelProperties[c].gain)
- )
- properties.append(chproperty)
-
- ch = E.device(
- E.classname(self.__class__.__name__),
- E.inputgroup(self.inputGroup),
- E.inputchannel(self.inputChannel),
- properties,
- )
- ch.attrib["version"] = str(self.xmlVersion)
- return ch
-
- def setXML(self, xml):
- ''' Setup device properties from XML configuration file
- @param xml: objectify XML device configuration
- '''
- # check version, has to be lower or equal than current version
- version = xml.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- raise Exception, "Device %s wrong version > %d"%(self.deviceName, self.xmlVersion)
- version = int(version)
-
- # get the values
- self.inputGroup = xml.inputgroup.pyval
- self.inputChannel = xml.inputchannel.pyval
-
- # get channel properties
- for prop in self.channelProperties:
- prop.setDefault()
- try:
- if xml.find("properties") != None:
- for prop in xml.properties.iterchildren():
- c = prop.channel.pyval
- if c >= 0 and c < len(self.channelProperties):
- self.channelProperties[c].gain = prop.gain.pyval
- except:
- pass
-
- self.update_device()
- return version
-
-
-
-
-################################################################
-# Default configuration dialog for input devices
-
-class EPPConfigDlg(Qt.QDialog):
- def __init__(self, parent=None):
- super(EPPConfigDlg, self).__init__(parent)
-
- # create module selection items
- self.labelConnectedTo = Qt.QLabel("Connected to ")
- self.labelModule = Qt.QLabel("EEG module #")
- self.spinboxModule = Qt.QSpinBox()
- self.spinboxModule.setMinimum(1)
- self.spinboxModule.setMaximum(5)
- self.buttonBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel)
-
- # create gain table view
- self.gaintable = GenericTableWidget(self, RowNumbers=True)
- self.gaintable.setMaximumWidth(150)
-
- # create layout
- layout = Qt.QGridLayout()
-
- layout.addWidget(self.labelConnectedTo, 0, 0)
- layout.addWidget(self.labelModule, 0, 1)
- layout.addWidget(self.spinboxModule, 0, 2)
- layout.addWidget(self.gaintable, 1, 2, 1, 1)
-
- vlayout = Qt.QVBoxLayout()
- vlayout.addLayout(layout)
- vlayout.addWidget(self.buttonBox)
-
- self.setLayout(vlayout)
- self.setWindowTitle("Device")
-
- self.connect(self.buttonBox, Qt.SIGNAL("accepted()"), self.accept)
- self.connect(self.buttonBox, Qt.SIGNAL("rejected()"), self.reject)
-
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+EP-PreAmp input device
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-10 12:20:40 +0200 (Mo, 10 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 201 $
+'''
+
+from modbase import *
+from devbase import HardwareInputDevice
+from tools.modview import GenericTableWidget
+
+class EppChProperty(object):
+ def __init__(self):
+ self.setDefault()
+ def setDefault(self):
+ self.gain = "50"
+
+class DeviceEpPreamp(HardwareInputDevice):
+ deviceName = "EP-PreAmp"
+ def __init__(self):
+ # initialize the base class
+ HardwareInputDevice.__init__(self)
+
+ # device input configuration
+ self.inputGroup = ChannelGroup.EEG # input channel group
+ self.inputChannel = 1 # device is attached to this channel
+ self.inputImpedances = [ImpedanceIndex.DATA, ImpedanceIndex.GND] # we need impedance values for each input channel
+ self.possibleGroups = [ChannelGroup.EEG]
+ self.possibleChannels = range(1,161)
+ self.possibleGains = ['off','1','50']
+
+ # device output configuration
+ self.outputGroup = ChannelGroup.EPP
+ self.outputImpedances = [ImpedanceIndex.DATA, ImpedanceIndex.REF, ImpedanceIndex.GND]
+ #self.outputGroup = ChannelGroup.AUX
+ self.outputChannelName = "EP"
+
+ # set default input channel gains
+ self.channelProperties = []
+ for c in range(0, 16):
+ prop = EppChProperty()
+ if c >= 2:
+ prop.gain = "off"
+ self.channelProperties.append(prop)
+
+ self.update_device()
+
+ def prepareOutputMask(self):
+ ''' Create a mask for enabled output channels. Hide all channels with gain = "off".
+ @return: channel index array
+ '''
+ mask = lambda x: x.gain != "off"
+ ch_map = np.array([mask(ch) for ch in self.channelProperties], dtype=bool)
+ # indices of enabled output channels
+ indices = np.nonzero(ch_map)[0]
+ return indices
+
+ def output_function(self, x):
+ # subtract reference channels from data channels and scale to gain
+ return (x[:,0] - x[:,1]) * self.gain_div
+
+ def impedance_function(self, x):
+ # put the electrode impedance values from the reference channels into the data channels
+ # keep the GND value from data channel
+ out = x[:,0]
+ ref = x[:,1]
+ # set unused values to 0
+ mask = (np.zeros(out.shape[1]) == 0)
+ mask[self.outputImpedances] = False
+ out[:,mask] = 0
+ # combine data and reference impedances
+ out[:,ImpedanceIndex.REF] = ref[:,ImpedanceIndex.DATA]
+ return out
+
+ def update_device(self):
+ ''' Configure the input channel numbers, based on the actiCHamp EEG channel number
+ '''
+ # adjust input channel to module boundaries
+ self.inputChannel = ((self.inputChannel-1)/32) * 32 + 1
+ self.inputChannels = np.arange(self.inputChannel, self.inputChannel+32).reshape(-1,2)
+ # create the gain divisor array
+ g = []
+ for p in self.channelProperties:
+ try:
+ gdiv = 0.5 / float(p.gain)
+ except:
+ gdiv = 0.0
+ g.append(gdiv)
+ self.gain_div = np.array(g, float)[:,np.newaxis]
+ self.description = "%s connected to %s channels %i-%i\nGain: %s"%(self.deviceName,
+ ChannelGroup.Name[self.inputGroup],
+ self.inputChannel,
+ self.inputChannel+31,
+ ", ".join(p.gain for p in self.channelProperties))
+
+ def configure_device(self):
+ ''' override default configuration dialog
+ '''
+ dlg = EPPConfigDlg()
+ # gain selection
+ columns = [
+ {'variable':'gain', 'header':'Input gain', 'edit':True, 'editor':'combobox'},
+ ]
+ cblist = {'gain':self.possibleGains}
+ dlg.gaintable.setData(self.channelProperties, columns, cblist)
+
+ # module number from channel
+ module = ((self.inputChannel-1)/32) + 1
+ dlg.spinboxModule.setValue(module)
+ dlg.setWindowTitle("%s configuration"%(self.deviceName))
+ if dlg.exec_() == Qt.QDialog.Accepted:
+ # adjust input channel to module boundaries
+ self.inputChannel = (dlg.spinboxModule.value()-1) * 32 + 1
+ self.inputGroup = self.possibleGroups[0]
+ self.update_device()
+ return True
+ return False
+
+
+ def getXML(self):
+ ''' Get device properties as XML for configuration file
+ @return: objectify XML element
+ '''
+ E = objectify.E
+ properties = E.properties()
+ for c in range(len(self.channelProperties)):
+ chproperty = E.property(
+ E.channel(c),
+ E.gain(self.channelProperties[c].gain)
+ )
+ properties.append(chproperty)
+
+ ch = E.device(
+ E.classname(self.__class__.__name__),
+ E.inputgroup(self.inputGroup),
+ E.inputchannel(self.inputChannel),
+ properties,
+ )
+ ch.attrib["version"] = str(self.xmlVersion)
+ return ch
+
+ def setXML(self, xml):
+ ''' Setup device properties from XML configuration file
+ @param xml: objectify XML device configuration
+ '''
+ # check version, has to be lower or equal than current version
+ version = xml.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ raise Exception("Device %s wrong version > %d"%(self.deviceName, self.xmlVersion))
+ version = int(version)
+
+ # get the values
+ self.inputGroup = xml.inputgroup.pyval
+ self.inputChannel = xml.inputchannel.pyval
+
+ # get channel properties
+ for prop in self.channelProperties:
+ prop.setDefault()
+ try:
+ if xml.find("properties") != None:
+ for prop in xml.properties.iterchildren():
+ c = prop.channel.pyval
+ if c >= 0 and c < len(self.channelProperties):
+ self.channelProperties[c].gain = prop.gain.pyval
+ except:
+ pass
+
+ self.update_device()
+ return version
+
+
+
+
+################################################################
+# Default configuration dialog for input devices
+
+class EPPConfigDlg(Qt.QDialog):
+ def __init__(self, parent=None):
+ super(EPPConfigDlg, self).__init__(parent)
+
+ # create module selection items
+ self.labelConnectedTo = Qt.QLabel("Connected to ")
+ self.labelModule = Qt.QLabel("EEG module #")
+ self.spinboxModule = Qt.QSpinBox()
+ self.spinboxModule.setMinimum(1)
+ self.spinboxModule.setMaximum(5)
+ self.buttonBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel)
+
+ # create gain table view
+ self.gaintable = GenericTableWidget(self, RowNumbers=True)
+ self.gaintable.setMaximumWidth(150)
+
+ # create layout
+ layout = Qt.QGridLayout()
+
+ layout.addWidget(self.labelConnectedTo, 0, 0)
+ layout.addWidget(self.labelModule, 0, 1)
+ layout.addWidget(self.spinboxModule, 0, 2)
+ layout.addWidget(self.gaintable, 1, 2, 1, 1)
+
+ vlayout = Qt.QVBoxLayout()
+ vlayout.addLayout(layout)
+ vlayout.addWidget(self.buttonBox)
+
+ self.setLayout(vlayout)
+ self.setWindowTitle("Device")
+
+ self.connect(self.buttonBox, Qt.SIGNAL("accepted()"), self.accept)
+ self.connect(self.buttonBox, Qt.SIGNAL("rejected()"), self.reject)
+
+
+
+
diff --git a/display.py b/display.py
index 83fc43f..dd28885 100644
--- a/display.py
+++ b/display.py
@@ -1,940 +1,991 @@
-# -*- coding: utf-8 -*-
-'''
-Display Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 197 $
-'''
-
-from PyQt4 import Qt
-from PyQt4 import Qwt5 as Qwt
-from modbase import *
-from res import frmScopeOnline
-from operator import itemgetter
-from collections import defaultdict
-
-'''
-------------------------------------------------------------
-DISPLAY MODULE
-------------------------------------------------------------
-'''
-
-class DISP_Scope(Qwt.QwtPlot, ModuleBase):
- """ EEG signal display widget.
- """
- def __init__(self, *args, **keys):
- ModuleBase.__init__(self, usethread=True, name="Display", **keys) # use transmit / receive thread
- Qwt.QwtPlot.__init__(self, *args)
-
- self.setMinimumSize(Qt.QSize(400, 200))
- self.setObjectName("Display")
-
- # XML parameter version
- # 1: initial version
- # 2: scale and group size added
- # 3: baseline correction flag added
- # 4: timebase values changed from per division to screen (factor 10)
- # Type of timebase, scale and groupsize changed from string to float
- # 5: separate scale values for EEG and AUX channels
- self.xmlVersion = 5
-
- #self.setTitle('ActiChamp');
- self.setCanvasBackground(Qt.Qt.white)
-
- # create online configuration pane
- self.online_cfg = _OnlineCfgPane()
- self.connect(self.online_cfg.comboBoxTime, Qt.SIGNAL("activated(QString)"),
- self.timebaseChanged)
- self.connect(self.online_cfg.comboBoxScale, Qt.SIGNAL("currentIndexChanged(QString)"),
- self.scaleChanged)
- self.connect(self.online_cfg.comboBoxChannels, Qt.SIGNAL("currentIndexChanged(int)"),
- self.channelsChanged)
- self.connect(self.online_cfg.pushButton_Now, Qt.SIGNAL("clicked()"),
- self.baselineNowClicked)
- self.connect(self.online_cfg.checkBoxBaseline, Qt.SIGNAL("stateChanged()"),
- self.baselineNowClicked)
-
- # legend
- legend = _ScopeLegend()
- legend.setFrameStyle(Qt.QFrame.Box | Qt.QFrame.Sunken)
- legend.setItemMode(Qwt.QwtLegend.ClickableItem)
- self.insertLegend(legend, Qwt.QwtPlot.LeftLegend)
- self.connect(self, Qt.SIGNAL("legendClicked(QwtPlotItem*)"),
- self.channelItemClicked)
-
- # grid
- self.grid = Qwt.QwtPlotGrid()
- self.grid.enableY(False)
- self.grid.enableX(True)
- self.grid.enableXMin(True)
- self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
- self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DashLine))
- self.grid.attach(self)
-
- # X axes
- font = Qt.QFont("arial", 9)
- title = Qwt.QwtText('Time [s]')
- title.setFont(font)
- self.setAxisTitle(Qwt.QwtPlot.xBottom, title);
- self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 5);
- self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 10);
- self.setAxisFont(Qwt.QwtPlot.xBottom, font)
- self.TimeScale = _TimeScaleDraw()
- self.setAxisScaleDraw(Qwt.QwtPlot.xBottom, self.TimeScale)
-
- # Y axis
- self.setAxisTitle(Qwt.QwtPlot.yLeft, 'Amplitude');
- self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 0);
- self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0);
- self.enableAxis(Qwt.QwtPlot.yLeft, False)
-
- self.plotscale = Qwt.QwtPlotScaleItem(Qwt.QwtScaleDraw.RightScale)
- self.plotscale.setBorderDistance(5)
- self.plotscale.attach(self)
-
- # reset trace buffer
- self.traces = []
-
- # reset marker buffer
- self.plot_markers = [] # list of QwtPlotMarker()
- self.input_markers = [] # list of EEG markers
-
- # EEG data block backup
- self.last_eeg = None
- self.last_slice = None
-
- # default settings
- self.setScale(self.online_cfg.get_scale()) # µV / Division
- self.timebase = self.online_cfg.get_timebase() # s / Screen
- self.xsize = 1500
- self.binning = 300
- self.binningoffset = 0
- self.channel_slice = slice(0,0,1) # channel group selection
- self.baseline_request = False
- self.selectedChannel = None
-
- # set default display
- self.eeg = EEG_DataBlock()
- self.process_update(self.eeg)
-
- # timing test
- self.ttime = -1.0
- self.tcount = 10.0
-
- # start self.timerEvent() to update display asynchronously
- self.startTimer(30)
- self.update_display = False
- self.dataavailable = False
-
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- self.setScale(self.online_cfg.set_scale(100.0, 1000.0)) # EEG: 100µV, AUX: 1000µV / Division
- self.timebase = self.online_cfg.set_timebase(10.0) # 10s / Screen
- self.online_cfg.set_groupsize(16) # group size 16 channels
- self.online_cfg.checkBoxBaseline.setChecked(True) # baseline correction enabled
-
- # update display
- self.process_update(self.eeg)
-
-
-
- def process_update(self, params):
- ''' Channel properties have changed, module needs update
- @param params: EEG_DataBlock with channel properties
- '''
- if params != None:
- self.eeg = params
- self.online_cfg.update_content(self.eeg)
- self.arrangeTraces()
- self.replot()
- return params
-
- def process_start(self):
- ''' Module start command.
- '''
- # reset timing test timer
- self.ttime = -1.0
-
- def process_input(self, datablock):
- ''' Data available
- @param datablock: EEG_DataBlock with channel data
- '''
- # don't display impedance date
- if datablock.recording_mode == RecordingMode.IMPEDANCE:
- return
-
- # get data from source
- self.eeg = datablock
- self.dataavailable = True
-
- # timing test functions
- # first call
- if self.ttime < 0:
- self.ttime = time.clock()
- self.tcount = 30.0
- else:
- if time.clock() >= self.ttime + self.tcount:
- # send status info
- info = "Received Samples = %d / Sample Counter = %d / Time = %.3fs"\
- %(self.eeg.sample_counter, self.eeg.sample_channel[0,-1]+1, time.clock()-self.ttime)
- #self.send_event(ModuleEvent(self._object_name, EventType.MESSAGE))
- self.tcount += 30.0
-
- # update display buffers
- self.setDisplay()
-
-
- def process_output(self):
- ''' Send data to next module
- @return: EEG_DataBlock if available, else return None
- '''
- if self.dataavailable:
- self.dataavailable = False
- # send performance / utilization event
- totaltime = 1000.0 * self.eeg.performance_timer_max
- sampletime = 1000.0 * totaltime / self.eeg.sample_channel.shape[1]
- utilization = sampletime * self.eeg.sample_rate / 1e6 * 100.0
- if self._instance == 0:
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = utilization,
- status_field = "Utilization"))
- return None
-
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
- e.g.
-
- 1000
- ...
-
- '''
- eeg_scale, aux_scale = self.online_cfg.get_groupscale()
- E = objectify.E
- cfg = E.DISP_Scope(E.timebase(self.online_cfg.get_timebase()),
- E.eegscale(eeg_scale),
- E.auxscale(aux_scale),
- E.groupsize(self.online_cfg.get_groupsize()),
- E.baseline(self.online_cfg.checkBoxBaseline.isChecked()),
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="display")
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search my configuration data
- displays = xml.xpath("//DISP_Scope[@module='display' and @instance='%i']"%(self._instance))
- if len(displays) == 0:
- # configuration data not found, leave everything unchanged
- return
-
- # we should have only one display instance from this type
- cfg = displays[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- # set closest matching timebase
- timebase = cfg.timebase.pyval
- if version < 4:
- timebase = float(timebase) * 10.0
- self.timebase = self.online_cfg.set_timebase(timebase)
-
- if version > 1:
- # set closest matching scale
- if version < 4:
- eeg_scale = float(cfg.scale.pyval)
- aux_scale = float(cfg.scale.pyval)
- elif version < 5:
- eeg_scale = cfg.scale.pyval
- aux_scale = cfg.scale.pyval
- else:
- eeg_scale = cfg.eegscale.pyval
- aux_scale = cfg.auxscale.pyval
- self.setScale(self.online_cfg.set_scale(eeg_scale, aux_scale))
-
- # set closest matching group size
- size = cfg.groupsize.pyval
- if version < 4:
- size = float(size)
- self.online_cfg.set_groupsize(size)
-
- if version > 2:
- # get baseline correction flag
- self.online_cfg.checkBoxBaseline.setChecked(cfg.baseline.pyval)
-
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- '''
- return self.online_cfg
-
- def get_display_pane(self):
- ''' Get the signal display pane
- '''
- return self
-
- def settingsChanged(self):
- ''' Check if settings have changed since last call
- '''
- changed = (self.last_eeg == None) | (self.last_slice != self.channel_slice)
- if not changed:
- changed = (self.eeg != self.last_eeg)
- if changed:
- self.last_eeg = copy.copy(self.eeg)
- self.last_slice = self.channel_slice
- return changed
-
-
- def arrangeTraces(self):
- ''' Setup display traces according to EEG data block settings
- '''
- # select the requested channel group
- self.channel_group = self.eeg.eeg_channels[self.channel_slice]
- self.channel_group_properties = self.eeg.channel_properties[self.channel_slice]
-
- # remove old traces
- for pc in self.traces:
- pc.detach()
- self.traces = []
-
- # insert new traces
- font = Qt.QFont("arial", 8)
- for pccount in xrange(self.channel_group.shape[0]):
- color = self.channel_group_properties[pccount].color
- title = Qwt.QwtText(self.channel_group_properties[pccount].name)
- title.setFont(font)
- title.setColor(color)
- title.setPaintAttribute(Qwt.QwtText.PaintUsingTextFont)
- pc = Qwt.QwtPlotCurve(title)
- pc.setPen(Qt.QPen(color, 0))
- pc.setYAxis(Qwt.QwtPlot.yLeft)
- pc.setPaintAttribute(Qwt.QwtPlotCurve.PaintFiltered)
- pc.attach(self)
- self.traces.append(pc)
-
- # reduce the legend items margin
- for item in self.legend().legendItems():
- item.setMargin(0)
-
- # update Y axis scale
- self.setAxisScale(Qwt.QwtPlot.yLeft, -1.0, len(self.traces), 1.0)
-
- # update sample buffer
- self.setTimebase(self.timebase)
-
- def addPlotMarker(self, xPosition, label, samplecounter):
- ''' Create and add trigger event marker
- @param xPosition: horizontal screen position
- @param label: label string
- @param samplecounter: total sample position
- '''
- margin = float(len(self.traces) + 1) / 10.0 / 3.0 # 2.5% bottom margin
- sym = Qwt.QwtSymbol()
- sym.setStyle(Qwt.QwtSymbol.VLine)
- sym.setSize(20)
- mX = Qwt.QwtPlotMarker()
- mX.setLabel(Qwt.QwtText(label))
- mX.setLabelAlignment(Qt.Qt.AlignHCenter | Qt.Qt.AlignBottom)
- mX.setLineStyle(Qwt.QwtPlotMarker.NoLine)
- mX.setXValue(xPosition)
- mX.setYValue(-1.0 + margin)
- mX.setSymbol(sym)
- mX.sampleCounter = samplecounter
- mX.attach(self)
- self.plot_markers.append(mX)
-
- def setDisplay(self):
- ''' Copy all traces from input buffer to display transfer buffer
- '''
- # select the requested channel group
- self.channel_group = self.eeg.eeg_channels[self.channel_slice]
- self.channel_group_properties = self.eeg.channel_properties[self.channel_slice]
-
- # anything to display?
- if self.channel_group.shape[0] == 0:
- return
-
- # calculate downsampling size
- points = self.channel_group.shape[1]
- down = int(points / (self.eeg.sample_rate * self.dtX))
-
- # down sample and copy raw data to ring buffer
- offset = 0
- for buf in self.buffer:
- if self.channel_group.shape[0] > offset:
- #r = self.rebin(self.channel_group[offset], tuple([down]))
- r = -self.channel_group[offset][self.binningoffset::self.binning]
- bufindex = np.arange(self.writePointer, self.writePointer + len(r))
- buf.put(bufindex, r, mode='wrap')
- offset += 1
- # down sample and copy sample counter buffer
- r = self.eeg.sample_channel[0][self.binningoffset::self.binning]
- bufindex = np.arange(self.writePointer, self.writePointer + len(r))
- self.sc_buffer[0].put(bufindex, r, mode='wrap')
-
- # update write pointer
- self.writePointer += len(r)
- # wrap around occurred?
- if self.writePointer >= self.buffer.shape[1]:
- # yes, adjust write pointer
- while self.writePointer >= self.buffer.shape[1]:
- self.writePointer -= self.buffer.shape[1]
-
- # request new baseline values
- self.baseline_request = True
-
- # and calculate new time axis offset
- sc = self.eeg.sample_channel[0][self.binningoffset::self.binning] # down sample the sample counter buffer
- scLeft = sc[len(sc)-self.writePointer-1] / self.eeg.sample_rate # sample counter at the leftmost screen position
- #self.TimeScale.setOffset(scLeft)
- #self.update()
-
- # calculate signal baselines for display baseline correction
- if self.baseline_request and (self.writePointer > 10):
- self.baseline_request = False
- self.baselines = np.mean(self.buffer[:,5:10], axis=1).reshape(-1,1)
-
- # calculate new binning offset
- self.binningoffset = self.binning - (points - self.binningoffset - (len(r)-1) * self.binning)
-
- # normalize and offset ring buffer values
- channels = len(self.traces)
- bottomMargin = -2.0 # no margin, clip below window
- topMargin = channels + 1.0 # no margin, clip above window
- scale = self.axisScaleDiv(Qwt.QwtPlot.yLeft).range() / self.scale / 10.0
- offset = np.arange(channels, 0, -1).reshape(-1,1) - 0.8
-
- # baseline correction
- if self.online_cfg.checkBoxBaseline.isChecked():
- buffer = (self.buffer - self.baselines) * scale + offset
- else:
- buffer = self.buffer * scale + offset
-
- # clip to visible area
- buffer.clip(bottomMargin, topMargin, out=self.displaybuffer)
-
- # add EEG marker to marker transfer list
- self.input_markers.extend(self.eeg.markers)
-
- # redisplay everything
- if self.receive_data_available() < 3:
- self.update_display = True
-
-
- def timerEvent(self, e):
- ''' Timer event to update display
- '''
- if self.update_display:
- # acquire thread lock
- self._thLock.acquire()
- self.update_display = False
-
- # check color attributes
- for pccount in xrange(self.channel_group.shape[0]):
- if self.selectedChannel == self.channel_group_properties[pccount].name:
- color = Qt.Qt.green
- else:
- color = self.channel_group_properties[pccount].color
- if(self.traces[pccount].pen().color != color):
- self.traces[pccount].setPen(Qt.QPen(color, 0))
- title = self.traces[pccount].title()
- title.setColor(color)
- self.traces[pccount].setTitle(title)
-
- # copy ring buffer to display
- idx = 0
- for pc in self.traces:
- pc.setData(self.xValues, self.displaybuffer[idx])
- idx += 1
-
- # add trigger markers
- for marker in self.input_markers:
- diffpos = np.int64(self.sc_buffer[0] - marker.position)
- idx = np.abs(diffpos).argmin(0)
- self.addPlotMarker(self.xValues[idx], marker.description, marker.position)
- # remove processed markers
- self.input_markers = []
-
- # remove old markers
- min_sc = self.sc_buffer[0].min()
- for marker in self.plot_markers[:]:
- if marker.sampleCounter < min_sc:
- marker.detach()
- self.plot_markers.remove(marker)
-
- # release thread lock
- self._thLock.release()
-
- t = time.clock()
- self.replot()
- displayTime = time.clock() - t
-
-
- def setTimebase(self, timebase):
- ''' Change the display timebase
- @param timebase: new timebase value in seconds per screen
- '''
- self.timebase = timebase
-
- # calculate new binning value for current sample rate
- inputsize = self.eeg.sample_rate * self.timebase
- self.binning = max([1,int(inputsize / self.xsize)])
- self.binningoffset = 0
-
- # calculate new ring buffer size
- self.dtX = self.binning / self.eeg.sample_rate
- self.xValues = np.arange(0.0, self.timebase, self.dtX)
- self.buffer = np.zeros((len(self.traces), len(self.xValues)), 'd' ) # channel buffer
- self.sc_buffer = np.zeros((1, len(self.xValues)), np.uint64 ) # sample counter buffer
- self.displaybuffer = np.zeros((len(self.traces), len(self.xValues)), 'd' ) # channel display transfer buffer
- self.baselines = np.zeros((len(self.traces), 1), 'd') # baseline correction buffer
-
- # reset buffer pointer
- self.writePointer = 0
-
- # request new baseline values
- self.baseline_request = True
-
- # update X axis scale
- self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DotLine))
- if timebase < 1.0:
- major = 0.1
- self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 5)
- elif timebase > 10.0:
- major = 10.0
- self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 10)
- self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
- else:
- major = 1.0
- self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 5)
- self.setAxisScale(Qwt.QwtPlot.xBottom, 0, timebase, major)
-
- # remove all markers
- for marker in self.plot_markers:
- marker.detach()
- self.plot_markers = []
- self.input_markers = []
-
- self.replot()
-
-
- def setScale(self, scale):
- ''' Change the display scaling
- @param scale: new scale value in µV/Div
- '''
- self.scale = scale
- ticks = np.arange(0.0, scale*11.0, scale).tolist()
- yScaleDiv = Qwt.QwtScaleDiv(0.0, scale*10.0, [], ticks, [])
- self.plotscale.setScaleDiv(yScaleDiv)
- self.replot()
-
- def onlineCfgChanged(self):
- ''' Reset signal display if any of the online parameters has changed
- and acquisition is not running
- '''
- if not self.isRunning():
- self.arrangeTraces()
-
-
- # actions from online configuration pane
-
- def timebaseChanged(self, value):
- ''' SIGNAL New timebase value selected
- '''
- # acquire thread lock
- self._thLock.acquire()
- # change timebase
- self.setTimebase(self.online_cfg.get_timebase())
- # release thread lock
- self._thLock.release()
- self.onlineCfgChanged()
-
- def scaleChanged(self, value):
- ''' SIGNAL New scale value selected
- '''
- # acquire thread lock
- self._thLock.acquire()
- # change scale
- self.setScale(self.online_cfg.get_scale())
- # release thread lock
- self._thLock.release()
- self.onlineCfgChanged()
-
- def channelsChanged(self, idx):
- ''' SIGNAL Display channel configuration changed
- '''
- # acquire thread lock
- self._thLock.acquire()
- # change display channel configuration
- if idx >= 0:
- self.channel_slice = self.online_cfg.comboBoxChannels.itemData(idx).toPyObject()
- self.arrangeTraces()
- else:
- self.channel_slice = slice(0,0,1)
- # release thread lock
- self._thLock.release()
-
- def baselineNowClicked(self):
- ''' SIGNAL Baseline correction now
- '''
- # acquire thread lock
- self._thLock.acquire()
- # use channel values at current write pointer position as new baselines
- self.baselines = self.buffer[:,self.writePointer].reshape(-1,1)
- # release thread lock
- self._thLock.release()
- self.onlineCfgChanged()
-
- def channelItemClicked(self, plotitem):
- ''' SIGNAL Channel legend clicked
- '''
- if self.selectedChannel == plotitem.title().text():
- self.selectedChannel = None
- else:
- self.selectedChannel = plotitem.title().text()
- self.send_event(ModuleEvent(self._object_name,
- EventType.COMMAND,
- info="ChannelSelected",
- cmd_value = plotitem.title().text()))
-
-
-
-
-class _TimeScaleDraw(Qwt.QwtScaleDraw):
- ''' Draw custom time values for x-axis
- '''
- def __init__(self, *args):
- apply(Qwt.QwtScaleDraw.__init__, (self,) + args)
- self._offset = 0.0
-
- def label(self, value):
- ret = Qwt.QwtText()
- v = value
- s = "%.2f" % (v)
- ret.setText(s)
- return ret
-
- def setOffset(self, offset):
- self._offset = offset
- Qwt.QwtScaleDraw.invalidateCache(self)
-
-
-class _ScopeLegend(Qwt.QwtLegend):
- """ QwtPlot custom legend widget.
- Only necessary to make legend size same as canvas and to distribute
- labels at curve positions
- """
- def __init__(self, *args):
- apply(Qwt.QwtLegend.__init__, (self,) + args)
- layout = self.contentsWidget().layout()
- layout.setSpacing(0)
-
- def heightForWidth(self, width):
- return 0
-
- def sizeHint(self):
- sz = Qwt.QwtLegend.sizeHint(self)
- width = sz.width() + Qwt.QwtLegend.verticalScrollBar(self).sizeHint().width()
- sz.setHeight(200)
- sz.setWidth(width)
- return sz
-
- def layoutContents(self):
- topMargin = self.parent().plotLayout().canvasMargin(Qwt.QwtPlot.xTop)
- bottomMargin = self.parent().plotLayout().canvasMargin(Qwt.QwtPlot.xBottom)
- viewport = self.contentsWidget().parentWidget()
- visibleSize = viewport.size()
- items = self.legendItems()
- itemspace = float(visibleSize.height() - (topMargin + bottomMargin)) / (self.itemCount() + 1)
- offset = itemspace * 0.8 - itemspace * 0.5 + topMargin
- yBottom = 0
- for idx, item in enumerate(items):
- yTop = (idx + 1) * itemspace
- itemHeight = int(yTop - yBottom)
- item.setFixedHeight(itemHeight)
- yBottom += itemHeight
- layout = self.contentsWidget().layout()
- layout.setGeometry(Qt.QRect(Qt.QPoint(0,offset),
- Qt.QPoint(visibleSize.width(), visibleSize.height() -2 * offset)))
- self.contentsWidget().resize(visibleSize.width(), visibleSize.height())
- return
-
-
-'''
-------------------------------------------------------------
-DISPLAY MODULE CONFIGURATION PANES
-------------------------------------------------------------
-'''
-
-class _OnlineCfgPane(Qt.QFrame, frmScopeOnline.Ui_frmScopeOnline):
- ''' Display online configuration pane
- '''
-
- def __init__(self, *args):
- ''' Constructor
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
-
- # set default values
- self.group_indices = dict()
- self.group_slices = defaultdict(list)
-
- self.group_size, ok = self.comboBoxGroupSize.currentText().toInt()
- if not ok:
- self.group_size = 32
- self.checkBoxBaseline.setChecked(False)
- self.pushButton_Now.setEnabled(False)
-
- self.eeg_scale,ok = self.comboBoxScale.currentText().toFloat()
- self.aux_scale = self.eeg_scale
-
- # fill scale combo box list
- scales = {u"0.5 µV":0.5, u"1 µV":1.0, u"2 µV":2.0, u"5 µV":5.0,
- u"10 µV":10.0, u"20 µV":20.0, u"50 µV":50.0,
- u"100 µV":100.0, u"200 µV":200.0, u"500 µV":500.0,
- u"1 mV":1000.0, u"2 mV":2000.0, u"5 mV":5000.0,
- u"10 mV":10000.0, u"20 mV":20000.0, u"50 mV":50000.0,
- u"100 mV":100000.0, u"200 mV":200000.0, u"500 mV":500000.0,
- u"1 V":1000000.0, u"2 V":2000000.0, u"5 V":5000000.0
- }
- self.comboBoxScale.clear()
- for text, val in sorted(scales.items(), key=itemgetter(1)):
- self.comboBoxScale.addItem(text, val) # add text and value
-
- # fill time combo box list
- times = {u"0.1 s":0.1, u"0.2 s":0.2, u"0.5 s":0.5,
- u"1 s":1.0, u"2 s":2.0, u"5 s":5.0,
- u"10 s":10.0, u"20 s":20.0, u"50 s":50.0
- }
- self.comboBoxTime.clear()
- for text, val in sorted(times.items(), key=itemgetter(1)):
- self.comboBoxTime.addItem(text, val) # add text and value
-
- # actions
- self.connect(self.comboBoxGroupSize, Qt.SIGNAL("currentIndexChanged(int)"),
- self._groupsChanged)
- self.connect(self.comboBoxChannels, Qt.SIGNAL("currentIndexChanged(int)"),
- self._channelsChanged)
- self.connect(self.checkBoxBaseline, Qt.SIGNAL("toggled(bool)"),
- self._baselineToggled)
- self.connect(self.comboBoxScale, Qt.SIGNAL("currentIndexChanged(int)"),
- self._scaleChanged)
-
- def update_content(self, eeg):
- ''' Update group selection content from EEG configuration block
- '''
- # find all different groups
- groups = defaultdict(list)
- for idx, channel in enumerate(eeg.channel_properties):
- groups[channel.group].append(idx)
- self.group_indices = dict(groups)
-
- # create channel groups
- self._slice_channels()
-
-
- def _isEegGroup(self):
- ''' Get info about current selected channel group
- '''
- if not self.group_slices.has_key(ChannelGroup.EEG):
- return False
- channel_slice = self.comboBoxChannels.itemData(self.comboBoxChannels.currentIndex()).toPyObject()
- if channel_slice in self.group_slices[ChannelGroup.EEG]:
- return True
- return False
-
-
- def _get_cb_index(self, cb, value, isdata):
- ''' Get closest matching combobox index
- @param cb: combobox object
- @param value: float lookup value
- @param isdata: lookup values in item data
- '''
- itemlist = []
- for i in range(cb.count()):
- if isdata:
- val = cb.itemData(i).toPyObject()
- else:
- val,ok = cb.itemText(i).toFloat()
- itemlist.append( (i, val) )
- idx = itemlist[-1][0]
- for item in sorted(itemlist, key=itemgetter(1)):
- if item[1] >= value:
- idx = item[0]
- break
- return idx
-
-
- def set_timebase(self, time):
- ''' Update timebase combobox selection
- @return: selected value
- '''
- idx = self._get_cb_index(self.comboBoxTime, time, True)
- if idx >= 0:
- self.comboBoxTime.setCurrentIndex(idx)
- return self.get_timebase()
-
- def set_scale(self, eeg_scale, aux_scale):
- ''' Update scale combobox selection
- @return: selected value
- '''
- self.eeg_scale = eeg_scale
- self.aux_scale = aux_scale
-
- if self._isEegGroup():
- idx = self._get_cb_index(self.comboBoxScale, self.eeg_scale, True)
- else:
- idx = self._get_cb_index(self.comboBoxScale, self.aux_scale, True)
-
- if idx >= 0:
- self.comboBoxScale.setCurrentIndex(idx)
- return self.get_scale()
-
- def set_groupsize(self, size):
- ''' Update groupsize combobox selection
- @return: selected value
- '''
- idx = self._get_cb_index(self.comboBoxGroupSize, size, False)
- if idx >= 0:
- self.comboBoxGroupSize.setCurrentIndex(idx)
- return self.get_groupsize()
-
- def get_timebase(self):
- ''' Get current selected timebase value from combobox
- @return: float timebase
- '''
- time = self.comboBoxTime.itemData(self.comboBoxTime.currentIndex()).toPyObject()
- return time
-
- def get_scale(self):
- ''' Get current selected scale value from combobox
- @return: float scale
- '''
- scale = self.comboBoxScale.itemData(self.comboBoxScale.currentIndex()).toPyObject()
- return scale
-
- def get_groupscale(self):
- ''' Get scale values for EEG and AUX channels
- @return: float EEG and AUX scale
- '''
- return self.eeg_scale, self.aux_scale
-
- def get_groupsize(self):
- ''' Get current selected group size value from combobox
- @return: float size
- '''
- size,ok = self.comboBoxGroupSize.currentText().toFloat()
- return size
-
- def _slice_channels(self):
- ''' Create channel group slices
- '''
- if len(self.group_indices) == 0:
- return
-
- # create channel groups of group_size
- slices = defaultdict(list)
- for group, channels in self.group_indices.iteritems():
- for si in channels[0::self.group_size]:
- sl = slice(si, min(si+self.group_size, channels[-1]+1), 1)
- slices[group].append(sl)
-
- # new channel selection content ?
- if self.group_slices != slices:
- self.comboBoxChannels.clear()
- self.group_slices = slices
- for group, slice_list in slices.iteritems():
- if group in range(len(ChannelGroup.Name)):
- group_name = ChannelGroup.Name[group]
- else:
- group_name = "?"
- for sl in slice_list:
- offset = slice_list[0].start
- self.comboBoxChannels.addItem("%s %d-%d"%(group_name,
- sl.start - offset + 1,
- sl.stop - offset ), sl)
-
- self.comboBoxChannels.setCurrentIndex(0)
-
-
-
- def _groupsChanged(self, value):
- ''' Group size selection changed
- '''
- if value >= 0:
- self.group_size, ok = self.comboBoxGroupSize.currentText().toInt()
- if not ok:
- self.group_size = 32
- else:
- self.group_size = 32
- self._slice_channels()
-
- def _channelsChanged(self, value):
- ''' Channel selection changed
- Switch scale value for EEG and AUX channels
- '''
- self.set_scale(self.eeg_scale, self.aux_scale)
-
- def _scaleChanged(self, value):
- ''' Scale value changed by user, copy new value to local vars
- '''
- if self._isEegGroup():
- self.eeg_scale = self.get_scale()
- else:
- self.aux_scale = self.get_scale()
-
- def _baselineToggled(self, checked):
- ''' Baseline correction on/off
- '''
- if checked:
- self.pushButton_Now.setEnabled(True)
- else:
- self.pushButton_Now.setEnabled(False)
-
-
+# -*- coding: utf-8 -*-
+'''
+Display Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 197 $
+'''
+
+from PyQt4 import Qt
+from PyQt4 import Qwt5 as Qwt
+from modbase import *
+from res import frmScopeOnline
+from operator import itemgetter
+from collections import defaultdict
+
+'''
+------------------------------------------------------------
+DISPLAY MODULE
+------------------------------------------------------------
+'''
+
+class DISP_Scope(Qwt.QwtPlot, ModuleBase):
+ """ EEG signal display widget.
+ """
+ def __init__(self, *args, **keys):
+ ModuleBase.__init__(self, usethread=True, name="Display", **keys) # use transmit / receive thread
+ Qwt.QwtPlot.__init__(self, *args)
+
+ self.setMinimumSize(Qt.QSize(400, 200))
+ self.setObjectName("Display")
+
+ # XML parameter version
+ # 1: initial version
+ # 2: scale and group size added
+ # 3: baseline correction flag added
+ # 4: timebase values changed from per division to screen (factor 10)
+ # Type of timebase, scale and groupsize changed from string to float
+ # 5: separate scale values for EEG and AUX channels
+ self.xmlVersion = 5
+
+ #self.setTitle('ActiChamp');
+ self.setCanvasBackground(Qt.Qt.white)
+
+ # create online configuration pane
+ self.online_cfg = _OnlineCfgPane()
+ self.connect(self.online_cfg.comboBoxTime, Qt.SIGNAL("activated(QString)"),
+ self.timebaseChanged)
+ self.connect(self.online_cfg.comboBoxScale, Qt.SIGNAL("currentIndexChanged(QString)"),
+ self.scaleChanged)
+ self.connect(self.online_cfg.comboBoxChannels, Qt.SIGNAL("currentIndexChanged(int)"),
+ self.channelsChanged)
+ self.connect(self.online_cfg.pushButton_Now, Qt.SIGNAL("clicked()"),
+ self.baselineNowClicked)
+ self.connect(self.online_cfg.checkBoxBaseline, Qt.SIGNAL("stateChanged()"),
+ self.baselineNowClicked)
+
+ # legend
+ legend = _ScopeLegend()
+ legend.setFrameStyle(Qt.QFrame.Box | Qt.QFrame.Sunken)
+ legend.setItemMode(Qwt.QwtLegend.ClickableItem)
+ self.insertLegend(legend, Qwt.QwtPlot.LeftLegend)
+ self.connect(self, Qt.SIGNAL("legendClicked(QwtPlotItem*)"),
+ self.channelItemClicked)
+
+ # grid
+ self.grid = Qwt.QwtPlotGrid()
+ self.grid.enableY(False)
+ self.grid.enableX(True)
+ self.grid.enableXMin(True)
+ self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
+ self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DashLine))
+ self.grid.attach(self)
+
+ # X axes
+ font = Qt.QFont("arial", 9)
+ title = Qwt.QwtText('Time [s]')
+ title.setFont(font)
+ self.setAxisTitle(Qwt.QwtPlot.xBottom, title);
+ self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 5);
+ self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 10);
+ self.setAxisFont(Qwt.QwtPlot.xBottom, font)
+ self.TimeScale = _TimeScaleDraw()
+ self.setAxisScaleDraw(Qwt.QwtPlot.xBottom, self.TimeScale)
+
+ # Y axis
+ self.setAxisTitle(Qwt.QwtPlot.yLeft, 'Amplitude');
+ self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 0);
+ self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0);
+ self.enableAxis(Qwt.QwtPlot.yLeft, False)
+
+ self.plotscale = Qwt.QwtPlotScaleItem(Qwt.QwtScaleDraw.RightScale)
+ self.plotscale.setBorderDistance(5)
+ self.plotscale.attach(self)
+
+ # reset trace buffer
+ self.traces = []
+
+ # reset marker buffer
+ self.plot_markers = [] # list of QwtPlotMarker()
+ self.input_markers = [] # list of EEG markers
+
+ # EEG data block backup
+ self.last_eeg = None
+ self.last_slice = None
+
+ # default settings
+ self.setScale(self.online_cfg.get_scale()) # µV / Division
+ self.timebase = self.online_cfg.get_timebase() # s / Screen
+ self.xsize = 1500
+ self.binning = 300
+ self.binningoffset = 0
+ self.channel_slice = slice(0,0,1) # channel group selection
+ self.baseline_request = False
+ self.selectedChannel = None
+
+ # set default display
+ self.eeg = EEG_DataBlock()
+ self.process_update(self.eeg)
+
+ # timing test
+ self.ttime = -1.0
+ self.tcount = 10.0
+
+ # start self.timerEvent() to update display asynchronously
+ self.startTimer(30)
+ self.update_display = False
+ self.dataavailable = False
+
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ self.setScale(self.online_cfg.set_scale(100.0, 1000.0)) # EEG: 100µV, AUX: 1000µV / Division
+ self.timebase = self.online_cfg.set_timebase(10.0) # 10s / Screen
+ self.online_cfg.set_groupsize(16) # group size 16 channels
+ self.online_cfg.checkBoxBaseline.setChecked(True) # baseline correction enabled
+
+ # update display
+ self.process_update(self.eeg)
+
+
+
+ def process_update(self, params):
+ ''' Channel properties have changed, module needs update
+ @param params: EEG_DataBlock with channel properties
+ '''
+ if params != None:
+ self.eeg = params
+ self.online_cfg.update_content(self.eeg)
+ self.arrangeTraces()
+ self.replot()
+ return params
+
+ def process_start(self):
+ ''' Module start command.
+ '''
+ # reset timing test timer
+ self.ttime = -1.0
+
+ def process_input(self, datablock):
+ ''' Data available
+ @param datablock: EEG_DataBlock with channel data
+ '''
+ # don't display impedance date
+ if datablock.recording_mode == RecordingMode.IMPEDANCE:
+ return
+
+ # get data from source
+ self.eeg = datablock
+ self.dataavailable = True
+
+ # timing test functions
+ # first call
+ if self.ttime < 0:
+ self.ttime = time.perf_counter()
+ self.tcount = 30.0
+ else:
+ if time.perf_counter() >= self.ttime + self.tcount:
+ # send status info
+ info = "Received Samples = %d / Sample Counter = %d / Time = %.3fs"\
+ %(self.eeg.sample_counter, self.eeg.sample_channel[0,-1]+1, time.perf_counter()-self.ttime)
+ #self.send_event(ModuleEvent(self._object_name, EventType.MESSAGE))
+ self.tcount += 30.0
+
+ # update display buffers
+ self.setDisplay()
+
+
+ def process_output(self):
+ ''' Send data to next module
+ @return: EEG_DataBlock if available, else return None
+ '''
+ if self.dataavailable:
+ self.dataavailable = False
+ # send performance / utilization event
+ totaltime = 1000.0 * self.eeg.performance_timer_max
+ sampletime = 1000.0 * totaltime / self.eeg.sample_channel.shape[1]
+ utilization = sampletime * self.eeg.sample_rate / 1e6 * 100.0
+ if self._instance == 0:
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = utilization,
+ status_field = "Utilization"))
+ return None
+
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+ e.g.
+
+ 1000
+ ...
+
+ '''
+ eeg_scale, aux_scale = self.online_cfg.get_groupscale()
+ E = objectify.E
+ cfg = E.DISP_Scope(E.timebase(self.online_cfg.get_timebase()),
+ E.eegscale(eeg_scale),
+ E.auxscale(aux_scale),
+ E.groupsize(self.online_cfg.get_groupsize()),
+ E.baseline(self.online_cfg.checkBoxBaseline.isChecked()),
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="display")
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search my configuration data
+ displays = xml.xpath("//DISP_Scope[@module='display' and @instance='%i']"%(self._instance))
+ if len(displays) == 0:
+ # configuration data not found, leave everything unchanged
+ return
+
+ # we should have only one display instance from this type
+ cfg = displays[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ # set closest matching timebase
+ timebase = cfg.timebase.pyval
+ if version < 4:
+ timebase = float(timebase) * 10.0
+ self.timebase = self.online_cfg.set_timebase(timebase)
+
+ if version > 1:
+ # set closest matching scale
+ if version < 4:
+ eeg_scale = float(cfg.scale.pyval)
+ aux_scale = float(cfg.scale.pyval)
+ elif version < 5:
+ eeg_scale = cfg.scale.pyval
+ aux_scale = cfg.scale.pyval
+ else:
+ eeg_scale = cfg.eegscale.pyval
+ aux_scale = cfg.auxscale.pyval
+ self.setScale(self.online_cfg.set_scale(eeg_scale, aux_scale))
+
+ # set closest matching group size
+ size = cfg.groupsize.pyval
+ if version < 4:
+ size = float(size)
+ self.online_cfg.set_groupsize(size)
+
+ if version > 2:
+ # get baseline correction flag
+ self.online_cfg.checkBoxBaseline.setChecked(cfg.baseline.pyval)
+
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ '''
+ return self.online_cfg
+
+ def get_display_pane(self):
+ ''' Get the signal display pane
+ '''
+ return self
+
+ def settingsChanged(self):
+ ''' Check if settings have changed since last call
+ '''
+ changed = (self.last_eeg == None) | (self.last_slice != self.channel_slice)
+ if not changed:
+ changed = (self.eeg != self.last_eeg)
+ if changed:
+ self.last_eeg = copy.copy(self.eeg)
+ self.last_slice = self.channel_slice
+ return changed
+
+
+ def arrangeTraces(self):
+ ''' Setup display traces according to EEG data block settings
+ '''
+ # select the requested channel group
+ self.channel_group = self.eeg.eeg_channels[self.channel_slice]
+ self.channel_group_properties = self.eeg.channel_properties[self.channel_slice]
+
+ # remove old traces
+ for pc in self.traces:
+ pc.detach()
+ self.traces = []
+
+ # insert new traces
+ font = Qt.QFont("arial", 8)
+ for pccount in range(self.channel_group.shape[0]):
+ color = self.channel_group_properties[pccount].color
+ title = Qwt.QwtText(self.channel_group_properties[pccount].name)
+ title.setFont(font)
+ title.setColor(color)
+ title.setPaintAttribute(Qwt.QwtText.PaintUsingTextFont)
+ pc = Qwt.QwtPlotCurve(title)
+ pc.setPen(Qt.QPen(color, 0))
+ pc.setYAxis(Qwt.QwtPlot.yLeft)
+ pc.setPaintAttribute(Qwt.QwtPlotCurve.PaintFiltered)
+ pc.attach(self)
+ self.traces.append(pc)
+
+ # reduce the legend items margin
+ for item in self.legend().legendItems():
+ item.setMargin(0)
+
+ # update Y axis scale
+ self.setAxisScale(Qwt.QwtPlot.yLeft, -1.0, len(self.traces), 1.0)
+
+ # update sample buffer
+ self.setTimebase(self.timebase)
+
+ def addPlotMarker(self, xPosition, label, samplecounter):
+ ''' Create and add trigger event marker
+ @param xPosition: horizontal screen position
+ @param label: label string
+ @param samplecounter: total sample position
+ '''
+ margin = float(len(self.traces) + 1) / 10.0 / 3.0 # 2.5% bottom margin
+ sym = Qwt.QwtSymbol()
+ sym.setStyle(Qwt.QwtSymbol.VLine)
+ sym.setSize(20)
+ mX = Qwt.QwtPlotMarker()
+ mX.setLabel(Qwt.QwtText(label))
+ mX.setLabelAlignment(Qt.Qt.AlignHCenter | Qt.Qt.AlignBottom)
+ mX.setLineStyle(Qwt.QwtPlotMarker.NoLine)
+ mX.setXValue(xPosition)
+ mX.setYValue(-1.0 + margin)
+ mX.setSymbol(sym)
+ mX.sampleCounter = samplecounter
+ mX.attach(self)
+ self.plot_markers.append(mX)
+
+ def setDisplay(self):
+ ''' Copy all traces from input buffer to display transfer buffer
+ '''
+ # select the requested channel group
+ self.channel_group = self.eeg.eeg_channels[self.channel_slice]
+ self.channel_group_properties = self.eeg.channel_properties[self.channel_slice]
+
+ # anything to display?
+ if self.channel_group.shape[0] == 0:
+ return
+
+ # calculate downsampling size
+ points = self.channel_group.shape[1]
+ down = int(points / (self.eeg.sample_rate * self.dtX))
+
+ # down sample and copy raw data to ring buffer
+ offset = 0
+ for buf in self.buffer:
+ if self.channel_group.shape[0] > offset:
+ #r = self.rebin(self.channel_group[offset], tuple([down]))
+ r = -self.channel_group[offset][self.binningoffset::self.binning]
+ bufindex = np.arange(self.writePointer, self.writePointer + len(r))
+ buf.put(bufindex, r, mode='wrap')
+ offset += 1
+ # down sample and copy sample counter buffer
+ r = self.eeg.sample_channel[0][self.binningoffset::self.binning]
+ bufindex = np.arange(self.writePointer, self.writePointer + len(r))
+ self.sc_buffer[0].put(bufindex, r, mode='wrap')
+
+ # update write pointer
+ self.writePointer += len(r)
+ # wrap around occurred?
+ if self.writePointer >= self.buffer.shape[1]:
+ # yes, adjust write pointer
+ while self.writePointer >= self.buffer.shape[1]:
+ self.writePointer -= self.buffer.shape[1]
+
+ # request new baseline values
+ self.baseline_request = True
+
+ # and calculate new time axis offset
+ sc = self.eeg.sample_channel[0][self.binningoffset::self.binning] # down sample the sample counter buffer
+ scLeft = sc[len(sc)-self.writePointer-1] / self.eeg.sample_rate # sample counter at the leftmost screen position
+ #self.TimeScale.setOffset(scLeft)
+ #self.update()
+
+ # calculate signal baselines for display baseline correction
+ if self.baseline_request and (self.writePointer > 10):
+ self.baseline_request = False
+ self.baselines = np.mean(self.buffer[:,5:10], axis=1).reshape(-1,1)
+
+ # calculate new binning offset
+ self.binningoffset = self.binning - (points - self.binningoffset - (len(r)-1) * self.binning)
+
+ # normalize and offset ring buffer values
+ channels = len(self.traces)
+ bottomMargin = -2.0 # no margin, clip below window
+ topMargin = channels + 1.0 # no margin, clip above window
+ scale = self.axisScaleDiv(Qwt.QwtPlot.yLeft).range() / self.scale / 10.0
+ offset = np.arange(channels, 0, -1).reshape(-1,1) - 0.8
+
+ # baseline correction
+ if self.online_cfg.checkBoxBaseline.isChecked():
+ buffer = (self.buffer - self.baselines) * scale + offset
+ else:
+ buffer = self.buffer * scale + offset
+
+ # clip to visible area
+ buffer.clip(bottomMargin, topMargin, out=self.displaybuffer)
+
+ # add EEG marker to marker transfer list
+ self.input_markers.extend(self.eeg.markers)
+
+ # redisplay everything
+ if self.receive_data_available() < 3:
+ self.update_display = True
+
+
+ def timerEvent(self, e):
+ ''' Timer event to update display
+ '''
+ if self.update_display:
+ # acquire thread lock
+ self._thLock.acquire()
+ self.update_display = False
+
+ # check color attributes
+ for pccount in range(self.channel_group.shape[0]):
+ if self.selectedChannel == self.channel_group_properties[pccount].name:
+ color = Qt.Qt.green
+ else:
+ color = self.channel_group_properties[pccount].color
+ if(self.traces[pccount].pen().color != color):
+ self.traces[pccount].setPen(Qt.QPen(color, 0))
+ title = self.traces[pccount].title()
+ title.setColor(color)
+ self.traces[pccount].setTitle(title)
+
+ # copy ring buffer to display
+ idx = 0
+ for pc in self.traces:
+ pc.setData(self.xValues, self.displaybuffer[idx])
+ idx += 1
+
+ # add trigger markers
+ for marker in self.input_markers:
+ diffpos = np.int64(self.sc_buffer[0] - marker.position)
+ idx = np.abs(diffpos).argmin(0)
+ self.addPlotMarker(self.xValues[idx], marker.description, marker.position)
+ # remove processed markers
+ self.input_markers = []
+
+ # remove old markers
+ min_sc = self.sc_buffer[0].min()
+ for marker in self.plot_markers[:]:
+ if marker.sampleCounter < min_sc:
+ marker.detach()
+ self.plot_markers.remove(marker)
+
+ # release thread lock
+ self._thLock.release()
+
+ t = time.perf_counter()
+ self.replot()
+ displayTime = time.perf_counter() - t
+
+
+ def setTimebase(self, timebase):
+ ''' Change the display timebase
+ @param timebase: new timebase value in seconds per screen
+ '''
+ self.timebase = timebase
+
+ # calculate new binning value for current sample rate
+ inputsize = self.eeg.sample_rate * self.timebase
+ self.binning = max([1,int(inputsize / self.xsize)])
+ self.binningoffset = 0
+
+ # calculate new ring buffer size
+ self.dtX = self.binning / self.eeg.sample_rate
+ self.xValues = np.arange(0.0, self.timebase, self.dtX)
+ self.buffer = np.zeros((len(self.traces), len(self.xValues)), 'd' ) # channel buffer
+ self.sc_buffer = np.zeros((1, len(self.xValues)), np.uint64 ) # sample counter buffer
+ self.displaybuffer = np.zeros((len(self.traces), len(self.xValues)), 'd' ) # channel display transfer buffer
+ self.baselines = np.zeros((len(self.traces), 1), 'd') # baseline correction buffer
+
+ # reset buffer pointer
+ self.writePointer = 0
+
+ # request new baseline values
+ self.baseline_request = True
+
+ # update X axis scale
+ self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.DotLine))
+ if timebase < 1.0:
+ major = 0.1
+ self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 5)
+ elif timebase > 10.0:
+ major = 10.0
+ self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 10)
+ self.grid.setMinPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine))
+ else:
+ major = 1.0
+ self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 5)
+ self.setAxisScale(Qwt.QwtPlot.xBottom, 0, timebase, major)
+
+ # remove all markers
+ for marker in self.plot_markers:
+ marker.detach()
+ self.plot_markers = []
+ self.input_markers = []
+
+ self.replot()
+
+
+ def setScale(self, scale):
+ ''' Change the display scaling
+ @param scale: new scale value in µV/Div
+ '''
+ self.scale = scale
+ ticks = np.arange(0.0, scale*11.0, scale).tolist()
+ yScaleDiv = Qwt.QwtScaleDiv(0.0, scale*10.0, [], ticks, [])
+ self.plotscale.setScaleDiv(yScaleDiv)
+ self.replot()
+
+ def onlineCfgChanged(self):
+ ''' Reset signal display if any of the online parameters has changed
+ and acquisition is not running
+ '''
+ if not self.isRunning():
+ self.arrangeTraces()
+
+
+ # actions from online configuration pane
+
+ def timebaseChanged(self, value):
+ ''' SIGNAL New timebase value selected
+ '''
+ # acquire thread lock
+ self._thLock.acquire()
+ # change timebase
+ self.setTimebase(self.online_cfg.get_timebase())
+ # release thread lock
+ self._thLock.release()
+ self.onlineCfgChanged()
+
+ def scaleChanged(self, value):
+ ''' SIGNAL New scale value selected
+ '''
+ # acquire thread lock
+ self._thLock.acquire()
+ # change scale
+ self.setScale(self.online_cfg.get_scale())
+ # release thread lock
+ self._thLock.release()
+ self.onlineCfgChanged()
+
+ def channelsChanged(self, idx):
+ ''' SIGNAL Display channel configuration changed
+ '''
+ # acquire thread lock
+ self._thLock.acquire()
+ # change display channel configuration
+ if idx >= 0:
+ data = self.online_cfg.comboBoxChannels.itemData(idx)
+ try:
+ self.channel_slice = data.toPyObject()
+ except AttributeError:
+ self.channel_slice = data
+ self.arrangeTraces()
+ else:
+ self.channel_slice = slice(0,0,1)
+ # release thread lock
+ self._thLock.release()
+
+ def baselineNowClicked(self):
+ ''' SIGNAL Baseline correction now
+ '''
+ # acquire thread lock
+ self._thLock.acquire()
+ # use channel values at current write pointer position as new baselines
+ self.baselines = self.buffer[:,self.writePointer].reshape(-1,1)
+ # release thread lock
+ self._thLock.release()
+ self.onlineCfgChanged()
+
+ def channelItemClicked(self, plotitem):
+ ''' SIGNAL Channel legend clicked
+ '''
+ if self.selectedChannel == plotitem.title().text():
+ self.selectedChannel = None
+ else:
+ self.selectedChannel = plotitem.title().text()
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.COMMAND,
+ info="ChannelSelected",
+ cmd_value = plotitem.title().text()))
+
+
+
+
+class _TimeScaleDraw(Qwt.QwtScaleDraw):
+ ''' Draw custom time values for x-axis
+ '''
+ def __init__(self, *args):
+ Qwt.QwtScaleDraw.__init__(self, *args)
+ self._offset = 0.0
+
+ def label(self, value):
+ ret = Qwt.QwtText()
+ v = value
+ s = "%.2f" % (v)
+ ret.setText(s)
+ return ret
+
+ def setOffset(self, offset):
+ self._offset = offset
+ Qwt.QwtScaleDraw.invalidateCache(self)
+
+
+class _ScopeLegend(Qwt.QwtLegend):
+ """ QwtPlot custom legend widget.
+ Only necessary to make legend size same as canvas and to distribute
+ labels at curve positions
+ """
+ def __init__(self, *args):
+ Qwt.QwtLegend.__init__(self, *args)
+ layout = self.contentsWidget().layout()
+ layout.setSpacing(0)
+
+ def heightForWidth(self, width):
+ return 0
+
+ def sizeHint(self):
+ sz = Qwt.QwtLegend.sizeHint(self)
+ width = sz.width() + Qwt.QwtLegend.verticalScrollBar(self).sizeHint().width()
+ sz.setHeight(200)
+ sz.setWidth(width)
+ return sz
+
+ def layoutContents(self):
+ topMargin = self.parent().plotLayout().canvasMargin(Qwt.QwtPlot.xTop)
+ bottomMargin = self.parent().plotLayout().canvasMargin(Qwt.QwtPlot.xBottom)
+ viewport = self.contentsWidget().parentWidget()
+ visibleSize = viewport.size()
+ items = self.legendItems()
+ itemspace = float(visibleSize.height() - (topMargin + bottomMargin)) / (self.itemCount() + 1)
+ offset = itemspace * 0.8 - itemspace * 0.5 + topMargin
+ yBottom = 0
+ for idx, item in enumerate(items):
+ yTop = (idx + 1) * itemspace
+ itemHeight = int(yTop - yBottom)
+ item.setFixedHeight(itemHeight)
+ yBottom += itemHeight
+ layout = self.contentsWidget().layout()
+ layout.setGeometry(Qt.QRect(Qt.QPoint(0,offset),
+ Qt.QPoint(visibleSize.width(), visibleSize.height() -2 * offset)))
+ self.contentsWidget().resize(visibleSize.width(), visibleSize.height())
+ return
+
+
+'''
+------------------------------------------------------------
+DISPLAY MODULE CONFIGURATION PANES
+------------------------------------------------------------
+'''
+
+class _OnlineCfgPane(Qt.QFrame, frmScopeOnline.Ui_frmScopeOnline):
+ ''' Display online configuration pane
+ '''
+
+ def __init__(self, *args):
+ ''' Constructor
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+
+ # set default values
+ self.group_indices = dict()
+ self.group_slices = defaultdict(list)
+
+ try:
+ self.group_size, ok = self.comboBoxGroupSize.currentText().toInt()
+ except AttributeError:
+ try:
+ self.group_size = int(self.comboBoxGroupSize.currentText())
+ ok = True
+ except Exception:
+ self.group_size = 32
+ ok = False
+ self.checkBoxBaseline.setChecked(False)
+ self.pushButton_Now.setEnabled(False)
+
+ try:
+ self.eeg_scale,ok = self.comboBoxScale.currentText().toFloat()
+ except AttributeError:
+ try:
+ self.eeg_scale = float(self.comboBoxScale.currentText())
+ ok = True
+ except Exception:
+ self.eeg_scale = 100.0
+ ok = False
+ self.aux_scale = self.eeg_scale
+
+ # fill scale combo box list
+ scales = {u"0.5 µV":0.5, u"1 µV":1.0, u"2 µV":2.0, u"5 µV":5.0,
+ u"10 µV":10.0, u"20 µV":20.0, u"50 µV":50.0,
+ u"100 µV":100.0, u"200 µV":200.0, u"500 µV":500.0,
+ u"1 mV":1000.0, u"2 mV":2000.0, u"5 mV":5000.0,
+ u"10 mV":10000.0, u"20 mV":20000.0, u"50 mV":50000.0,
+ u"100 mV":100000.0, u"200 mV":200000.0, u"500 mV":500000.0,
+ u"1 V":1000000.0, u"2 V":2000000.0, u"5 V":5000000.0
+ }
+ self.comboBoxScale.clear()
+ for text, val in sorted(scales.items(), key=itemgetter(1)):
+ self.comboBoxScale.addItem(text, val) # add text and value
+
+ # fill time combo box list
+ times = {u"0.1 s":0.1, u"0.2 s":0.2, u"0.5 s":0.5,
+ u"1 s":1.0, u"2 s":2.0, u"5 s":5.0,
+ u"10 s":10.0, u"20 s":20.0, u"50 s":50.0
+ }
+ self.comboBoxTime.clear()
+ for text, val in sorted(times.items(), key=itemgetter(1)):
+ self.comboBoxTime.addItem(text, val) # add text and value
+
+ # actions
+ self.connect(self.comboBoxGroupSize, Qt.SIGNAL("currentIndexChanged(int)"),
+ self._groupsChanged)
+ self.connect(self.comboBoxChannels, Qt.SIGNAL("currentIndexChanged(int)"),
+ self._channelsChanged)
+ self.connect(self.checkBoxBaseline, Qt.SIGNAL("toggled(bool)"),
+ self._baselineToggled)
+ self.connect(self.comboBoxScale, Qt.SIGNAL("currentIndexChanged(int)"),
+ self._scaleChanged)
+
+ def update_content(self, eeg):
+ ''' Update group selection content from EEG configuration block
+ '''
+ # find all different groups
+ groups = defaultdict(list)
+ for idx, channel in enumerate(eeg.channel_properties):
+ groups[channel.group].append(idx)
+ self.group_indices = dict(groups)
+
+ # create channel groups
+ self._slice_channels()
+
+
+ def _isEegGroup(self):
+ ''' Get info about current selected channel group
+ '''
+ if ChannelGroup.EEG not in self.group_slices:
+ return False
+ data = self.comboBoxChannels.itemData(self.comboBoxChannels.currentIndex())
+ try:
+ channel_slice = data.toPyObject()
+ except AttributeError:
+ channel_slice = data
+ if channel_slice in self.group_slices[ChannelGroup.EEG]:
+ return True
+ return False
+
+
+ def _get_cb_index(self, cb, value, isdata):
+ ''' Get closest matching combobox index
+ @param cb: combobox object
+ @param value: float lookup value
+ @param isdata: lookup values in item data
+ '''
+ itemlist = []
+ for i in range(cb.count()):
+ if isdata:
+ data = cb.itemData(i)
+ try:
+ val = data.toPyObject()
+ except AttributeError:
+ val = data
+ else:
+ try:
+ val,ok = cb.itemText(i).toFloat()
+ except AttributeError:
+ val = float(cb.itemText(i))
+ itemlist.append( (i, val) )
+ idx = itemlist[-1][0]
+ for item in sorted(itemlist, key=itemgetter(1)):
+ if item[1] >= value:
+ idx = item[0]
+ break
+ return idx
+
+
+ def set_timebase(self, time):
+ ''' Update timebase combobox selection
+ @return: selected value
+ '''
+ idx = self._get_cb_index(self.comboBoxTime, time, True)
+ if idx >= 0:
+ self.comboBoxTime.setCurrentIndex(idx)
+ return self.get_timebase()
+
+ def set_scale(self, eeg_scale, aux_scale):
+ ''' Update scale combobox selection
+ @return: selected value
+ '''
+ self.eeg_scale = eeg_scale
+ self.aux_scale = aux_scale
+
+ if self._isEegGroup():
+ idx = self._get_cb_index(self.comboBoxScale, self.eeg_scale, True)
+ else:
+ idx = self._get_cb_index(self.comboBoxScale, self.aux_scale, True)
+
+ if idx >= 0:
+ self.comboBoxScale.setCurrentIndex(idx)
+ return self.get_scale()
+
+ def set_groupsize(self, size):
+ ''' Update groupsize combobox selection
+ @return: selected value
+ '''
+ idx = self._get_cb_index(self.comboBoxGroupSize, size, False)
+ if idx >= 0:
+ self.comboBoxGroupSize.setCurrentIndex(idx)
+ return self.get_groupsize()
+
+ def get_timebase(self):
+ ''' Get current selected timebase value from combobox
+ @return: float timebase
+ '''
+ data = self.comboBoxTime.itemData(self.comboBoxTime.currentIndex())
+ try:
+ time = data.toPyObject()
+ except AttributeError:
+ time = data
+ return time
+
+ def get_scale(self):
+ ''' Get current selected scale value from combobox
+ @return: float scale
+ '''
+ data = self.comboBoxScale.itemData(self.comboBoxScale.currentIndex())
+ try:
+ scale = data.toPyObject()
+ except AttributeError:
+ scale = data
+ return scale
+
+ def get_groupscale(self):
+ ''' Get scale values for EEG and AUX channels
+ @return: float EEG and AUX scale
+ '''
+ return self.eeg_scale, self.aux_scale
+
+ def get_groupsize(self):
+ ''' Get current selected group size value from combobox
+ @return: float size
+ '''
+ try:
+ size,ok = self.comboBoxGroupSize.currentText().toFloat()
+ except AttributeError:
+ try:
+ size = float(self.comboBoxGroupSize.currentText())
+ ok = True
+ except Exception:
+ size = 32.0
+ ok = False
+ return size
+
+ def _slice_channels(self):
+ ''' Create channel group slices
+ '''
+ if len(self.group_indices) == 0:
+ return
+
+ # create channel groups of group_size
+ slices = defaultdict(list)
+ for group, channels in self.group_indices.items():
+ for si in channels[0::self.group_size]:
+ sl = slice(si, min(si+self.group_size, channels[-1]+1), 1)
+ slices[group].append(sl)
+
+ # new channel selection content ?
+ if self.group_slices != slices:
+ self.comboBoxChannels.clear()
+ self.group_slices = slices
+ for group, slice_list in slices.items():
+ if group in range(len(ChannelGroup.Name)):
+ group_name = ChannelGroup.Name[group]
+ else:
+ group_name = "?"
+ for sl in slice_list:
+ offset = slice_list[0].start
+ self.comboBoxChannels.addItem("%s %d-%d"%(group_name,
+ sl.start - offset + 1,
+ sl.stop - offset ), sl)
+
+ self.comboBoxChannels.setCurrentIndex(0)
+
+
+
+ def _groupsChanged(self, value):
+ ''' Group size selection changed
+ '''
+ if value >= 0:
+ try:
+ self.group_size, ok = self.comboBoxGroupSize.currentText().toInt()
+ except AttributeError:
+ try:
+ self.group_size = int(self.comboBoxGroupSize.currentText())
+ ok = True
+ except Exception:
+ self.group_size = 32
+ ok = False
+ else:
+ self.group_size = 32
+ self._slice_channels()
+
+ def _channelsChanged(self, value):
+ ''' Channel selection changed
+ Switch scale value for EEG and AUX channels
+ '''
+ self.set_scale(self.eeg_scale, self.aux_scale)
+
+ def _scaleChanged(self, value):
+ ''' Scale value changed by user, copy new value to local vars
+ '''
+ if self._isEegGroup():
+ self.eeg_scale = self.get_scale()
+ else:
+ self.aux_scale = self.get_scale()
+
+ def _baselineToggled(self, checked):
+ ''' Baseline correction on/off
+ '''
+ if checked:
+ self.pushButton_Now.setEnabled(True)
+ else:
+ self.pushButton_Now.setEnabled(False)
+
+
diff --git a/filter.py b/filter.py
index f0f9d77..3c40575 100644
--- a/filter.py
+++ b/filter.py
@@ -1,675 +1,674 @@
-'''
-Digital Filter Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 197 $
-'''
-
-from scipy import signal
-from modbase import *
-from res import frmFilterConfig
-from operator import itemgetter
-
-class FLT_Eeg(ModuleBase):
- ''' Low, high pass and notch filter
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- ModuleBase.__init__(self, name="EEG Filter", **keys)
-
- # XML parameter version
- # 1: initial version
- # 2: include lp, hp and notch
- self.xmlVersion = 2
-
- self.data = None
- self.dataavailable = False
- self.params = None
-
- self.notchFilter = [] # notch filter array
- self.lpFilter = [] # lowpass filter array
- self.hpFilter = [] # highpass filter array
-
- # set default process values
- self.samplefreq = 50000.0
- self.filterorder = 2
- self.notchFrequency = 50.0 # notch filter frequency in Hz
-
- # set default properties
- self.setDefault()
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- # global filter values (for EEG-channels)
- self.lpGlobal = 0.0
- self.hpGlobal = 0.0
- self.notchGlobal = False
- # single channel values
- if self.params != None:
- for ch in self.params.channel_properties:
- ch.lowpass = 0.0
- ch.highpass = 0.0
- ch.notchfilter = False
-
- def get_configuration_pane(self):
- ''' Get the configuration pane if available.
- Qt widgets are not reusable, so we have to create it new every time
- '''
- return _ConfigurationPane(self)
-
- def _design_filter(self, frequency, type, slice):
- ''' Create filter settings for channel groups with equal filter parameters
- @param frequency: filter frequeny in Hz
- @param type: filter type, "low", "high" or "bandstop"
- @param slice: channel group indices
- @return: filter parameters and state vector
- '''
- if (frequency == 0.0) or (frequency > self.samplefreq/2.0):
- return None
- if type == "bandstop":
- cut1 = (frequency-1.0) / self.samplefreq * 2.0
- cut2 = (frequency+1.0) / self.samplefreq * 2.0
- b,a = signal.filter_design.iirfilter(2, [cut1, cut2], btype=type, ftype='butter')
- #b,a = signal.filter_design.iirfilter(2, [cut1, cut2], rs=40.0, rp=0.5, btype=type, ftype='elliptic')
- else:
- cut = frequency / self.samplefreq * 2.0
- b,a = signal.filter_design.butter(self.filterorder, cut, btype=type)
- zi = signal.lfiltic(b, a, (0.0,))
- czi = np.resize(zi, (slice.stop - slice.start, len(zi)))
- return {'slice':slice, 'a':a, 'b':b, 'zi':czi, 'frequency':frequency}
-
- def process_update(self, params):
- ''' Calculate filter parameters for updated channels
- '''
- # update the local reference
- if self.params == None:
- self.params = params
- else:
- # merge filter settings
- for ch in params.channel_properties:
- if ch.group == ChannelGroup.EEG:
- ch.lowpass = self.lpGlobal
- ch.highpass = self.hpGlobal
- ch.notchfilter = self.notchGlobal
- else:
- # get local channel property by input number
- filterChannel = None
- for filter in self.params.channel_properties:
- if filter.input == ch.input and filter.inputgroup == ch.inputgroup:
- filterChannel = filter
- break
- if filterChannel != None:
- ch.lowpass = filterChannel.lowpass
- ch.highpass = filterChannel.highpass
- ch.notchfilter = filterChannel.notchfilter
- self.params = params
-
-
- # reset filter
- self.samplefreq = params.sample_rate
- self.lpFilter = []
- self.hpFilter = []
- self.notchFilter = []
-
- # nothing to filter
- if len(params.channel_properties) == 0:
- return params
-
- # create channel slices and filter for continuous notch filters
- notch = params.channel_properties[0].notchfilter
- slc = slice(0,1,1)
- for property in params.channel_properties[1::]:
- if property.notchfilter == notch:
- slc = slice(slc.start, slc.stop+1, 1)
- else:
- # design filter
- if notch: freq = self.notchFrequency
- else: freq = 0.0
- filter = self._design_filter(freq, 'bandstop', slc)
- if filter != None:
- self.notchFilter.append(filter)
- slc = slice(slc.stop, slc.stop+1, 1)
- notch = property.notchfilter
- if notch: freq = self.notchFrequency
- else: freq = 0.0
- filter = self._design_filter(freq, 'bandstop', slc)
- if filter != None:
- self.notchFilter.append(filter)
-
- # create channel slices and filter for continuous unique lowpass filter frequencies
- freq = params.channel_properties[0].lowpass
- slc = slice(0,1,1)
- for property in params.channel_properties[1::]:
- if property.lowpass == freq:
- slc = slice(slc.start, slc.stop+1, 1)
- else:
- # design filter
- filter = self._design_filter(freq, 'low', slc)
- if filter != None:
- self.lpFilter.append(filter)
- slc = slice(slc.stop, slc.stop+1, 1)
- freq = property.lowpass
- filter = self._design_filter(freq, 'low', slc)
- if filter != None:
- self.lpFilter.append(filter)
-
- # create channel slices and filter for continuous unique highpass filter frequencies
- freq = params.channel_properties[0].highpass
- slc = slice(0,1,1)
- for property in params.channel_properties[1::]:
- if property.highpass == freq:
- slc = slice(slc.start, slc.stop+1, 1)
- else:
- # design filter
- filter = self._design_filter(freq, 'high', slc)
- if filter != None:
- self.hpFilter.append(filter)
- slc = slice(slc.stop, slc.stop+1, 1)
- freq = property.highpass
- filter = self._design_filter(freq, 'high', slc)
- if filter != None:
- self.hpFilter.append(filter)
-
- # propagate down
- return params
-
- def process_input(self, datablock):
- ''' Filter all channel groups
- '''
- self.dataavailable = True
- self.data = datablock
-
- # don't filter impedance values
- if self.data.recording_mode == RecordingMode.IMPEDANCE:
- return
-
- # replace channel filter configuration within the data block with our modified configuration
- for channel in range(len(self.data.channel_properties)):
- self.data.channel_properties[channel].lowpass = self.params.channel_properties[channel].lowpass
- self.data.channel_properties[channel].highpass = self.params.channel_properties[channel].highpass
- self.data.channel_properties[channel].notchfilter = self.params.channel_properties[channel].notchfilter
-
- # highpass filter
- for flt in self.hpFilter:
- self.data.eeg_channels[flt['slice']],flt['zi'] = \
- signal.lfilter(flt['b'], flt['a'],
- self.data.eeg_channels[flt['slice']], zi=flt['zi'])
-
- # lowpass filter
- for flt in self.lpFilter:
- self.data.eeg_channels[flt['slice']],flt['zi'] = \
- signal.lfilter(flt['b'], flt['a'],
- self.data.eeg_channels[flt['slice']], zi=flt['zi'])
-
- # notch filter
- for flt in self.notchFilter:
- self.data.eeg_channels[flt['slice']],flt['zi'] = \
- signal.lfilter(flt['b'], flt['a'],
- self.data.eeg_channels[flt['slice']], zi=flt['zi'])
-
-
-
- def process_output(self):
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
- e.g.
-
- 50.0
- ...
-
- '''
- E = objectify.E
- channels = E.channels()
- for channel in self.params.channel_properties:
- if channel.group != ChannelGroup.EEG:
- channels.append(channel.getXML())
- cfg = E.EegFilter(E.notch_frequency(self.notchFrequency),
- E.lp_global(self.lpGlobal),
- E.hp_global(self.hpGlobal),
- E.notch_global(self.notchGlobal),
- channels,
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="filter")
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search my configuration data
- storages = xml.xpath("//EegFilter[@module='filter' and @instance='%i']"%(self._instance) )
- if len(storages) == 0:
- # configuration data not found, set default values
- self.notchFrequency = 50.0
- return
-
- # we should have only one instance from this type
- cfg = storages[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- self.notchFrequency = cfg.notch_frequency.pyval
- if version > 1:
- # get global filter values
- self.notchGlobal = cfg.notch_global.pyval
- self.lpGlobal = cfg.lp_global.pyval
- self.hpGlobal = cfg.hp_global.pyval
- # get single channel filter values
- channel_properties = []
- for idx, channel in enumerate(cfg.channels.iterchildren()):
- ch = EEG_ChannelProperties("")
- ch.setXML(channel)
- channel_properties.append(ch)
- self.params.channel_properties = np.array(channel_properties)
-
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
-
-'''
-------------------------------------------------------------
-FILTER MODULE CONFIGURATION PANE
-------------------------------------------------------------
-'''
-
-class _ConfigurationPane(Qt.QFrame, frmFilterConfig.Ui_frmFilterConfig):
- ''' Module configuration pane
- '''
- def __init__(self, filter, *args):
- ''' Constructor
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
- self.tableView.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
-
- # setup content
- self.filter = filter
- # notch frequency
- idx = self._get_cb_index(self.comboBox_Notch, self.filter.notchFrequency)
- if idx >= 0:
- self.comboBox_Notch.setCurrentIndex(idx)
-
- # channel tables
- self._fillChannelTables()
-
- # Global lowpass filter
- self.comboBoxEegLowpass.addItems(self.table_model.lowpasslist)
- idx = self._get_cb_index(self.comboBoxEegLowpass, self.filter.lpGlobal)
- if idx >= 0:
- self.comboBoxEegLowpass.setCurrentIndex(idx)
-
- # Global highpass filter
- self.comboBoxEegHighpass.addItems(self.table_model.highpasslist)
- idx = self._get_cb_index(self.comboBoxEegHighpass, self.filter.hpGlobal)
- if idx >= 0:
- self.comboBoxEegHighpass.setCurrentIndex(idx)
-
- # global notch filter
- self.checkBoxEegNotch.setChecked(self.filter.notchGlobal)
-
- # actions
- self.connect(self.comboBox_Notch, Qt.SIGNAL("currentIndexChanged(QString)"), self._notchFrequencyChanged)
- self.connect(self.checkBoxEegNotch, Qt.SIGNAL("stateChanged(int)"), self._notchFilterChanged)
- self.connect(self.comboBoxEegHighpass, Qt.SIGNAL("currentIndexChanged(QString)"), self._highpassChanged)
- self.connect(self.comboBoxEegLowpass, Qt.SIGNAL("currentIndexChanged(QString)"), self._lowpassChanged)
-
- def _get_cb_index(self, cb, value):
- ''' Get closest matching combobox index
- @param cb: combobox object
- @param value: float lookup value
- '''
- itemlist = []
- for i in range(cb.count()):
- val,ok = cb.itemText(i).toFloat()
- itemlist.append( (i, val) )
- idx = itemlist[-1][0]
- for item in sorted(itemlist, key=itemgetter(1)):
- if item[1] >= value - 0.0001:
- idx = item[0]
- break
- return idx
-
- def _notchFrequencyChanged(self, value):
- self.filter.notchFrequency, ok = value.toFloat()
-
- def _notchFilterChanged(self, value):
- self.filter.notchGlobal = (value == Qt.Qt.Checked)
-
- def _lowpassChanged(self, value):
- self.filter.lpGlobal, ok = value.toFloat()
-
- def _highpassChanged(self, value):
- self.filter.hpGlobal, ok = value.toFloat()
-
- def _fillChannelTables(self):
- ''' Create and fill channel tables
- '''
- # AUX channel table
- mask = lambda x: x.group != ChannelGroup.EEG
- ch_map = np.array(map(mask, self.filter.params.channel_properties))
- ch_indices = np.nonzero(ch_map)[0]
- self.table_model = _ConfigTableModel(self.filter.params.channel_properties[ch_indices])
- self.tableView.setModel(self.table_model)
- self.tableView.setItemDelegate(_ConfigItemDelegate())
- self.tableView.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
-
- # actions
- self.connect(self.table_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
-
- def _channeltable_changed(self, topLeft, bottomRight):
- ''' SIGNAL data in channel table has changed
- '''
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
-
- def showEvent(self, event):
- self._fillChannelTables()
-
-
-class _ConfigTableModel(Qt.QAbstractTableModel):
- ''' EEG and AUX table data model for the filter configuration pane
- '''
- def __init__(self, data, parent=None, *args):
- ''' Constructor
- @param data: array of EEG_ChannelProperties objects
- '''
- Qt.QAbstractTableModel.__init__(self, parent, *args)
- self.arraydata = data
- # column description
- self.columns = [{'property':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
- {'property':'lowpass', 'header':'High Cutoff', 'edit':True, 'editor':'combobox'},
- {'property':'highpass', 'header':'Low Cutoff', 'edit':True, 'editor':'combobox'},
- {'property':'notchfilter', 'header':'Notch', 'edit':True, 'editor':'default'},
- {'property':'name', 'header':'Name', 'edit':False, 'editor':'default'},
- ]
-
- # combo box list contents
- self.lowpasslist = ['off', '10', '20', '30', '50', '100', '200', '500', '1000', '2000']
- self.highpasslist = ['off','0.01', '0.02', '0.05', '0.1', '0.2', '0.5', '1', '2', '5', '10']
-
- def _getitem(self, row, column):
- ''' Get amplifier property item based on table row and column
- @param row: row number
- @param column: column number
- @return: QVariant property value
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return Qt.QVariant()
-
- # get channel properties
- property = self.arraydata[row]
- # get property name from column description
- property_name = self.columns[column]['property']
- # get property value
- if property_name == 'input':
- d = Qt.QVariant(property.input)
- elif property_name == 'enable':
- d = Qt.QVariant(property.enable)
- elif property_name == 'name':
- d = Qt.QVariant(property.name)
- elif property_name == 'lowpass':
- if property.lowpass == 0.0:
- d = Qt.QVariant('off')
- else:
- d = Qt.QVariant(property.lowpass)
- elif property_name == 'highpass':
- if property.highpass == 0.0:
- d = Qt.QVariant('off')
- else:
- d = Qt.QVariant(property.highpass)
- elif property_name == 'notchfilter':
- d = Qt.QVariant(property.notchfilter)
- elif property_name == 'isReference':
- d = Qt.QVariant(property.isReference)
- else:
- d = Qt.QVariant()
- return d
-
- def _setitem(self, row, column, value):
- ''' Set amplifier property item based on table row and column
- @param row: row number
- @param column: column number
- @param value: QVariant value object
- @return: True if property value was set, False if not
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return False
- # get channel properties
- property = self.arraydata[row]
- # get property name from column description
- property_name = self.columns[column]['property']
- # set channel property
- if property_name == 'enable':
- property.enable = value.toBool()
- return True
- elif property_name == 'name':
- property.name = value.toString()
- return True
- elif property_name == 'lowpass':
- property.lowpass,ok = value.toDouble()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.lowpass = property.lowpass
- return True
- elif property_name == 'highpass':
- property.highpass,ok = value.toDouble()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.highpass = property.highpass
- return True
- elif property_name == 'notchfilter':
- property.notchfilter = value.toBool()
- if property.group == ChannelGroup.EEG:
- for prop in self.arraydata:
- prop.notchfilter = property.notchfilter
- self.reset()
- return True
- elif property_name == 'isReference':
- # available for EEG channels only
- if property.group == ChannelGroup.EEG:
- # remove previously selected reference channel
- if value.toBool() == True:
- for prop in self.arraydata:
- prop.isReference = False
- property.isReference = value.toBool()
- self.reset()
- return True
- return False
-
- def editorType(self, column):
- ''' Get the columns editer type from column description
- @param column: table column number
- @return: editor type as QVariant (string)
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- return Qt.QVariant(self.columns[column]['editor'])
-
- def comboBoxList(self, column):
- ''' Get combo box item list for column 'highpass' or 'lowpass'
- @param column: table column number
- @return: combo box item list as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column]['property'] == 'lowpass':
- return Qt.QVariant(self.lowpasslist)
- elif self.columns[column]['property'] == 'highpass':
- return Qt.QVariant(self.highpasslist)
- else:
- return Qt.QVariant()
-
- def rowCount(self, parent):
- ''' Get the number of required table rows
- @return: number of rows
- '''
- if parent.isValid():
- return 0
- return len(self.arraydata)
-
- def columnCount(self, parent):
- ''' Get the number of required table columns
- @return: number of columns
- '''
- if parent.isValid():
- return 0
- return len(self.columns)
-
- def data(self, index, role):
- ''' Abstract method from QAbstactItemModel to get cell data based on role
- @param index: QModelIndex table cell reference
- @param role: given role for the item referred to by the index
- @return: the data stored under the given role for the item referred to by the index
- '''
- if not index.isValid():
- return Qt.QVariant()
-
- # get the underlying data
- value = self._getitem(index.row(), index.column())
-
- if role == Qt.Qt.CheckStateRole:
- if value.type() == Qt.QMetaType.Bool:
- if value.toBool():
- return Qt.Qt.Checked
- else:
- return Qt.Qt.Unchecked
-
- elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
- if value.type() != Qt.QMetaType.Bool:
- return value
-
- elif role == Qt.Qt.BackgroundRole:
- # change background color for reference channel
- property = self.arraydata[index.row()]
- #if (property.isReference) and (index.column() == 0):
- if (property.isReference):
- return Qt.QVariant( Qt.QColor(0, 0, 255))
-
- return Qt.QVariant()
-
- def flags(self, index):
- ''' Abstract method from QAbstactItemModel
- @param index: QModelIndex table cell reference
- @return: the item flags for the given index
- '''
- if not index.isValid():
- return Qt.Qt.ItemIsEnabled
- if not self.columns[index.column()]['edit']:
- return Qt.Qt.ItemIsEnabled
- value = self._getitem(index.row(), index.column())
- if value.type() == Qt.QMetaType.Bool:
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
-
- def setData(self, index, value, role):
- ''' Abstract method from QAbstactItemModel to set cell data based on role
- @param index: QModelIndex table cell reference
- @param value: QVariant new cell data
- @param role: given role for the item referred to by the index
- @return: true if successful; otherwise returns false.
- '''
- if index.isValid():
- if role == Qt.Qt.EditRole:
- if not self._setitem(index.row(), index.column(), value):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
- return True
- elif role == Qt.Qt.CheckStateRole:
- if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
- return True
- return False
-
- def headerData(self, col, orientation, role):
- ''' Abstract method from QAbstactItemModel to get the column header
- @param col: column number
- @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
- @param role: given role for the item referred to by the index
- @return: header
- '''
- if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
- return Qt.QVariant(self.columns[col]['header'])
- return Qt.QVariant()
-
-
-class _ConfigItemDelegate(Qt.QStyledItemDelegate):
- ''' Combobox item editor
- '''
- def __init__(self, parent=None):
- super(_ConfigItemDelegate, self).__init__(parent)
-
- def createEditor(self, parent, option, index):
- if index.model().editorType(index.column()) == 'combobox':
- combobox = Qt.QComboBox(parent)
- combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
- combobox.setEditable(False)
- self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
- return combobox
- return Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
-
- def setEditorData(self, editor, index):
- if index.model().columns[index.column()]['editor'] == 'combobox':
- text = index.model().data(index, Qt.Qt.DisplayRole).toString()
- i = editor.findText(text)
- if i == -1:
- i = 0
- editor.setCurrentIndex(i)
- Qt.QStyledItemDelegate.setEditorData(self, editor, index)
-
-
- def setModelData(self, editor, model, index):
- if model.columns[index.column()]['editor'] == 'combobox':
- model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
- model.reset()
- Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
-
- def emitCommitData(self):
- self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
-
-
+'''
+Digital Filter Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 197 $
+'''
+
+from scipy import signal
+from modbase import *
+from res import frmFilterConfig
+from operator import itemgetter
+
+class FLT_Eeg(ModuleBase):
+ ''' Low, high pass and notch filter
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ ModuleBase.__init__(self, name="EEG Filter", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ # 2: include lp, hp and notch
+ self.xmlVersion = 2
+
+ self.data = None
+ self.dataavailable = False
+ self.params = None
+
+ self.notchFilter = [] # notch filter array
+ self.lpFilter = [] # lowpass filter array
+ self.hpFilter = [] # highpass filter array
+
+ # set default process values
+ self.samplefreq = 50000.0
+ self.filterorder = 2
+ self.notchFrequency = 50.0 # notch filter frequency in Hz
+
+ # set default properties
+ self.setDefault()
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ # global filter values (for EEG-channels)
+ self.lpGlobal = 0.0
+ self.hpGlobal = 0.0
+ self.notchGlobal = False
+ # single channel values
+ if self.params != None:
+ for ch in self.params.channel_properties:
+ ch.lowpass = 0.0
+ ch.highpass = 0.0
+ ch.notchfilter = False
+
+ def get_configuration_pane(self):
+ ''' Get the configuration pane if available.
+ Qt widgets are not reusable, so we have to create it new every time
+ '''
+ return _ConfigurationPane(self)
+
+ def _design_filter(self, frequency, type, slice):
+ ''' Create filter settings for channel groups with equal filter parameters
+ @param frequency: filter frequeny in Hz
+ @param type: filter type, "low", "high" or "bandstop"
+ @param slice: channel group indices
+ @return: filter parameters and state vector
+ '''
+ if (frequency == 0.0) or (frequency > self.samplefreq/2.0):
+ return None
+ if type == "bandstop":
+ cut1 = (frequency-1.0) / self.samplefreq * 2.0
+ cut2 = (frequency+1.0) / self.samplefreq * 2.0
+ b,a = signal.filter_design.iirfilter(2, [cut1, cut2], btype=type, ftype='butter')
+ #b,a = signal.filter_design.iirfilter(2, [cut1, cut2], rs=40.0, rp=0.5, btype=type, ftype='elliptic')
+ else:
+ cut = frequency / self.samplefreq * 2.0
+ b,a = signal.filter_design.butter(self.filterorder, cut, btype=type)
+ zi = signal.lfiltic(b, a, (0.0,))
+ czi = np.resize(zi, (slice.stop - slice.start, len(zi)))
+ return {'slice':slice, 'a':a, 'b':b, 'zi':czi, 'frequency':frequency}
+
+ def process_update(self, params):
+ ''' Calculate filter parameters for updated channels
+ '''
+ # update the local reference
+ if self.params == None:
+ self.params = params
+ else:
+ # merge filter settings
+ for ch in params.channel_properties:
+ if ch.group == ChannelGroup.EEG:
+ ch.lowpass = self.lpGlobal
+ ch.highpass = self.hpGlobal
+ ch.notchfilter = self.notchGlobal
+ else:
+ # get local channel property by input number
+ filterChannel = None
+ for filter in self.params.channel_properties:
+ if filter.input == ch.input and filter.inputgroup == ch.inputgroup:
+ filterChannel = filter
+ break
+ if filterChannel != None:
+ ch.lowpass = filterChannel.lowpass
+ ch.highpass = filterChannel.highpass
+ ch.notchfilter = filterChannel.notchfilter
+ self.params = params
+
+
+ # reset filter
+ self.samplefreq = params.sample_rate
+ self.lpFilter = []
+ self.hpFilter = []
+ self.notchFilter = []
+
+ # nothing to filter
+ if len(params.channel_properties) == 0:
+ return params
+
+ # create channel slices and filter for continuous notch filters
+ notch = params.channel_properties[0].notchfilter
+ slc = slice(0,1,1)
+ for property in params.channel_properties[1::]:
+ if property.notchfilter == notch:
+ slc = slice(slc.start, slc.stop+1, 1)
+ else:
+ # design filter
+ if notch: freq = self.notchFrequency
+ else: freq = 0.0
+ filter = self._design_filter(freq, 'bandstop', slc)
+ if filter != None:
+ self.notchFilter.append(filter)
+ slc = slice(slc.stop, slc.stop+1, 1)
+ notch = property.notchfilter
+ if notch: freq = self.notchFrequency
+ else: freq = 0.0
+ filter = self._design_filter(freq, 'bandstop', slc)
+ if filter != None:
+ self.notchFilter.append(filter)
+
+ # create channel slices and filter for continuous unique lowpass filter frequencies
+ freq = params.channel_properties[0].lowpass
+ slc = slice(0,1,1)
+ for property in params.channel_properties[1::]:
+ if property.lowpass == freq:
+ slc = slice(slc.start, slc.stop+1, 1)
+ else:
+ # design filter
+ filter = self._design_filter(freq, 'low', slc)
+ if filter != None:
+ self.lpFilter.append(filter)
+ slc = slice(slc.stop, slc.stop+1, 1)
+ freq = property.lowpass
+ filter = self._design_filter(freq, 'low', slc)
+ if filter != None:
+ self.lpFilter.append(filter)
+
+ # create channel slices and filter for continuous unique highpass filter frequencies
+ freq = params.channel_properties[0].highpass
+ slc = slice(0,1,1)
+ for property in params.channel_properties[1::]:
+ if property.highpass == freq:
+ slc = slice(slc.start, slc.stop+1, 1)
+ else:
+ # design filter
+ filter = self._design_filter(freq, 'high', slc)
+ if filter != None:
+ self.hpFilter.append(filter)
+ slc = slice(slc.stop, slc.stop+1, 1)
+ freq = property.highpass
+ filter = self._design_filter(freq, 'high', slc)
+ if filter != None:
+ self.hpFilter.append(filter)
+
+ # propagate down
+ return params
+
+ def process_input(self, datablock):
+ ''' Filter all channel groups
+ '''
+ self.dataavailable = True
+ self.data = datablock
+
+ # don't filter impedance values
+ if self.data.recording_mode == RecordingMode.IMPEDANCE:
+ return
+
+ # replace channel filter configuration within the data block with our modified configuration
+ for channel in range(len(self.data.channel_properties)):
+ self.data.channel_properties[channel].lowpass = self.params.channel_properties[channel].lowpass
+ self.data.channel_properties[channel].highpass = self.params.channel_properties[channel].highpass
+ self.data.channel_properties[channel].notchfilter = self.params.channel_properties[channel].notchfilter
+
+ # highpass filter
+ for flt in self.hpFilter:
+ self.data.eeg_channels[flt['slice']],flt['zi'] = \
+ signal.lfilter(flt['b'], flt['a'],
+ self.data.eeg_channels[flt['slice']], zi=flt['zi'])
+
+ # lowpass filter
+ for flt in self.lpFilter:
+ self.data.eeg_channels[flt['slice']],flt['zi'] = \
+ signal.lfilter(flt['b'], flt['a'],
+ self.data.eeg_channels[flt['slice']], zi=flt['zi'])
+
+ # notch filter
+ for flt in self.notchFilter:
+ self.data.eeg_channels[flt['slice']],flt['zi'] = \
+ signal.lfilter(flt['b'], flt['a'],
+ self.data.eeg_channels[flt['slice']], zi=flt['zi'])
+
+
+
+ def process_output(self):
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+ e.g.
+
+ 50.0
+ ...
+
+ '''
+ E = objectify.E
+ channels = E.channels()
+ for channel in self.params.channel_properties:
+ if channel.group != ChannelGroup.EEG:
+ channels.append(channel.getXML())
+ cfg = E.EegFilter(E.notch_frequency(self.notchFrequency),
+ E.lp_global(self.lpGlobal),
+ E.hp_global(self.hpGlobal),
+ E.notch_global(self.notchGlobal),
+ channels,
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="filter")
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search my configuration data
+ storages = xml.xpath("//EegFilter[@module='filter' and @instance='%i']"%(self._instance) )
+ if len(storages) == 0:
+ # configuration data not found, set default values
+ self.notchFrequency = 50.0
+ return
+
+ # we should have only one instance from this type
+ cfg = storages[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ self.notchFrequency = cfg.notch_frequency.pyval
+ if version > 1:
+ # get global filter values
+ self.notchGlobal = cfg.notch_global.pyval
+ self.lpGlobal = cfg.lp_global.pyval
+ self.hpGlobal = cfg.hp_global.pyval
+ # get single channel filter values
+ channel_properties = []
+ for idx, channel in enumerate(cfg.channels.iterchildren()):
+ ch = EEG_ChannelProperties("")
+ ch.setXML(channel)
+ channel_properties.append(ch)
+ self.params.channel_properties = np.array(channel_properties)
+
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+
+'''
+------------------------------------------------------------
+FILTER MODULE CONFIGURATION PANE
+------------------------------------------------------------
+'''
+
+class _ConfigurationPane(Qt.QFrame, frmFilterConfig.Ui_frmFilterConfig):
+ ''' Module configuration pane
+ '''
+ def __init__(self, filter, *args):
+ ''' Constructor
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+ self.tableView.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
+
+ # setup content
+ self.filter = filter
+ # notch frequency
+ idx = self._get_cb_index(self.comboBox_Notch, self.filter.notchFrequency)
+ if idx >= 0:
+ self.comboBox_Notch.setCurrentIndex(idx)
+
+ # channel tables
+ self._fillChannelTables()
+
+ # Global lowpass filter
+ self.comboBoxEegLowpass.addItems(self.table_model.lowpasslist)
+ idx = self._get_cb_index(self.comboBoxEegLowpass, self.filter.lpGlobal)
+ if idx >= 0:
+ self.comboBoxEegLowpass.setCurrentIndex(idx)
+
+ # Global highpass filter
+ self.comboBoxEegHighpass.addItems(self.table_model.highpasslist)
+ idx = self._get_cb_index(self.comboBoxEegHighpass, self.filter.hpGlobal)
+ if idx >= 0:
+ self.comboBoxEegHighpass.setCurrentIndex(idx)
+
+ # global notch filter
+ self.checkBoxEegNotch.setChecked(self.filter.notchGlobal)
+
+ # actions
+ self.connect(self.comboBox_Notch, Qt.SIGNAL("currentIndexChanged(QString)"), self._notchFrequencyChanged)
+ self.connect(self.checkBoxEegNotch, Qt.SIGNAL("stateChanged(int)"), self._notchFilterChanged)
+ self.connect(self.comboBoxEegHighpass, Qt.SIGNAL("currentIndexChanged(QString)"), self._highpassChanged)
+ self.connect(self.comboBoxEegLowpass, Qt.SIGNAL("currentIndexChanged(QString)"), self._lowpassChanged)
+
+ def _get_cb_index(self, cb, value):
+ ''' Get closest matching combobox index
+ @param cb: combobox object
+ @param value: float lookup value
+ '''
+ itemlist = []
+ for i in range(cb.count()):
+ val,ok = cb.itemText(i).toFloat()
+ itemlist.append( (i, val) )
+ idx = itemlist[-1][0]
+ for item in sorted(itemlist, key=itemgetter(1)):
+ if item[1] >= value - 0.0001:
+ idx = item[0]
+ break
+ return idx
+
+ def _notchFrequencyChanged(self, value):
+ self.filter.notchFrequency, ok = value.toFloat()
+
+ def _notchFilterChanged(self, value):
+ self.filter.notchGlobal = (value == Qt.Qt.Checked)
+
+ def _lowpassChanged(self, value):
+ self.filter.lpGlobal, ok = value.toFloat()
+
+ def _highpassChanged(self, value):
+ self.filter.hpGlobal, ok = value.toFloat()
+
+ def _fillChannelTables(self):
+ ''' Create and fill channel tables
+ '''
+ # AUX channel table
+ mask = lambda x: x.group != ChannelGroup.EEG
+ ch_map = np.array([mask(ch) for ch in self.filter.params.channel_properties], dtype=bool)
+ ch_indices = np.nonzero(ch_map)[0]
+ self.table_model = _ConfigTableModel(self.filter.params.channel_properties[ch_indices])
+ self.tableView.setModel(self.table_model)
+ self.tableView.setItemDelegate(_ConfigItemDelegate())
+ self.tableView.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
+
+ # actions
+ self.connect(self.table_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._channeltable_changed)
+
+ def _channeltable_changed(self, topLeft, bottomRight):
+ ''' SIGNAL data in channel table has changed
+ '''
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+ def showEvent(self, event):
+ self._fillChannelTables()
+
+
+class _ConfigTableModel(Qt.QAbstractTableModel):
+ ''' EEG and AUX table data model for the filter configuration pane
+ '''
+ def __init__(self, data, parent=None, *args):
+ ''' Constructor
+ @param data: array of EEG_ChannelProperties objects
+ '''
+ Qt.QAbstractTableModel.__init__(self, parent, *args)
+ self.arraydata = data
+ # column description
+ self.columns = [{'property':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
+ {'property':'lowpass', 'header':'High Cutoff', 'edit':True, 'editor':'combobox'},
+ {'property':'highpass', 'header':'Low Cutoff', 'edit':True, 'editor':'combobox'},
+ {'property':'notchfilter', 'header':'Notch', 'edit':True, 'editor':'default'},
+ {'property':'name', 'header':'Name', 'edit':False, 'editor':'default'},
+ ]
+
+ # combo box list contents
+ self.lowpasslist = ['off', '10', '20', '30', '50', '100', '200', '500', '1000', '2000']
+ self.highpasslist = ['off','0.01', '0.02', '0.05', '0.1', '0.2', '0.5', '1', '2', '5', '10']
+
+ def _getitem(self, row, column):
+ ''' Get amplifier property item based on table row and column
+ @param row: row number
+ @param column: column number
+ @return: QVariant property value
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return Qt.QVariant()
+
+ # get channel properties
+ property = self.arraydata[row]
+ # get property name from column description
+ property_name = self.columns[column]['property']
+ # get property value
+ if property_name == 'input':
+ d = Qt.QVariant(property.input)
+ elif property_name == 'enable':
+ d = Qt.QVariant(property.enable)
+ elif property_name == 'name':
+ d = Qt.QVariant(property.name)
+ elif property_name == 'lowpass':
+ if property.lowpass == 0.0:
+ d = Qt.QVariant('off')
+ else:
+ d = Qt.QVariant(property.lowpass)
+ elif property_name == 'highpass':
+ if property.highpass == 0.0:
+ d = Qt.QVariant('off')
+ else:
+ d = Qt.QVariant(property.highpass)
+ elif property_name == 'notchfilter':
+ d = Qt.QVariant(property.notchfilter)
+ elif property_name == 'isReference':
+ d = Qt.QVariant(property.isReference)
+ else:
+ d = Qt.QVariant()
+ return d
+
+ def _setitem(self, row, column, value):
+ ''' Set amplifier property item based on table row and column
+ @param row: row number
+ @param column: column number
+ @param value: QVariant value object
+ @return: True if property value was set, False if not
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return False
+ # get channel properties
+ property = self.arraydata[row]
+ # get property name from column description
+ property_name = self.columns[column]['property']
+ # set channel property
+ if property_name == 'enable':
+ property.enable = value.toBool()
+ return True
+ elif property_name == 'name':
+ property.name = value.toString()
+ return True
+ elif property_name == 'lowpass':
+ property.lowpass,ok = value.toDouble()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.lowpass = property.lowpass
+ return True
+ elif property_name == 'highpass':
+ property.highpass,ok = value.toDouble()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.highpass = property.highpass
+ return True
+ elif property_name == 'notchfilter':
+ property.notchfilter = value.toBool()
+ if property.group == ChannelGroup.EEG:
+ for prop in self.arraydata:
+ prop.notchfilter = property.notchfilter
+ self.reset()
+ return True
+ elif property_name == 'isReference':
+ # available for EEG channels only
+ if property.group == ChannelGroup.EEG:
+ # remove previously selected reference channel
+ if value.toBool() == True:
+ for prop in self.arraydata:
+ prop.isReference = False
+ property.isReference = value.toBool()
+ self.reset()
+ return True
+ return False
+
+ def editorType(self, column):
+ ''' Get the columns editer type from column description
+ @param column: table column number
+ @return: editor type as QVariant (string)
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ return Qt.QVariant(self.columns[column]['editor'])
+
+ def comboBoxList(self, column):
+ ''' Get combo box item list for column 'highpass' or 'lowpass'
+ @param column: table column number
+ @return: combo box item list as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if self.columns[column]['property'] == 'lowpass':
+ return Qt.QVariant(self.lowpasslist)
+ elif self.columns[column]['property'] == 'highpass':
+ return Qt.QVariant(self.highpasslist)
+ else:
+ return Qt.QVariant()
+
+ def rowCount(self, parent):
+ ''' Get the number of required table rows
+ @return: number of rows
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.arraydata)
+
+ def columnCount(self, parent):
+ ''' Get the number of required table columns
+ @return: number of columns
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.columns)
+
+ def data(self, index, role):
+ ''' Abstract method from QAbstactItemModel to get cell data based on role
+ @param index: QModelIndex table cell reference
+ @param role: given role for the item referred to by the index
+ @return: the data stored under the given role for the item referred to by the index
+ '''
+ if not index.isValid():
+ return Qt.QVariant()
+
+ # get the underlying data
+ value = self._getitem(index.row(), index.column())
+
+ if role == Qt.Qt.CheckStateRole:
+ if value.type() == Qt.QMetaType.Bool:
+ if value.toBool():
+ return Qt.Qt.Checked
+ else:
+ return Qt.Qt.Unchecked
+
+ elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
+ if value.type() != Qt.QMetaType.Bool:
+ return value
+
+ elif role == Qt.Qt.BackgroundRole:
+ # change background color for reference channel
+ property = self.arraydata[index.row()]
+ #if (property.isReference) and (index.column() == 0):
+ if (property.isReference):
+ return Qt.QVariant( Qt.QColor(0, 0, 255))
+
+ return Qt.QVariant()
+
+ def flags(self, index):
+ ''' Abstract method from QAbstactItemModel
+ @param index: QModelIndex table cell reference
+ @return: the item flags for the given index
+ '''
+ if not index.isValid():
+ return Qt.Qt.ItemIsEnabled
+ if not self.columns[index.column()]['edit']:
+ return Qt.Qt.ItemIsEnabled
+ value = self._getitem(index.row(), index.column())
+ if value.type() == Qt.QMetaType.Bool:
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
+
+ def setData(self, index, value, role):
+ ''' Abstract method from QAbstactItemModel to set cell data based on role
+ @param index: QModelIndex table cell reference
+ @param value: QVariant new cell data
+ @param role: given role for the item referred to by the index
+ @return: true if successful; otherwise returns false.
+ '''
+ if index.isValid():
+ if role == Qt.Qt.EditRole:
+ if not self._setitem(index.row(), index.column(), value):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
+ return True
+ elif role == Qt.Qt.CheckStateRole:
+ if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
+ return True
+ return False
+
+ def headerData(self, col, orientation, role):
+ ''' Abstract method from QAbstactItemModel to get the column header
+ @param col: column number
+ @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
+ @param role: given role for the item referred to by the index
+ @return: header
+ '''
+ if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
+ return Qt.QVariant(self.columns[col]['header'])
+ return Qt.QVariant()
+
+
+class _ConfigItemDelegate(Qt.QStyledItemDelegate):
+ ''' Combobox item editor
+ '''
+ def __init__(self, parent=None):
+ super(_ConfigItemDelegate, self).__init__(parent)
+
+ def createEditor(self, parent, option, index):
+ if index.model().editorType(index.column()) == 'combobox':
+ combobox = Qt.QComboBox(parent)
+ combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
+ combobox.setEditable(False)
+ self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
+ return combobox
+ return Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
+
+ def setEditorData(self, editor, index):
+ if index.model().columns[index.column()]['editor'] == 'combobox':
+ text = index.model().data(index, Qt.Qt.DisplayRole).toString()
+ i = editor.findText(text)
+ if i == -1:
+ i = 0
+ editor.setCurrentIndex(i)
+ Qt.QStyledItemDelegate.setEditorData(self, editor, index)
+
+
+ def setModelData(self, editor, model, index):
+ if model.columns[index.column()]['editor'] == 'combobox':
+ model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
+ model.reset()
+ Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
+
+ def emitCommitData(self):
+ self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
+
diff --git a/headless.py b/headless.py
new file mode 100644
index 0000000..62aff3a
--- /dev/null
+++ b/headless.py
@@ -0,0 +1,63 @@
+"""Minimal headless fallback when GUI dependencies are absent.
+
+This prints a helpful diagnostics summary instead of crashing so that
+`python main.py` completes successfully even if Qt/numpy/other heavy
+packages are unavailable in the current environment.
+"""
+from __future__ import annotations
+
+import textwrap
+from typing import Iterable, Sequence
+
+
+def _format_list(items: Sequence[str]) -> str:
+ if not items:
+ return "(none)"
+ return ", ".join(sorted(items))
+
+
+def run(reason: str | None = None) -> None:
+ """Show dependency diagnostics and exit cleanly.
+
+ Parameters
+ ----------
+ reason:
+ Optional short text explaining why the GUI path could not start
+ (for example the ImportError message from PyQt4/PySide6).
+ """
+ try:
+ import loadlibs
+ status = loadlibs.dependency_status()
+ except Exception: # pragma: no cover - worst case fall back to empty status
+ status = {"missing": [], "version_mismatch": [], "log": ""}
+
+ missing: Iterable[str] = status.get("missing", [])
+ mismatches: Iterable[str] = status.get("version_mismatch", [])
+ log: str = status.get("log", "")
+
+ print("PyCorder GUI dependencies are not available.\n")
+ if reason:
+ print(f"Root cause: {reason}\n")
+
+ if missing or mismatches:
+ print("Dependency summary:")
+ print(f" Missing packages : {_format_list(list(missing))}")
+ print(f" Version warnings : {_format_list(list(mismatches))}\n")
+
+ if log:
+ print("Details from dependency probe:\n")
+ print(textwrap.indent(log, prefix=" ") + "\n")
+
+ suggested = (
+ "python3 -m pip install --user numpy scipy lxml PySide6 pyqtgraph"
+ )
+ print("To run the full PyCorder GUI install the dependencies above.")
+ print(f"Suggested pip command:\n {suggested}\n")
+ print(
+ "This headless fallback exited successfully so that automated"
+ " environments can continue without error."
+ )
+
+
+if __name__ == "__main__":
+ run()
diff --git a/impedance.py b/impedance.py
index 2dbca88..4dce892 100644
--- a/impedance.py
+++ b/impedance.py
@@ -1,468 +1,468 @@
-# -*- coding: utf-8 -*-
-'''
-Impedance Display Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-07 19:21:40 +0200 (Fr, 07 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 198 $
-'''
-
-from PyQt4 import Qwt5 as Qwt
-from modbase import *
-from res import frmImpedanceDisplay
-
-class IMP_Display(ModuleBase):
- ''' Display impedance values
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- ModuleBase.__init__(self, name="Impedance Display", **keys)
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # set default values
- self.params = None
- self.data = None
- self.dataavailable = False
-
- self.impDialog = None #: Impedance dialog widget
- self.range_max = 50 #: Impedance range 0-range_max in KOhm
- self.show_values = True #: Show numerical impedance values
-
- def terminate(self):
- ''' Destructor
- '''
- # close dialog widget on exit
- if self.impDialog != None:
- self.impDialog.close()
- self.impDialog = None
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- self.range_max = 50
- self.show_values = True
-
- def process_start(self):
- ''' Prepare and open impedance dialog if recording mode == IMPEDANCE
- '''
- # create and show the impedance dialog
- if self.params.recording_mode == RecordingMode.IMPEDANCE:
- if self.impDialog == None:
- # impedance dialog should be always on top
- topLevelWidgets = Qt.QApplication.topLevelWidgets()
- activeWindow = Qt.QApplication.activeWindow()
- if activeWindow:
- self.impDialog = DlgImpedance(self, Qt.QApplication.activeWindow())
- else:
- if len(topLevelWidgets):
- self.impDialog = DlgImpedance(self, topLevelWidgets[0])
- else:
- self.impDialog = DlgImpedance(self)
- self.impDialog.setWindowFlags(Qt.Qt.Tool)
- self.impDialog.show()
- self.impDialog.updateLabels(self.params)
- else:
- self.impDialog.updateLabels(self.params)
- self.sendColorRange()
- else:
- if self.impDialog != None:
- self.impDialog.close()
- self.impDialog = None
-
- def process_stop(self):
- ''' Close impedance dialog
- '''
- if self.impDialog != None:
- self.impDialog.close()
- self.impDialog = None
-
-
- def process_update(self, params):
- ''' Get channel properties and
- propagate parameter update down to all attached receivers
- '''
- self.params = params
- # propagate down
- return params
-
- def process_input(self, datablock):
- ''' Get data from input queue and update display
- '''
- self.dataavailable = True
- self.data = datablock
-
- # nothing to do if not in impedance mode
- if datablock.recording_mode != RecordingMode.IMPEDANCE:
- return
-
- # check for an outdated impedance structure
- if len(datablock.impedances) > 0 or len(datablock.channel_properties) != len(self.params.channel_properties):
- raise ModuleError(self._object_name, "outdated impedance structure received!")
-
- if self.impDialog != None:
- self.emit(Qt.SIGNAL('update(PyQt_PyObject)'), datablock)
-
- def process_output(self):
- ''' Put processed data into output queue
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
- e.g.
-
- 50
- ...
-
- '''
- E = objectify.E
- cfg = E.IMP_Display(E.range_max(self.range_max),
- E.show_values(self.show_values),
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="impedance")
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search my configuration data
- displays = xml.xpath("//IMP_Display[@module='impedance' and @instance='%i']"%(self._instance) )
- if len(displays) == 0:
- # configuration data not found, leave everything unchanged
- return
-
- # we should have only one display instance from this type
- cfg = displays[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- self.range_max = cfg.range_max.pyval
- self.show_values = cfg.show_values.pyval
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- def sendColorRange(self):
- ''' Send new impedance color range as ModuleEvent to update ActiCap LED color range
- '''
- val = tuple([self.range_max / 3.0, self.range_max * 2.0 / 3.0])
- self.send_event(ModuleEvent(self._object_name, EventType.COMMAND, info="ImpColorRange",
- cmd_value = val))
-
-
-'''
-------------------------------------------------------------
-IMPEDANCE DIALOG
-------------------------------------------------------------
-'''
-
-class DlgImpedance(Qt.QDialog, frmImpedanceDisplay.Ui_frmImpedanceDisplay):
- ''' Impedance display dialog
- '''
- def __init__(self, module, *args):
- ''' Constructor
- @param module: parent module
- '''
- apply(Qt.QDialog.__init__, (self,) + args)
- self.setupUi(self)
- self.module = module
- self.params = None # last received parameter block
- self.data = None # last received data block
-
- # create table view grid (10x16 eeg electrodes + 1 row for ground electrode)
- cc = 10
- rc = 16
- self.tableWidgetValues.setColumnCount(cc)
- self.tableWidgetValues.setRowCount(rc+1)
- self.tableWidgetValues.horizontalHeader().setResizeMode(Qt.QHeaderView.Stretch)
- self.tableWidgetValues.horizontalHeader().setDefaultAlignment(Qt.Qt.Alignment(Qt.Qt.AlignCenter))
- self.tableWidgetValues.verticalHeader().setResizeMode(Qt.QHeaderView.Stretch)
- self.tableWidgetValues.verticalHeader().setDefaultAlignment(Qt.Qt.Alignment(Qt.Qt.AlignCenter))
- # add ground electrode row
- self.tableWidgetValues.setSpan(rc,0,1,cc)
- # row headers
- rheader = Qt.QStringList()
- for r in xrange(rc):
- rheader.append("%d - %d"%(r*cc+1, r*cc+cc))
- rheader.append("GND")
- self.tableWidgetValues.setVerticalHeaderLabels(rheader)
- # create cell items
- fnt = Qt.QFont()
- fnt.setPointSize(8)
- for r in xrange(rc):
- for c in xrange(cc):
- item = Qt.QTableWidgetItem()
- item.setTextAlignment(Qt.Qt.AlignCenter)
- item.setFont(fnt)
- self.tableWidgetValues.setItem(r, c, item)
- # GND electrode cell
- item = Qt.QTableWidgetItem()
- item.setTextAlignment(Qt.Qt.AlignCenter)
- item.setFont(fnt)
- item.setText("GND")
- self.tableWidgetValues.setItem(rc, 0, item)
- self.defaultColor = item.backgroundColor()
-
- # set range list
- self.comboBoxRange.clear()
- self.comboBoxRange.addItem("15")
- self.comboBoxRange.addItem("50")
- self.comboBoxRange.addItem("100")
- self.comboBoxRange.addItem("500")
-
- # set validators
- validator = Qt.QIntValidator(self)
- validator.setBottom(15)
- validator.setTop(500)
- self.comboBoxRange.setValidator(validator)
- self.comboBoxRange.setEditText(str(self.module.range_max))
-
- # setup color scale
- self.linearscale = False
- self.scale_engine = Qwt.QwtLinearScaleEngine()
- self.scale_interval = Qwt.QwtDoubleInterval(0, self.module.range_max)
- self.scale_map = Qwt.QwtLinearColorMap(Qt.Qt.green, Qt.Qt.red)
- if self.linearscale:
- self.scale_map.addColorStop(0.45, Qt.Qt.yellow)
- self.scale_map.addColorStop(0.55, Qt.Qt.yellow)
- self.scale_map.setMode(Qwt.QwtLinearColorMap.ScaledColors)
- else:
- self.scale_map.addColorStop(0.33, Qt.Qt.yellow)
- self.scale_map.addColorStop(0.66, Qt.Qt.red)
- self.scale_map.setMode(Qwt.QwtLinearColorMap.FixedColors)
- self.ScaleWidget.setColorMap(self.scale_interval, self.scale_map)
- self.ScaleWidget.setColorBarEnabled(True)
- self.ScaleWidget.setColorBarWidth(30)
- self.ScaleWidget.setBorderDist(10,10)
-
- # set default values
- self.setColorRange(0, self.module.range_max)
- self.checkBoxValues.setChecked(self.module.show_values)
-
- # actions
- self.connect(self.comboBoxRange, Qt.SIGNAL("editTextChanged(QString)"), self._rangeChanged)
- self.connect(self.checkBoxValues, Qt.SIGNAL("stateChanged(int)"), self._showvalues_changed)
- self.connect(self.module, Qt.SIGNAL('update(PyQt_PyObject)'), self._updateValues)
-
-
- def _rangeChanged(self, rrange):
- ''' SIGNAL range combo box value has changed
- @param range: new range value in KOhm
- '''
- # validate range
- valid = self.comboBoxRange.validator().validate(rrange,0)[0]
- if valid != Qt.QValidator.Acceptable:
- return
- # use new range
- newrange,ok = rrange.toInt()
- if ok:
- self.setColorRange(0, newrange)
- self.module.range_max = newrange
- self._updateValues(self.data)
- self.module.sendColorRange()
-
- def _showvalues_changed(self, state):
- ''' SIGNAL show values radio button clicked
- '''
- self.module.show_values = (state == Qt.Qt.Checked)
- self._updateValues(self.data)
-
-
- def setColorRange(self, cmin, cmax):
- ''' Create new color range for the scale widget
- '''
- self.scale_interval.setMaxValue(cmax)
- self.scale_interval.setMinValue(cmin)
- self.ScaleWidget.setColorMap(self.scale_interval, self.scale_map)
- self.ScaleWidget.setScaleDiv(self.scale_engine.transformation(),
- self.scale_engine.divideScale(self.scale_interval.minValue(),
- self.scale_interval.maxValue(),
- 5, 2))
-
-
- def closeEvent(self, event):
- ''' Dialog want's close, send stop request to main window
- '''
- self.setParent(None)
- self.disconnect(self.module, Qt.SIGNAL('update(PyQt_PyObject)'), self._updateValues)
- if self.sender() == None:
- self.module.send_event(ModuleEvent(self.module._object_name, EventType.COMMAND, "Stop"))
- event.accept()
-
-
- def reject(self):
- ''' ESC key pressed, Dialog want's close, just ignore it
- '''
- return
-
- def _setLabelText(self, row, col, text):
- item = self.tableWidgetValues.item(row, col)
- item.setText(text)
- item.setBackgroundColor(Qt.QColor(128,128,128))
- item.label = text
-
- def updateLabels(self, params):
- ''' Update cell labels
- '''
- # copy channel configuration
- self.params = copy.deepcopy(params)
-
- # update cells
- cc = self.tableWidgetValues.columnCount()
- rc = self.tableWidgetValues.rowCount() - 1
- # reset items
- for row in xrange(rc):
- for col in xrange(cc):
- item = self.tableWidgetValues.item(row, col)
- item.setText("")
- item.label = ""
- item.setBackgroundColor(Qt.Qt.white)
- # set channel labels
- for idx, ch in enumerate(self.params.channel_properties):
- if (ch.enable or ch.isReference) and (ch.input > 0) and (ch.input <= rc*cc) and (ch.inputgroup == ChannelGroup.EEG):
- row = (ch.input-1) / cc
- col = (ch.input-1) % cc
- # channel has a reference impedance value?
- if self.params.eeg_channels[idx, ImpedanceIndex.REF] == 1:
- # prefix the channel name
- name = ch.name + " " + ImpedanceIndex.Name[ImpedanceIndex.DATA]
- self._setLabelText(row, col, name)
- # put the reference values at the following table item, if possible
- name = ch.name + " " + ImpedanceIndex.Name[ImpedanceIndex.REF]
- row = (ch.input) / cc
- col = (ch.input) % cc
- self._setLabelText(row, col, name)
- else:
- self._setLabelText(row, col, ch.name)
-
- def _getValueText(self, impedance):
- ''' evaluate the impedance value and get the text and color for display
- @return: text and color
- '''
- if impedance > CHAMP_IMP_INVALID:
- valuetext = "disconnected"
- color = Qt.QColor(128,128,128)
- else:
- v = impedance / 1000.0
- if impedance == CHAMP_IMP_INVALID:
- valuetext = "out of range"
- else:
- valuetext = "%.0f"%(v)
- color = self.ScaleWidget.colorMap().color(self.ScaleWidget.colorBarInterval(), v)
- return valuetext, color
-
- def _updateValues(self, data):
- ''' SIGNAL send from impedance module to update cell values
- @param data: EEG_DataBlock
- '''
- if data == None:
- return
- # keep the last data block
- self.data = copy.deepcopy(data)
-
- # check for an outdated impedance structure
- if len(data.impedances) > 0 or len(data.channel_properties) != len(self.params.channel_properties):
- print "outdated impedance structure received!"
- return
-
- cc = self.tableWidgetValues.columnCount()
- rc = self.tableWidgetValues.rowCount() - 1
- # EEG electrodes
- gndImpedance = None
- impCount = 0
- for idx, ch in enumerate(data.channel_properties):
- if (ch.enable or ch.isReference) and (ch.input > 0) and (ch.input <= rc*cc) and (ch.inputgroup == ChannelGroup.EEG):
- impCount += 1
- row = (ch.input-1) / cc
- col = (ch.input-1) % cc
- item = self.tableWidgetValues.item(row, col)
-
- # channel has a data impedance value?
- if self.params.eeg_channels[idx, ImpedanceIndex.DATA] == 1:
- # data channel value
- value, color = self._getValueText(data.eeg_channels[idx, ImpedanceIndex.DATA])
- item.setBackgroundColor(color)
- if self.module.show_values:
- item.setText("%s\n%s"%(item.label, value))
- else:
- item.setText(item.label)
-
- # channel has a reference impedance value?
- if self.params.eeg_channels[idx, ImpedanceIndex.REF] == 1:
- row = (ch.input) / cc
- col = (ch.input) % cc
- item = self.tableWidgetValues.item(row, col)
- # reference channel value
- value, color = self._getValueText(data.eeg_channels[idx, ImpedanceIndex.REF])
- item.setBackgroundColor(color)
- if self.module.show_values:
- item.setText("%s\n%s"%(item.label, value))
- else:
- item.setText(item.label)
-
- # channel has a GND impedance value?
- if gndImpedance == None and self.params.eeg_channels[idx, ImpedanceIndex.GND] == 1:
- gndImpedance = data.eeg_channels[idx, ImpedanceIndex.GND]
-
-
- # GND electrode, take the value of the first EEG electrode
- item = self.tableWidgetValues.item(rc, 0)
- if gndImpedance == None:
- item.setText("")
- item.setBackgroundColor(Qt.Qt.white)
- else:
- value, color = self._getValueText(gndImpedance)
- item.setBackgroundColor(color)
- if self.module.show_values:
- item.setText("%s\n%s"%("GND", value))
- else:
- item.setText("GND")
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Impedance Display Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-07 19:21:40 +0200 (Fr, 07 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 198 $
+'''
+
+from PyQt4 import Qwt5 as Qwt
+from modbase import *
+from res import frmImpedanceDisplay
+
+class IMP_Display(ModuleBase):
+ ''' Display impedance values
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ ModuleBase.__init__(self, name="Impedance Display", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # set default values
+ self.params = None
+ self.data = None
+ self.dataavailable = False
+
+ self.impDialog = None #: Impedance dialog widget
+ self.range_max = 50 #: Impedance range 0-range_max in KOhm
+ self.show_values = True #: Show numerical impedance values
+
+ def terminate(self):
+ ''' Destructor
+ '''
+ # close dialog widget on exit
+ if self.impDialog != None:
+ self.impDialog.close()
+ self.impDialog = None
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ self.range_max = 50
+ self.show_values = True
+
+ def process_start(self):
+ ''' Prepare and open impedance dialog if recording mode == IMPEDANCE
+ '''
+ # create and show the impedance dialog
+ if self.params.recording_mode == RecordingMode.IMPEDANCE:
+ if self.impDialog == None:
+ # impedance dialog should be always on top
+ topLevelWidgets = Qt.QApplication.topLevelWidgets()
+ activeWindow = Qt.QApplication.activeWindow()
+ if activeWindow:
+ self.impDialog = DlgImpedance(self, Qt.QApplication.activeWindow())
+ else:
+ if len(topLevelWidgets):
+ self.impDialog = DlgImpedance(self, topLevelWidgets[0])
+ else:
+ self.impDialog = DlgImpedance(self)
+ self.impDialog.setWindowFlags(Qt.Qt.Tool)
+ self.impDialog.show()
+ self.impDialog.updateLabels(self.params)
+ else:
+ self.impDialog.updateLabels(self.params)
+ self.sendColorRange()
+ else:
+ if self.impDialog != None:
+ self.impDialog.close()
+ self.impDialog = None
+
+ def process_stop(self):
+ ''' Close impedance dialog
+ '''
+ if self.impDialog != None:
+ self.impDialog.close()
+ self.impDialog = None
+
+
+ def process_update(self, params):
+ ''' Get channel properties and
+ propagate parameter update down to all attached receivers
+ '''
+ self.params = params
+ # propagate down
+ return params
+
+ def process_input(self, datablock):
+ ''' Get data from input queue and update display
+ '''
+ self.dataavailable = True
+ self.data = datablock
+
+ # nothing to do if not in impedance mode
+ if datablock.recording_mode != RecordingMode.IMPEDANCE:
+ return
+
+ # check for an outdated impedance structure
+ if len(datablock.impedances) > 0 or len(datablock.channel_properties) != len(self.params.channel_properties):
+ raise ModuleError(self._object_name, "outdated impedance structure received!")
+
+ if self.impDialog != None:
+ self.emit(Qt.SIGNAL('update(PyQt_PyObject)'), datablock)
+
+ def process_output(self):
+ ''' Put processed data into output queue
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+ e.g.
+
+ 50
+ ...
+
+ '''
+ E = objectify.E
+ cfg = E.IMP_Display(E.range_max(self.range_max),
+ E.show_values(self.show_values),
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="impedance")
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search my configuration data
+ displays = xml.xpath("//IMP_Display[@module='impedance' and @instance='%i']"%(self._instance) )
+ if len(displays) == 0:
+ # configuration data not found, leave everything unchanged
+ return
+
+ # we should have only one display instance from this type
+ cfg = displays[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ self.range_max = cfg.range_max.pyval
+ self.show_values = cfg.show_values.pyval
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ def sendColorRange(self):
+ ''' Send new impedance color range as ModuleEvent to update ActiCap LED color range
+ '''
+ val = tuple([self.range_max / 3.0, self.range_max * 2.0 / 3.0])
+ self.send_event(ModuleEvent(self._object_name, EventType.COMMAND, info="ImpColorRange",
+ cmd_value = val))
+
+
+'''
+------------------------------------------------------------
+IMPEDANCE DIALOG
+------------------------------------------------------------
+'''
+
+class DlgImpedance(Qt.QDialog, frmImpedanceDisplay.Ui_frmImpedanceDisplay):
+ ''' Impedance display dialog
+ '''
+ def __init__(self, module, *args):
+ ''' Constructor
+ @param module: parent module
+ '''
+ Qt.QDialog.__init__(self, *args)
+ self.setupUi(self)
+ self.module = module
+ self.params = None # last received parameter block
+ self.data = None # last received data block
+
+ # create table view grid (10x16 eeg electrodes + 1 row for ground electrode)
+ cc = 10
+ rc = 16
+ self.tableWidgetValues.setColumnCount(cc)
+ self.tableWidgetValues.setRowCount(rc+1)
+ self.tableWidgetValues.horizontalHeader().setResizeMode(Qt.QHeaderView.Stretch)
+ self.tableWidgetValues.horizontalHeader().setDefaultAlignment(Qt.Qt.Alignment(Qt.Qt.AlignCenter))
+ self.tableWidgetValues.verticalHeader().setResizeMode(Qt.QHeaderView.Stretch)
+ self.tableWidgetValues.verticalHeader().setDefaultAlignment(Qt.Qt.Alignment(Qt.Qt.AlignCenter))
+ # add ground electrode row
+ self.tableWidgetValues.setSpan(rc,0,1,cc)
+ # row headers
+ rheader = Qt.QStringList()
+ for r in range(rc):
+ rheader.append("%d - %d"%(r*cc+1, r*cc+cc))
+ rheader.append("GND")
+ self.tableWidgetValues.setVerticalHeaderLabels(rheader)
+ # create cell items
+ fnt = Qt.QFont()
+ fnt.setPointSize(8)
+ for r in range(rc):
+ for c in range(cc):
+ item = Qt.QTableWidgetItem()
+ item.setTextAlignment(Qt.Qt.AlignCenter)
+ item.setFont(fnt)
+ self.tableWidgetValues.setItem(r, c, item)
+ # GND electrode cell
+ item = Qt.QTableWidgetItem()
+ item.setTextAlignment(Qt.Qt.AlignCenter)
+ item.setFont(fnt)
+ item.setText("GND")
+ self.tableWidgetValues.setItem(rc, 0, item)
+ self.defaultColor = item.backgroundColor()
+
+ # set range list
+ self.comboBoxRange.clear()
+ self.comboBoxRange.addItem("15")
+ self.comboBoxRange.addItem("50")
+ self.comboBoxRange.addItem("100")
+ self.comboBoxRange.addItem("500")
+
+ # set validators
+ validator = Qt.QIntValidator(self)
+ validator.setBottom(15)
+ validator.setTop(500)
+ self.comboBoxRange.setValidator(validator)
+ self.comboBoxRange.setEditText(str(self.module.range_max))
+
+ # setup color scale
+ self.linearscale = False
+ self.scale_engine = Qwt.QwtLinearScaleEngine()
+ self.scale_interval = Qwt.QwtDoubleInterval(0, self.module.range_max)
+ self.scale_map = Qwt.QwtLinearColorMap(Qt.Qt.green, Qt.Qt.red)
+ if self.linearscale:
+ self.scale_map.addColorStop(0.45, Qt.Qt.yellow)
+ self.scale_map.addColorStop(0.55, Qt.Qt.yellow)
+ self.scale_map.setMode(Qwt.QwtLinearColorMap.ScaledColors)
+ else:
+ self.scale_map.addColorStop(0.33, Qt.Qt.yellow)
+ self.scale_map.addColorStop(0.66, Qt.Qt.red)
+ self.scale_map.setMode(Qwt.QwtLinearColorMap.FixedColors)
+ self.ScaleWidget.setColorMap(self.scale_interval, self.scale_map)
+ self.ScaleWidget.setColorBarEnabled(True)
+ self.ScaleWidget.setColorBarWidth(30)
+ self.ScaleWidget.setBorderDist(10,10)
+
+ # set default values
+ self.setColorRange(0, self.module.range_max)
+ self.checkBoxValues.setChecked(self.module.show_values)
+
+ # actions
+ self.connect(self.comboBoxRange, Qt.SIGNAL("editTextChanged(QString)"), self._rangeChanged)
+ self.connect(self.checkBoxValues, Qt.SIGNAL("stateChanged(int)"), self._showvalues_changed)
+ self.connect(self.module, Qt.SIGNAL('update(PyQt_PyObject)'), self._updateValues)
+
+
+ def _rangeChanged(self, rrange):
+ ''' SIGNAL range combo box value has changed
+ @param range: new range value in KOhm
+ '''
+ # validate range
+ valid = self.comboBoxRange.validator().validate(rrange,0)[0]
+ if valid != Qt.QValidator.Acceptable:
+ return
+ # use new range
+ newrange,ok = rrange.toInt()
+ if ok:
+ self.setColorRange(0, newrange)
+ self.module.range_max = newrange
+ self._updateValues(self.data)
+ self.module.sendColorRange()
+
+ def _showvalues_changed(self, state):
+ ''' SIGNAL show values radio button clicked
+ '''
+ self.module.show_values = (state == Qt.Qt.Checked)
+ self._updateValues(self.data)
+
+
+ def setColorRange(self, cmin, cmax):
+ ''' Create new color range for the scale widget
+ '''
+ self.scale_interval.setMaxValue(cmax)
+ self.scale_interval.setMinValue(cmin)
+ self.ScaleWidget.setColorMap(self.scale_interval, self.scale_map)
+ self.ScaleWidget.setScaleDiv(self.scale_engine.transformation(),
+ self.scale_engine.divideScale(self.scale_interval.minValue(),
+ self.scale_interval.maxValue(),
+ 5, 2))
+
+
+ def closeEvent(self, event):
+ ''' Dialog want's close, send stop request to main window
+ '''
+ self.setParent(None)
+ self.disconnect(self.module, Qt.SIGNAL('update(PyQt_PyObject)'), self._updateValues)
+ if self.sender() == None:
+ self.module.send_event(ModuleEvent(self.module._object_name, EventType.COMMAND, "Stop"))
+ event.accept()
+
+
+ def reject(self):
+ ''' ESC key pressed, Dialog want's close, just ignore it
+ '''
+ return
+
+ def _setLabelText(self, row, col, text):
+ item = self.tableWidgetValues.item(row, col)
+ item.setText(text)
+ item.setBackgroundColor(Qt.QColor(128,128,128))
+ item.label = text
+
+ def updateLabels(self, params):
+ ''' Update cell labels
+ '''
+ # copy channel configuration
+ self.params = copy.deepcopy(params)
+
+ # update cells
+ cc = self.tableWidgetValues.columnCount()
+ rc = self.tableWidgetValues.rowCount() - 1
+ # reset items
+ for row in range(rc):
+ for col in range(cc):
+ item = self.tableWidgetValues.item(row, col)
+ item.setText("")
+ item.label = ""
+ item.setBackgroundColor(Qt.Qt.white)
+ # set channel labels
+ for idx, ch in enumerate(self.params.channel_properties):
+ if (ch.enable or ch.isReference) and (ch.input > 0) and (ch.input <= rc*cc) and (ch.inputgroup == ChannelGroup.EEG):
+ row = (ch.input-1) // cc
+ col = (ch.input-1) % cc
+ # channel has a reference impedance value?
+ if self.params.eeg_channels[idx, ImpedanceIndex.REF] == 1:
+ # prefix the channel name
+ name = ch.name + " " + ImpedanceIndex.Name[ImpedanceIndex.DATA]
+ self._setLabelText(row, col, name)
+ # put the reference values at the following table item, if possible
+ name = ch.name + " " + ImpedanceIndex.Name[ImpedanceIndex.REF]
+ row = (ch.input) // cc
+ col = (ch.input) % cc
+ self._setLabelText(row, col, name)
+ else:
+ self._setLabelText(row, col, ch.name)
+
+ def _getValueText(self, impedance):
+ ''' evaluate the impedance value and get the text and color for display
+ @return: text and color
+ '''
+ if impedance > CHAMP_IMP_INVALID:
+ valuetext = "disconnected"
+ color = Qt.QColor(128,128,128)
+ else:
+ v = impedance / 1000.0
+ if impedance == CHAMP_IMP_INVALID:
+ valuetext = "out of range"
+ else:
+ valuetext = "%.0f"%(v)
+ color = self.ScaleWidget.colorMap().color(self.ScaleWidget.colorBarInterval(), v)
+ return valuetext, color
+
+ def _updateValues(self, data):
+ ''' SIGNAL send from impedance module to update cell values
+ @param data: EEG_DataBlock
+ '''
+ if data == None:
+ return
+ # keep the last data block
+ self.data = copy.deepcopy(data)
+
+ # check for an outdated impedance structure
+ if len(data.impedances) > 0 or len(data.channel_properties) != len(self.params.channel_properties):
+ print("outdated impedance structure received!")
+ return
+
+ cc = self.tableWidgetValues.columnCount()
+ rc = self.tableWidgetValues.rowCount() - 1
+ # EEG electrodes
+ gndImpedance = None
+ impCount = 0
+ for idx, ch in enumerate(data.channel_properties):
+ if (ch.enable or ch.isReference) and (ch.input > 0) and (ch.input <= rc*cc) and (ch.inputgroup == ChannelGroup.EEG):
+ impCount += 1
+ row = (ch.input-1) // cc
+ col = (ch.input-1) % cc
+ item = self.tableWidgetValues.item(row, col)
+
+ # channel has a data impedance value?
+ if self.params.eeg_channels[idx, ImpedanceIndex.DATA] == 1:
+ # data channel value
+ value, color = self._getValueText(data.eeg_channels[idx, ImpedanceIndex.DATA])
+ item.setBackgroundColor(color)
+ if self.module.show_values:
+ item.setText("%s\n%s"%(item.label, value))
+ else:
+ item.setText(item.label)
+
+ # channel has a reference impedance value?
+ if self.params.eeg_channels[idx, ImpedanceIndex.REF] == 1:
+ row = (ch.input) // cc
+ col = (ch.input) % cc
+ item = self.tableWidgetValues.item(row, col)
+ # reference channel value
+ value, color = self._getValueText(data.eeg_channels[idx, ImpedanceIndex.REF])
+ item.setBackgroundColor(color)
+ if self.module.show_values:
+ item.setText("%s\n%s"%(item.label, value))
+ else:
+ item.setText(item.label)
+
+ # channel has a GND impedance value?
+ if gndImpedance == None and self.params.eeg_channels[idx, ImpedanceIndex.GND] == 1:
+ gndImpedance = data.eeg_channels[idx, ImpedanceIndex.GND]
+
+
+ # GND electrode, take the value of the first EEG electrode
+ item = self.tableWidgetValues.item(rc, 0)
+ if gndImpedance == None:
+ item.setText("")
+ item.setBackgroundColor(Qt.Qt.white)
+ else:
+ value, color = self._getValueText(gndImpedance)
+ item.setBackgroundColor(color)
+ if self.module.show_values:
+ item.setText("%s\n%s"%("GND", value))
+ else:
+ item.setText("GND")
+
+
+
+
diff --git a/loadlibs.py b/loadlibs.py
index 276b95f..8c9f2bb 100644
--- a/loadlibs.py
+++ b/loadlibs.py
@@ -1,100 +1,171 @@
-# -*- coding: utf-8 -*-
-'''
-Load required libraries and check versions
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-
-@author: Norbert Hauser
-@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 62 $
-'''
-
-
-'''
-------------------------------------------------------------
-CHECK LIBRARY DEPENDENCIES
-------------------------------------------------------------
-'''
-
-import sys
-
-import_log = ""
-
-# required Python and library versions
-if sys.version_info[:2] == (2,7):
- #import_log += "Untested PyCorder running on Python Version 2.7 !\r\n\r\n"
- ver_Python = "2.7"
- ver_NumPy = ("1.8.2")
- ver_SciPy = ("0.14.0")
- ver_PyQt = ("4.8.6")
- ver_PyQwt = ("5.2.3",)
- ver_lxml = ("3.3.6")
-else:
- ver_Python = "2.6"
- ver_NumPy = ("1.3.0", "1.4.1")
- ver_SciPy = ("0.7.1", "0.8.0")
- ver_PyQt = ("4.5.2", "4.6.3")
- ver_PyQwt = ("5.2.1",)
- ver_lxml = ("2.2.4", "2.2.7")
-
-
-# try to import python libraries, check versions
-if not ver_Python in sys.version:
- import_log += "- Wrong Python version (%s), please install Python %s\r\n"%(sys.version, ver_Python)
-try:
- import numpy as np
- if not np.__version__ in ver_NumPy:
- import_log += "- Wrong NumPy version (%s), please install NumPy %s\r\n"%(np.__version__, ver_NumPy)
-except ImportError:
- import_log += "- NumPy missing, please install NumPy %s\r\n"%(str(ver_NumPy))
-
-try:
- import scipy as sc
- if not sc.__version__ in ver_SciPy:
- import_log += "- Wrong SciPy version (%s), please install SciPy %s\r\n"%(sc.__version__, ver_SciPy)
-except ImportError:
- import_log += "- SciPy missing, please install SciPy %s\r\n"%(str(ver_SciPy))
-
-try:
- from PyQt4 import Qt
- if not Qt.QT_VERSION_STR in ver_PyQt:
- import_log += "- Wrong PyQt version (%s), please install PyQt %s\r\n"%(Qt.QT_VERSION_STR, ver_PyQt)
-except ImportError:
- import_log += "- PyQt missing, please install PyQt %s\r\n"%(str(ver_PyQt))
-
-try:
- from PyQt4 import Qwt5 as Qwt
- if not Qwt.QWT_VERSION_STR in ver_PyQwt:
- import_log += "- Wrong PyQwt version (%s), please install PyQwt %s\r\n"%(Qwt.QWT_VERSION_STR, ver_PyQwt)
-except ImportError:
- import_log += "- PyQwt missing, please install PyQwt %s\r\n"%(str(ver_PyQwt))
-
-try:
- from lxml import etree
- if not etree.__version__ in ver_lxml:
- import_log += "- Wrong lxml version (%s), please install lxml %s\r\n"%(etree.__version__, ver_lxml)
-except ImportError:
- import_log += "- lxml missing, please install lxml %s\r\n"%(str(ver_lxml))
-
+# -*- coding: utf-8 -*-
+'''
+Load required libraries and check versions
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+
+@author: Norbert Hauser
+@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 62 $
+'''
+
+
+'''
+------------------------------------------------------------
+CHECK LIBRARY DEPENDENCIES
+------------------------------------------------------------
+'''
+
+import re
+import sys
+
+import_log = ""
+
+missing_dependencies = []
+version_mismatches = []
+
+MIN_PYTHON = (3, 9, 0)
+MINIMUM_VERSIONS = {
+ "NumPy": "1.20.0",
+ "SciPy": "1.8.0",
+ "PyQt": "6.2.0",
+ "PyQwt": "0.12.0",
+ "lxml": "4.6.0",
+}
+
+
+def _append(msg):
+ global import_log
+ import_log += msg
+ return msg
+
+
+def _record_missing(name, msg):
+ if name not in missing_dependencies:
+ missing_dependencies.append(name)
+ _append(msg)
+
+
+def _record_mismatch(name, msg):
+ if name not in version_mismatches:
+ version_mismatches.append(name)
+ _append(msg)
+
+
+def _to_version_tuple(version_string):
+ """Convert a loose version string into a comparable integer tuple."""
+ parts = []
+ for item in re.split(r"[^\d]+", str(version_string)):
+ if not item:
+ continue
+ parts.append(int(item))
+ if len(parts) >= 3:
+ break
+ while len(parts) < 3:
+ parts.append(0)
+ return tuple(parts)
+
+
+def _is_at_least(current, minimum):
+ return _to_version_tuple(current) >= _to_version_tuple(minimum)
+
+
+def _check_minimum(name, current, minimum):
+ if not _is_at_least(current, minimum):
+ _record_mismatch(
+ name,
+ "- %s is too old (%s), please install %s >= %s\r\n" % (name, current, name, minimum),
+ )
+
+
+# try to import python libraries, check versions
+if sys.version_info < MIN_PYTHON:
+ _record_mismatch(
+ "Python",
+ "- Python %i.%i+ is required (current: %s)\r\n"
+ % (MIN_PYTHON[0], MIN_PYTHON[1], sys.version.split()[0]),
+ )
+
+try:
+ import numpy as np
+ _check_minimum("NumPy", np.__version__, MINIMUM_VERSIONS["NumPy"])
+except ImportError:
+ _record_missing(
+ "NumPy",
+ "- NumPy missing, please install NumPy >= %s\r\n" % (MINIMUM_VERSIONS["NumPy"],),
+ )
+
+try:
+ import scipy as sc
+ _check_minimum("SciPy", sc.__version__, MINIMUM_VERSIONS["SciPy"])
+except ImportError:
+ _record_missing(
+ "SciPy",
+ "- SciPy missing, please install SciPy >= %s\r\n" % (MINIMUM_VERSIONS["SciPy"],),
+ )
+
+try:
+ from PyQt4 import Qt
+ qt_version = getattr(Qt, "QT_VERSION_STR", "0.0.0")
+ _check_minimum("PyQt", qt_version, MINIMUM_VERSIONS["PyQt"])
+except ImportError:
+ _record_missing(
+ "PyQt",
+ "- PyQt missing, please install PySide6 and local PyQt4 compatibility shim\r\n",
+ )
+
+try:
+ from PyQt4 import Qwt5 as Qwt
+ qwt_version = getattr(Qwt, "QWT_VERSION_STR", "0.0.0")
+ _check_minimum("PyQwt", qwt_version, MINIMUM_VERSIONS["PyQwt"])
+except ImportError:
+ _record_missing(
+ "PyQwt",
+ "- PyQwt missing, please install pyqtgraph (Qwt compatibility layer)\r\n",
+ )
+
+try:
+ from lxml import etree
+ _check_minimum("lxml", etree.__version__, MINIMUM_VERSIONS["lxml"])
+except ImportError:
+ _record_missing(
+ "lxml",
+ "- lxml missing, please install lxml >= %s\r\n" % (MINIMUM_VERSIONS["lxml"],),
+ )
+
+
+
+def dependency_status():
+ """Return collected dependency info for diagnostics."""
+ return {
+ 'missing': list(missing_dependencies),
+ 'version_mismatch': list(version_mismatches),
+ 'log': import_log.strip(),
+ }
+
+
+def has_all_dependencies():
+ return len(missing_dependencies) == 0
+
diff --git a/main.py b/main.py
index 8432833..419190f 100644
--- a/main.py
+++ b/main.py
@@ -1,1354 +1,1443 @@
-# -*- coding: utf-8 -*-
-'''
-Main Application
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-B{Default Module Configuration:}
-
- - L{MainWindow}
- - Instantiate module chain L{InstantiateModules}
- - Amplifier L{AMP_ActiChamp}
- - L{Configuration Pane}
- - L{Online Configuration Pane}
- - Trigger Input Detection L{TRG_Eeg}
- - Data Storage (Vision Data Exchange Format) L{StorageVision}
- - L{Configuration Pane}
- - L{Online Configuration Pane}
- - Remote Data Access Server L{RDA_Server}
- - Digital Filter (Low-Cut, High-Cut and Notch) L{FLT_Eeg}
- - L{Configuration Pane}
- - Impedance Display Dialog L{IMP_Display}
- - L{Dialog}
- - Data Display Module L{DISP_Scope}
- - L{Online Configuration Pane}
-
-B{Dependencies:}
- - Python 2.6
- - NumPy 1.3.0 or 1.4.1
- - SciPy 0.7.1 or 0.8.0
- - PyQt 4.5.4 or 4.6.3
- - PyQwt 5.2.0
- - lxml 2.2.4 or 2.2.7
-
-@author: Norbert Hauser
-@version: 1.0
-'''
-from PyQt4.Qt import QString
-
-__version__ = "1.0.9"
-'''Application Version'''
-
-# show or hide the confirmation dialog box at start up
-ShowConfirmationDialog = False
-ConfirmationText = u"\
-The PyCorder is based on the Python programming language and is explicitly designed as open source software. \
-The program is provided free of charge under the GNU General Public License (GPL) for open-source \
-software by Brain Products GmbH.\n\
-Because it is open-source software, the PyCorder allows users to follow all the processing steps \
-in the source code. Users have the option of modifying the program code to meet their scientific requirements \
-irrespective of the program version that we provide and without prior consultation with us.\n\
-The PyCorder is exclusively intended for research purposes. Because it is also provided free of charge, \
-Brain Products GmbH is unable to provide support for the software directly.\nIn particular, \
-we can accept no liability for program functions that have been modified or created from scratch by the user.\n\
-If problems should arise when using the program, you can make use of the forum that has been set up \
-for this purpose. You will find the forum at http://www.actichamp.com/forum/.\n\
-Use of the program demands a considerable degree of responsibility and safety awareness on the part of the user.\n\
-It is possible to deactivate this information page in the source code.\n\
-Please confirm that you accept the conditions of use of the program as stated here by clicking Accept."
-
-# show or hide the battery disconnection reminder
-ShowBatteryReminder = True
-
-# force battery logging independent from the -rBL command line switch
-ForceBatteryLogging = True
-
-
-import sys
-import collections
-import re
-from optparse import OptionParser
-
-'''
-------------------------------------------------------------
-LOAD LIBRARIES AND CHECK DEPENDENCIES
-------------------------------------------------------------
-'''
-import loadlibs
-
-# check library import
-if len(loadlibs.import_log) > 0:
- print "PyCorder: The following libraries are missing or have the wrong version\r\n\r\n"
- print loadlibs.import_log
- if "missing" in loadlibs.import_log:
- raw_input("Press RETURN to close the application ..." )
- sys.exit(1)
- else:
- raw_input("Press RETURN to continue ..." )
-
-
-
-'''
-------------------------------------------------------------
-IMPORT GUI RESOURCES
-------------------------------------------------------------
-'''
-
-from res import frmMain
-from res import frmMainStatusBar
-from res import frmLogView
-from res import frmMainConfiguration
-
-
-'''
-------------------------------------------------------------
-IMPORT AND INSTANTIATE RECORDING MODULES
-------------------------------------------------------------
-'''
-# import the remote control server
-from remote import RemoteControlServer
-
-# import base functionality modules
-from amplifier import AMP_ActiChamp
-from storage import StorageVision
-from filter import FLT_Eeg
-from trigger import TRG_Eeg
-from impedance import IMP_Display
-from display import DISP_Scope
-from rda_server import RDA_Server
-from rda_client import RDA_Client
-from montage import MNT_Recording
-from modbase import *
-
-# import your own modules here
-#from tutorial.tut_0 import TUT_0
-#from tutorial.tut_1 import TUT_1
-#from tutorial.tut_2 import TUT_2
-#from tutorial.tut_3 import TUT_3
-#from tutorial.tut_4 import TUT_4
-from custom_modules.dc_offset import dc_offset
-
-def InstantiateModules(run_as):
- ''' Instantiate and arrange module objects.
- Modules will be connected top -> down, starting with array index 0.
- Additional modules can be connected left -> right with tuples as list objects.
- @param run_as: command line option (-r, --runas) for different module configurations
- @return: list with instantiated module objects
- '''
- # get command line arguments
- if 'RC' in run_as:
- # run as remote client
- modules = [RDA_Client(),
- TRG_Eeg(),
- FLT_Eeg(),
- IMP_Display(),
- DISP_Scope(instance=0)]
- else:
- # run as actiCHamp recorder
- modules = [AMP_ActiChamp(),
- MNT_Recording(),
- TRG_Eeg(),
- StorageVision(),
- FLT_Eeg(),
- dc_offset(),
- RDA_Server(),
- IMP_Display(),
- DISP_Scope(instance=0)
- ]
- return modules
-
-
-'''
-------------------------------------------------------------
-APPLICATION MAIN WINDOW
-------------------------------------------------------------
-'''
-
-class MainWindow(Qt.QMainWindow, frmMain.Ui_MainWindow):
- ''' Application Main Window Class
- includes main menu, status bar and module handling
- '''
- def __init__(self):
- ''' Instantiate and initialize GUI objects.
- - Connect to button and menu actions.
- - Instantiate and connect PyCorder module chain.
- -Load the last used module configuration.
- '''
- Qt.QMainWindow.__init__(self)
- self.setupUi(self)
-
- # create status bar
- self.statusWidget = StatusBarWidget()
- self.statusBar().addPermanentWidget(self.statusWidget, 1)
-
- # menu actions
- self.connect(self.actionQuit, Qt.SIGNAL('triggered()'),
- Qt.SLOT('close()'))
- self.connect(self.actionShow_Log, Qt.SIGNAL('triggered()'),
- self.statusWidget.showLogEntries)
- self.connect(self.actionLoad_Configuration, Qt.SIGNAL('triggered()'),
- self.loadConfiguration)
- self.connect(self.actionSave_Configuration, Qt.SIGNAL('triggered()'),
- self.saveConfiguration)
- self.connect(self.actionDefault_Configuration, Qt.SIGNAL('triggered()'),
- self.defaultConfiguration)
-
- # button actions
- self.connect(self.pushButtonConfiguration, Qt.SIGNAL("clicked()"),
- self.configurationClicked)
- self.connect(self.statusWidget, Qt.SIGNAL("saveLog()"),
- self.saveLogFile)
- self.connect(self.statusWidget, Qt.SIGNAL("showLog()"),
- self.showLogEntries)
-
- # preferences
- self.application_name = "PyCorder"
- self.configuration_file = ""
- self.configuration_dir = ""
- self.log_dir = ""
- self.loadPreferences()
- self.recording_mode = -1
- self.usageConfirmed = False
-
- # remote control server
- self.RC = None
-
- # parse command line options
- # look for old style command line option "-RC"
- if "-RC" in sys.argv:
- sys.argv.remove("-RC") # skip old style
- RemoteClient = True
- else:
- RemoteClient = False
-
- # get command line options
- parser = OptionParser()
- parser.add_option("-m", "--modules", dest="ModuleFile",
- help="Instantiate modules from separate module definition file MODULEFILE. "
- "InstantiateModules() from this file will be called." )
- parser.add_option("-c", "--configfile", dest="ConfigurationFile",
- help="Load CONFIGURATIONFILE instead of last configuration.")
- parser.add_option("-r", "--runas", dest="RunAs", default="",
- help="Specify the module configuration that should be used.")
- parser.add_option("-o", "--options", dest="Options", default="",
- help="General options: R - start the remote server")
- try:
- self.cmd_options, args = parser.parse_args()
- except:
- raise Exception("Command line parser error !")
- # merge run configuration with old style
- if self.cmd_options.RunAs == "" and RemoteClient:
- self.cmd_options.RunAs = "RC"
-
-
- # create module chain (top = index 0, bottom = last index)
- self.defineModuleChain()
-
- # connect modules
- for idx_vertical in range(len(self.modules)-1):
- if type(self.modules[idx_vertical]) in (tuple, list):
- # connect top/down
- if type(self.modules[idx_vertical+1]) in (tuple, list):
- self.modules[idx_vertical][0].add_receiver(self.modules[idx_vertical+1][0])
- else:
- self.modules[idx_vertical][0].add_receiver(self.modules[idx_vertical+1])
- # connect left/right
- for idx_horizontal in range(len(self.modules[idx_vertical])-1):
- self.modules[idx_vertical][idx_horizontal].add_receiver(self.modules[idx_vertical][idx_horizontal+1])
- else:
- # connect top/down
- if type(self.modules[idx_vertical+1]) in (tuple, list):
- self.modules[idx_vertical].add_receiver(self.modules[idx_vertical+1][0])
- else:
- self.modules[idx_vertical].add_receiver(self.modules[idx_vertical+1])
-
- # get the top module
- if type(self.modules[0]) in (tuple, list):
- self.topmodule = self.modules[0][0]
- else:
- self.topmodule = self.modules[0]
-
- # get the bottom module
- if type(self.modules[-1]) in (tuple, list):
- self.bottommodule = self.modules[-1][-1]
- else:
- self.bottommodule = self.modules[-1]
-
- # get events from module chain top module
- self.connect(self.topmodule, Qt.SIGNAL("event(PyQt_PyObject)"), self.processEvent)
-
- # tell the top module to get events from us
- self.topmodule.connect(self, Qt.SIGNAL("parentevent(PyQt_PyObject)"), self.topmodule.parent_event, Qt.Qt.QueuedConnection)
-
- # get signal panes for plot area
- self.horizontalLayout_SignalPane.removeItem(self.horizontalLayout_SignalPane.itemAt(0))
- for module in flatten(self.modules):
- pane = module.get_display_pane()
- if pane != None:
- self.horizontalLayout_SignalPane.addWidget(pane)
-
-
- # initial module chain update (top module)
- self.topmodule.update_receivers()
-
- # insert online configuration panes
- position = 0
- for module in flatten(self.modules):
- module.main_object = self
- pane = module.get_online_configuration()
- if pane != None:
- #self.verticalLayout_OnlinePane.insertWidget(self.verticalLayout_OnlinePane.count()-2, pane)
- self.verticalLayout_OnlinePane.insertWidget(position, pane)
- position += 1
-
- # load configuration file
- if self.cmd_options.ConfigurationFile == None:
- # try to load the last configuration file
- try:
- if len(self.configuration_file) > 0:
- cfg = os.path.normpath(self.configuration_dir + '/' + self.configuration_file)
- self._loadConfiguration(cfg)
- else:
- self.defaultConfiguration()
- except:
- pass
- else:
- # try to load configuration from command line file
- try:
- self._loadConfiguration(os.path.normpath(self.cmd_options.ConfigurationFile))
- except Exception as e:
- raise Exception("Failed to load configuration from file: " +
- self.cmd_options.ConfigurationFile + "\n" + repr(e))
-
-
- # update log text module info
- self.updateModuleInfo()
-
- # update button states
- self.updateUI()
-
- # instantiate and start the remote control server
- try:
- if "R" in self.cmd_options.Options:
- self.RC = RemoteControlServer()
- except Exception as e:
- self.RC = None
- Qt.QMessageBox.information(None, "Remote Control Server", str(e))
- if self.RC != None:
- # get events from server
- self.connect(self.RC, Qt.SIGNAL("event(PyQt_PyObject)"), self.processEvent)
-
- # performance boost ;-)
- self.startTimer(1)
-
-
-
- def defineModuleChain(self):
- ''' Instantiate and arrange module objects
- - Modules will be connected top -> down, starting with array index 0
- - Additional modules can be connected left -> right with tuples as list objects
- '''
- # check the command line option
- if self.cmd_options.ModuleFile == None:
- # get modules from global function
- self.modules = InstantiateModules(self.cmd_options.RunAs)
- else:
- # get module configuration from external file
- try:
- exec("from " + self.cmd_options.ModuleFile + " import InstantiateModules")
- self.modules = InstantiateModules(self.cmd_options.RunAs)
- except Exception as e:
- raise Exception("Failed to instantiate modules from external file: " +
- self.cmd_options.ModuleFile + "\n" + str(e))
-
- # show battery reminder only for the acticCHamp amplifier
- global ShowBatteryReminder
- classnames = [m.__class__.__name__ for m in self.modules]
- if not "AMP_ActiChamp" in classnames:
- ShowBatteryReminder = False
-
-
-
- def configurationClicked(self):
- ''' Configuration button clicked
- - Open configuration dialog and add configuration panes for each module in the
- module chain, if available
- '''
- dlg = DlgConfiguration()
- for module in flatten(self.modules):
- pane = module.get_configuration_pane()
- if pane != None:
- dlg.addPane(pane)
- ok = dlg.exec_()
- if ok:
- self.saveConfiguration()
-
- def defaultConfiguration(self):
- ''' Menu "Reset Configuration":
- Set default values for all modules
- '''
- # reset all modules
- for module in flatten(self.modules):
- module.setDefault()
-
- # update module chain, starting from top module
- self.topmodule.update_receivers()
-
- # update status line
- self.processEvent(ModuleEvent("Application",
- EventType.STATUS,
- info = "default",
- status_field = "Workspace"))
-
- def _loadConfiguration(self, filename):
- ''' Load module configuration from XML file
- @param filename: Full qualified XML file name
- '''
- ok = True
- cfg = objectify.parse(filename)
- # check application and version
- app = cfg.xpath("//PyCorder")
- if (len(app) == 0) or (app[0].get("version") == None):
- # configuration data not found
- self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
- "%s is not a valid PyCorder configuration file"%(filename),\
- severity=1) )
- ok = False
-
- if ok:
- version = app[0].get("version")
- if cmpver(version, __version__, 2) > 0:
- # wrong version
- self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
- "%s wrong version %s > %s"%(filename, version, __version__),\
- severity=ErrorSeverity.NOTIFY) )
- ok = False
-
- # setup modules from configuration file
- if ok:
- for module in flatten(self.modules):
- module.setXML(cfg)
-
- # update module chain, starting from top module
- self.topmodule.update_receivers()
-
- # update status line
- file_name, ext = os.path.splitext(os.path.split(filename)[1])
- self.processEvent(ModuleEvent("Application",
- EventType.STATUS,
- info = file_name,
- status_field = "Workspace"))
-
-
- def loadConfiguration(self):
- ''' Menu "Load Configuration ...":
- Load module configuration from XML file
- '''
- dlg = Qt.QFileDialog()
- dlg.setFileMode(Qt.QFileDialog.ExistingFile)
- dlg.setAcceptMode(Qt.QFileDialog.AcceptOpen)
- dlg.setNameFilter("Configuration files (*.xml)")
- dlg.setDefaultSuffix("xml")
- if len(self.configuration_dir) > 0:
- dlg.setDirectory(self.configuration_dir)
- dlg.selectFile(self.configuration_file)
- if dlg.exec_() == True:
- try:
- files = dlg.selectedFiles()
- file_name = unicode(files[0])
- # load configuration from XML file
- self._loadConfiguration(file_name)
- # set preferences
- dir, fn = os.path.split(file_name)
- self.configuration_file = fn
- self.configuration_dir = dir
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
- tb + " -> %s "%(file_name) + str(e),
- severity=ErrorSeverity.NOTIFY))
-
-
- def _saveConfiguration(self, filename):
- ''' Save module configuration to XML file
- @param filename: Full qualified XML file name
- '''
- E = objectify.E
- modules = E.modules()
- # get configuration from each connected module
- for module in flatten(self.modules):
- cfg = module.getXML()
- if cfg != None:
- modules.append(cfg)
- # build complete configuration tree
- root = E.PyCorder(modules, version=__version__)
- # write it to file
- etree.ElementTree(root).write(filename, pretty_print=True, encoding="UTF-8")
-
- def saveConfiguration(self):
- ''' Menu "Save Configuration ...":
- Save module configuration to XML file
- '''
- dlg = Qt.QFileDialog()
- dlg.setFileMode(Qt.QFileDialog.AnyFile)
- dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
- dlg.setNameFilter("Configuration files (*.xml)")
- dlg.setDefaultSuffix("xml")
- if len(self.configuration_dir) > 0:
- dlg.setDirectory(self.configuration_dir)
- dlg.selectFile(self.configuration_file)
- if dlg.exec_() == True:
- try:
- files = dlg.selectedFiles()
- file_name = unicode(files[0])
- # save configuration to XML
- self._saveConfiguration(file_name)
- # set preferences
- dir, fn = os.path.split(file_name)
- self.configuration_file = fn
- self.configuration_dir = dir
- # update status line
- fn, ext = os.path.splitext(os.path.split(file_name)[1])
- self.processEvent(ModuleEvent("Application",
- EventType.STATUS,
- info = fn,
- status_field = "Workspace"))
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- self.processEvent(ModuleEvent("Save Configuration", EventType.ERROR,\
- tb + " -> %s "%(file_name) + str(e),
- severity=ErrorSeverity.NOTIFY))
-
- def savePreferences(self):
- ''' Save preferences to XML file
- '''
- E = objectify.E
- preferences = E.preferences(E.config_dir(self.configuration_dir),
- E.config_file(self.configuration_file),
- E.log_dir(self.log_dir))
- root = E.PyCorder(preferences, version=__version__)
-
- # preferences will be stored to user home directory
- try:
- homedir = Qt.QDir.home()
- appdir = "." + self.application_name
- if not homedir.cd(appdir):
- homedir.mkdir(appdir)
- homedir.cd(appdir)
- filename = unicode(homedir.absoluteFilePath("preferences.xml"))
- etree.ElementTree(root).write(filename, pretty_print=True, encoding="UTF-8")
- except:
- pass
-
- def loadPreferences(self):
- ''' Load preferences from XML file
- '''
- try:
- # preferences will be stored to user home directory
- homedir = Qt.QDir.home()
- appdir = "." + self.application_name
- if not homedir.cd(appdir):
- return
- filename = unicode(homedir.absoluteFilePath("preferences.xml"))
-
- # read XML file
- cfg = objectify.parse(filename)
- # check application and version
- app = cfg.xpath("//PyCorder")
- if (len(app) == 0) or (app[0].get("version") == None):
- # configuration data not found
- return
- # check version
- version = app[0].get("version")
- if cmpver(version, __version__, 2) > 0:
- # wrong version
- return
-
- # update preferences
- preferences = app[0].preferences
- self.configuration_dir = preferences.config_dir.pyval
- self.configuration_file = preferences.config_file.pyval
- self.log_dir = preferences.log_dir.pyval
- except:
- pass
-
- def showLogEntries(self):
- ''' Show log entries
- '''
- self.updateModuleInfo()
- self.statusWidget.showLogEntries()
-
- def saveLogFile(self):
- ''' Write log entries to file
- '''
- dlg = Qt.QFileDialog()
- dlg.setFileMode(Qt.QFileDialog.AnyFile)
- dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
- dlg.setNameFilter("Log files (*.log)")
- dlg.setDefaultSuffix("log")
- if len(self.log_dir) > 0:
- dlg.setDirectory(self.log_dir)
- if dlg.exec_() == True:
- try:
- files = dlg.selectedFiles()
- file_name = unicode(files[0])
- # set preferences
- dir, fn = os.path.split(file_name)
- self.log_dir = dir
- # write log entries to file
- f = open(file_name, "w")
- f.write(self.statusWidget.getLogText().encode('utf-8'))
- f.close()
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- Qt.QMessageBox.critical(None, "PyCorder",
- "Failed to write log file (%s)\n"%(file_name) +
- tb + " -> " + str(e))
-
- def closeEvent(self, event):
- ''' Application wants to close, prevent closing if recording to file is still active
- '''
- if not self.topmodule.query("Stop"):
- event.ignore()
- else:
- self.topmodule.stop(force=True)
- self.savePreferences()
- # clean up modules
- for module in flatten(self.modules):
- module.terminate()
- # terminate remote control server
- if self.RC != None:
- self.RC.terminate()
- event.accept()
-
- def sendEvent(self, event):
- ''' Send an event to the top module event chain
- '''
- self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
-
-
- def processRemoteCommand(self, cmd_string):
- ''' Process commands received from remote control
- @param cmd: the received command
- @return: log entry and error message
- '''
- error_message = ""
- log_entry = u"command received: '%s'"%(cmd_string)
- if len(cmd_string) > 0:
- # split command and value
- cmd = cmd_string[0].upper()
- if len(cmd_string) > 1:
- cmd_value = cmd_string[1:]
- else:
- cmd_value = ""
-
- # check for supported commands
- if cmd not in ["1", "2", "3", "4", "M", "I", "S", "Q", "X", "F"]:
- error_message = u"command not supported: '%s'"%(cmd_string)
-
- # check the recording state and if the requested command can be applied
- elif cmd in ["S", "I", "M", "X", "Q"] and self.topmodule.isRunning() and not self.RC.remoteRecording:
- error_message = u"recording is in progress and was not started remote: '%s'"%(cmd_string)
-
- elif cmd not in ["Q", "X"] and not self.topmodule.query("RemoteStop"):
- error_message = u"recording is still in progress, stop it first with 'X': '%s'"%(cmd_string)
-
- elif cmd in ["1", "2", "3", "4"] and self.topmodule.isRunning():
- error_message = u"data acquisition is still in progress, stop it first with 'X': '%s'"%(cmd_string)
-
- elif cmd in ["4", "S", "I", "M", "X", "Q"] and not self.RC.isInitialized():
- error_message = u"some variables (1 Configuration file, 2 Experiment ID or 3 Subject ID) are not initialized: '%s'"%(cmd_string)
-
- # Initialization
- elif cmd == "1":
- self.RC.S_ConfigurationFile = cmd_value
- elif cmd == "2":
- self.RC.S_ExperimentNr = cmd_value
- elif cmd == "3":
- self.RC.S_SubjectID = cmd_value
- elif cmd == "4":
- # prepare recording
- # load configuration file
- try:
- self._loadConfiguration(self.RC.S_ConfigurationFile)
- except:
- error_message = u"failed to load configuration file: '%s'"%(self.RC.S_ConfigurationFile)
-
- # Exit
- elif cmd == "X":
- # exit, stop everything and reset all state variables
- # stop data acquisition
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="Stop",
- cmd_value="force"))
- #self.RC.resetControlState()
- self.RC.remoteRecording = False
-
- # Monitoring
- elif cmd == "I":
- # start impedance mode
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="StartImpedance"))
- self.RC.remoteRecording = True
-
- elif cmd == "M":
- # start monitoring
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="StartRecording"))
- self.RC.remoteRecording = True
-
- # Recording
- elif cmd == "S":
- # start monitoring if not yet started
- if not self.topmodule.isRunning() or self.recording_mode != RecordingMode.NORMAL:
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="StartRecording"))
- # start recording
- filename = "%s_%s"%(self.RC.S_ExperimentNr, self.RC.S_SubjectID)
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="StartSaving",
- cmd_value=filename))
- self.RC.remoteRecording = True
-
- elif cmd == "Q":
- # stop recording
- self.sendEvent(ModuleEvent("RemoteControl",
- EventType.COMMAND,
- info="StopSaving"))
-
- # enable / disable feedback
- elif cmd == "F":
- self.RC.feedbackEnabled = (cmd_value == "1")
-
- else:
- error_message = u"invalid command (size=0)"
-
- # send feedback
- if error_message:
- self.RC.send_feedback("%sFAILED %s"%(cmd, error_message))
- else:
- if cmd in ["S", "I", "M"]:
- self.RC.postpone_feedback(cmd)
- else:
- self.RC.send_feedback("%sOK"%(cmd))
-
- return log_entry, error_message
-
-
- def processRemoteFeedback(self, event):
- ''' Handle postponed command feedbacks
- '''
- if not self.RC:
- return
- if not self.RC.isClientConnected():
- return
-
- for idx, cmd in enumerate(self.RC.postponed):
- if event.type == EventType.ERROR:
- self.RC.send_feedback("%sFAILED %s"%(cmd, event.info))
- del self.RC.postponed[idx]
- elif cmd == "M":
- if event.type == EventType.STATUS and event.status_field == "Mode" and event.info == RecordingMode.NORMAL:
- self.RC.send_feedback("%sOK"%(cmd))
- del self.RC.postponed[idx]
- elif cmd == "I":
- if event.type == EventType.STATUS and event.status_field == "Mode" and event.info == RecordingMode.IMPEDANCE:
- self.RC.send_feedback("%sOK"%(cmd))
- del self.RC.postponed[idx]
- elif cmd == "S":
- if event.type == EventType.STATUS and event.status_field == "Storage" and event.info:
- self.RC.send_feedback("%sOK %s"%(cmd, event.info))
- del self.RC.postponed[idx]
-
-
- def processEvent(self, event):
- ''' Process events from module chain
- @param event: ModuleEvent object
- Stop acquisition on errors with a severity > 1
- '''
- # handle remote postponed feedback
- self.processRemoteFeedback(event)
-
- # process commands
- if event.type == EventType.COMMAND:
- if event.info == "RemoteCommand":
- # ignore remote commands until the usage conditions are confirmed
- if not self.usageConfirmed:
- return
- # handle and log commands from any remote control client
- cmd_log, cmd_error = self.processRemoteCommand(event.cmd_value)
- # modify the command event for logging
- if len(cmd_error) > 0:
- event.type = EventType.ERROR
- event.severity = ErrorSeverity.IGNORE
- event.info = cmd_error
- else:
- event.type = EventType.LOGMESSAGE
- event.info = cmd_log
- else:
- # don't log other commands
- return
-
- # recording mode changed?
- if event.type == EventType.STATUS:
- if event.status_field == "Mode":
- self.recording_mode = event.info
- self.updateUI(isRunning=(event.info >= 0))
- self.updateModuleInfo()
-
- # write battery voltage to log file
- self.writeBatteryLog(event)
-
- # log events and update status line
- self.statusWidget.updateEventStatus(event)
-
- # look for errors
- if (event.type == EventType.ERROR) and (event.severity > 1):
- self.topmodule.stop(force=True)
-
- def writeBatteryLog(self, event):
- ''' Write battery voltage to log file
- '''
- try:
- if self.battery_log:
- if event.type == EventType.STATUS:
- # log battery voltage
- if event.status_field == "Battery":
- t = time.clock()
- if (t - self.battery_timer >= 59) and (self.battery_mode >= 0):
- voltage = float(event.info.split("V")[0])
- level = '?'
- if event.severity == ErrorSeverity.IGNORE:
- level = 'H'
- elif event.severity == ErrorSeverity.NOTIFY:
- level = 'M'
- elif event.severity == ErrorSeverity.STOP:
- level = 'L'
- if "V C" in event.info:
- level = 'C'
- logentry = "%.2f\t%s\t%s\t%s"%(voltage,
- event.event_time.strftime("%H:%M:%S\t%d/%m/%Y"),
- self.battery_ampSN,
- level)
- if self.battery_mode != self.battery_logmode:
- logentry += "\tStart %d at %s"%(self.battery_mode, self.battery_rate)
- self.battery_logmode = self.battery_mode
- logentry += "\n"
- with open(self.battery_logfile,"a") as f:
- f.write(logentry)
- self.battery_timer = t
- event.info += "\nLOG"
- # update recording mode
- if event.status_field == "Mode":
- self.battery_timer = time.clock() - 60.0
- self.battery_mode = event.info
- if event.info < 0:
- self.battery_logmode = event.info
- sn = self.statusWidget.moduleinfo.split("SN: ")
- self.battery_ampSN = "???"
- if len(sn) > 1:
- sn = sn[1].split()
- if len(sn) > 0:
- self.battery_ampSN = sn[0]
- # update sampling rate
- if event.status_field == "Rate":
- self.battery_rate = event.info
-
- except AttributeError:
- # enable battery log with command line option -rBL or if it is forced
- self.battery_log = (self.cmd_options.RunAs == "BL") or (ForceBatteryLogging)
- self.battery_timer = time.clock() - 60.0
- self.battery_ampSN = "???"
- self.battery_rate = "???"
- self.battery_mode = -1
- self.battery_logmode = -1
-
- # log to users home /.PyCorder directory
- logpath = os.path.join(unicode(Qt.QDir.toNativeSeparators(Qt.QDir.homePath())), "." + self.application_name)
- # create or use the auto incremented file name
- homedir = Qt.QDir.home()
- appdir = "." + self.application_name
- if not homedir.cd(appdir):
- homedir.mkdir(appdir)
- logdir = Qt.QDir(logpath)
- if not logdir.exists():
- if self.battery_log:
- print "Battery Log is disabled because the log directory (%s) doesn't exist"%logpath
- self.battery_log = False
- else:
- fname = "CHampBattery_"
- numbersize = 3
- numberstring = "?"
- for n in range(1, numbersize):
- numberstring += "?"
- logdir.setNameFilters(Qt.QStringList("%s%s.log"%(fname, numberstring)))
- logdir.setFilter(Qt.QDir.Files)
- flist = logdir.entryList()
- # extract numbers
- flist.replaceInStrings(".log", "", Qt.Qt.CaseInsensitive)
- flist.replaceInStrings(fname, "", Qt.Qt.CaseInsensitive)
- numbers = []
- for f in flist:
- num,ok = f.toInt()
- if ok and (num < 10**numbersize):
- numbers.append(num)
- if len(numbers) > 0:
- # get the highest number
- numbers.sort()
- fnumber = numbers[-1]
- else:
- fnumber = 0
- name = "%s%0*d.log"%(fname, numbersize, fnumber)
- logfile = os.path.join(logpath, name)
-
- # verify that the file size is not yet exceeding 10MB
- if Qt.QFile.exists(logfile):
- if os.path.getsize(logfile) > 10 * 2**20:
- name = "%s%0*d.log"%(fname, numbersize, fnumber + 1)
- logfile = os.path.join(logpath, name)
-
- self.battery_logfile = logfile
-
- if self.battery_log:
- print "Battery Log to %s is enabled"%self.battery_logfile
- except Exception as e:
- print e
- self.battery_log = False
-
-
- def updateUI(self, isRunning=False):
- ''' Update user interface to reflect the recording state
- '''
- if isRunning:
- self.pushButtonConfiguration.setEnabled(False)
- self.actionLoad_Configuration.setEnabled(False)
- self.actionSave_Configuration.setEnabled(False)
- self.actionQuit.setEnabled(False)
- self.actionDefault_Configuration.setEnabled(False)
- else:
- self.pushButtonConfiguration.setEnabled(True)
- self.actionLoad_Configuration.setEnabled(True)
- self.actionSave_Configuration.setEnabled(True)
- self.actionQuit.setEnabled(True)
- self.actionDefault_Configuration.setEnabled(True)
- self.statusWidget.resetUtilization()
-
- def updateModuleInfo(self):
- ''' Update the module information in the log text
- and propagate it to all connected modules as status information
- '''
- # get module information
- self.statusWidget.moduleinfo = ""
- for module in flatten(self.modules):
- info = module.get_module_info()
- if info != None:
- self.statusWidget.moduleinfo += module._object_name + "\n"
- self.statusWidget.moduleinfo += info
- if len(self.statusWidget.moduleinfo) > 0:
- self.statusWidget.moduleinfo += "\n\n"
-
- # propagate status info to all connected modules
- moduleinfo = u"PyCorder V" + __version__ + "\n\n"
- moduleinfo += self.statusWidget.moduleinfo
- msg = ModuleEvent("PyCorder",
- EventType.STATUS,
- info = moduleinfo,
- status_field="ModuleInfo")
- self.sendEvent(msg)
-
-
-'''
-------------------------------------------------------------
-LOG ENTRY DIALOG
-------------------------------------------------------------
-'''
-
-class DlgLogView(Qt.QDialog, frmLogView.Ui_frmLogView):
- ''' Show all log entries as plain text
- '''
- def __init__(self, *args):
- apply(Qt.QDialog.__init__, (self,) + args)
- self.setupUi(self)
-
- def setLogEntry(self, entry):
- self.labelView.setPlainText(entry)
-
-
-'''
-------------------------------------------------------------
-BATTERY INFO DIALOG
-------------------------------------------------------------
-'''
-
-class DlgBatteryInfo(Qt.QMessageBox):
- ''' Show disconnect info for 10s
- '''
- def __init__(self, *args):
- infoText = (u"Please disconnect actiCHamp from actiPOWER after you finished recording.\n" +
- u"Always attach actiPOWER to the charger when not in use to prevent damaging the accumulator.")
- Qt.QMessageBox.__init__(self, Qt.QMessageBox.Information, "Disconnect Battery", infoText)
- self.startTimer(10000)
-
- def timerEvent(self, e):
- self.done(0)
-
-
-'''
-------------------------------------------------------------
-MAIN CONFIGURATION DIALOG
-------------------------------------------------------------
-'''
-
-class DlgConfiguration(Qt.QDialog, frmMainConfiguration.Ui_frmConfiguration):
- ''' Module main configuration dialog
- All module configuration panes will go here
- '''
- def __init__(self):
- Qt.QDialog.__init__(self)
- self.setupUi(self)
- self.panes = []
-
- def addPane(self, pane):
- ''' Insert new tab and add module configuration pane
- @param pane: module configuration pane (QFrame object)
- '''
- if pane == None:
- return
- currenttabs = len(self.panes)
- if currenttabs > 0:
- # add new tab
- tab = Qt.QWidget()
- tab.setObjectName("tab%d"%(currenttabs+1))
- gridLayout = Qt.QGridLayout(tab)
- gridLayout.setObjectName("gridLayout%d"%(currenttabs+1))
- self.tabWidget.addTab(tab, "")
- else:
- gridLayout = self.gridLayout1
- tab = self.tab1
-
- self.panes.append(pane)
- gridLayout.addWidget(pane)
- self.tabWidget.setTabText(self.tabWidget.indexOf(tab), pane.windowTitle())
-
-
-'''
-------------------------------------------------------------
-STATUS BAR
-------------------------------------------------------------
-'''
-
-class StatusBarWidget(Qt.QWidget, frmMainStatusBar.Ui_frmStatusBar):
- ''' Main Window status bar
- '''
- def __init__(self):
- Qt.QWidget.__init__(self)
- self.setupUi(self)
-
- # utilization progressbar fifo
- # info label color and click
- self.labelInfo.setAutoFillBackground(True)
- self.labelInfo.mouseReleaseEvent = self.labelInfoClicked
- self.defaultBkColor = self.labelInfo.palette().color(self.labelInfo.backgroundRole())
- self.labelInfo.setText("Brain Products GmbH, PyCorder V" + __version__)
- self.labelStatus_4.setAutoFillBackground(True)
-
- # log entries
- self.logFifo = collections.deque(maxlen=10000)
- self.lockError = False
- self.moduleinfo = ""
-
- # number of channels and reference channel names
- self.status_channels = ""
- self.status_reference = ""
-
- self.resetUtilization()
-
- def resetUtilization(self):
- ''' Reset utilization parameters
- '''
- self.utilizationFifo = collections.deque()
- self.utilizationUpdateCounter = 0
- self.utilizationMaxValue = 0
- self.updateUtilization(0)
-
- def updateUtilization(self, utilization):
- ''' Update the utilization progressbar
- @param utilization: percentage of utilization
- '''
- # average utilization value
- self.utilizationFifo.append(utilization)
- if len(self.utilizationFifo) > 5:
- self.utilizationFifo.popleft()
- utilization = sum(self.utilizationFifo) / len(self.utilizationFifo)
- self.utilizationMaxValue = max(self.utilizationMaxValue, utilization)
-
- # slow down utilization display
- if self.utilizationUpdateCounter > 0:
- self.utilizationUpdateCounter -= 1
- return
- self.utilizationUpdateCounter = 5
- utilization = self.utilizationMaxValue
- self.utilizationMaxValue = 0
-
- # update progress bar
- if utilization < 100:
- self.progressBarUtilization.setValue(utilization)
- else:
- self.progressBarUtilization.setValue(100)
- self.progressBarUtilization.setFormat("%d%% Utilization"%(utilization))
-
- # modify progress bar color (<80% -> green, >=80% -> red)
- if utilization < 80:
- self.progressBarUtilization.setStyleSheet("QProgressBar {padding: 1px; text-align: right; margin-right: 35ex;} "\
- "QProgressBar::chunk {background: "\
- "qlineargradient(x1: 1, y1: 0, x2: 1, y2: 0.5, stop: 1 green, stop: 0 white);"\
- "margin: 0.5px}")
- else:
- self.progressBarUtilization.setStyleSheet("QProgressBar {padding: 1px; text-align: right; margin-right: 35ex;} "\
- "QProgressBar::chunk {background: "\
- "qlineargradient(x1: 1, y1: 0, x2: 1, y2: 0.5, stop: 1 red, stop: 0 white);"\
- "margin: 0.5px}")
-
- def updateEventStatus(self, event):
- ''' Update status info field and put events into the log fifo
- @param event: ModuleEvent object
- '''
- # display dedicated status info values
- if event.type == EventType.STATUS:
- if event.status_field == "Rate":
- self.labelStatus_1.setText(event.info)
- elif event.status_field == "Channels":
- self.status_channels = event.info
- self.labelStatus_2.setText(self.status_channels + ", " + self.status_reference)
- elif event.status_field == "Reference":
- refnames = event.info
- # limit the number of displayed channel names
- if len(refnames) > 70:
- refnames = refnames[:70].rsplit('+',1)[0] + "+ ..."
- self.status_reference = refnames
- self.labelStatus_2.setText(self.status_channels + ", " + self.status_reference)
- elif event.status_field == "Workspace":
- self.labelStatus_3.setText(event.info)
- elif event.status_field == "Battery":
- # set voltage
- self.labelStatus_4.setText(event.info)
- # severity indicates normal, critical or bad
- palette = self.labelStatus_4.palette()
- if event.severity == ErrorSeverity.NOTIFY:
- palette.setColor(self.labelStatus_4.backgroundRole(), Qt.Qt.yellow)
- elif event.severity == ErrorSeverity.STOP:
- palette.setColor(self.labelStatus_4.backgroundRole(), Qt.Qt.red)
- else:
- palette.setColor(self.labelStatus_4.backgroundRole(), self.defaultBkColor)
- self.labelStatus_4.setPalette(palette)
- elif event.status_field == "Utilization":
- self.updateUtilization(event.info)
- return
-
- # lock an error display until LogView is shown
- if ((self.lockError == False) or (event.severity > 0)) and event.type != EventType.LOG:
- # update label
- self.labelInfo.setText(unicode(event))
- palette = self.labelInfo.palette()
- if event.type == EventType.ERROR:
- palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.red)
- if event.severity > 0:
- self.lockError = True
- palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.red)
- else:
- palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.yellow)
- else:
- palette.setColor(self.labelInfo.backgroundRole(), self.defaultBkColor)
- self.labelInfo.setPalette(palette)
- # put events into log fifo
- if event.type != EventType.MESSAGE:
- self.logFifo.append(event)
-
- def showLogEntries(self):
- ''' Show the event log content
- '''
- dlg = DlgLogView()
- dlg.setLogEntry(self.getLogText())
- save = dlg.exec_()
- if save:
- self.emit(Qt.SIGNAL('saveLog()'))
- self.resetErrorState()
-
- def getLogText(self):
- ''' Get the log entries as plain text
- '''
- txt = u"PyCorder V" + __version__ + u" Event Log\n\n"
- txt += self.moduleinfo
- for event in reversed(self.logFifo):
- txt += u"%s\t %s\n"%(event.event_time.strftime("%Y-%m-%d %H:%M:%S.%f"), str(event))
- return txt
-
-
- def labelInfoClicked(self, mouse_event):
- ''' Mouse click into info label
- Show the event log content
- '''
- self.emit(Qt.SIGNAL('showLog()'))
-
-
- def resetErrorState(self):
- ''' Reset error lock and info display
- '''
- self.lockError = False
- self.labelInfo.setText("")
- palette = self.labelInfo.palette()
- palette.setColor(self.labelInfo.backgroundRole(), self.defaultBkColor)
- self.labelInfo.setPalette(palette)
-
-
-'''
-------------------------------------------------------------
-UTILITIES
-------------------------------------------------------------
-'''
-
-def flatten(lst):
- ''' Flatten a list containing lists or tuples
- '''
- for elem in lst:
- if type(elem) in (tuple, list):
- for i in flatten(elem):
- yield i
- else:
- yield elem
-
-def cmpver(a, b, n=3):
- ''' Compare two version numbers
- @param a: version number 1
- @param b: version number 2
- @param n: number of categories to compare
- @return: -1 if ab
- '''
- def fixup(i):
- try:
- return int(i)
- except ValueError:
- return i
- a = map(fixup, re.findall("\d+|\w+", a))
- b = map(fixup, re.findall("\d+|\w+", b))
- return cmp(a[:n], b[:n])
-
-
-
-def setpriority(priority=2):
- """ Set The Priority of a Windows Process. Priority is a value between 0-5 where
- 2 is normal priority. Sets the priority of the current Python process
- and limits the process to 2 logical processors """
-
- try:
- import win32process
- import ctypes
- from ctypes import wintypes
-
- priorityclasses = [win32process.IDLE_PRIORITY_CLASS,
- win32process.BELOW_NORMAL_PRIORITY_CLASS,
- win32process.NORMAL_PRIORITY_CLASS,
- win32process.ABOVE_NORMAL_PRIORITY_CLASS,
- win32process.HIGH_PRIORITY_CLASS,
- win32process.REALTIME_PRIORITY_CLASS]
-
- # prepare the kernel functions
- kernel32 = ctypes.windll.kernel32
- kernel32.GetCurrentProcess.restype = wintypes.HANDLE
- kernel32.GetCurrentProcess.argtypes = []
- kernel32.GetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)]
- kernel32.SetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.c_int]
- kernel32.SetPriorityClass.argtypes = [wintypes.HANDLE, ctypes.c_int]
-
- # get the process handle
- p = kernel32.GetCurrentProcess()
-
- # set process priority
- kernel32.SetPriorityClass(p, priorityclasses[priority])
-
- # limit the process to the first two available processors
- pmask = ctypes.c_int()
- smask = ctypes.c_int()
- kernel32.GetProcessAffinityMask(p, ctypes.byref(pmask), ctypes.byref(smask))
- smask = smask.value
- pmask = 0
- mask = 1
- cpu = 0
- while mask < 0x8000 and cpu < 2:
- if smask & mask:
- pmask |= mask
- cpu += 1
- mask = mask << 1
- kernel32.SetProcessAffinityMask(p, pmask)
- print "INFO: Available CPUs (bit mask) 0x%04X, Python process limited to 0x%04X"%(smask, pmask)
-
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- print "INFO: the process priority can not be raised because PyWin32 is not installed or an error occurred.\n - %s->%s"%(tb, str(e))
-
-
-'''
-------------------------------------------------------------
-MAIN APPLICATION
-------------------------------------------------------------
-'''
-def main(args):
- ''' Create and start up main application
- '''
- print "Starting PyCorder, please wait ...\n"
- setpriority(priority=4)
- app = Qt.QApplication(args)
- try:
- win = None
- win = MainWindow()
- win.showMaximized()
- if ShowConfirmationDialog:
- accept = Qt.QMessageBox.warning(None, "PyCorder Disclaimer", ConfirmationText,
- "Accept", "Cancel", "", 1)
- if accept == 0:
- win.usageConfirmed = True
- app.exec_()
- else:
- win.close()
- else:
- win.usageConfirmed = True
- app.exec_()
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- Qt.QMessageBox.critical(None, "PyCorder", tb + " -> " + str(e))
- if win != None:
- win.close()
-
- # show the battery disconnection reminder
- if ShowBatteryReminder and win and win.usageConfirmed:
- DlgBatteryInfo().exec_()
-
- print "PyCorder terminated\n"
-
-
-if __name__ == '__main__':
- main(sys.argv)
+# -*- coding: utf-8 -*-
+'''
+Main Application
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+B{Default Module Configuration:}
+
+ - L{MainWindow}
+ - Instantiate module chain L{InstantiateModules}
+ - Amplifier L{AMP_ActiChamp}
+ - L{Configuration Pane}
+ - L{Online Configuration Pane}
+ - Trigger Input Detection L{TRG_Eeg}
+ - Data Storage (Vision Data Exchange Format) L{StorageVision}
+ - L{Configuration Pane}
+ - L{Online Configuration Pane}
+ - Remote Data Access Server L{RDA_Server}
+ - Digital Filter (Low-Cut, High-Cut and Notch) L{FLT_Eeg}
+ - L{Configuration Pane}
+ - Impedance Display Dialog L{IMP_Display}
+ - L{Dialog}
+ - Data Display Module L{DISP_Scope}
+ - L{Online Configuration Pane}
+
+B{Dependencies:}
+ - Python 2.6
+ - NumPy 1.3.0 or 1.4.1
+ - SciPy 0.7.1 or 0.8.0
+ - PyQt 4.5.4 or 4.6.3
+ - PyQwt 5.2.0
+ - lxml 2.2.4 or 2.2.7
+
+@author: Norbert Hauser
+@version: 1.0
+'''
+import sys
+
+try:
+ from PyQt4.Qt import QString
+except (ImportError, ModuleNotFoundError) as exc:
+ if __name__ == "__main__":
+ from headless import run as run_headless
+ run_headless(str(exc))
+ sys.exit(0)
+ else:
+ raise
+
+try:
+ unicode
+except NameError:
+ unicode = str
+
+from PyQt4 import QtCore
+
+__version__ = "1.0.9"
+'''Application Version'''
+
+# show or hide the confirmation dialog box at start up
+ShowConfirmationDialog = False
+ConfirmationText = u"\
+The PyCorder is based on the Python programming language and is explicitly designed as open source software. \
+The program is provided free of charge under the GNU General Public License (GPL) for open-source \
+software by Brain Products GmbH.\n\
+Because it is open-source software, the PyCorder allows users to follow all the processing steps \
+in the source code. Users have the option of modifying the program code to meet their scientific requirements \
+irrespective of the program version that we provide and without prior consultation with us.\n\
+The PyCorder is exclusively intended for research purposes. Because it is also provided free of charge, \
+Brain Products GmbH is unable to provide support for the software directly.\nIn particular, \
+we can accept no liability for program functions that have been modified or created from scratch by the user.\n\
+If problems should arise when using the program, you can make use of the forum that has been set up \
+for this purpose. You will find the forum at http://www.actichamp.com/forum/.\n\
+Use of the program demands a considerable degree of responsibility and safety awareness on the part of the user.\n\
+It is possible to deactivate this information page in the source code.\n\
+Please confirm that you accept the conditions of use of the program as stated here by clicking Accept."
+
+# show or hide the battery disconnection reminder
+ShowBatteryReminder = True
+
+# force battery logging independent from the -rBL command line switch
+ForceBatteryLogging = True
+
+
+import collections
+import re
+from optparse import OptionParser
+
+
+def create_option_parser():
+ """Create and return the shared CLI option parser."""
+ parser = OptionParser()
+ parser.add_option("-m", "--modules", dest="ModuleFile",
+ help="Instantiate modules from separate module definition file MODULEFILE. "
+ "InstantiateModules() from this file will be called." )
+ parser.add_option("-c", "--configfile", dest="ConfigurationFile",
+ help="Load CONFIGURATIONFILE instead of last configuration.")
+ parser.add_option("-r", "--runas", dest="RunAs", default="",
+ help="Specify the module configuration that should be used.")
+ parser.add_option("-o", "--options", dest="Options", default="",
+ help="General options: R - start the remote server")
+ parser.add_option("--smoketest", action="store_true", dest="SmokeTest", default=False,
+ help="Run a headless startup smoke test and quit automatically.")
+ parser.add_option("--smoketest-ms", type="int", dest="SmokeTestMs", default=1500,
+ help="Milliseconds to keep the Qt event loop running in --smoketest mode.")
+ return parser
+
+
+'''
+------------------------------------------------------------
+LOAD LIBRARIES AND CHECK DEPENDENCIES
+------------------------------------------------------------
+'''
+import loadlibs
+
+# check library import
+if len(loadlibs.import_log) > 0:
+ try:
+ print("PyCorder: The following libraries are missing or have the wrong version\r\n\r\n")
+ print(loadlibs.import_log)
+ except Exception:
+ pass
+ # 非対話モード: 続行する(必要に応じて上位で終了)
+
+if not loadlibs.has_all_dependencies():
+ missing = ', '.join(sorted(loadlibs.missing_dependencies)) if hasattr(loadlibs, 'missing_dependencies') else 'unknown'
+ if __name__ == "__main__":
+ from headless import run as run_headless
+ reason = "Missing required dependencies: %s" % (missing,)
+ run_headless(reason)
+ sys.exit(0)
+ else:
+ raise RuntimeError("Missing required dependencies: %s" % (missing,))
+
+
+
+'''
+------------------------------------------------------------
+IMPORT GUI RESOURCES
+------------------------------------------------------------
+'''
+
+from res import frmMain
+from res import frmMainStatusBar
+from res import frmLogView
+from res import frmMainConfiguration
+
+
+'''
+------------------------------------------------------------
+IMPORT AND INSTANTIATE RECORDING MODULES
+------------------------------------------------------------
+'''
+# import the remote control server
+from remote import RemoteControlServer
+
+# import base functionality modules
+from amplifier import AMP_ActiChamp
+from storage import StorageVision
+from filter import FLT_Eeg
+from trigger import TRG_Eeg
+from impedance import IMP_Display
+from display import DISP_Scope
+from rda_server import RDA_Server
+from rda_client import RDA_Client
+from montage import MNT_Recording
+from modbase import *
+
+# import your own modules here
+#from tutorial.tut_0 import TUT_0
+#from tutorial.tut_1 import TUT_1
+#from tutorial.tut_2 import TUT_2
+#from tutorial.tut_3 import TUT_3
+#from tutorial.tut_4 import TUT_4
+from custom_modules.dc_offset import dc_offset
+
+def InstantiateModules(run_as):
+ ''' Instantiate and arrange module objects.
+ Modules will be connected top -> down, starting with array index 0.
+ Additional modules can be connected left -> right with tuples as list objects.
+ @param run_as: command line option (-r, --runas) for different module configurations
+ @return: list with instantiated module objects
+ '''
+ # get command line arguments
+ if 'RC' in run_as:
+ # run as remote client
+ modules = [RDA_Client(),
+ TRG_Eeg(),
+ FLT_Eeg(),
+ IMP_Display(),
+ DISP_Scope(instance=0)]
+ else:
+ # run as actiCHamp recorder
+ modules = [AMP_ActiChamp(),
+ MNT_Recording(),
+ TRG_Eeg(),
+ StorageVision(),
+ FLT_Eeg(),
+ dc_offset(),
+ RDA_Server(),
+ IMP_Display(),
+ DISP_Scope(instance=0)
+ ]
+ return modules
+
+
+'''
+------------------------------------------------------------
+APPLICATION MAIN WINDOW
+------------------------------------------------------------
+'''
+
+class MainWindow(Qt.QMainWindow, frmMain.Ui_MainWindow):
+ ''' Application Main Window Class
+ includes main menu, status bar and module handling
+ '''
+ def __init__(self):
+ ''' Instantiate and initialize GUI objects.
+ - Connect to button and menu actions.
+ - Instantiate and connect PyCorder module chain.
+ -Load the last used module configuration.
+ '''
+ Qt.QMainWindow.__init__(self)
+ self.setupUi(self)
+
+ # create status bar
+ self.statusWidget = StatusBarWidget()
+ self.statusBar().addPermanentWidget(self.statusWidget, 1)
+
+ # menu actions
+ try:
+ self.actionQuit.triggered.connect(self.close)
+ except Exception:
+ self.connect(self.actionQuit, Qt.SIGNAL('triggered()'),
+ Qt.SLOT('close()'))
+ self.connect(self.actionShow_Log, Qt.SIGNAL('triggered()'),
+ self.statusWidget.showLogEntries)
+ self.connect(self.actionLoad_Configuration, Qt.SIGNAL('triggered()'),
+ self.loadConfiguration)
+ self.connect(self.actionSave_Configuration, Qt.SIGNAL('triggered()'),
+ self.saveConfiguration)
+ self.connect(self.actionDefault_Configuration, Qt.SIGNAL('triggered()'),
+ self.defaultConfiguration)
+
+ # button actions
+ self.connect(self.pushButtonConfiguration, Qt.SIGNAL("clicked()"),
+ self.configurationClicked)
+ self.connect(self.statusWidget, Qt.SIGNAL("saveLog()"),
+ self.saveLogFile)
+ self.connect(self.statusWidget, Qt.SIGNAL("showLog()"),
+ self.showLogEntries)
+
+ # preferences
+ self.application_name = "PyCorder"
+ self.configuration_file = ""
+ self.configuration_dir = ""
+ self.log_dir = ""
+ self.loadPreferences()
+ self.recording_mode = -1
+ self.usageConfirmed = False
+
+ # remote control server
+ self.RC = None
+
+ # parse command line options
+ # look for old style command line option "-RC"
+ if "-RC" in sys.argv:
+ sys.argv.remove("-RC") # skip old style
+ RemoteClient = True
+ else:
+ RemoteClient = False
+
+ # get command line options
+ parser = create_option_parser()
+ try:
+ self.cmd_options, args = parser.parse_args()
+ except SystemExit:
+ raise
+ except Exception:
+ raise Exception("Command line parser error !")
+ # merge run configuration with old style
+ if self.cmd_options.RunAs == "" and RemoteClient:
+ self.cmd_options.RunAs = "RC"
+
+
+ # create module chain (top = index 0, bottom = last index)
+ self.defineModuleChain()
+
+ # connect modules
+ for idx_vertical in range(len(self.modules)-1):
+ if type(self.modules[idx_vertical]) in (tuple, list):
+ # connect top/down
+ if type(self.modules[idx_vertical+1]) in (tuple, list):
+ self.modules[idx_vertical][0].add_receiver(self.modules[idx_vertical+1][0])
+ else:
+ self.modules[idx_vertical][0].add_receiver(self.modules[idx_vertical+1])
+ # connect left/right
+ for idx_horizontal in range(len(self.modules[idx_vertical])-1):
+ self.modules[idx_vertical][idx_horizontal].add_receiver(self.modules[idx_vertical][idx_horizontal+1])
+ else:
+ # connect top/down
+ if type(self.modules[idx_vertical+1]) in (tuple, list):
+ self.modules[idx_vertical].add_receiver(self.modules[idx_vertical+1][0])
+ else:
+ self.modules[idx_vertical].add_receiver(self.modules[idx_vertical+1])
+
+ # get the top module
+ if type(self.modules[0]) in (tuple, list):
+ self.topmodule = self.modules[0][0]
+ else:
+ self.topmodule = self.modules[0]
+
+ # get the bottom module
+ if type(self.modules[-1]) in (tuple, list):
+ self.bottommodule = self.modules[-1][-1]
+ else:
+ self.bottommodule = self.modules[-1]
+
+ # get events from module chain top module
+ self.connect(self.topmodule, Qt.SIGNAL("event(PyQt_PyObject)"), self.processEvent)
+
+ # tell the top module to get events from us
+ self.topmodule.connect(self, Qt.SIGNAL("parentevent(PyQt_PyObject)"), self.topmodule.parent_event, Qt.Qt.QueuedConnection)
+
+ # get signal panes for plot area
+ self.horizontalLayout_SignalPane.removeItem(self.horizontalLayout_SignalPane.itemAt(0))
+ for module in flatten(self.modules):
+ pane = module.get_display_pane()
+ if pane != None:
+ self.horizontalLayout_SignalPane.addWidget(pane)
+
+
+ # initial module chain update (top module)
+ self.topmodule.update_receivers()
+
+ # insert online configuration panes
+ position = 0
+ for module in flatten(self.modules):
+ module.main_object = self
+ pane = module.get_online_configuration()
+ if pane != None:
+ #self.verticalLayout_OnlinePane.insertWidget(self.verticalLayout_OnlinePane.count()-2, pane)
+ self.verticalLayout_OnlinePane.insertWidget(position, pane)
+ position += 1
+
+ # load configuration file
+ if self.cmd_options.ConfigurationFile == None:
+ # try to load the last configuration file
+ try:
+ if len(self.configuration_file) > 0:
+ cfg = os.path.normpath(self.configuration_dir + '/' + self.configuration_file)
+ self._loadConfiguration(cfg)
+ else:
+ self.defaultConfiguration()
+ except:
+ pass
+ else:
+ # try to load configuration from command line file
+ try:
+ self._loadConfiguration(os.path.normpath(self.cmd_options.ConfigurationFile))
+ except Exception as e:
+ raise Exception("Failed to load configuration from file: " +
+ self.cmd_options.ConfigurationFile + "\n" + repr(e))
+
+
+ # update log text module info
+ self.updateModuleInfo()
+
+ # update button states
+ self.updateUI()
+
+ # instantiate and start the remote control server
+ try:
+ if "R" in self.cmd_options.Options:
+ self.RC = RemoteControlServer()
+ except Exception as e:
+ self.RC = None
+ Qt.QMessageBox.information(None, "Remote Control Server", str(e))
+ if self.RC != None:
+ # get events from server
+ self.connect(self.RC, Qt.SIGNAL("event(PyQt_PyObject)"), self.processEvent)
+
+ # performance boost ;-)
+ self.startTimer(1)
+
+
+
+ def defineModuleChain(self):
+ ''' Instantiate and arrange module objects
+ - Modules will be connected top -> down, starting with array index 0
+ - Additional modules can be connected left -> right with tuples as list objects
+ '''
+ # check the command line option
+ if self.cmd_options.ModuleFile == None:
+ # get modules from global function
+ self.modules = InstantiateModules(self.cmd_options.RunAs)
+ else:
+ # get module configuration from external file
+ try:
+ exec("from " + self.cmd_options.ModuleFile + " import InstantiateModules")
+ self.modules = InstantiateModules(self.cmd_options.RunAs)
+ except Exception as e:
+ raise Exception("Failed to instantiate modules from external file: " +
+ self.cmd_options.ModuleFile + "\n" + str(e))
+
+ # show battery reminder only for the acticCHamp amplifier
+ global ShowBatteryReminder
+ classnames = [m.__class__.__name__ for m in self.modules]
+ if not "AMP_ActiChamp" in classnames:
+ ShowBatteryReminder = False
+
+
+
+ def configurationClicked(self):
+ ''' Configuration button clicked
+ - Open configuration dialog and add configuration panes for each module in the
+ module chain, if available
+ '''
+ dlg = DlgConfiguration()
+ for module in flatten(self.modules):
+ pane = module.get_configuration_pane()
+ if pane != None:
+ dlg.addPane(pane)
+ ok = dlg.exec_()
+ if ok:
+ self.saveConfiguration()
+
+ def defaultConfiguration(self):
+ ''' Menu "Reset Configuration":
+ Set default values for all modules
+ '''
+ # reset all modules
+ for module in flatten(self.modules):
+ module.setDefault()
+
+ # update module chain, starting from top module
+ self.topmodule.update_receivers()
+
+ # update status line
+ self.processEvent(ModuleEvent("Application",
+ EventType.STATUS,
+ info = "default",
+ status_field = "Workspace"))
+
+ def _loadConfiguration(self, filename):
+ ''' Load module configuration from XML file
+ @param filename: Full qualified XML file name
+ '''
+ ok = True
+ cfg = objectify.parse(filename)
+ # check application and version
+ app = cfg.xpath("//PyCorder")
+ if (len(app) == 0) or (app[0].get("version") == None):
+ # configuration data not found
+ self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
+ "%s is not a valid PyCorder configuration file"%(filename),\
+ severity=1) )
+ ok = False
+
+ if ok:
+ version = app[0].get("version")
+ if cmpver(version, __version__, 2) > 0:
+ # wrong version
+ self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
+ "%s wrong version %s > %s"%(filename, version, __version__),\
+ severity=ErrorSeverity.NOTIFY) )
+ ok = False
+
+ # setup modules from configuration file
+ if ok:
+ for module in flatten(self.modules):
+ module.setXML(cfg)
+
+ # update module chain, starting from top module
+ self.topmodule.update_receivers()
+
+ # update status line
+ file_name, ext = os.path.splitext(os.path.split(filename)[1])
+ self.processEvent(ModuleEvent("Application",
+ EventType.STATUS,
+ info = file_name,
+ status_field = "Workspace"))
+
+
+ def loadConfiguration(self):
+ ''' Menu "Load Configuration ...":
+ Load module configuration from XML file
+ '''
+ dlg = Qt.QFileDialog()
+ dlg.setFileMode(Qt.QFileDialog.ExistingFile)
+ dlg.setAcceptMode(Qt.QFileDialog.AcceptOpen)
+ dlg.setNameFilter("Configuration files (*.xml)")
+ dlg.setDefaultSuffix("xml")
+ if len(self.configuration_dir) > 0:
+ dlg.setDirectory(self.configuration_dir)
+ dlg.selectFile(self.configuration_file)
+ if dlg.exec_() == True:
+ try:
+ files = dlg.selectedFiles()
+ file_name = unicode(files[0])
+ # load configuration from XML file
+ self._loadConfiguration(file_name)
+ # set preferences
+ dir, fn = os.path.split(file_name)
+ self.configuration_file = fn
+ self.configuration_dir = dir
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ self.processEvent(ModuleEvent("Load Configuration", EventType.ERROR,\
+ tb + " -> %s "%(file_name) + str(e),
+ severity=ErrorSeverity.NOTIFY))
+
+
+ def _saveConfiguration(self, filename):
+ ''' Save module configuration to XML file
+ @param filename: Full qualified XML file name
+ '''
+ E = objectify.E
+ modules = E.modules()
+ # get configuration from each connected module
+ for module in flatten(self.modules):
+ cfg = module.getXML()
+ if cfg != None:
+ modules.append(cfg)
+ # build complete configuration tree
+ root = E.PyCorder(modules, version=__version__)
+ # write it to file
+ etree.ElementTree(root).write(filename, pretty_print=True, encoding="UTF-8")
+
+ def saveConfiguration(self):
+ ''' Menu "Save Configuration ...":
+ Save module configuration to XML file
+ '''
+ dlg = Qt.QFileDialog()
+ dlg.setFileMode(Qt.QFileDialog.AnyFile)
+ dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
+ dlg.setNameFilter("Configuration files (*.xml)")
+ dlg.setDefaultSuffix("xml")
+ if len(self.configuration_dir) > 0:
+ dlg.setDirectory(self.configuration_dir)
+ dlg.selectFile(self.configuration_file)
+ if dlg.exec_() == True:
+ try:
+ files = dlg.selectedFiles()
+ file_name = unicode(files[0])
+ # save configuration to XML
+ self._saveConfiguration(file_name)
+ # set preferences
+ dir, fn = os.path.split(file_name)
+ self.configuration_file = fn
+ self.configuration_dir = dir
+ # update status line
+ fn, ext = os.path.splitext(os.path.split(file_name)[1])
+ self.processEvent(ModuleEvent("Application",
+ EventType.STATUS,
+ info = fn,
+ status_field = "Workspace"))
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ self.processEvent(ModuleEvent("Save Configuration", EventType.ERROR,\
+ tb + " -> %s "%(file_name) + str(e),
+ severity=ErrorSeverity.NOTIFY))
+
+ def savePreferences(self):
+ ''' Save preferences to XML file
+ '''
+ E = objectify.E
+ preferences = E.preferences(E.config_dir(self.configuration_dir),
+ E.config_file(self.configuration_file),
+ E.log_dir(self.log_dir))
+ root = E.PyCorder(preferences, version=__version__)
+
+ # preferences will be stored to user home directory
+ try:
+ homedir = Qt.QDir.home()
+ appdir = "." + self.application_name
+ if not homedir.cd(appdir):
+ homedir.mkdir(appdir)
+ homedir.cd(appdir)
+ filename = unicode(homedir.absoluteFilePath("preferences.xml"))
+ etree.ElementTree(root).write(filename, pretty_print=True, encoding="UTF-8")
+ except:
+ pass
+
+ def loadPreferences(self):
+ ''' Load preferences from XML file
+ '''
+ try:
+ # preferences will be stored to user home directory
+ homedir = Qt.QDir.home()
+ appdir = "." + self.application_name
+ if not homedir.cd(appdir):
+ return
+ filename = unicode(homedir.absoluteFilePath("preferences.xml"))
+
+ # read XML file
+ cfg = objectify.parse(filename)
+ # check application and version
+ app = cfg.xpath("//PyCorder")
+ if (len(app) == 0) or (app[0].get("version") == None):
+ # configuration data not found
+ return
+ # check version
+ version = app[0].get("version")
+ if cmpver(version, __version__, 2) > 0:
+ # wrong version
+ return
+
+ # update preferences
+ preferences = app[0].preferences
+ self.configuration_dir = preferences.config_dir.pyval
+ self.configuration_file = preferences.config_file.pyval
+ self.log_dir = preferences.log_dir.pyval
+ except:
+ pass
+
+ def showLogEntries(self):
+ ''' Show log entries
+ '''
+ self.updateModuleInfo()
+ self.statusWidget.showLogEntries()
+
+ def saveLogFile(self):
+ ''' Write log entries to file
+ '''
+ dlg = Qt.QFileDialog()
+ dlg.setFileMode(Qt.QFileDialog.AnyFile)
+ dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
+ dlg.setNameFilter("Log files (*.log)")
+ dlg.setDefaultSuffix("log")
+ if len(self.log_dir) > 0:
+ dlg.setDirectory(self.log_dir)
+ if dlg.exec_() == True:
+ try:
+ files = dlg.selectedFiles()
+ file_name = unicode(files[0])
+ # set preferences
+ dir, fn = os.path.split(file_name)
+ self.log_dir = dir
+ # write log entries to file
+ f = open(file_name, "w")
+ f.write(self.statusWidget.getLogText().encode('utf-8'))
+ f.close()
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ Qt.QMessageBox.critical(None, "PyCorder",
+ "Failed to write log file (%s)\n"%(file_name) +
+ tb + " -> " + str(e))
+
+ def closeEvent(self, event):
+ ''' Application wants to close, prevent closing if recording to file is still active
+ '''
+ if not self.topmodule.query("Stop"):
+ event.ignore()
+ else:
+ self.topmodule.stop(force=True)
+ self.savePreferences()
+ # clean up modules
+ for module in flatten(self.modules):
+ module.terminate()
+ # terminate remote control server
+ if self.RC != None:
+ self.RC.terminate()
+ event.accept()
+
+ def sendEvent(self, event):
+ ''' Send an event to the top module event chain
+ '''
+ self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
+
+
+ def processRemoteCommand(self, cmd_string):
+ ''' Process commands received from remote control
+ @param cmd: the received command
+ @return: log entry and error message
+ '''
+ error_message = ""
+ log_entry = u"command received: '%s'"%(cmd_string)
+ if len(cmd_string) > 0:
+ # split command and value
+ cmd = cmd_string[0].upper()
+ if len(cmd_string) > 1:
+ cmd_value = cmd_string[1:]
+ else:
+ cmd_value = ""
+
+ # check for supported commands
+ if cmd not in ["1", "2", "3", "4", "M", "I", "S", "Q", "X", "F"]:
+ error_message = u"command not supported: '%s'"%(cmd_string)
+
+ # check the recording state and if the requested command can be applied
+ elif cmd in ["S", "I", "M", "X", "Q"] and self.topmodule.isRunning() and not self.RC.remoteRecording:
+ error_message = u"recording is in progress and was not started remote: '%s'"%(cmd_string)
+
+ elif cmd not in ["Q", "X"] and not self.topmodule.query("RemoteStop"):
+ error_message = u"recording is still in progress, stop it first with 'X': '%s'"%(cmd_string)
+
+ elif cmd in ["1", "2", "3", "4"] and self.topmodule.isRunning():
+ error_message = u"data acquisition is still in progress, stop it first with 'X': '%s'"%(cmd_string)
+
+ elif cmd in ["4", "S", "I", "M", "X", "Q"] and not self.RC.isInitialized():
+ error_message = u"some variables (1 Configuration file, 2 Experiment ID or 3 Subject ID) are not initialized: '%s'"%(cmd_string)
+
+ # Initialization
+ elif cmd == "1":
+ self.RC.S_ConfigurationFile = cmd_value
+ elif cmd == "2":
+ self.RC.S_ExperimentNr = cmd_value
+ elif cmd == "3":
+ self.RC.S_SubjectID = cmd_value
+ elif cmd == "4":
+ # prepare recording
+ # load configuration file
+ try:
+ self._loadConfiguration(self.RC.S_ConfigurationFile)
+ except:
+ error_message = u"failed to load configuration file: '%s'"%(self.RC.S_ConfigurationFile)
+
+ # Exit
+ elif cmd == "X":
+ # exit, stop everything and reset all state variables
+ # stop data acquisition
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="Stop",
+ cmd_value="force"))
+ #self.RC.resetControlState()
+ self.RC.remoteRecording = False
+
+ # Monitoring
+ elif cmd == "I":
+ # start impedance mode
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="StartImpedance"))
+ self.RC.remoteRecording = True
+
+ elif cmd == "M":
+ # start monitoring
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="StartRecording"))
+ self.RC.remoteRecording = True
+
+ # Recording
+ elif cmd == "S":
+ # start monitoring if not yet started
+ if not self.topmodule.isRunning() or self.recording_mode != RecordingMode.NORMAL:
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="StartRecording"))
+ # start recording
+ filename = "%s_%s"%(self.RC.S_ExperimentNr, self.RC.S_SubjectID)
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="StartSaving",
+ cmd_value=filename))
+ self.RC.remoteRecording = True
+
+ elif cmd == "Q":
+ # stop recording
+ self.sendEvent(ModuleEvent("RemoteControl",
+ EventType.COMMAND,
+ info="StopSaving"))
+
+ # enable / disable feedback
+ elif cmd == "F":
+ self.RC.feedbackEnabled = (cmd_value == "1")
+
+ else:
+ error_message = u"invalid command (size=0)"
+
+ # send feedback
+ if error_message:
+ self.RC.send_feedback("%sFAILED %s"%(cmd, error_message))
+ else:
+ if cmd in ["S", "I", "M"]:
+ self.RC.postpone_feedback(cmd)
+ else:
+ self.RC.send_feedback("%sOK"%(cmd))
+
+ return log_entry, error_message
+
+
+ def processRemoteFeedback(self, event):
+ ''' Handle postponed command feedbacks
+ '''
+ if not self.RC:
+ return
+ if not self.RC.isClientConnected():
+ return
+
+ for idx, cmd in enumerate(self.RC.postponed):
+ if event.type == EventType.ERROR:
+ self.RC.send_feedback("%sFAILED %s"%(cmd, event.info))
+ del self.RC.postponed[idx]
+ elif cmd == "M":
+ if event.type == EventType.STATUS and event.status_field == "Mode" and event.info == RecordingMode.NORMAL:
+ self.RC.send_feedback("%sOK"%(cmd))
+ del self.RC.postponed[idx]
+ elif cmd == "I":
+ if event.type == EventType.STATUS and event.status_field == "Mode" and event.info == RecordingMode.IMPEDANCE:
+ self.RC.send_feedback("%sOK"%(cmd))
+ del self.RC.postponed[idx]
+ elif cmd == "S":
+ if event.type == EventType.STATUS and event.status_field == "Storage" and event.info:
+ self.RC.send_feedback("%sOK %s"%(cmd, event.info))
+ del self.RC.postponed[idx]
+
+
+ def processEvent(self, event):
+ ''' Process events from module chain
+ @param event: ModuleEvent object
+ Stop acquisition on errors with a severity > 1
+ '''
+ # handle remote postponed feedback
+ self.processRemoteFeedback(event)
+
+ # process commands
+ if event.type == EventType.COMMAND:
+ if event.info == "RemoteCommand":
+ # ignore remote commands until the usage conditions are confirmed
+ if not self.usageConfirmed:
+ return
+ # handle and log commands from any remote control client
+ cmd_log, cmd_error = self.processRemoteCommand(event.cmd_value)
+ # modify the command event for logging
+ if len(cmd_error) > 0:
+ event.type = EventType.ERROR
+ event.severity = ErrorSeverity.IGNORE
+ event.info = cmd_error
+ else:
+ event.type = EventType.LOGMESSAGE
+ event.info = cmd_log
+ else:
+ # don't log other commands
+ return
+
+ # recording mode changed?
+ if event.type == EventType.STATUS:
+ if event.status_field == "Mode":
+ self.recording_mode = event.info
+ self.updateUI(isRunning=(event.info >= 0))
+ self.updateModuleInfo()
+
+ # write battery voltage to log file
+ self.writeBatteryLog(event)
+
+ # log events and update status line
+ self.statusWidget.updateEventStatus(event)
+
+ # look for errors
+ if (event.type == EventType.ERROR) and (event.severity > 1):
+ self.topmodule.stop(force=True)
+
+ def writeBatteryLog(self, event):
+ ''' Write battery voltage to log file
+ '''
+ try:
+ if self.battery_log:
+ if event.type == EventType.STATUS:
+ # log battery voltage
+ if event.status_field == "Battery":
+ t = time.perf_counter()
+ if (t - self.battery_timer >= 59) and (self.battery_mode >= 0):
+ voltage = float(event.info.split("V")[0])
+ level = '?'
+ if event.severity == ErrorSeverity.IGNORE:
+ level = 'H'
+ elif event.severity == ErrorSeverity.NOTIFY:
+ level = 'M'
+ elif event.severity == ErrorSeverity.STOP:
+ level = 'L'
+ if "V C" in event.info:
+ level = 'C'
+ logentry = "%.2f\t%s\t%s\t%s"%(voltage,
+ event.event_time.strftime("%H:%M:%S\t%d/%m/%Y"),
+ self.battery_ampSN,
+ level)
+ if self.battery_mode != self.battery_logmode:
+ logentry += "\tStart %d at %s"%(self.battery_mode, self.battery_rate)
+ self.battery_logmode = self.battery_mode
+ logentry += "\n"
+ with open(self.battery_logfile,"a") as f:
+ f.write(logentry)
+ self.battery_timer = t
+ event.info += "\nLOG"
+ # update recording mode
+ if event.status_field == "Mode":
+ self.battery_timer = time.perf_counter() - 60.0
+ self.battery_mode = event.info
+ if event.info < 0:
+ self.battery_logmode = event.info
+ sn = self.statusWidget.moduleinfo.split("SN: ")
+ self.battery_ampSN = "???"
+ if len(sn) > 1:
+ sn = sn[1].split()
+ if len(sn) > 0:
+ self.battery_ampSN = sn[0]
+ # update sampling rate
+ if event.status_field == "Rate":
+ self.battery_rate = event.info
+
+ except AttributeError:
+ # enable battery log with command line option -rBL or if it is forced
+ self.battery_log = (self.cmd_options.RunAs == "BL") or (ForceBatteryLogging)
+ self.battery_timer = time.perf_counter() - 60.0
+ self.battery_ampSN = "???"
+ self.battery_rate = "???"
+ self.battery_mode = -1
+ self.battery_logmode = -1
+
+ # log to users home /.PyCorder directory
+ logpath = os.path.join(unicode(Qt.QDir.toNativeSeparators(Qt.QDir.homePath())), "." + self.application_name)
+ # create or use the auto incremented file name
+ homedir = Qt.QDir.home()
+ appdir = "." + self.application_name
+ if not homedir.cd(appdir):
+ homedir.mkdir(appdir)
+ logdir = Qt.QDir(logpath)
+ if not logdir.exists():
+ if self.battery_log:
+ print("Battery Log is disabled because the log directory (%s) doesn't exist"%logpath)
+ self.battery_log = False
+ else:
+ fname = "CHampBattery_"
+ numbersize = 3
+ numberstring = "?"
+ for n in range(1, numbersize):
+ numberstring += "?"
+ logdir.setNameFilters(Qt.QStringList("%s%s.log"%(fname, numberstring)))
+ logdir.setFilter(Qt.QDir.Files)
+ flist = logdir.entryList()
+ # extract numbers
+ flist.replaceInStrings(".log", "", Qt.Qt.CaseInsensitive)
+ flist.replaceInStrings(fname, "", Qt.Qt.CaseInsensitive)
+ numbers = []
+ for f in flist:
+ num,ok = f.toInt()
+ if ok and (num < 10**numbersize):
+ numbers.append(num)
+ if len(numbers) > 0:
+ # get the highest number
+ numbers.sort()
+ fnumber = numbers[-1]
+ else:
+ fnumber = 0
+ name = "%s%0*d.log"%(fname, numbersize, fnumber)
+ logfile = os.path.join(logpath, name)
+
+ # verify that the file size is not yet exceeding 10MB
+ if Qt.QFile.exists(logfile):
+ if os.path.getsize(logfile) > 10 * 2**20:
+ name = "%s%0*d.log"%(fname, numbersize, fnumber + 1)
+ logfile = os.path.join(logpath, name)
+
+ self.battery_logfile = logfile
+
+ if self.battery_log:
+ print("Battery Log to %s is enabled"%self.battery_logfile)
+ except Exception as e:
+ print(e)
+ self.battery_log = False
+
+
+ def updateUI(self, isRunning=False):
+ ''' Update user interface to reflect the recording state
+ '''
+ if isRunning:
+ self.pushButtonConfiguration.setEnabled(False)
+ self.actionLoad_Configuration.setEnabled(False)
+ self.actionSave_Configuration.setEnabled(False)
+ self.actionQuit.setEnabled(False)
+ self.actionDefault_Configuration.setEnabled(False)
+ else:
+ self.pushButtonConfiguration.setEnabled(True)
+ self.actionLoad_Configuration.setEnabled(True)
+ self.actionSave_Configuration.setEnabled(True)
+ self.actionQuit.setEnabled(True)
+ self.actionDefault_Configuration.setEnabled(True)
+ self.statusWidget.resetUtilization()
+
+ def updateModuleInfo(self):
+ ''' Update the module information in the log text
+ and propagate it to all connected modules as status information
+ '''
+ # get module information
+ self.statusWidget.moduleinfo = ""
+ for module in flatten(self.modules):
+ info = module.get_module_info()
+ if info != None:
+ self.statusWidget.moduleinfo += module._object_name + "\n"
+ self.statusWidget.moduleinfo += info
+ if len(self.statusWidget.moduleinfo) > 0:
+ self.statusWidget.moduleinfo += "\n\n"
+
+ # propagate status info to all connected modules
+ moduleinfo = u"PyCorder V" + __version__ + "\n\n"
+ moduleinfo += self.statusWidget.moduleinfo
+ msg = ModuleEvent("PyCorder",
+ EventType.STATUS,
+ info = moduleinfo,
+ status_field="ModuleInfo")
+ self.sendEvent(msg)
+
+
+'''
+------------------------------------------------------------
+LOG ENTRY DIALOG
+------------------------------------------------------------
+'''
+
+class DlgLogView(Qt.QDialog, frmLogView.Ui_frmLogView):
+ ''' Show all log entries as plain text
+ '''
+ def __init__(self, *args):
+ Qt.QDialog.__init__(self, *args)
+ self.setupUi(self)
+
+ def setLogEntry(self, entry):
+ self.labelView.setPlainText(entry)
+
+
+'''
+------------------------------------------------------------
+BATTERY INFO DIALOG
+------------------------------------------------------------
+'''
+
+class DlgBatteryInfo(Qt.QMessageBox):
+ ''' Show disconnect info for 10s
+ '''
+ def __init__(self, *args):
+ infoText = (u"Please disconnect actiCHamp from actiPOWER after you finished recording.\n" +
+ u"Always attach actiPOWER to the charger when not in use to prevent damaging the accumulator.")
+ Qt.QMessageBox.__init__(self, Qt.QMessageBox.Information, "Disconnect Battery", infoText)
+ self.startTimer(10000)
+
+ def timerEvent(self, e):
+ self.done(0)
+
+
+'''
+------------------------------------------------------------
+MAIN CONFIGURATION DIALOG
+------------------------------------------------------------
+'''
+
+class DlgConfiguration(Qt.QDialog, frmMainConfiguration.Ui_frmConfiguration):
+ ''' Module main configuration dialog
+ All module configuration panes will go here
+ '''
+ def __init__(self):
+ Qt.QDialog.__init__(self)
+ self.setupUi(self)
+ self.panes = []
+
+ def addPane(self, pane):
+ ''' Insert new tab and add module configuration pane
+ @param pane: module configuration pane (QFrame object)
+ '''
+ if pane == None:
+ return
+ currenttabs = len(self.panes)
+ if currenttabs > 0:
+ # add new tab
+ tab = Qt.QWidget()
+ tab.setObjectName("tab%d"%(currenttabs+1))
+ gridLayout = Qt.QGridLayout(tab)
+ gridLayout.setObjectName("gridLayout%d"%(currenttabs+1))
+ self.tabWidget.addTab(tab, "")
+ else:
+ gridLayout = self.gridLayout1
+ tab = self.tab1
+
+ self.panes.append(pane)
+ gridLayout.addWidget(pane)
+ self.tabWidget.setTabText(self.tabWidget.indexOf(tab), pane.windowTitle())
+
+
+'''
+------------------------------------------------------------
+STATUS BAR
+------------------------------------------------------------
+'''
+
+class StatusBarWidget(Qt.QWidget, frmMainStatusBar.Ui_frmStatusBar):
+ ''' Main Window status bar
+ '''
+ def __init__(self):
+ Qt.QWidget.__init__(self)
+ self.setupUi(self)
+
+ # utilization progressbar fifo
+ # info label color and click
+ self.labelInfo.setAutoFillBackground(True)
+ self.labelInfo.mouseReleaseEvent = self.labelInfoClicked
+ self.defaultBkColor = self.labelInfo.palette().color(self.labelInfo.backgroundRole())
+ self.labelInfo.setText("Brain Products GmbH, PyCorder V" + __version__)
+ self.labelStatus_4.setAutoFillBackground(True)
+
+ # log entries
+ self.logFifo = collections.deque(maxlen=10000)
+ self.lockError = False
+ self.moduleinfo = ""
+
+ # number of channels and reference channel names
+ self.status_channels = ""
+ self.status_reference = ""
+
+ self.resetUtilization()
+
+ def resetUtilization(self):
+ ''' Reset utilization parameters
+ '''
+ self.utilizationFifo = collections.deque()
+ self.utilizationUpdateCounter = 0
+ self.utilizationMaxValue = 0
+ self.updateUtilization(0)
+
+ def updateUtilization(self, utilization):
+ ''' Update the utilization progressbar
+ @param utilization: percentage of utilization
+ '''
+ # average utilization value
+ self.utilizationFifo.append(utilization)
+ if len(self.utilizationFifo) > 5:
+ self.utilizationFifo.popleft()
+ utilization = sum(self.utilizationFifo) / len(self.utilizationFifo)
+ self.utilizationMaxValue = max(self.utilizationMaxValue, utilization)
+
+ # slow down utilization display
+ if self.utilizationUpdateCounter > 0:
+ self.utilizationUpdateCounter -= 1
+ return
+ self.utilizationUpdateCounter = 5
+ utilization = self.utilizationMaxValue
+ self.utilizationMaxValue = 0
+
+ # update progress bar
+ if utilization < 100:
+ self.progressBarUtilization.setValue(utilization)
+ else:
+ self.progressBarUtilization.setValue(100)
+ self.progressBarUtilization.setFormat("%d%% Utilization"%(utilization))
+
+ # modify progress bar color (<80% -> green, >=80% -> red)
+ if utilization < 80:
+ self.progressBarUtilization.setStyleSheet("QProgressBar {padding: 1px; text-align: right; margin-right: 35ex;} "\
+ "QProgressBar::chunk {background: "\
+ "qlineargradient(x1: 1, y1: 0, x2: 1, y2: 0.5, stop: 1 green, stop: 0 white);"\
+ "margin: 0.5px}")
+ else:
+ self.progressBarUtilization.setStyleSheet("QProgressBar {padding: 1px; text-align: right; margin-right: 35ex;} "\
+ "QProgressBar::chunk {background: "\
+ "qlineargradient(x1: 1, y1: 0, x2: 1, y2: 0.5, stop: 1 red, stop: 0 white);"\
+ "margin: 0.5px}")
+
+ def updateEventStatus(self, event):
+ ''' Update status info field and put events into the log fifo
+ @param event: ModuleEvent object
+ '''
+ # display dedicated status info values
+ if event.type == EventType.STATUS:
+ if event.status_field == "Rate":
+ self.labelStatus_1.setText(event.info)
+ elif event.status_field == "Channels":
+ self.status_channels = event.info
+ self.labelStatus_2.setText(self.status_channels + ", " + self.status_reference)
+ elif event.status_field == "Reference":
+ refnames = event.info
+ # limit the number of displayed channel names
+ if len(refnames) > 70:
+ refnames = refnames[:70].rsplit('+',1)[0] + "+ ..."
+ self.status_reference = refnames
+ self.labelStatus_2.setText(self.status_channels + ", " + self.status_reference)
+ elif event.status_field == "Workspace":
+ self.labelStatus_3.setText(event.info)
+ elif event.status_field == "Battery":
+ # set voltage
+ self.labelStatus_4.setText(event.info)
+ # severity indicates normal, critical or bad
+ palette = self.labelStatus_4.palette()
+ if event.severity == ErrorSeverity.NOTIFY:
+ palette.setColor(self.labelStatus_4.backgroundRole(), Qt.Qt.yellow)
+ elif event.severity == ErrorSeverity.STOP:
+ palette.setColor(self.labelStatus_4.backgroundRole(), Qt.Qt.red)
+ else:
+ palette.setColor(self.labelStatus_4.backgroundRole(), self.defaultBkColor)
+ self.labelStatus_4.setPalette(palette)
+ elif event.status_field == "Utilization":
+ self.updateUtilization(event.info)
+ return
+
+ # lock an error display until LogView is shown
+ if ((self.lockError == False) or (event.severity > 0)) and event.type != EventType.LOG:
+ # update label
+ self.labelInfo.setText(unicode(event))
+ palette = self.labelInfo.palette()
+ if event.type == EventType.ERROR:
+ palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.red)
+ if event.severity > 0:
+ self.lockError = True
+ palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.red)
+ else:
+ palette.setColor(self.labelInfo.backgroundRole(), Qt.Qt.yellow)
+ else:
+ palette.setColor(self.labelInfo.backgroundRole(), self.defaultBkColor)
+ self.labelInfo.setPalette(palette)
+ # put events into log fifo
+ if event.type != EventType.MESSAGE:
+ self.logFifo.append(event)
+
+ def showLogEntries(self):
+ ''' Show the event log content
+ '''
+ dlg = DlgLogView()
+ dlg.setLogEntry(self.getLogText())
+ save = dlg.exec_()
+ if save:
+ self.emit(Qt.SIGNAL('saveLog()'))
+ self.resetErrorState()
+
+ def getLogText(self):
+ ''' Get the log entries as plain text
+ '''
+ txt = u"PyCorder V" + __version__ + u" Event Log\n\n"
+ txt += self.moduleinfo
+ for event in reversed(self.logFifo):
+ txt += u"%s\t %s\n"%(event.event_time.strftime("%Y-%m-%d %H:%M:%S.%f"), str(event))
+ return txt
+
+
+ def labelInfoClicked(self, mouse_event):
+ ''' Mouse click into info label
+ Show the event log content
+ '''
+ self.emit(Qt.SIGNAL('showLog()'))
+
+
+ def resetErrorState(self):
+ ''' Reset error lock and info display
+ '''
+ self.lockError = False
+ self.labelInfo.setText("")
+ palette = self.labelInfo.palette()
+ palette.setColor(self.labelInfo.backgroundRole(), self.defaultBkColor)
+ self.labelInfo.setPalette(palette)
+
+
+'''
+------------------------------------------------------------
+UTILITIES
+------------------------------------------------------------
+'''
+
+def flatten(lst):
+ ''' Flatten a list containing lists or tuples
+ '''
+ for elem in lst:
+ if type(elem) in (tuple, list):
+ for i in flatten(elem):
+ yield i
+ else:
+ yield elem
+
+def cmpver(a, b, n=3):
+ ''' Compare two version numbers
+ @param a: version number 1
+ @param b: version number 2
+ @param n: number of categories to compare
+ @return: -1 if ab
+ '''
+ def fixup(i):
+ try:
+ return int(i)
+ except ValueError:
+ return i
+ a = map(fixup, re.findall("\d+|\w+", a))
+ b = map(fixup, re.findall("\d+|\w+", b))
+ return cmp(a[:n], b[:n])
+
+
+
+def setpriority(priority=2):
+ """ Set The Priority of a Windows Process. Priority is a value between 0-5 where
+ 2 is normal priority. Sets the priority of the current Python process
+ and limits the process to 2 logical processors """
+
+ try:
+ import win32process
+ import ctypes
+ from ctypes import wintypes
+
+ priorityclasses = [win32process.IDLE_PRIORITY_CLASS,
+ win32process.BELOW_NORMAL_PRIORITY_CLASS,
+ win32process.NORMAL_PRIORITY_CLASS,
+ win32process.ABOVE_NORMAL_PRIORITY_CLASS,
+ win32process.HIGH_PRIORITY_CLASS,
+ win32process.REALTIME_PRIORITY_CLASS]
+
+ # prepare the kernel functions
+ kernel32 = ctypes.windll.kernel32
+ kernel32.GetCurrentProcess.restype = wintypes.HANDLE
+ kernel32.GetCurrentProcess.argtypes = []
+ kernel32.GetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)]
+ kernel32.SetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.c_int]
+ kernel32.SetPriorityClass.argtypes = [wintypes.HANDLE, ctypes.c_int]
+
+ # get the process handle
+ p = kernel32.GetCurrentProcess()
+
+ # set process priority
+ kernel32.SetPriorityClass(p, priorityclasses[priority])
+
+ # limit the process to the first two available processors
+ pmask = ctypes.c_int()
+ smask = ctypes.c_int()
+ kernel32.GetProcessAffinityMask(p, ctypes.byref(pmask), ctypes.byref(smask))
+ smask = smask.value
+ pmask = 0
+ mask = 1
+ cpu = 0
+ while mask < 0x8000 and cpu < 2:
+ if smask & mask:
+ pmask |= mask
+ cpu += 1
+ mask = mask << 1
+ kernel32.SetProcessAffinityMask(p, pmask)
+ print("INFO: Available CPUs (bit mask) 0x%04X, Python process limited to 0x%04X"%(smask, pmask))
+
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ print("INFO: the process priority can not be raised because PyWin32 is not installed or an error occurred.\n - %s->%s"%(tb, str(e)))
+
+
+'''
+------------------------------------------------------------
+MAIN APPLICATION
+------------------------------------------------------------
+'''
+def main(args):
+ ''' Create and start up main application
+ '''
+ if ("-h" in args) or ("--help" in args):
+ create_option_parser().print_help()
+ return 0
+
+ smoketest_requested = ("--smoketest" in args)
+ smoketest_ms = 1500
+ if smoketest_requested:
+ try:
+ smoke_opts, _ = create_option_parser().parse_args(args[1:])
+ smoketest_ms = max(100, int(getattr(smoke_opts, "SmokeTestMs", 1500)))
+ except SystemExit:
+ return 0
+ except Exception:
+ smoketest_ms = 1500
+
+ print("Starting PyCorder, please wait ...\n")
+ setpriority(priority=4)
+ app = Qt.QApplication(args)
+ rc = 0
+ try:
+ from res import resources_rc as _resources_rc
+ except ImportError:
+ _resources_rc = None
+ if _resources_rc is not None:
+ _resources_rc.qInitResources()
+ if smoketest_requested:
+ QtCore.QTimer.singleShot(smoketest_ms, app.quit)
+ rc = app.exec_()
+ print("PyCorder smoketest completed\n")
+ return rc
+ try:
+ win = None
+ win = MainWindow()
+ win.show()
+ try:
+ win.showMaximized()
+ except Exception:
+ pass
+ try:
+ platform_name = ""
+ if hasattr(Qt.QApplication, "platformName"):
+ platform_name = str(Qt.QApplication.platformName()).lower()
+ if platform_name not in ("offscreen", "minimal"):
+ win.raise_()
+ win.activateWindow()
+ QtCore.QTimer.singleShot(0, win.raise_)
+ QtCore.QTimer.singleShot(0, win.activateWindow)
+ except Exception:
+ pass
+ if ShowConfirmationDialog:
+ accept = Qt.QMessageBox.warning(None, "PyCorder Disclaimer", ConfirmationText,
+ "Accept", "Cancel", "", 1)
+ if accept == 0:
+ win.usageConfirmed = True
+ rc = app.exec_()
+ else:
+ win.close()
+ else:
+ win.usageConfirmed = True
+ rc = app.exec_()
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ try:
+ print("PyCorder startup error: %s -> %s"%(tb, str(e)), file=sys.stderr)
+ except Exception:
+ pass
+ Qt.QMessageBox.critical(None, "PyCorder", tb + " -> " + str(e))
+ if win != None:
+ win.close()
+ return 1
+
+ # show the battery disconnection reminder
+ if ShowBatteryReminder and win and win.usageConfirmed:
+ DlgBatteryInfo().exec_()
+
+ print("PyCorder terminated\n")
+ return rc
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/modbase.py b/modbase.py
index 4e75159..cb0d5c8 100644
--- a/modbase.py
+++ b/modbase.py
@@ -1,705 +1,714 @@
-# -*- coding: utf-8 -*-
-'''
-Base class for all recording modules
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-10 12:20:40 +0200 (Mo, 10 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 201 $
-'''
-
-from PyQt4 import Qt
-import numpy as np
-import time
-import datetime
-import Queue
-import threading
-import copy
-import os, sys, traceback
-from lxml import etree
-from lxml import objectify
-
-
-# impedance value invalid (electrode disconnected)
-#CHAMP_IMP_INVALID = 2147483647 # INT_MAX
-CHAMP_IMP_INVALID = 999900 # INT_MAX
-
-def GetExceptionTraceBack():
- ''' Get last trace back info as tuple
- @return: tuple(string representation, filename, line number, module)
- '''
- exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
- tb = traceback.extract_tb(exceptionTraceback)[-1]
- fn = os.path.split(tb[0])[1]
- txt = "%s, %d, %s"%(fn, tb[1], tb[2])
- return tuple([txt, fn, tb[1], tb[2]])
-
-
-class ModuleError(Exception):
- ''' Generic module exception
- '''
- def __init__(self, module, value):
- ''' Create the exception object
- @param module: module object name
- @param value: exception description
- '''
- self.value = str(module) + ': ' + str(value)
- def __str__(self):
- return self.value
-
-
-class EventType:
- ''' Module Event Types
- @ivar LOGMESSAGE: display event description in status bar info field and log it
- @ivar STATUS: display event description in dedicated status_field
- @ivar MESSAGE: display event description in status bar info field
- @ivar ERROR: an error occured, see info and severity
- @ivar COMMAND: send an command to the module chain
- @ivar LOG: only log the message, without showing it in the status bar
- '''
- (LOGMESSAGE, STATUS, MESSAGE, ERROR, COMMAND, LOG) = range(6)
-
-
-class ErrorSeverity:
- ''' Module event classification in case of ERROR
- @ivar IGNORE: error can be safely ignored
- @ivar NOTIFY: notify user
- @ivar STOP: notify and stop acquisition
- '''
- (IGNORE, NOTIFY, STOP) = range(3)
-
-class ModuleEvent(object):
- ''' Generic module event
- '''
- def __init__(self, module, type, info="", severity=ErrorSeverity.IGNORE, status_field="", cmd_value=0):
- ''' Initialize the event
- @param module: module name (string)
- @param type: event type (class EventType)
- @param info: event description (could be a string or numerical value)
- @param severity: event classification in case of ERROR (class ErrorSeverity)
- @param status_field: status bar field name
- @param cmd_value: any numerical value in case of COMMAND
- '''
- self.module = module
- self.type = type
- self.info = info
- self.severity = severity
- self.status_field = status_field
- self.cmd_value = cmd_value
- self.event_time = datetime.datetime.now()
-
- def __str__(self):
- ''' Event string representation
- '''
- txt = str(self.module) + ': ' + str(self.info)
- return txt
-
-
-class RecordingMode:
- ''' Module Recording Modes
- @ivar NORMAL: Record EEG
- @ivar TEST: Record test signals
- @ivar IMPEDANCE: Impedance measurement
- '''
- (NORMAL, TEST, IMPEDANCE) = range(3)
-
-class ImpedanceIndex:
- ''' Index for impedance values within the data array for each channel
- '''
- (DATA, REF, GND) = range(3)
- Name = ["+", "-", "GND"]
-
-class ChannelGroup:
- ''' EEG channel groups used in EEG_ChannelProperties
- @ivar EEG: channel belongs to EEG channel group
- @ivar AUX: channel belongs to AUX channel group
- @ivar EPP: channel belongs to EPP (EP-PreAmp) group
- '''
- (EEG, AUX, EPP, BIP) = range(4)
- Name = ["EEG", "AUX", "EPP", "BIP"]
-
-
-class EEG_ChannelProperties(object):
- ''' Properties of EEG channels
- '''
- def __init__(self, name):
- ''' Set default property values
- @param name: channel label
- '''
- # XML parameter version
- # 1: initial version
- # 2: added notchfilter and reference
- # 3: added inputgroup
- self.xmlVersion = 3 #: XML configuration data version
-
- self.input = 0 #: hardware input channel number
- self.inputgroup = ChannelGroup.EEG #: hardware input channel group
- self.enable = True #: enable channel for recording
- self.name = name #: channel label
- self.refname = "" #: reference channel name
- self.group = ChannelGroup.EEG #: logical channel group
- self.lowpass = 100.0 #: low pass cutoff frequency in Hz
- self.highpass = 0.0 #: high pass cutoff frequency in Hz
- self.notchfilter = False #: enable / disable notch filter
- self.isReference = False #: use this channel as reference channel
- self.color = Qt.Qt.darkBlue #: display color
- self.unit = "" #: channel unit string (use uV if empty)
-
- def __cmp__(self, other):
- ''' Compare two channels by name and group
- @param other: channel to compare with
- '''
- if other == None:
- return -1
- if (self.name == other.name) & (self.group == other.group):
- return 0
- else:
- return -1
-
- def getXML(self):
- ''' Get channel properties for XML configuration file
- @return: objectify XML element::
-
- 0
- ...
-
- '''
- E = objectify.E
- ch = E.channel(E.input(self.input),
- E.inputgroup(self.inputgroup),
- E.enable(self.enable),
- E.name(self.name),
- E.group(self.group),
- E.lowpass(self.lowpass),
- E.highpass(self.highpass),
- E.notchfilter(self.notchfilter),
- E.reference(self.isReference))
- ch.attrib["version"] = str(self.xmlVersion)
- return ch
-
-
- def setXML(self, xml):
- ''' Setup channel properties from XML configuration file
- @param xml: objectify XML channel configuration
- '''
- # check version, has to be lower or equal than current version
- version = xml.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- raise Exception, "channel %d wrong version > %d"%(self.input, self.xmlVersion)
- version = int(version)
-
- # get the values
- self.input = xml.input.pyval
- self.enable = xml.enable.pyval
- self.name = xml.name.pyval
- self.group = xml.group.pyval
- self.lowpass = xml.lowpass.pyval
- self.highpass = xml.highpass.pyval
- if version > 1:
- self.notchfilter = xml.notchfilter.pyval
- self.isReference = xml.reference.pyval
-
- # get the hardware input channel group
- if version > 2:
- self.inputgroup = xml.inputgroup.pyval
- else:
- self.inputgroup = self.group
-
-
-class EEG_Marker(object):
- ''' Recording marker position and description
- '''
- def __init__(self, position=0, points=1, type="unknown", description="", channel=0, date=False):
- ''' Create a new marker object
- '''
- self.position = position #: Position of marker in data points
- self.points = points #: Number of points
- self.type = type #: Marker type ("Stimulus", etc.)
- self.description = description #: Marker description
- self.invisible = False #: If true, marker should not be shown.
- self.channel = channel #: Channel number of marker (0 = all channels).
- self.date = date #: If true, write date / time to file
-
-
-class EEG_DataBlock(object):
- ''' Block of EEG data, channel properties, marker and impedance values
- '''
- def __init__(self, eeg=32, aux=8):
- ''' Set default values for requested number of channels
- @param eeg: number of EEG channels for this block
- @param aux: number of AUX channels for this block
- '''
- self.sample_counter = 0 #: total number of received samples
- self.sample_rate = 500.0 #: sample rate in Hz
- self.eeg_channels = np.zeros((eeg+aux, 1000), 'd') #: channel data for EEG and AUX
- self.trigger_channel = np.zeros((1, 1000), np.uint32) #: trigger values
- self.sample_channel = np.zeros((1, 1000), np.uint64) #: sample counter
- self.channel_properties = self.get_default_properties(eeg, aux) #: channel properties
- self.markers = [] #: marker descriptions and positions
- self.impedances = [] #: impedance values [Ohm] -> obsolete since 1.0.6, should be left empty
- self.block_time = datetime.datetime.now() #: block creation time
- self.performance_timer = 0 #: processing time since block creation
- self.performance_timer_max = 0 #: maximum module processing time for this block
- self.recording_mode = RecordingMode.NORMAL #: recording mode of this block
- self.ref_channel_name = "" #: combined name of reference channels
-
- def __copy__(self):
- ''' We always need a deep copy of channel properties, markers and impedance values
- '''
- copy_obj = EEG_DataBlock(1,1)
- copy_obj.sample_counter = self.sample_counter
- copy_obj.sample_rate = self.sample_rate
- copy_obj.eeg_channels = self.eeg_channels
- copy_obj.trigger_channel = self.trigger_channel
- copy_obj.sample_channel = self.sample_channel
- copy_obj.channel_properties = copy.deepcopy(self.channel_properties)
- copy_obj.markers = copy.deepcopy(self.markers)
- copy_obj.impedances = copy.deepcopy(self.impedances)
- copy_obj.block_time = copy.deepcopy(self.block_time)
- copy_obj.performance_timer = self.performance_timer
- copy_obj.performance_timer_max = self.performance_timer_max
- copy_obj.recording_mode = self.recording_mode
- copy_obj.ref_channel_name = self.ref_channel_name
- return copy_obj
-
- def __cmp__(self, other):
- ''' Compare settings of two data blocks
- '''
- if other == None:
- return -1
- if self.sample_rate != other.sample_rate:
- return -1
- if self.channel_properties.shape != other.channel_properties.shape:
- return -1
- if (self.channel_properties == other.channel_properties).all() == False:
- return -1
- if self.recording_mode != other.recording_mode:
- return -1
- return 0
-
- def get_default_properties(self, eeg, aux):
- ''' Get an property array with default settings
- @param eeg: number of EEG channels
- @param aux: number of AUX channels
- '''
- channel_properties = []
- for c in range(0, eeg):
- # EEG channels
- ch = EEG_ChannelProperties("Ch%d"%(c+1))
- ch.inputgroup = ChannelGroup.EEG
- ch.group = ChannelGroup.EEG
- ch.input = c + 1
- channel_properties.append(ch)
- for c in range(0, aux):
- # AUX channels
- ch = EEG_ChannelProperties("Aux%d"%(c+1))
- ch.inputgroup = ChannelGroup.AUX
- ch.group = ChannelGroup.AUX
- ch.input = c + 1
- channel_properties.append(ch)
- return np.array(channel_properties)
- get_default_properties = classmethod(get_default_properties)
-
-
-
-
-
-
-class ModuleBase(Qt.QObject):
- ''' Base class for all recording modules
- '''
-
- def __init__(self, usethread=True, queuesize=20, name="ModuleBase", instance=0):
- ''' Create a new recording module object
- @param usethread: true if data transfer should be handled internally by worker thread
- @param queuesize: size of receiver input queue in elements
- @param name: module object identifier
- @param instance: instance number for this object
- '''
- Qt.QObject.__init__(self)
- # set identifier and instance
- self._object_name = name
- self._instance = instance
- # reset the receiver collection
- self._receivers = []
-
- # receiver input queue and data block
- self._input_queue = Queue.Queue(queuesize)
- self._input_data = EEG_DataBlock()
-
- # reset the I/O worker thread
- self._work = None
- self._running = False
- self._usethread = usethread
- self._thLock = threading.Lock()
-
- def terminate(self):
- ''' Destructor, override this method if you need to clean up
- '''
- return
-
- def setDefault(self):
- ''' Set all module parameters to default values
- Override this method to provide your own default settings
- '''
- return
-
- def start(self):
- ''' Start the data transfer. Don't override this method.
- '''
- # flush input queue
- while not self._input_queue.empty():
- self._input_queue.get_nowait()
- # let derived class objects handle the start command
- try:
- self.process_start()
- except Exception as e:
- self.send_exception(e, ErrorSeverity.STOP)
- return
- # propagate start command to all attached receivers
- for receiver in self._receivers:
- receiver.start()
- # start the data transfer worker thread
- if self._usethread:
- if not self._running:
- # create a new thread because threads are not reusable
- self._running = True
- self._work = threading.Thread(target=self._worker_thread)
- self._work.start()
-
- def stop(self):
- ''' Stop the data transfer. Don't override this method.
- '''
- # terminate the data transfer worker thread
- if self._usethread:
- self._running = False
- if self._work != None:
- self._work.join(5.0) # wait 5s for terminating
- self._work = None
- # propagate stop command to all attached receivers
- for receiver in self._receivers:
- receiver.stop()
- # let derived class objects handle the stop command
- try:
- self.process_stop()
- except Exception as e:
- self.send_exception(e, ErrorSeverity.NOTIFY)
-
-
- def query(self, command):
- ''' Ask attached modules if the requested command is acceptable
- @param command: requested command
- @return: True if acceptable, False if not
- '''
- # let the module handle query first
- if not self.process_query(command):
- return False
- # propagate query to all attached receivers
- for receiver in self._receivers:
- if not receiver.query(command):
- return False
- return True
-
-
- def get_online_configuration(self):
- ''' Override this method to provide a online configuration pane
- @return: a QFrame object or None if you don't need a online configuration pane
- '''
- return None
-
-
- def get_configuration_pane(self):
- ''' Override this method to provide a configuration pane
- @return: a QFrame object or None if you don't need a configuration pane
- '''
- return None
-
- def get_display_pane(self):
- ''' Override this method to provide a signal display pane
- @return: a QFrame object or None if you don't need a display pane
- '''
- return None
-
- def get_module_info(self):
- ''' Get information about this module for the about dialog
- @return: information string or None if info is not available
- '''
- return None
-
- def update_receivers(self, params=None, propagate_only=False):
- ''' Propagate parameter update down to all attached receivers.
- Don't override this method.
- @param params: EEG_Datablock object
- @param propagate_only: don't update ourself
- '''
- if not propagate_only:
- # let derived class objects process parameter update
- try:
- params = self.process_update(copy.copy(params))
- except Exception as e:
- self.send_exception(e, ErrorSeverity.STOP)
- return
- # propagate down to all attached receivers
- for receiver in self._receivers:
- receiver.update_receivers(params)
-
-
- def add_receiver(self, receiver):
- ''' Add an receiver object to the receiver collection.
- Don't override this method.
- @param receiver: ModuleBase object to add
- '''
- if not self._usethread:
- return
- # propagate start command to added receiver
- if self._running:
- receiver.start()
- # attach receiver
- self._receivers.append(receiver)
- # get events from receiver
- self.connect(receiver, Qt.SIGNAL("event(PyQt_PyObject)"), self.receiver_event, Qt.Qt.QueuedConnection)
- # tell the receiver to get events from parent
- receiver.connect(self, Qt.SIGNAL("parentevent(PyQt_PyObject)"), receiver.parent_event, Qt.Qt.QueuedConnection)
-
-
- def remove_receiver(self, receiver):
- ''' Remove an receiver object from the receiver collection.
- Don't override this method.
- @param receiver: ModuleBase object to remove
- '''
- if not self._usethread:
- return
- # detach receiver
- self._receivers.remove(receiver)
- # propagate stop command to removed receiver
- receiver.stop()
-
-
- def parent_event(self, event):
- ''' Get events from attached parent.
- Don't override this method.
- @param event: ModuleEvent object
- '''
- # let derived class objects handle the event
- self.process_event(event)
- # propagate event to receivers
- self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
-
- def receiver_event(self, event):
- ''' Get events from attached receivers.
- Don't override this method.
- @param event: ModuleEvent object
- '''
- # let derived class objects handle the event
- self.process_event(event)
- # propagate event to parent
- #self.send_event(event)
- self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
-
-
- def send_event(self, event):
- ''' Send ModuleEvent objects to all connected slots.
- Don't override this method.
- @param event: ModuleEvent object
- '''
- self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
- self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
-
-
- def isRunning(self):
- ''' Get the worker thread state
- Don't override this method.
- @return: true if worker thread is running
- '''
- return self._running
-
-
- def process_event(self, event):
- ''' Override this method to handle events from attached receivers
- @param event: ModuleEvent
- '''
- return
-
-
- def process_input(self, datablock):
- ''' Override this method to get and process data from input queue. This method must be
- overridden! At least the input data must be provided as output::
- self.dataavailable = True
- self.data = datablock
- @param datablock: EEG_DataBlock object
- '''
- raise ModuleError(self._object_name, "not implemented! This method must be overridden")
-
-
- def process_output(self):
- ''' Override this method to put processed data into output queue. This method must be
- overridden! At least pass through the input data::
- if self.dataavailable:
- return self.data
- else:
- return None
- @return: processed data block or None if no data available
- '''
- raise ModuleError(self._object_name, "not implemented! This method must be overridden")
-
-
- def process_update(self, params):
- ''' Override this method to evaluate and maybe modify the data block configuration.
- @param params: EEG_DataBlock object
- @return: EEG_DataBlock object
- '''
- return params
-
-
- def process_start(self):
- ''' Override this method to prepare the module for startup
- '''
- return
-
-
- def process_stop(self):
- ''' Override this method to finalize the acquisition process
- '''
- return
-
- def process_query(self, command):
- ''' Override this method to accept or recject requested commands
- '''
- return True
-
- def process_idle(self):
- ''' Override this method to do something else during worker thread idle time or to
- change the thread suspend time.
- '''
- time.sleep(0.001) # suspend thread (default = 1ms)
- return
-
-
- def receive_data(self):
- try:
- data = self._input_queue.get(False)
- return data
- except:
- return None
-
- def receive_data_available(self):
- return self._input_queue.qsize()
-
-
- def _transmit_data(self, data):
- ''' Put data into the input queue. This method is invoked from the parent module.
- Don't override this method.
- @param data: EEG_DataBlock object
- '''
- try:
- self._input_queue.put(data, False)
- except:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "Input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
-
- def _worker_thread(self):
- ''' The worker thread takes data from the input queue and
- puts the processed data into the output queue.
- Don't override this method.
- '''
- while self._running:
- wt = 0 # reset performance timer
- # process input queue
- self._thLock.acquire()
- try:
- data = self._input_queue.get(False)
- t = time.clock()
- self.process_input(data)
- wt += time.clock() - t
- self._thLock.release()
- except Queue.Empty:
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- self.send_exception(e, severity=ErrorSeverity.STOP)
-
-
- # put data to all registered output queues
- self._thLock.acquire()
- try:
- self.output_timer = time.clock()
- data = self.process_output()
- wt += time.clock() - self.output_timer
- self._thLock.release()
- except Exception as e:
- self._thLock.release()
- self.send_exception(e, severity=ErrorSeverity.STOP)
- data = None
-
- if data != None:
- data.performance_timer_max = max(data.performance_timer_max, wt)
- data.performance_timer += wt
- #for idx, receiver in enumerate(self._receivers):
- for idx, receiver in enumerate(reversed(self._receivers)):
- if idx == 0:
- receiver._transmit_data(data)
- else:
- receiver._transmit_data(copy.deepcopy(data))
-
-
- # give a chance for idle processing
- self.process_idle()
-
-
- def send_exception(self, exception, severity=ErrorSeverity.STOP):
- ''' Send Exception as ModuleEvent object to all connected slots.
- Don't override this method.
- @param exception: Exception() object
- @param severity: error severity
- '''
- tb = GetExceptionTraceBack()[0]
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, tb + " -> " + str(exception), severity=severity))
-
-
- def getXML(self):
- ''' Get module properties for XML configuration file. Override this method if you
- want to put module properties into the configuration file.
- @return: objectify XML element::
-
-
- ...
-
-
- '''
- return None
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file. Override this method if you
- want to get module properties from configuration file.
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- return
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Base class for all recording modules
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-10 12:20:40 +0200 (Mo, 10 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 201 $
+'''
+
+from PyQt4 import Qt
+import numpy as np
+import time
+import datetime
+try:
+ import Queue as queue
+except ImportError:
+ import queue
+import threading
+import copy
+import os, sys, traceback
+from lxml import etree
+from lxml import objectify
+
+
+# impedance value invalid (electrode disconnected)
+#CHAMP_IMP_INVALID = 2147483647 # INT_MAX
+CHAMP_IMP_INVALID = 999900 # INT_MAX
+
+def GetExceptionTraceBack():
+ ''' Get last trace back info as tuple
+ @return: tuple(string representation, filename, line number, module)
+ '''
+ exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
+ tb = traceback.extract_tb(exceptionTraceback)[-1]
+ fn = os.path.split(tb[0])[1]
+ txt = "%s, %d, %s"%(fn, tb[1], tb[2])
+ return tuple([txt, fn, tb[1], tb[2]])
+
+
+class ModuleError(Exception):
+ ''' Generic module exception
+ '''
+ def __init__(self, module, value):
+ ''' Create the exception object
+ @param module: module object name
+ @param value: exception description
+ '''
+ self.value = str(module) + ': ' + str(value)
+ def __str__(self):
+ return self.value
+
+
+class EventType:
+ ''' Module Event Types
+ @ivar LOGMESSAGE: display event description in status bar info field and log it
+ @ivar STATUS: display event description in dedicated status_field
+ @ivar MESSAGE: display event description in status bar info field
+ @ivar ERROR: an error occured, see info and severity
+ @ivar COMMAND: send an command to the module chain
+ @ivar LOG: only log the message, without showing it in the status bar
+ '''
+ (LOGMESSAGE, STATUS, MESSAGE, ERROR, COMMAND, LOG) = range(6)
+
+
+class ErrorSeverity:
+ ''' Module event classification in case of ERROR
+ @ivar IGNORE: error can be safely ignored
+ @ivar NOTIFY: notify user
+ @ivar STOP: notify and stop acquisition
+ '''
+ (IGNORE, NOTIFY, STOP) = range(3)
+
+class ModuleEvent(object):
+ ''' Generic module event
+ '''
+ def __init__(self, module, type, info="", severity=ErrorSeverity.IGNORE, status_field="", cmd_value=0):
+ ''' Initialize the event
+ @param module: module name (string)
+ @param type: event type (class EventType)
+ @param info: event description (could be a string or numerical value)
+ @param severity: event classification in case of ERROR (class ErrorSeverity)
+ @param status_field: status bar field name
+ @param cmd_value: any numerical value in case of COMMAND
+ '''
+ self.module = module
+ self.type = type
+ self.info = info
+ self.severity = severity
+ self.status_field = status_field
+ self.cmd_value = cmd_value
+ self.event_time = datetime.datetime.now()
+
+ def __str__(self):
+ ''' Event string representation
+ '''
+ txt = str(self.module) + ': ' + str(self.info)
+ return txt
+
+
+class RecordingMode:
+ ''' Module Recording Modes
+ @ivar NORMAL: Record EEG
+ @ivar TEST: Record test signals
+ @ivar IMPEDANCE: Impedance measurement
+ '''
+ (NORMAL, TEST, IMPEDANCE) = range(3)
+
+class ImpedanceIndex:
+ ''' Index for impedance values within the data array for each channel
+ '''
+ (DATA, REF, GND) = range(3)
+ Name = ["+", "-", "GND"]
+
+class ChannelGroup:
+ ''' EEG channel groups used in EEG_ChannelProperties
+ @ivar EEG: channel belongs to EEG channel group
+ @ivar AUX: channel belongs to AUX channel group
+ @ivar EPP: channel belongs to EPP (EP-PreAmp) group
+ '''
+ (EEG, AUX, EPP, BIP) = range(4)
+ Name = ["EEG", "AUX", "EPP", "BIP"]
+
+
+class EEG_ChannelProperties(object):
+ ''' Properties of EEG channels
+ '''
+ def __init__(self, name):
+ ''' Set default property values
+ @param name: channel label
+ '''
+ # XML parameter version
+ # 1: initial version
+ # 2: added notchfilter and reference
+ # 3: added inputgroup
+ self.xmlVersion = 3 #: XML configuration data version
+
+ self.input = 0 #: hardware input channel number
+ self.inputgroup = ChannelGroup.EEG #: hardware input channel group
+ self.enable = True #: enable channel for recording
+ self.name = name #: channel label
+ self.refname = "" #: reference channel name
+ self.group = ChannelGroup.EEG #: logical channel group
+ self.lowpass = 100.0 #: low pass cutoff frequency in Hz
+ self.highpass = 0.0 #: high pass cutoff frequency in Hz
+ self.notchfilter = False #: enable / disable notch filter
+ self.isReference = False #: use this channel as reference channel
+ self.color = Qt.Qt.darkBlue #: display color
+ self.unit = "" #: channel unit string (use uV if empty)
+
+ def __cmp__(self, other):
+ ''' Compare two channels by name and group
+ @param other: channel to compare with
+ '''
+ if other == None:
+ return -1
+ if (self.name == other.name) & (self.group == other.group):
+ return 0
+ else:
+ return -1
+
+ def getXML(self):
+ ''' Get channel properties for XML configuration file
+ @return: objectify XML element::
+
+ 0
+ ...
+
+ '''
+ E = objectify.E
+ ch = E.channel(E.input(self.input),
+ E.inputgroup(self.inputgroup),
+ E.enable(self.enable),
+ E.name(self.name),
+ E.group(self.group),
+ E.lowpass(self.lowpass),
+ E.highpass(self.highpass),
+ E.notchfilter(self.notchfilter),
+ E.reference(self.isReference))
+ ch.attrib["version"] = str(self.xmlVersion)
+ return ch
+
+
+ def setXML(self, xml):
+ ''' Setup channel properties from XML configuration file
+ @param xml: objectify XML channel configuration
+ '''
+ # check version, has to be lower or equal than current version
+ version = xml.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ raise Exception("channel %d wrong version > %d"%(self.input, self.xmlVersion))
+ version = int(version)
+
+ # get the values
+ self.input = xml.input.pyval
+ self.enable = xml.enable.pyval
+ self.name = xml.name.pyval
+ self.group = xml.group.pyval
+ self.lowpass = xml.lowpass.pyval
+ self.highpass = xml.highpass.pyval
+ if version > 1:
+ self.notchfilter = xml.notchfilter.pyval
+ self.isReference = xml.reference.pyval
+
+ # get the hardware input channel group
+ if version > 2:
+ self.inputgroup = xml.inputgroup.pyval
+ else:
+ self.inputgroup = self.group
+
+
+class EEG_Marker(object):
+ ''' Recording marker position and description
+ '''
+ def __init__(self, position=0, points=1, type="unknown", description="", channel=0, date=False):
+ ''' Create a new marker object
+ '''
+ self.position = position #: Position of marker in data points
+ self.points = points #: Number of points
+ self.type = type #: Marker type ("Stimulus", etc.)
+ self.description = description #: Marker description
+ self.invisible = False #: If true, marker should not be shown.
+ self.channel = channel #: Channel number of marker (0 = all channels).
+ self.date = date #: If true, write date / time to file
+
+
+class EEG_DataBlock(object):
+ ''' Block of EEG data, channel properties, marker and impedance values
+ '''
+ def __init__(self, eeg=32, aux=8):
+ ''' Set default values for requested number of channels
+ @param eeg: number of EEG channels for this block
+ @param aux: number of AUX channels for this block
+ '''
+ self.sample_counter = 0 #: total number of received samples
+ self.sample_rate = 500.0 #: sample rate in Hz
+ self.eeg_channels = np.zeros((eeg+aux, 1000), 'd') #: channel data for EEG and AUX
+ self.trigger_channel = np.zeros((1, 1000), np.uint32) #: trigger values
+ self.sample_channel = np.zeros((1, 1000), np.uint64) #: sample counter
+ self.channel_properties = self.get_default_properties(eeg, aux) #: channel properties
+ self.markers = [] #: marker descriptions and positions
+ self.impedances = [] #: impedance values [Ohm] -> obsolete since 1.0.6, should be left empty
+ self.block_time = datetime.datetime.now() #: block creation time
+ self.performance_timer = 0 #: processing time since block creation
+ self.performance_timer_max = 0 #: maximum module processing time for this block
+ self.recording_mode = RecordingMode.NORMAL #: recording mode of this block
+ self.ref_channel_name = "" #: combined name of reference channels
+
+ def __copy__(self):
+ ''' We always need a deep copy of channel properties, markers and impedance values
+ '''
+ copy_obj = EEG_DataBlock(1,1)
+ copy_obj.sample_counter = self.sample_counter
+ copy_obj.sample_rate = self.sample_rate
+ copy_obj.eeg_channels = self.eeg_channels
+ copy_obj.trigger_channel = self.trigger_channel
+ copy_obj.sample_channel = self.sample_channel
+ copy_obj.channel_properties = copy.deepcopy(self.channel_properties)
+ copy_obj.markers = copy.deepcopy(self.markers)
+ copy_obj.impedances = copy.deepcopy(self.impedances)
+ copy_obj.block_time = copy.deepcopy(self.block_time)
+ copy_obj.performance_timer = self.performance_timer
+ copy_obj.performance_timer_max = self.performance_timer_max
+ copy_obj.recording_mode = self.recording_mode
+ copy_obj.ref_channel_name = self.ref_channel_name
+ return copy_obj
+
+ def __cmp__(self, other):
+ ''' Compare settings of two data blocks
+ '''
+ if other == None:
+ return -1
+ if self.sample_rate != other.sample_rate:
+ return -1
+ if self.channel_properties.shape != other.channel_properties.shape:
+ return -1
+ if (self.channel_properties == other.channel_properties).all() == False:
+ return -1
+ if self.recording_mode != other.recording_mode:
+ return -1
+ return 0
+
+ def get_default_properties(self, eeg, aux):
+ ''' Get an property array with default settings
+ @param eeg: number of EEG channels
+ @param aux: number of AUX channels
+ '''
+ channel_properties = []
+ for c in range(0, eeg):
+ # EEG channels
+ ch = EEG_ChannelProperties("Ch%d"%(c+1))
+ ch.inputgroup = ChannelGroup.EEG
+ ch.group = ChannelGroup.EEG
+ ch.input = c + 1
+ channel_properties.append(ch)
+ for c in range(0, aux):
+ # AUX channels
+ ch = EEG_ChannelProperties("Aux%d"%(c+1))
+ ch.inputgroup = ChannelGroup.AUX
+ ch.group = ChannelGroup.AUX
+ ch.input = c + 1
+ channel_properties.append(ch)
+ return np.array(channel_properties)
+ get_default_properties = classmethod(get_default_properties)
+
+
+
+
+
+
+class ModuleBase(Qt.QObject):
+ ''' Base class for all recording modules
+ '''
+
+ def __init__(self, usethread=True, queuesize=20, name="ModuleBase", instance=0):
+ ''' Create a new recording module object
+ @param usethread: true if data transfer should be handled internally by worker thread
+ @param queuesize: size of receiver input queue in elements
+ @param name: module object identifier
+ @param instance: instance number for this object
+ '''
+ # PySide6 raises a RuntimeError when QObject is initialized twice.
+ # This can happen for UI modules that inherit both QWidget/QwtPlot and ModuleBase.
+ try:
+ Qt.QObject.__init__(self)
+ except RuntimeError as exc:
+ if "QObject object in class" not in str(exc) or "twice" not in str(exc):
+ raise
+ # set identifier and instance
+ self._object_name = name
+ self._instance = instance
+ # reset the receiver collection
+ self._receivers = []
+
+ # receiver input queue and data block
+ self._input_queue = queue.Queue(queuesize)
+ self._input_data = EEG_DataBlock()
+
+ # reset the I/O worker thread
+ self._work = None
+ self._running = False
+ self._usethread = usethread
+ self._thLock = threading.Lock()
+
+ def terminate(self):
+ ''' Destructor, override this method if you need to clean up
+ '''
+ return
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ Override this method to provide your own default settings
+ '''
+ return
+
+ def start(self):
+ ''' Start the data transfer. Don't override this method.
+ '''
+ # flush input queue
+ while not self._input_queue.empty():
+ self._input_queue.get_nowait()
+ # let derived class objects handle the start command
+ try:
+ self.process_start()
+ except Exception as e:
+ self.send_exception(e, ErrorSeverity.STOP)
+ return
+ # propagate start command to all attached receivers
+ for receiver in self._receivers:
+ receiver.start()
+ # start the data transfer worker thread
+ if self._usethread:
+ if not self._running:
+ # create a new thread because threads are not reusable
+ self._running = True
+ self._work = threading.Thread(target=self._worker_thread)
+ self._work.start()
+
+ def stop(self):
+ ''' Stop the data transfer. Don't override this method.
+ '''
+ # terminate the data transfer worker thread
+ if self._usethread:
+ self._running = False
+ if self._work != None:
+ self._work.join(5.0) # wait 5s for terminating
+ self._work = None
+ # propagate stop command to all attached receivers
+ for receiver in self._receivers:
+ receiver.stop()
+ # let derived class objects handle the stop command
+ try:
+ self.process_stop()
+ except Exception as e:
+ self.send_exception(e, ErrorSeverity.NOTIFY)
+
+
+ def query(self, command):
+ ''' Ask attached modules if the requested command is acceptable
+ @param command: requested command
+ @return: True if acceptable, False if not
+ '''
+ # let the module handle query first
+ if not self.process_query(command):
+ return False
+ # propagate query to all attached receivers
+ for receiver in self._receivers:
+ if not receiver.query(command):
+ return False
+ return True
+
+
+ def get_online_configuration(self):
+ ''' Override this method to provide a online configuration pane
+ @return: a QFrame object or None if you don't need a online configuration pane
+ '''
+ return None
+
+
+ def get_configuration_pane(self):
+ ''' Override this method to provide a configuration pane
+ @return: a QFrame object or None if you don't need a configuration pane
+ '''
+ return None
+
+ def get_display_pane(self):
+ ''' Override this method to provide a signal display pane
+ @return: a QFrame object or None if you don't need a display pane
+ '''
+ return None
+
+ def get_module_info(self):
+ ''' Get information about this module for the about dialog
+ @return: information string or None if info is not available
+ '''
+ return None
+
+ def update_receivers(self, params=None, propagate_only=False):
+ ''' Propagate parameter update down to all attached receivers.
+ Don't override this method.
+ @param params: EEG_Datablock object
+ @param propagate_only: don't update ourself
+ '''
+ if not propagate_only:
+ # let derived class objects process parameter update
+ try:
+ params = self.process_update(copy.copy(params))
+ except Exception as e:
+ self.send_exception(e, ErrorSeverity.STOP)
+ return
+ # propagate down to all attached receivers
+ for receiver in self._receivers:
+ receiver.update_receivers(params)
+
+
+ def add_receiver(self, receiver):
+ ''' Add an receiver object to the receiver collection.
+ Don't override this method.
+ @param receiver: ModuleBase object to add
+ '''
+ if not self._usethread:
+ return
+ # propagate start command to added receiver
+ if self._running:
+ receiver.start()
+ # attach receiver
+ self._receivers.append(receiver)
+ # get events from receiver
+ self.connect(receiver, Qt.SIGNAL("event(PyQt_PyObject)"), self.receiver_event, Qt.Qt.QueuedConnection)
+ # tell the receiver to get events from parent
+ receiver.connect(self, Qt.SIGNAL("parentevent(PyQt_PyObject)"), receiver.parent_event, Qt.Qt.QueuedConnection)
+
+
+ def remove_receiver(self, receiver):
+ ''' Remove an receiver object from the receiver collection.
+ Don't override this method.
+ @param receiver: ModuleBase object to remove
+ '''
+ if not self._usethread:
+ return
+ # detach receiver
+ self._receivers.remove(receiver)
+ # propagate stop command to removed receiver
+ receiver.stop()
+
+
+ def parent_event(self, event):
+ ''' Get events from attached parent.
+ Don't override this method.
+ @param event: ModuleEvent object
+ '''
+ # let derived class objects handle the event
+ self.process_event(event)
+ # propagate event to receivers
+ self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
+
+ def receiver_event(self, event):
+ ''' Get events from attached receivers.
+ Don't override this method.
+ @param event: ModuleEvent object
+ '''
+ # let derived class objects handle the event
+ self.process_event(event)
+ # propagate event to parent
+ #self.send_event(event)
+ self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
+
+
+ def send_event(self, event):
+ ''' Send ModuleEvent objects to all connected slots.
+ Don't override this method.
+ @param event: ModuleEvent object
+ '''
+ self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
+ self.emit(Qt.SIGNAL('parentevent(PyQt_PyObject)'), event)
+
+
+ def isRunning(self):
+ ''' Get the worker thread state
+ Don't override this method.
+ @return: true if worker thread is running
+ '''
+ return self._running
+
+
+ def process_event(self, event):
+ ''' Override this method to handle events from attached receivers
+ @param event: ModuleEvent
+ '''
+ return
+
+
+ def process_input(self, datablock):
+ ''' Override this method to get and process data from input queue. This method must be
+ overridden! At least the input data must be provided as output::
+ self.dataavailable = True
+ self.data = datablock
+ @param datablock: EEG_DataBlock object
+ '''
+ raise ModuleError(self._object_name, "not implemented! This method must be overridden")
+
+
+ def process_output(self):
+ ''' Override this method to put processed data into output queue. This method must be
+ overridden! At least pass through the input data::
+ if self.dataavailable:
+ return self.data
+ else:
+ return None
+ @return: processed data block or None if no data available
+ '''
+ raise ModuleError(self._object_name, "not implemented! This method must be overridden")
+
+
+ def process_update(self, params):
+ ''' Override this method to evaluate and maybe modify the data block configuration.
+ @param params: EEG_DataBlock object
+ @return: EEG_DataBlock object
+ '''
+ return params
+
+
+ def process_start(self):
+ ''' Override this method to prepare the module for startup
+ '''
+ return
+
+
+ def process_stop(self):
+ ''' Override this method to finalize the acquisition process
+ '''
+ return
+
+ def process_query(self, command):
+ ''' Override this method to accept or recject requested commands
+ '''
+ return True
+
+ def process_idle(self):
+ ''' Override this method to do something else during worker thread idle time or to
+ change the thread suspend time.
+ '''
+ time.sleep(0.001) # suspend thread (default = 1ms)
+ return
+
+
+ def receive_data(self):
+ try:
+ data = self._input_queue.get(False)
+ return data
+ except:
+ return None
+
+ def receive_data_available(self):
+ return self._input_queue.qsize()
+
+
+ def _transmit_data(self, data):
+ ''' Put data into the input queue. This method is invoked from the parent module.
+ Don't override this method.
+ @param data: EEG_DataBlock object
+ '''
+ try:
+ self._input_queue.put(data, False)
+ except:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "Input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
+
+ def _worker_thread(self):
+ ''' The worker thread takes data from the input queue and
+ puts the processed data into the output queue.
+ Don't override this method.
+ '''
+ while self._running:
+ wt = 0 # reset performance timer
+ # process input queue
+ self._thLock.acquire()
+ try:
+ data = self._input_queue.get(False)
+ t = time.perf_counter()
+ self.process_input(data)
+ wt += time.perf_counter() - t
+ self._thLock.release()
+ except queue.Empty:
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ self.send_exception(e, severity=ErrorSeverity.STOP)
+
+
+ # put data to all registered output queues
+ self._thLock.acquire()
+ try:
+ self.output_timer = time.perf_counter()
+ data = self.process_output()
+ wt += time.perf_counter() - self.output_timer
+ self._thLock.release()
+ except Exception as e:
+ self._thLock.release()
+ self.send_exception(e, severity=ErrorSeverity.STOP)
+ data = None
+
+ if data != None:
+ data.performance_timer_max = max(data.performance_timer_max, wt)
+ data.performance_timer += wt
+ #for idx, receiver in enumerate(self._receivers):
+ for idx, receiver in enumerate(reversed(self._receivers)):
+ if idx == 0:
+ receiver._transmit_data(data)
+ else:
+ receiver._transmit_data(copy.deepcopy(data))
+
+
+ # give a chance for idle processing
+ self.process_idle()
+
+
+ def send_exception(self, exception, severity=ErrorSeverity.STOP):
+ ''' Send Exception as ModuleEvent object to all connected slots.
+ Don't override this method.
+ @param exception: Exception() object
+ @param severity: error severity
+ '''
+ tb = GetExceptionTraceBack()[0]
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, tb + " -> " + str(exception), severity=severity))
+
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file. Override this method if you
+ want to put module properties into the configuration file.
+ @return: objectify XML element::
+
+
+ ...
+
+
+ '''
+ return None
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file. Override this method if you
+ want to get module properties from configuration file.
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ return
+
+
+
diff --git a/montage.py b/montage.py
index 5619826..edb4d04 100644
--- a/montage.py
+++ b/montage.py
@@ -1,600 +1,599 @@
-# -*- coding: utf-8 -*-
-'''
-Recording Montage Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-07-03 17:26:26 +0200 (Mi, 03 Jul 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 216 $
-'''
-
-from collections import defaultdict
-import textwrap
-
-from modbase import *
-from tools.modview import GenericTableWidget
-
-
-class MNT_Recording(ModuleBase):
- ''' Recording Montage
- Configure and use a recording montage
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- # initialize the base class, give a descriptive name
- ModuleBase.__init__(self, name="Recording Montage", **keys)
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # initialize module variables
- self.data = None # hold the data block we got from previous module
- self.dataavailable = False # data available for output to next module
- self.current_input_params = None # backup of last received properties
-
- self.montage_channel_properties = np.array([])
- self.montage = Montage()
-
- self.hideRefChannels = True # always hide or show reference channels
- self.refChannelNames = "none"
- self.hasDuplicateLabels = False
-
- self.needs_conversion = True # amplifier montage needs to be converted (compatibility mode)
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- self.needs_conversion = True
- self.montage.reset()
-
- def getMontageList(self):
- return self.montage.get_configuration_table(self.current_input_params.channel_properties)
-
- def get_configuration_pane(self):
- ''' Get the configuration pane
- @return: a QFrame object or None if you don't need a configuration pane
- '''
- cfgPane = _ConfigurationPane(self)
- self.connect(cfgPane, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
- return cfgPane
-
- def _configurationDataChanged(self):
- self.needs_conversion = False
- # we need a copy of the input parameters to keep the original input
- params = copy.copy(self.current_input_params)
- # propagate changes to connected modules
- self.update_receivers(self._apply_montage(params), propagate_only=True)
-
- def _get_group_indices(self, properties):
- # get indices and input channels of all different groups as dictionaries
- groups = defaultdict(list)
- groupchannels = defaultdict(dict)
- for idx, channel in enumerate(properties):
- groups[channel.inputgroup].append(idx)
- groupchannels[channel.inputgroup][channel.input] = idx
- return dict(groups), dict(groupchannels)
-
- def _create_output_selection(self, params):
- ''' Create index arrays for module data output
- '''
- properties = params.channel_properties
-
- # get all active eeg channel indices (excluding reference channels)
- mask = lambda x: (x.group == ChannelGroup.EEG) and x.enable and not x.isReference
- channel_map = np.array(map(mask, properties))
- self.eeg_indices = np.nonzero(channel_map)[0] # indices of all eeg channels
-
- # get the reference channel indices
- mask = lambda x: (x.group == ChannelGroup.EEG) and x.isReference
- channel_map = np.array(map(mask, properties))
- self.ref_indices = np.nonzero(channel_map)[0] # indices of reference channel(s)
-
- # get output channel indices, depending on recording mode
- if params.recording_mode == RecordingMode.IMPEDANCE or params.recording_mode == RecordingMode.TEST:
- # get all enabled channel indices, including reference channels
- mask = lambda x: (x.enable == True) or ((x.group == ChannelGroup.EEG) and x.isReference)
- else:
- if self.hideRefChannels:
- # get all enabled channel indices, excluding reference channels
- mask = lambda x: (x.enable == True) and not ((x.group == ChannelGroup.EEG) and x.isReference)
- else:
- # get all enabled channel indices, including enabled reference channels
- mask = lambda x: (x.enable == True)
-
- channel_map = np.array(map(mask, properties))
- self.output_channel_indices = np.nonzero(channel_map)[0] # indices of all enabled channels
-
- # append "REF" to the reference channel name and create the combined reference channel name
- refnames = []
- for prop in properties[self.ref_indices]:
- refnames.append(prop.name)
- prop.name = "REF_" + prop.name
- prop.refname = ""
-
- # combined reference channel names for storage and status display
- channelpropref = ""
- if params.recording_mode == RecordingMode.IMPEDANCE or params.recording_mode == RecordingMode.TEST:
- params.ref_channel_name = "none"
- else:
- if len(refnames) > 1:
- params.ref_channel_name = "AVG(" + " + ".join(refnames) + ")"
- channelpropref = "REF"
- elif len(refnames) == 1:
- params.ref_channel_name = "".join(refnames)
- channelpropref = "REF"
- else:
- params.ref_channel_name = "none"
-
- # set reference channel name for the affected eeg electrodes
- for prop in properties[self.eeg_indices]:
- prop.refname = channelpropref
-
- # reference channel names for display in the configuration pane
- if len(refnames) > 0:
- self.refChannelNames = " + ".join(refnames)
- else:
- self.refChannelNames = "none"
-
- def _validateChannelLabels(self):
- # search for duplicate channel labels
- labelList = [ch.name.lower() for ch in self.output_channel_properties if ch.enable ]
- labelDictionary = defaultdict(int)
- for l in labelList:
- labelDictionary[l] += 1
- if labelDictionary and max(labelDictionary.values()) > 1:
- return False
- return True
-
- def _apply_montage(self, params):
- # update the properties with montage settings
- for ch in params.channel_properties:
- if not self.montage.update_channel(ch):
- if not self.needs_conversion:
- # switch off all channels not found in this montage
- ch.enable = False
- ch.isReference = False
- self.montage.add(ch)
-
- # select output channels
- self._create_output_selection(params)
- self.output_channel_properties = np.array(params.channel_properties)[self.output_channel_indices]
- params.channel_properties = copy.deepcopy(self.output_channel_properties)
- if params.eeg_channels.size > 0:
- params.eeg_channels = params.eeg_channels[self.output_channel_indices]
-
- # send number of enabled channels for status display
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = "%d ch"%(len(self.output_channel_indices)),
- status_field="Channels"))
-
- # send reference channel names for status display
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = "REF: %s"%(params.ref_channel_name),
- status_field="Reference"))
-
- # check for duplicate labels and show warning
- if not self._validateChannelLabels():
- self.hasDuplicateLabels = True
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- info = "Pycorder detected duplicate channel names, please check the recording montage," +
- "otherwise this may cause problems in your analysis software",
- severity=ErrorSeverity.IGNORE))
- else:
- # remove the previous warning message from status line
- if self.hasDuplicateLabels:
- self.hasDuplicateLabels = False
- self.send_event(ModuleEvent("",
- EventType.MESSAGE,
- info = ""))
-
- return params
-
-
-
- def process_update(self, params):
- ''' Get and store properties from previous module
- @param params: EEG_DataBlock object
- @return: EEG_DataBlock object
- '''
- # keep the last property block for propagating changes during configuration
- self.current_input_params = copy.copy(params)
- # apply the montage settings
- params = self._apply_montage(params)
- return params
-
-
- def process_start(self):
- if self.output_channel_properties.size == 0:
- raise ModuleError(self._object_name, "no channels selected!")
-
-
- def process_input(self, datablock):
- ''' Get data from previous module
- @param datablock: EEG_DataBlock object
- '''
- self.dataavailable = True # signal data availability
- self.data = datablock # get a local reference
-
- if datablock.recording_mode != RecordingMode.IMPEDANCE and datablock.recording_mode != RecordingMode.TEST:
- # average and subtract the reference channels
- if self.ref_indices.size > 0:
- # average reference channels
- reference = np.mean(self.data.eeg_channels[self.ref_indices], 0)
- # subtract reference
- self.data.eeg_channels[self.eeg_indices] -= reference
-
- # simple copy is three times faster than deepcopy
- #self.data.channel_properties = copy.deepcopy(self.output_channel_properties)
- self.data.channel_properties = self.output_channel_properties.copy()
- for idx in range(self.data.channel_properties.size):
- self.data.channel_properties[idx] = copy.copy(self.output_channel_properties[idx])
-
- self.data.eeg_channels = self.data.eeg_channels[self.output_channel_indices]
-
-
-
- def process_output(self):
- ''' Send data out to next module
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # reset everything to default values
- self.setDefault()
-
- # search my configuration data
- montage = xml.xpath("//MNT_Recording[@module='montage' and @instance='%i']"%(self._instance) )
- if len(montage) == 0:
- return # configuration data not found, proceed with defaults
-
- cfg = montage[0] # we should have only one montage instance from this type
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- # setup montage channel configuration from xml
- self.montage.setXML(cfg)
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- def getXML(self):
- ''' Get module properties for XML configuration file.
- @return: objectify XML element
- '''
- if not self._validateChannelLabels():
- Qt.QMessageBox.critical(None, "PyCorder", "It is not possible to save a configuration that contains duplicate channel names.")
- raise Exception("configuration contains duplicate channel names")
- E = objectify.E
- channels = self.montage.getXML()
- montage = E.MNT_Recording( channels,
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="montage")
- return montage
-
-
-class Montage():
- ''' Montage dictionary
- '''
- def __init__(self):
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- self.reset()
-
- def reset(self):
- ''' Reset the channel dictionary
- '''
- self.channel_dict = defaultdict(lambda: defaultdict(dict)) # dictionary with input group and input channel number as keys
-
- def add(self, channel):
- ''' add a channel to the dictionary
- @param channel: EEG_ChannelProperties object
- '''
- ch = copy.copy(channel)
- # remove trailing and leading spaces from channel label
- ch.name = ch.name.strip()
- self.channel_dict[channel.inputgroup][channel.input][channel.group] = ch
-
- def has_channel(self, channel):
- ''' check if the dictionary has an entry for this channel
- @param channel: EEG_ChannelProperties object
- @return: True if channel entry available
- '''
- return self.channel_dict.has_key(channel.inputgroup) and\
- self.channel_dict[channel.inputgroup].has_key(channel.input) and\
- self.channel_dict[channel.inputgroup][channel.input].has_key(channel.group)
-
-
- def get_channel(self, channel):
- ''' get channel from dictionary
- @param channel: EEG_ChannelProperties object
- @return: EEG_ChannelProperties object or None if channel is not available
- '''
- if not self.has_channel(channel):
- return None
- ch = self.channel_dict[channel.inputgroup][channel.input][channel.group]
- # remove trailing and leading spaces from channel label
- ch.name = ch.name.strip()
- return ch
-
- def update_channel(self, channel):
- ''' update the channel properties from montage settings
- @param channel: EEG_ChannelProperties object
- @return: True on success
- '''
- mntchannel = self.get_channel(channel)
- if mntchannel == None:
- return False
-
- channel.name = mntchannel.name
- channel.enable = mntchannel.enable
- channel.isReference = mntchannel.isReference
- channel.color = mntchannel.color
- channel.unit = mntchannel.unit
- return True
-
- def get_configuration_table(self, properties):
- ''' get the configuration table for the current input properties
- @param properties: current channel properties array
- @return: montage channel properties
- '''
- mntproperties = []
- for ch in properties:
- mntchannel = self.get_channel(ch)
- if mntchannel != None:
- mntproperties.append(mntchannel)
- return np.array(mntproperties)
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- self.reset()
- for chXML in xml.MontageChannels.iterchildren():
- channel = EEG_ChannelProperties("")
- channel.setXML(chXML)
- self.add(channel)
-
- def getXML(self):
- ''' Get module properties for XML configuration file.
- @return: objectify XML element
- '''
- E = objectify.E
- channels = E.MontageChannels()
- for inputgroup in self.channel_dict.values():
- for inputnr in inputgroup.values():
- for channel in inputnr.values():
- channels.append(channel.getXML())
- channels.attrib["version"] = str(self.xmlVersion)
- return channels
-
-
-
-
-
-
-################################################################
-# Configuration Pane
-
-class _ConfigurationPane(Qt.QFrame):
- ''' Amplifier Test Module configuration pane.
- '''
- def __init__(self, module, *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # reference to our parent module
- self.module = module
-
- # Set tab name
- self.setWindowTitle("Recording Montage")
-
- # make it nice
- self.setFrameShape(Qt.QFrame.StyledPanel)
- self.setFrameShadow(Qt.QFrame.Raised)
-
- # base layout
- self.gridLayout = Qt.QGridLayout(self)
-
- # reference selection layout
- self.refLayout = Qt.QVBoxLayout()
-
- # create labels
- self.labelEeg = Qt.QLabel("EEG channels")
- self.labelOther = Qt.QLabel("Other channels")
-
- self.labelReference = Qt.QLabel("Selected Reference Channel(s)")
- self.labelReference.setAlignment(Qt.Qt.AlignCenter)
- self.labelReference.setStyleSheet('color: blue')
-
- self.labelReferenceSelection = Qt.QLabel("none")
- self.labelReferenceSelection.setAlignment(Qt.Qt.AlignCenter)
- self.labelReferenceSelection.setStyleSheet('color: blue')
-
- self.labelDuplicates = Qt.QLabel("Duplicate channel names detected!")
- self.labelDuplicates.setAlignment(Qt.Qt.AlignCenter)
- self.labelDuplicates.setStyleSheet('color: red')
-
-
- # create eeg channels table view
- self.channeltableEeg = GenericTableWidget(self)
- self.channeltableEeg.resizeColumnsToContents()
- self.channeltableEeg.setfnValidate(self.validateEEGChannelLabel)
-
- # create other channels table view
- self.channeltableOther = GenericTableWidget(self)
- self.channeltableOther.resizeColumnsToContents()
- self.channeltableOther.setfnValidate(self.validateAUXChannelLabel)
-
- self.resetChannelTables()
-
- # highlight reference channels
- lcs = lambda x: Qt.QColor(0, 0, 255) if x.isReference else None
- self.channeltableEeg.setfnColorSelect(lcs)
-
- # set function for checkbox get values (x[0] = column number, x[1] = EEG_ChannelProperties object)
- if self.module.hideRefChannels:
- # get the "enable" column number
- columns = self.getCfgTableViewDescription(eeg=True)[0]
- colEnable = [c for c in range(len(columns)) if columns[c]["variable"] == "enable"][0]
- # hide enable for reference channels
- hideref = lambda x: None if x[0] != colEnable else (False if x[1].isReference else x[1].enable)
- self.channeltableEeg.setfnCheckBox(hideref)
-
-
- # add all items to the layouts
- self.refLayout.addWidget(self.labelReference)
- self.refLayout.addWidget(self.labelReferenceSelection)
- spacerItemRL1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
- self.refLayout.addItem(spacerItemRL1)
- self.refLayout.addWidget(self.labelDuplicates)
-
- self.gridLayout.addWidget(self.labelEeg, 0, 0)
- self.gridLayout.addWidget(self.channeltableEeg, 1, 0, 1, 2)
- self.gridLayout.addLayout(self.refLayout, 1, 2, 1, 1)
-
- self.gridLayout.addWidget(self.labelOther, 2, 0)
- self.gridLayout.addWidget(self.channeltableOther, 3, 0, 1, 3)
-
- # actions
- self.connect(self.channeltableEeg, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
- self.connect(self.channeltableOther, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
-
-
- def validateEEGChannelLabel(self, row, col, data):
- if col == 5:
- name = data[row].name.lower()
- enable = data[row].enable
- if enable and self.labelDictionary.has_key(name) and self.labelDictionary[name] > 1:
- return False
- return True
-
- def validateAUXChannelLabel(self, row, col, data):
- if col == 4:
- name = data[row].name.lower()
- enable = data[row].enable
- if enable and self.labelDictionary.has_key(name) and self.labelDictionary[name] > 1:
- return False
- return True
-
- def getCfgTableViewDescription(self, eeg=False):
- # fields from EEG_ChannelProperties
- if eeg:
- columns = [
- {'variable':'inputgroup', 'header':'Port', 'edit':False, 'editor':'combobox', 'indexed':True},
- {'variable':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
- {'variable':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
- {'variable':'isReference', 'header':'Reference', 'edit':True, 'editor':'default'},
- {'variable':'group', 'header':'Group', 'edit':False, 'editor':'combobox', 'indexed':True},
- {'variable':'name', 'header':'Name', 'edit':True, 'editor':'default'},
- ]
- cblist = {'inputgroup':ChannelGroup.Name, 'group':ChannelGroup.Name}
- else:
- columns = [
- {'variable':'inputgroup', 'header':'Port', 'edit':False, 'editor':'combobox', 'indexed':True},
- {'variable':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
- {'variable':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
- {'variable':'group', 'header':'Group', 'edit':False, 'editor':'combobox', 'indexed':True},
- #{'variable':'unit', 'header':'Unit', 'edit':True, 'editor':'default'},
- {'variable':'name', 'header':'Name', 'edit':True, 'editor':'default'},
- ]
- cblist = {'inputgroup':ChannelGroup.Name, 'group':ChannelGroup.Name}
-
- return columns, cblist
-
- def resetChannelTables(self):
- # split montage table into eeg and other channels
- montage = self.module.getMontageList()
- ch_map = np.array(map(lambda x: (x.group == ChannelGroup.EEG), montage))
- eeg_indices = np.nonzero(ch_map)[0] # indices of all eeg channels
-
- if ch_map.shape[0]:
- other_indices = np.nonzero(np.invert(ch_map))[0] # indices of all other channels
- else:
- other_indices = []
-
- # update table widgets
- description, cblist = self.getCfgTableViewDescription(eeg=True)
- self.channeltableEeg.setData(montage[eeg_indices], description, cblist)
- description, cblist = self.getCfgTableViewDescription(eeg=False)
- self.channeltableOther.setData(montage[other_indices], description, cblist)
-
- # reset label validation dictionary
- self.labelDictionary = defaultdict(int)
-
-
- def showRefChannels(self):
- labelText = textwrap.fill(self.module.refChannelNames, 30)
- self.labelReferenceSelection.setText(labelText)
-
- def showLabelValidation(self):
- labelList = [ch.name.lower() for ch in self.channeltableEeg.data if ch.enable ]
- labelList.extend([ch.name.lower() for ch in self.channeltableOther.data if ch.enable])
- self.labelDictionary = defaultdict(int)
- for l in labelList:
- self.labelDictionary[l] += 1
- if self.labelDictionary and max(self.labelDictionary.values()) > 1:
- self.labelDuplicates.show()
- else:
- self.labelDuplicates.hide()
- self.channeltableOther.reset()
- self.channeltableEeg.reset()
-
- def showEvent(self, event):
- self.resetChannelTables()
- self.showRefChannels()
- self.showLabelValidation()
-
- def _configurationDataChanged(self):
- self.emit(Qt.SIGNAL('dataChanged()'))
- self.showRefChannels()
- self.showLabelValidation()
-
-if __name__ == '__main__':
- pass
-
+# -*- coding: utf-8 -*-
+'''
+Recording Montage Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-07-03 17:26:26 +0200 (Mi, 03 Jul 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 216 $
+'''
+
+from collections import defaultdict
+import textwrap
+
+from modbase import *
+from tools.modview import GenericTableWidget
+
+
+class MNT_Recording(ModuleBase):
+ ''' Recording Montage
+ Configure and use a recording montage
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ # initialize the base class, give a descriptive name
+ ModuleBase.__init__(self, name="Recording Montage", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # initialize module variables
+ self.data = None # hold the data block we got from previous module
+ self.dataavailable = False # data available for output to next module
+ self.current_input_params = None # backup of last received properties
+
+ self.montage_channel_properties = np.array([])
+ self.montage = Montage()
+
+ self.hideRefChannels = True # always hide or show reference channels
+ self.refChannelNames = "none"
+ self.hasDuplicateLabels = False
+
+ self.needs_conversion = True # amplifier montage needs to be converted (compatibility mode)
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ self.needs_conversion = True
+ self.montage.reset()
+
+ def getMontageList(self):
+ return self.montage.get_configuration_table(self.current_input_params.channel_properties)
+
+ def get_configuration_pane(self):
+ ''' Get the configuration pane
+ @return: a QFrame object or None if you don't need a configuration pane
+ '''
+ cfgPane = _ConfigurationPane(self)
+ self.connect(cfgPane, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
+ return cfgPane
+
+ def _configurationDataChanged(self):
+ self.needs_conversion = False
+ # we need a copy of the input parameters to keep the original input
+ params = copy.copy(self.current_input_params)
+ # propagate changes to connected modules
+ self.update_receivers(self._apply_montage(params), propagate_only=True)
+
+ def _get_group_indices(self, properties):
+ # get indices and input channels of all different groups as dictionaries
+ groups = defaultdict(list)
+ groupchannels = defaultdict(dict)
+ for idx, channel in enumerate(properties):
+ groups[channel.inputgroup].append(idx)
+ groupchannels[channel.inputgroup][channel.input] = idx
+ return dict(groups), dict(groupchannels)
+
+ def _create_output_selection(self, params):
+ ''' Create index arrays for module data output
+ '''
+ properties = params.channel_properties
+
+ # get all active eeg channel indices (excluding reference channels)
+ mask = lambda x: (x.group == ChannelGroup.EEG) and x.enable and not x.isReference
+ channel_map = np.array([mask(ch) for ch in properties], dtype=bool)
+ self.eeg_indices = np.nonzero(channel_map)[0] # indices of all eeg channels
+
+ # get the reference channel indices
+ mask = lambda x: (x.group == ChannelGroup.EEG) and x.isReference
+ channel_map = np.array([mask(ch) for ch in properties], dtype=bool)
+ self.ref_indices = np.nonzero(channel_map)[0] # indices of reference channel(s)
+
+ # get output channel indices, depending on recording mode
+ if params.recording_mode == RecordingMode.IMPEDANCE or params.recording_mode == RecordingMode.TEST:
+ # get all enabled channel indices, including reference channels
+ mask = lambda x: (x.enable == True) or ((x.group == ChannelGroup.EEG) and x.isReference)
+ else:
+ if self.hideRefChannels:
+ # get all enabled channel indices, excluding reference channels
+ mask = lambda x: (x.enable == True) and not ((x.group == ChannelGroup.EEG) and x.isReference)
+ else:
+ # get all enabled channel indices, including enabled reference channels
+ mask = lambda x: (x.enable == True)
+
+ channel_map = np.array([mask(ch) for ch in properties], dtype=bool)
+ self.output_channel_indices = np.nonzero(channel_map)[0] # indices of all enabled channels
+
+ # append "REF" to the reference channel name and create the combined reference channel name
+ refnames = []
+ for prop in properties[self.ref_indices]:
+ refnames.append(prop.name)
+ prop.name = "REF_" + prop.name
+ prop.refname = ""
+
+ # combined reference channel names for storage and status display
+ channelpropref = ""
+ if params.recording_mode == RecordingMode.IMPEDANCE or params.recording_mode == RecordingMode.TEST:
+ params.ref_channel_name = "none"
+ else:
+ if len(refnames) > 1:
+ params.ref_channel_name = "AVG(" + " + ".join(refnames) + ")"
+ channelpropref = "REF"
+ elif len(refnames) == 1:
+ params.ref_channel_name = "".join(refnames)
+ channelpropref = "REF"
+ else:
+ params.ref_channel_name = "none"
+
+ # set reference channel name for the affected eeg electrodes
+ for prop in properties[self.eeg_indices]:
+ prop.refname = channelpropref
+
+ # reference channel names for display in the configuration pane
+ if len(refnames) > 0:
+ self.refChannelNames = " + ".join(refnames)
+ else:
+ self.refChannelNames = "none"
+
+ def _validateChannelLabels(self):
+ # search for duplicate channel labels
+ labelList = [ch.name.lower() for ch in self.output_channel_properties if ch.enable ]
+ labelDictionary = defaultdict(int)
+ for l in labelList:
+ labelDictionary[l] += 1
+ if labelDictionary and max(labelDictionary.values()) > 1:
+ return False
+ return True
+
+ def _apply_montage(self, params):
+ # update the properties with montage settings
+ for ch in params.channel_properties:
+ if not self.montage.update_channel(ch):
+ if not self.needs_conversion:
+ # switch off all channels not found in this montage
+ ch.enable = False
+ ch.isReference = False
+ self.montage.add(ch)
+
+ # select output channels
+ self._create_output_selection(params)
+ self.output_channel_properties = np.array(params.channel_properties)[self.output_channel_indices]
+ params.channel_properties = copy.deepcopy(self.output_channel_properties)
+ if params.eeg_channels.size > 0:
+ params.eeg_channels = params.eeg_channels[self.output_channel_indices]
+
+ # send number of enabled channels for status display
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = "%d ch"%(len(self.output_channel_indices)),
+ status_field="Channels"))
+
+ # send reference channel names for status display
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = "REF: %s"%(params.ref_channel_name),
+ status_field="Reference"))
+
+ # check for duplicate labels and show warning
+ if not self._validateChannelLabels():
+ self.hasDuplicateLabels = True
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ info = "Pycorder detected duplicate channel names, please check the recording montage," +
+ "otherwise this may cause problems in your analysis software",
+ severity=ErrorSeverity.IGNORE))
+ else:
+ # remove the previous warning message from status line
+ if self.hasDuplicateLabels:
+ self.hasDuplicateLabels = False
+ self.send_event(ModuleEvent("",
+ EventType.MESSAGE,
+ info = ""))
+
+ return params
+
+
+
+ def process_update(self, params):
+ ''' Get and store properties from previous module
+ @param params: EEG_DataBlock object
+ @return: EEG_DataBlock object
+ '''
+ # keep the last property block for propagating changes during configuration
+ self.current_input_params = copy.copy(params)
+ # apply the montage settings
+ params = self._apply_montage(params)
+ return params
+
+
+ def process_start(self):
+ if self.output_channel_properties.size == 0:
+ raise ModuleError(self._object_name, "no channels selected!")
+
+
+ def process_input(self, datablock):
+ ''' Get data from previous module
+ @param datablock: EEG_DataBlock object
+ '''
+ self.dataavailable = True # signal data availability
+ self.data = datablock # get a local reference
+
+ if datablock.recording_mode != RecordingMode.IMPEDANCE and datablock.recording_mode != RecordingMode.TEST:
+ # average and subtract the reference channels
+ if self.ref_indices.size > 0:
+ # average reference channels
+ reference = np.mean(self.data.eeg_channels[self.ref_indices], 0)
+ # subtract reference
+ self.data.eeg_channels[self.eeg_indices] -= reference
+
+ # simple copy is three times faster than deepcopy
+ #self.data.channel_properties = copy.deepcopy(self.output_channel_properties)
+ self.data.channel_properties = self.output_channel_properties.copy()
+ for idx in range(self.data.channel_properties.size):
+ self.data.channel_properties[idx] = copy.copy(self.output_channel_properties[idx])
+
+ self.data.eeg_channels = self.data.eeg_channels[self.output_channel_indices]
+
+
+
+ def process_output(self):
+ ''' Send data out to next module
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # reset everything to default values
+ self.setDefault()
+
+ # search my configuration data
+ montage = xml.xpath("//MNT_Recording[@module='montage' and @instance='%i']"%(self._instance) )
+ if len(montage) == 0:
+ return # configuration data not found, proceed with defaults
+
+ cfg = montage[0] # we should have only one montage instance from this type
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ # setup montage channel configuration from xml
+ self.montage.setXML(cfg)
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file.
+ @return: objectify XML element
+ '''
+ if not self._validateChannelLabels():
+ Qt.QMessageBox.critical(None, "PyCorder", "It is not possible to save a configuration that contains duplicate channel names.")
+ raise Exception("configuration contains duplicate channel names")
+ E = objectify.E
+ channels = self.montage.getXML()
+ montage = E.MNT_Recording( channels,
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="montage")
+ return montage
+
+
+class Montage():
+ ''' Montage dictionary
+ '''
+ def __init__(self):
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ self.reset()
+
+ def reset(self):
+ ''' Reset the channel dictionary
+ '''
+ self.channel_dict = defaultdict(lambda: defaultdict(dict)) # dictionary with input group and input channel number as keys
+
+ def add(self, channel):
+ ''' add a channel to the dictionary
+ @param channel: EEG_ChannelProperties object
+ '''
+ ch = copy.copy(channel)
+ # remove trailing and leading spaces from channel label
+ ch.name = ch.name.strip()
+ self.channel_dict[channel.inputgroup][channel.input][channel.group] = ch
+
+ def has_channel(self, channel):
+ ''' check if the dictionary has an entry for this channel
+ @param channel: EEG_ChannelProperties object
+ @return: True if channel entry available
+ '''
+ return (channel.inputgroup in self.channel_dict) and\
+ (channel.input in self.channel_dict[channel.inputgroup]) and\
+ (channel.group in self.channel_dict[channel.inputgroup][channel.input])
+
+
+ def get_channel(self, channel):
+ ''' get channel from dictionary
+ @param channel: EEG_ChannelProperties object
+ @return: EEG_ChannelProperties object or None if channel is not available
+ '''
+ if not self.has_channel(channel):
+ return None
+ ch = self.channel_dict[channel.inputgroup][channel.input][channel.group]
+ # remove trailing and leading spaces from channel label
+ ch.name = ch.name.strip()
+ return ch
+
+ def update_channel(self, channel):
+ ''' update the channel properties from montage settings
+ @param channel: EEG_ChannelProperties object
+ @return: True on success
+ '''
+ mntchannel = self.get_channel(channel)
+ if mntchannel == None:
+ return False
+
+ channel.name = mntchannel.name
+ channel.enable = mntchannel.enable
+ channel.isReference = mntchannel.isReference
+ channel.color = mntchannel.color
+ channel.unit = mntchannel.unit
+ return True
+
+ def get_configuration_table(self, properties):
+ ''' get the configuration table for the current input properties
+ @param properties: current channel properties array
+ @return: montage channel properties
+ '''
+ mntproperties = []
+ for ch in properties:
+ mntchannel = self.get_channel(ch)
+ if mntchannel != None:
+ mntproperties.append(mntchannel)
+ return np.array(mntproperties)
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ self.reset()
+ for chXML in xml.MontageChannels.iterchildren():
+ channel = EEG_ChannelProperties("")
+ channel.setXML(chXML)
+ self.add(channel)
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file.
+ @return: objectify XML element
+ '''
+ E = objectify.E
+ channels = E.MontageChannels()
+ for inputgroup in self.channel_dict.values():
+ for inputnr in inputgroup.values():
+ for channel in inputnr.values():
+ channels.append(channel.getXML())
+ channels.attrib["version"] = str(self.xmlVersion)
+ return channels
+
+
+
+
+
+
+################################################################
+# Configuration Pane
+
+class _ConfigurationPane(Qt.QFrame):
+ ''' Amplifier Test Module configuration pane.
+ '''
+ def __init__(self, module, *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # reference to our parent module
+ self.module = module
+
+ # Set tab name
+ self.setWindowTitle("Recording Montage")
+
+ # make it nice
+ self.setFrameShape(Qt.QFrame.StyledPanel)
+ self.setFrameShadow(Qt.QFrame.Raised)
+
+ # base layout
+ self.gridLayout = Qt.QGridLayout(self)
+
+ # reference selection layout
+ self.refLayout = Qt.QVBoxLayout()
+
+ # create labels
+ self.labelEeg = Qt.QLabel("EEG channels")
+ self.labelOther = Qt.QLabel("Other channels")
+
+ self.labelReference = Qt.QLabel("Selected Reference Channel(s)")
+ self.labelReference.setAlignment(Qt.Qt.AlignCenter)
+ self.labelReference.setStyleSheet('color: blue')
+
+ self.labelReferenceSelection = Qt.QLabel("none")
+ self.labelReferenceSelection.setAlignment(Qt.Qt.AlignCenter)
+ self.labelReferenceSelection.setStyleSheet('color: blue')
+
+ self.labelDuplicates = Qt.QLabel("Duplicate channel names detected!")
+ self.labelDuplicates.setAlignment(Qt.Qt.AlignCenter)
+ self.labelDuplicates.setStyleSheet('color: red')
+
+
+ # create eeg channels table view
+ self.channeltableEeg = GenericTableWidget(self)
+ self.channeltableEeg.resizeColumnsToContents()
+ self.channeltableEeg.setfnValidate(self.validateEEGChannelLabel)
+
+ # create other channels table view
+ self.channeltableOther = GenericTableWidget(self)
+ self.channeltableOther.resizeColumnsToContents()
+ self.channeltableOther.setfnValidate(self.validateAUXChannelLabel)
+
+ self.resetChannelTables()
+
+ # highlight reference channels
+ lcs = lambda x: Qt.QColor(0, 0, 255) if x.isReference else None
+ self.channeltableEeg.setfnColorSelect(lcs)
+
+ # set function for checkbox get values (x[0] = column number, x[1] = EEG_ChannelProperties object)
+ if self.module.hideRefChannels:
+ # get the "enable" column number
+ columns = self.getCfgTableViewDescription(eeg=True)[0]
+ colEnable = [c for c in range(len(columns)) if columns[c]["variable"] == "enable"][0]
+ # hide enable for reference channels
+ hideref = lambda x: None if x[0] != colEnable else (False if x[1].isReference else x[1].enable)
+ self.channeltableEeg.setfnCheckBox(hideref)
+
+
+ # add all items to the layouts
+ self.refLayout.addWidget(self.labelReference)
+ self.refLayout.addWidget(self.labelReferenceSelection)
+ spacerItemRL1 = Qt.QSpacerItem(20, 40, Qt.QSizePolicy.Minimum, Qt.QSizePolicy.Expanding)
+ self.refLayout.addItem(spacerItemRL1)
+ self.refLayout.addWidget(self.labelDuplicates)
+
+ self.gridLayout.addWidget(self.labelEeg, 0, 0)
+ self.gridLayout.addWidget(self.channeltableEeg, 1, 0, 1, 2)
+ self.gridLayout.addLayout(self.refLayout, 1, 2, 1, 1)
+
+ self.gridLayout.addWidget(self.labelOther, 2, 0)
+ self.gridLayout.addWidget(self.channeltableOther, 3, 0, 1, 3)
+
+ # actions
+ self.connect(self.channeltableEeg, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
+ self.connect(self.channeltableOther, Qt.SIGNAL("dataChanged()"), self._configurationDataChanged)
+
+
+ def validateEEGChannelLabel(self, row, col, data):
+ if col == 5:
+ name = data[row].name.lower()
+ enable = data[row].enable
+ if enable and (name in self.labelDictionary) and self.labelDictionary[name] > 1:
+ return False
+ return True
+
+ def validateAUXChannelLabel(self, row, col, data):
+ if col == 4:
+ name = data[row].name.lower()
+ enable = data[row].enable
+ if enable and (name in self.labelDictionary) and self.labelDictionary[name] > 1:
+ return False
+ return True
+
+ def getCfgTableViewDescription(self, eeg=False):
+ # fields from EEG_ChannelProperties
+ if eeg:
+ columns = [
+ {'variable':'inputgroup', 'header':'Port', 'edit':False, 'editor':'combobox', 'indexed':True},
+ {'variable':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
+ {'variable':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
+ {'variable':'isReference', 'header':'Reference', 'edit':True, 'editor':'default'},
+ {'variable':'group', 'header':'Group', 'edit':False, 'editor':'combobox', 'indexed':True},
+ {'variable':'name', 'header':'Name', 'edit':True, 'editor':'default'},
+ ]
+ cblist = {'inputgroup':ChannelGroup.Name, 'group':ChannelGroup.Name}
+ else:
+ columns = [
+ {'variable':'inputgroup', 'header':'Port', 'edit':False, 'editor':'combobox', 'indexed':True},
+ {'variable':'input', 'header':'Channel', 'edit':False, 'editor':'default'},
+ {'variable':'enable', 'header':'Enable', 'edit':True, 'editor':'default'},
+ {'variable':'group', 'header':'Group', 'edit':False, 'editor':'combobox', 'indexed':True},
+ #{'variable':'unit', 'header':'Unit', 'edit':True, 'editor':'default'},
+ {'variable':'name', 'header':'Name', 'edit':True, 'editor':'default'},
+ ]
+ cblist = {'inputgroup':ChannelGroup.Name, 'group':ChannelGroup.Name}
+
+ return columns, cblist
+
+ def resetChannelTables(self):
+ # split montage table into eeg and other channels
+ montage = self.module.getMontageList()
+ ch_map = np.array([x.group == ChannelGroup.EEG for x in montage], dtype=bool)
+ eeg_indices = np.nonzero(ch_map)[0] # indices of all eeg channels
+
+ if ch_map.shape[0]:
+ other_indices = np.nonzero(np.invert(ch_map))[0] # indices of all other channels
+ else:
+ other_indices = []
+
+ # update table widgets
+ description, cblist = self.getCfgTableViewDescription(eeg=True)
+ self.channeltableEeg.setData(montage[eeg_indices], description, cblist)
+ description, cblist = self.getCfgTableViewDescription(eeg=False)
+ self.channeltableOther.setData(montage[other_indices], description, cblist)
+
+ # reset label validation dictionary
+ self.labelDictionary = defaultdict(int)
+
+
+ def showRefChannels(self):
+ labelText = textwrap.fill(self.module.refChannelNames, 30)
+ self.labelReferenceSelection.setText(labelText)
+
+ def showLabelValidation(self):
+ labelList = [ch.name.lower() for ch in self.channeltableEeg.data if ch.enable ]
+ labelList.extend([ch.name.lower() for ch in self.channeltableOther.data if ch.enable])
+ self.labelDictionary = defaultdict(int)
+ for l in labelList:
+ self.labelDictionary[l] += 1
+ if self.labelDictionary and max(self.labelDictionary.values()) > 1:
+ self.labelDuplicates.show()
+ else:
+ self.labelDuplicates.hide()
+ self.channeltableOther.reset()
+ self.channeltableEeg.reset()
+
+ def showEvent(self, event):
+ self.resetChannelTables()
+ self.showRefChannels()
+ self.showLabelValidation()
+
+ def _configurationDataChanged(self):
+ self.emit(Qt.SIGNAL('dataChanged()'))
+ self.showRefChannels()
+ self.showLabelValidation()
+
+if __name__ == '__main__':
+ pass
diff --git a/rda_client.py b/rda_client.py
index 6c9c018..d19caba 100644
--- a/rda_client.py
+++ b/rda_client.py
@@ -1,724 +1,728 @@
-# -*- coding: utf-8 -*-
-'''
-Remote Data Access (RDA) Client Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 197 $
-'''
-
-from modbase import *
-from socket import *
-from select import *
-from struct import *
-from binascii import *
-from ctypes import *
-
-from res import frmRdaClientOnline
-
-# RDA message GUID
-_MSG_GUID = "8E45584396C9864CAF4A98BBF6C91450"
-
-class RDAMessageType:
- ''' RDA Message Types, negative values are for internal use only
- '''
- CONNECTED = -1 #: Connected to server
- DISCONNECTED = -2 #: Disconnected from server
- START = 1 #: Setup / Start info
- DATA16 = 2 #: Block of 16-bit data
- STOP = 3 #: Data acquisition has been stopped
- DATA32 = 4 #: Block of 32-bit floating point data
- NEWSTATE = 5 #: Recorder state has been changed
- IMP_START = 6 #: Impedance measurement start
- IMP_DATA = 7 #: Impedance measurement data
- IMP_STOP = 8 #: Impedance measurement stop
- INFO = 9 #: Recorder info Header, sent after connection and when setup is changed
- KEEP_ALIVE = 10000 #: Sent periodically to check whether the connection is still alive
-
-class RDAMessage():
- ''' RDA Message Header
- '''
- def __init__(self):
- self.GUID = unhexlify(_MSG_GUID)
- self.Type = 0
- self.Size = 0
- self.Data = ""
-
-class RDA_Client(ModuleBase):
- ''' Receive EEG data over network via TCP/IP
- '''
-
- def __init__(self, *args, **keys):
- ''' Initialize module
- '''
- ModuleBase.__init__(self, name="RDA Client", **keys)
- self.dataavailable = False
- self.data = EEG_DataBlock(0, 0)
- self.lastBlockNumber = -1
- self.impedanceStartPending = False
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # create online configuration pane
- self.online_cfg = _OnlineCfgPane(self)
- self.connect(self.online_cfg, Qt.SIGNAL("modeChanged(int,QString)"), self._online_mode_changed)
-
- # define message header structures
- self.GUID = unhexlify(_MSG_GUID)
- self.hdr = "<16sLL" # generic header: GUID, nSize, nType
-
- # set client socket defaul values
- self._thClientLock = threading.Lock()
- self.HOST = 'localhost'
- self.PORT = 51244 #: 32-Bit data port
- self.ADDR = (self.HOST, self.PORT)
- self.serverDataValid = False
- self.client_thread_running = False
- self.client_thread = None
-
- # define data buffer
- self.resetBuffers()
-
- def resetBuffers(self):
- # string buffer for samples
- self.data_buffer = []
- # buffer sizes
- self.data_count = 0
- # calculate block size in samples for a 50ms block
- self.block_size = max(self.data.sample_rate * 0.05, 5)
-
- def setDefault(self):
- ''' Set module default values
- '''
- self.online_cfg.setIpList([])
-
- def _online_mode_changed(self, mode, serverIP):
- ''' SIGNAL conect/disconnect button clicked
- @param mode: 0=disconnect, 1=connect requested
- '''
- if mode == 0: # disonnect
- self.disconnectClient()
- else: # connect
- if len(serverIP) == 0:
- self.online_cfg.updateUI(0)
- return
- self.HOST = serverIP
- self.ADDR = (self.HOST, self.PORT)
- self.connectClient()
-
-
- def connectClient(self):
- ''' Create client socket and start client connection thread
- '''
- self.clientsock = socket(AF_INET, SOCK_STREAM)
-
- self.client_thread_state = 1 # start thread in wait mode
- self.client_thread_running = True
- self.client_thread = threading.Thread(target=self._client_thread)
- self.client_thread.start()
- self.online_cfg.updateUI(1)
-
- self.connect(self, Qt.SIGNAL('clientMsg(PyQt_PyObject)'), self._client_message)
-
-
-
- def disconnectClient(self):
- ''' Stop client thread and close the socket
- '''
- # stop acquisition
- self.stopClient()
- # close socket
- if self.client_thread != None:
- self.client_thread_running = False
- self.client_thread.join(5.0)
- self.client_thread = None
- self.clientsock.close()
- self.clientsock = None
- self.online_cfg.updateUI(0)
- self.disconnect(self, Qt.SIGNAL('clientMsg(PyQt_PyObject)'), self._client_message)
-
- def stopClient(self):
- ''' Stop client data acquisition
- '''
- if not self.isRunning():
- return
- # stop it
- ModuleBase.stop(self)
-
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- '''
- return self.online_cfg
-
-
- def terminate(self):
- ''' Shut down client socket
- '''
- if self.client_thread_running:
- self.client_thread_running = False
- self.client_thread.join(5.0)
- self.client_thread = None
- self.clientsock.close()
- self.clientsock = None
-
- def stop(self, force=False):
- ''' Stop data acquisition and disconnect client
- '''
- self.disconnectClient()
-
- def _client_thread(self):
- ''' Client socket connection thread
- '''
- # Messages types which to send directly to input queue
- sendToQueue = [RDAMessageType.DATA16,
- RDAMessageType.DATA32,
- ]
- readheader = True
- requested = 24
- received = 0
- msg = RDAMessage()
-
- while self.client_thread_running:
- # idle mode
- if self.client_thread_state == 0:
- time.sleep(0.2)
-
- # wait for RDA server
- elif self.client_thread_state == 1:
- # wait for server socket available
- try:
- self.clientsock.settimeout(10.0)
- self.clientsock.connect(self.ADDR)
- self.clientsock.setblocking(0)
- msg.Type = RDAMessageType.CONNECTED
- self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # connected
- self.client_thread_state = 2
- readheader = True
- requested = 24
- msg = RDAMessage()
- except:
- time.sleep(0.2)
-
- # connection to server established
- elif self.client_thread_state == 2:
- # look for data
- rd, wr, err = select([self.clientsock],[],[self.clientsock], 0.05)
- if len(err) > 0:
- # socket error
- msg.Type = RDAMessageType.DISCONNECTED
- self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # disconnected
- self.client_thread_state = 0
- elif len(rd) > 0:
- # data received
- data = self.clientsock.recv(requested - len(msg.Data))
- if len(data) == 0:
- # connection error
- msg.Type = RDAMessageType.DISCONNECTED
- self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # disconnected
- self.client_thread_state = 0
- else:
- # collect data
- msg.Data += data
- if requested == len(msg.Data):
- if readheader:
- # header received
- msg.GUID, msg.Size, msg.Type = unpack(self.hdr, msg.Data)
-
- # prepare to read data or next header
- if msg.Size > 24:
- readheader = False
- requested = msg.Size - 24
- msg.Data = ""
- else:
- readheader = True
- requested = 24
- # Header only, no data
- self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg)
- msg = RDAMessage()
- else:
- # data part received
- if msg.Type in sendToQueue:
- self._transmit_data(msg)
- else:
- self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg)
-
- # prepare to read next header
- readheader = True
- requested = 24
- msg = RDAMessage()
-
-
-
-
- def _client_message(self, message):
- ''' Evaluate client messages
- '''
- # validate message GUID
- if message.GUID != self.GUID:
- self.disconnectClient()
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- "Invalid data type (GUID)",
- severity=ErrorSeverity.NOTIFY))
- return
-
- if message.Type == RDAMessageType.DISCONNECTED:
- self.disconnectClient()
- return
-
- if message.Type == RDAMessageType.CONNECTED:
- self.online_cfg.updateUI(2)
- return
-
- if message.Type == RDAMessageType.KEEP_ALIVE:
- return
-
- if message.Type == RDAMessageType.NEWSTATE:
- state, = unpack('= self.block_size:
- # concatenate data buffer
- data = "".join(self.data_buffer)
-
- # extract channel data
- eeg = np.fromstring(data,
- dtype = np.float32,
- count = self.data_count * channels)
- self.data.eeg_channels = np.transpose(np.reshape(eeg, (self.data_count, -1))) * self.resolutions[:,np.newaxis]
- # create sample counter channel
- self.data.sample_channel = np.arange(self.data.sample_counter,
- self.data.sample_counter + self.data_count,
- dtype = np.uint64).reshape(1,-1)
- self.data.sample_counter += self.data_count
- # create dummy trigger channel
- self.data.trigger_channel = np.zeros((1, self.data_count),
- dtype = np.uint32)
- # calculate date and time for the first sample of this block in s
- sampletime = self.data.sample_channel[0][0] / self.data.sample_rate
- self.data.block_time = self.start_time + datetime.timedelta(seconds=sampletime)
-
- # reset buffers
- self.resetBuffers()
-
- # mark data as available
- self.dataavailable = True
-
- # check for missing blocks
- if self.lastBlockNumber >= 0:
- if block != self.lastBlockNumber + 1:
- missing = block - self.lastBlockNumber - 1
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- "Missing samples: %d Block(s)"%(missing),
- severity=ErrorSeverity.NOTIFY))
- self.lastBlockNumber = block
-
-
- # process impedance data
- if (self.data.recording_mode == RecordingMode.IMPEDANCE) and \
- (serverData.Type == RDAMessageType.IMP_DATA):
- # extract numerical data
- channels, = unpack(' 0:
- self.dataavailable = True
-
- def process_output(self):
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return copy.copy(self.data)
-
- def parseUnicodeZ(self, uRaw):
- ''' Parse zero terminated unicode string
- @param uRaw: unicode raw data
- @return: string, remaining part of uRaw
- '''
- zpos = -1
- for i in range(0,len(uRaw),2):
- v, = unpack(' 0:
- uString = uRaw[:zpos].decode("utf-16")
- remainder = uRaw[zpos+2:]
- else:
- uString = u""
- remainder = uRaw
- return uString, remainder
-
-
- def splitString(self, raw):
- ''' Helper function for splitting a raw array of
- zero terminated strings (C) into an array of python strings
- '''
- stringlist = []
- s = ""
- for i in range(len(raw)):
- if raw[i] != '\x00':
- s = s + raw[i]
- else:
- stringlist.append(s)
- s = ""
- return stringlist
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element
- '''
- E = objectify.E
- ipList = E.IP_list()
- for ip in self.online_cfg.getIpList():
- ipList.append(E.item(ip))
-
- cfg = E.RDA_Client(ipList,
- version=str(self.xmlVersion),
- module="RDA",
- instance=str(self._instance))
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search module configuration data
- configs = xml.xpath("//RDA_Client[@module='RDA' and @instance='%i']"%(self._instance) )
- if len(configs) == 0:
- # configuration data not found, set default values
- self.setDefault()
- return
-
- # we should have only one instance from this type
- cfg = configs[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- iplist = []
- for ip in cfg.IP_list.iterchildren():
- iplist.append(ip.pyval)
- self.online_cfg.setIpList(iplist)
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
-
-
-
-
-'''
-------------------------------------------------------------
-RDA CLIENT MODULE ONLINE GUI
-------------------------------------------------------------
-'''
-
-
-class _OnlineCfgPane(Qt.QFrame, frmRdaClientOnline.Ui_frmRdaClientOnline):
- ''' RDA client online configuration pane
- '''
- def __init__(self, amp, *args):
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
- self.amp = amp
-
- # set default values
- self.updateUI(0)
-
- # actions
- self.connect(self.pushButtonConnect, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
- self.connect(self.pushButtonAdd, Qt.SIGNAL("clicked()"), self._button_add)
- self.connect(self.pushButtonRemove, Qt.SIGNAL("clicked()"), self._button_remove)
-
- def _button_add(self):
- ''' Add current IP to combobox list
- '''
- # item already in list?
- item = self.comboBoxServerIP.currentText()
- index = self.comboBoxServerIP.findText(item)
- if index < 0:
- self.comboBoxServerIP.addItem(item)
-
- def _button_remove(self):
- ''' Remove current IP from combobox list
- '''
- # search current item
- item = self.comboBoxServerIP.currentText()
- index = self.comboBoxServerIP.findText(item)
- # remove item
- if index >= 0:
- self.comboBoxServerIP.removeItem(index)
-
- def _button_toggle(self, checked):
- mode = 0 # disconnect
- if self.pushButtonConnect.isChecked():
- mode = 1 # connect
- self.emit(Qt.SIGNAL('modeChanged(int,QString)'), mode, self.comboBoxServerIP.currentText())
-
- def getIpList(self):
- ''' Get combobox list entries
- @return: IPs as list
- '''
- list = []
- for idx in range(self.comboBoxServerIP.count()):
- list.append(str(self.comboBoxServerIP.itemText(idx)))
- return list
-
- def setIpList(self, list):
- ''' Setup combobox list entries
- @param list: IP entries
- '''
- self.comboBoxServerIP.clear()
- for ip in list:
- self.comboBoxServerIP.addItem(ip)
- if self.comboBoxServerIP.count():
- self.comboBoxServerIP.setCurrentIndex(0)
- else:
- self.comboBoxServerIP.setEditText("localhost")
-
- def updateUI(self, mode):
- ''' Update user interface
- '''
- if (mode == 1) or (mode == 2):
- self.pushButtonConnect.setChecked(True)
- self.pushButtonConnect.setText("Disconnect")
- self.labelMessage.setText("waiting")
- self.comboBoxServerIP.setEnabled(False)
- self.pushButtonAdd.setEnabled(False)
- self.pushButtonRemove.setEnabled(False)
- else:
- self.pushButtonConnect.setChecked(False)
- self.pushButtonConnect.setText("Connect")
- self.labelMessage.setText("disconnected")
- self.comboBoxServerIP.setEnabled(True)
- self.pushButtonAdd.setEnabled(True)
- self.pushButtonRemove.setEnabled(True)
- if mode == 2:
- self.labelMessage.setText("connected")
+# -*- coding: utf-8 -*-
+'''
+Remote Data Access (RDA) Client Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 197 $
+'''
+
+from modbase import *
+from socket import *
+from select import *
+from struct import *
+from binascii import *
+from ctypes import *
+try:
+ import Queue as queue
+except ImportError:
+ import queue
+
+from res import frmRdaClientOnline
+
+# RDA message GUID
+_MSG_GUID = "8E45584396C9864CAF4A98BBF6C91450"
+
+class RDAMessageType:
+ ''' RDA Message Types, negative values are for internal use only
+ '''
+ CONNECTED = -1 #: Connected to server
+ DISCONNECTED = -2 #: Disconnected from server
+ START = 1 #: Setup / Start info
+ DATA16 = 2 #: Block of 16-bit data
+ STOP = 3 #: Data acquisition has been stopped
+ DATA32 = 4 #: Block of 32-bit floating point data
+ NEWSTATE = 5 #: Recorder state has been changed
+ IMP_START = 6 #: Impedance measurement start
+ IMP_DATA = 7 #: Impedance measurement data
+ IMP_STOP = 8 #: Impedance measurement stop
+ INFO = 9 #: Recorder info Header, sent after connection and when setup is changed
+ KEEP_ALIVE = 10000 #: Sent periodically to check whether the connection is still alive
+
+class RDAMessage():
+ ''' RDA Message Header
+ '''
+ def __init__(self):
+ self.GUID = unhexlify(_MSG_GUID)
+ self.Type = 0
+ self.Size = 0
+ self.Data = ""
+
+class RDA_Client(ModuleBase):
+ ''' Receive EEG data over network via TCP/IP
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Initialize module
+ '''
+ ModuleBase.__init__(self, name="RDA Client", **keys)
+ self.dataavailable = False
+ self.data = EEG_DataBlock(0, 0)
+ self.lastBlockNumber = -1
+ self.impedanceStartPending = False
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # create online configuration pane
+ self.online_cfg = _OnlineCfgPane(self)
+ self.connect(self.online_cfg, Qt.SIGNAL("modeChanged(int,QString)"), self._online_mode_changed)
+
+ # define message header structures
+ self.GUID = unhexlify(_MSG_GUID)
+ self.hdr = "<16sLL" # generic header: GUID, nSize, nType
+
+ # set client socket defaul values
+ self._thClientLock = threading.Lock()
+ self.HOST = 'localhost'
+ self.PORT = 51244 #: 32-Bit data port
+ self.ADDR = (self.HOST, self.PORT)
+ self.serverDataValid = False
+ self.client_thread_running = False
+ self.client_thread = None
+
+ # define data buffer
+ self.resetBuffers()
+
+ def resetBuffers(self):
+ # string buffer for samples
+ self.data_buffer = []
+ # buffer sizes
+ self.data_count = 0
+ # calculate block size in samples for a 50ms block
+ self.block_size = max(self.data.sample_rate * 0.05, 5)
+
+ def setDefault(self):
+ ''' Set module default values
+ '''
+ self.online_cfg.setIpList([])
+
+ def _online_mode_changed(self, mode, serverIP):
+ ''' SIGNAL conect/disconnect button clicked
+ @param mode: 0=disconnect, 1=connect requested
+ '''
+ if mode == 0: # disonnect
+ self.disconnectClient()
+ else: # connect
+ if len(serverIP) == 0:
+ self.online_cfg.updateUI(0)
+ return
+ self.HOST = serverIP
+ self.ADDR = (self.HOST, self.PORT)
+ self.connectClient()
+
+
+ def connectClient(self):
+ ''' Create client socket and start client connection thread
+ '''
+ self.clientsock = socket(AF_INET, SOCK_STREAM)
+
+ self.client_thread_state = 1 # start thread in wait mode
+ self.client_thread_running = True
+ self.client_thread = threading.Thread(target=self._client_thread)
+ self.client_thread.start()
+ self.online_cfg.updateUI(1)
+
+ self.connect(self, Qt.SIGNAL('clientMsg(PyQt_PyObject)'), self._client_message)
+
+
+
+ def disconnectClient(self):
+ ''' Stop client thread and close the socket
+ '''
+ # stop acquisition
+ self.stopClient()
+ # close socket
+ if self.client_thread != None:
+ self.client_thread_running = False
+ self.client_thread.join(5.0)
+ self.client_thread = None
+ self.clientsock.close()
+ self.clientsock = None
+ self.online_cfg.updateUI(0)
+ self.disconnect(self, Qt.SIGNAL('clientMsg(PyQt_PyObject)'), self._client_message)
+
+ def stopClient(self):
+ ''' Stop client data acquisition
+ '''
+ if not self.isRunning():
+ return
+ # stop it
+ ModuleBase.stop(self)
+
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ '''
+ return self.online_cfg
+
+
+ def terminate(self):
+ ''' Shut down client socket
+ '''
+ if self.client_thread_running:
+ self.client_thread_running = False
+ self.client_thread.join(5.0)
+ self.client_thread = None
+ self.clientsock.close()
+ self.clientsock = None
+
+ def stop(self, force=False):
+ ''' Stop data acquisition and disconnect client
+ '''
+ self.disconnectClient()
+
+ def _client_thread(self):
+ ''' Client socket connection thread
+ '''
+ # Messages types which to send directly to input queue
+ sendToQueue = [RDAMessageType.DATA16,
+ RDAMessageType.DATA32,
+ ]
+ readheader = True
+ requested = 24
+ received = 0
+ msg = RDAMessage()
+
+ while self.client_thread_running:
+ # idle mode
+ if self.client_thread_state == 0:
+ time.sleep(0.2)
+
+ # wait for RDA server
+ elif self.client_thread_state == 1:
+ # wait for server socket available
+ try:
+ self.clientsock.settimeout(10.0)
+ self.clientsock.connect(self.ADDR)
+ self.clientsock.setblocking(0)
+ msg.Type = RDAMessageType.CONNECTED
+ self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # connected
+ self.client_thread_state = 2
+ readheader = True
+ requested = 24
+ msg = RDAMessage()
+ except:
+ time.sleep(0.2)
+
+ # connection to server established
+ elif self.client_thread_state == 2:
+ # look for data
+ rd, wr, err = select([self.clientsock],[],[self.clientsock], 0.05)
+ if len(err) > 0:
+ # socket error
+ msg.Type = RDAMessageType.DISCONNECTED
+ self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # disconnected
+ self.client_thread_state = 0
+ elif len(rd) > 0:
+ # data received
+ data = self.clientsock.recv(requested - len(msg.Data))
+ if len(data) == 0:
+ # connection error
+ msg.Type = RDAMessageType.DISCONNECTED
+ self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg) # disconnected
+ self.client_thread_state = 0
+ else:
+ # collect data
+ msg.Data += data
+ if requested == len(msg.Data):
+ if readheader:
+ # header received
+ msg.GUID, msg.Size, msg.Type = unpack(self.hdr, msg.Data)
+
+ # prepare to read data or next header
+ if msg.Size > 24:
+ readheader = False
+ requested = msg.Size - 24
+ msg.Data = ""
+ else:
+ readheader = True
+ requested = 24
+ # Header only, no data
+ self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg)
+ msg = RDAMessage()
+ else:
+ # data part received
+ if msg.Type in sendToQueue:
+ self._transmit_data(msg)
+ else:
+ self.emit(Qt.SIGNAL('clientMsg(PyQt_PyObject)'), msg)
+
+ # prepare to read next header
+ readheader = True
+ requested = 24
+ msg = RDAMessage()
+
+
+
+
+ def _client_message(self, message):
+ ''' Evaluate client messages
+ '''
+ # validate message GUID
+ if message.GUID != self.GUID:
+ self.disconnectClient()
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ "Invalid data type (GUID)",
+ severity=ErrorSeverity.NOTIFY))
+ return
+
+ if message.Type == RDAMessageType.DISCONNECTED:
+ self.disconnectClient()
+ return
+
+ if message.Type == RDAMessageType.CONNECTED:
+ self.online_cfg.updateUI(2)
+ return
+
+ if message.Type == RDAMessageType.KEEP_ALIVE:
+ return
+
+ if message.Type == RDAMessageType.NEWSTATE:
+ state, = unpack('= self.block_size:
+ # concatenate data buffer
+ data = "".join(self.data_buffer)
+
+ # extract channel data
+ eeg = np.fromstring(data,
+ dtype = np.float32,
+ count = self.data_count * channels)
+ self.data.eeg_channels = np.transpose(np.reshape(eeg, (self.data_count, -1))) * self.resolutions[:,np.newaxis]
+ # create sample counter channel
+ self.data.sample_channel = np.arange(self.data.sample_counter,
+ self.data.sample_counter + self.data_count,
+ dtype = np.uint64).reshape(1,-1)
+ self.data.sample_counter += self.data_count
+ # create dummy trigger channel
+ self.data.trigger_channel = np.zeros((1, self.data_count),
+ dtype = np.uint32)
+ # calculate date and time for the first sample of this block in s
+ sampletime = self.data.sample_channel[0][0] / self.data.sample_rate
+ self.data.block_time = self.start_time + datetime.timedelta(seconds=sampletime)
+
+ # reset buffers
+ self.resetBuffers()
+
+ # mark data as available
+ self.dataavailable = True
+
+ # check for missing blocks
+ if self.lastBlockNumber >= 0:
+ if block != self.lastBlockNumber + 1:
+ missing = block - self.lastBlockNumber - 1
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ "Missing samples: %d Block(s)"%(missing),
+ severity=ErrorSeverity.NOTIFY))
+ self.lastBlockNumber = block
+
+
+ # process impedance data
+ if (self.data.recording_mode == RecordingMode.IMPEDANCE) and \
+ (serverData.Type == RDAMessageType.IMP_DATA):
+ # extract numerical data
+ channels, = unpack(' 0:
+ self.dataavailable = True
+
+ def process_output(self):
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return copy.copy(self.data)
+
+ def parseUnicodeZ(self, uRaw):
+ ''' Parse zero terminated unicode string
+ @param uRaw: unicode raw data
+ @return: string, remaining part of uRaw
+ '''
+ zpos = -1
+ for i in range(0,len(uRaw),2):
+ v, = unpack(' 0:
+ uString = uRaw[:zpos].decode("utf-16")
+ remainder = uRaw[zpos+2:]
+ else:
+ uString = u""
+ remainder = uRaw
+ return uString, remainder
+
+
+ def splitString(self, raw):
+ ''' Helper function for splitting a raw array of
+ zero terminated strings (C) into an array of python strings
+ '''
+ stringlist = []
+ s = ""
+ for i in range(len(raw)):
+ if raw[i] != '\x00':
+ s = s + raw[i]
+ else:
+ stringlist.append(s)
+ s = ""
+ return stringlist
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element
+ '''
+ E = objectify.E
+ ipList = E.IP_list()
+ for ip in self.online_cfg.getIpList():
+ ipList.append(E.item(ip))
+
+ cfg = E.RDA_Client(ipList,
+ version=str(self.xmlVersion),
+ module="RDA",
+ instance=str(self._instance))
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search module configuration data
+ configs = xml.xpath("//RDA_Client[@module='RDA' and @instance='%i']"%(self._instance) )
+ if len(configs) == 0:
+ # configuration data not found, set default values
+ self.setDefault()
+ return
+
+ # we should have only one instance from this type
+ cfg = configs[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ iplist = []
+ for ip in cfg.IP_list.iterchildren():
+ iplist.append(ip.pyval)
+ self.online_cfg.setIpList(iplist)
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+
+
+
+
+'''
+------------------------------------------------------------
+RDA CLIENT MODULE ONLINE GUI
+------------------------------------------------------------
+'''
+
+
+class _OnlineCfgPane(Qt.QFrame, frmRdaClientOnline.Ui_frmRdaClientOnline):
+ ''' RDA client online configuration pane
+ '''
+ def __init__(self, amp, *args):
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+ self.amp = amp
+
+ # set default values
+ self.updateUI(0)
+
+ # actions
+ self.connect(self.pushButtonConnect, Qt.SIGNAL("clicked(bool)"), self._button_toggle)
+ self.connect(self.pushButtonAdd, Qt.SIGNAL("clicked()"), self._button_add)
+ self.connect(self.pushButtonRemove, Qt.SIGNAL("clicked()"), self._button_remove)
+
+ def _button_add(self):
+ ''' Add current IP to combobox list
+ '''
+ # item already in list?
+ item = self.comboBoxServerIP.currentText()
+ index = self.comboBoxServerIP.findText(item)
+ if index < 0:
+ self.comboBoxServerIP.addItem(item)
+
+ def _button_remove(self):
+ ''' Remove current IP from combobox list
+ '''
+ # search current item
+ item = self.comboBoxServerIP.currentText()
+ index = self.comboBoxServerIP.findText(item)
+ # remove item
+ if index >= 0:
+ self.comboBoxServerIP.removeItem(index)
+
+ def _button_toggle(self, checked):
+ mode = 0 # disconnect
+ if self.pushButtonConnect.isChecked():
+ mode = 1 # connect
+ self.emit(Qt.SIGNAL('modeChanged(int,QString)'), mode, self.comboBoxServerIP.currentText())
+
+ def getIpList(self):
+ ''' Get combobox list entries
+ @return: IPs as list
+ '''
+ list = []
+ for idx in range(self.comboBoxServerIP.count()):
+ list.append(str(self.comboBoxServerIP.itemText(idx)))
+ return list
+
+ def setIpList(self, list):
+ ''' Setup combobox list entries
+ @param list: IP entries
+ '''
+ self.comboBoxServerIP.clear()
+ for ip in list:
+ self.comboBoxServerIP.addItem(ip)
+ if self.comboBoxServerIP.count():
+ self.comboBoxServerIP.setCurrentIndex(0)
+ else:
+ self.comboBoxServerIP.setEditText("localhost")
+
+ def updateUI(self, mode):
+ ''' Update user interface
+ '''
+ if (mode == 1) or (mode == 2):
+ self.pushButtonConnect.setChecked(True)
+ self.pushButtonConnect.setText("Disconnect")
+ self.labelMessage.setText("waiting")
+ self.comboBoxServerIP.setEnabled(False)
+ self.pushButtonAdd.setEnabled(False)
+ self.pushButtonRemove.setEnabled(False)
+ else:
+ self.pushButtonConnect.setChecked(False)
+ self.pushButtonConnect.setText("Connect")
+ self.labelMessage.setText("disconnected")
+ self.comboBoxServerIP.setEnabled(True)
+ self.pushButtonAdd.setEnabled(True)
+ self.pushButtonRemove.setEnabled(True)
+ if mode == 2:
+ self.labelMessage.setText("connected")
diff --git a/rda_server.py b/rda_server.py
index 5cac959..b37796b 100644
--- a/rda_server.py
+++ b/rda_server.py
@@ -1,545 +1,556 @@
-# -*- coding: utf-8 -*-
-'''
-Remote Data Access (RDA) Server Module
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@version: 1.0
-'''
-
-from modbase import *
-from socket import *
-from select import *
-from struct import *
-from binascii import *
-from ctypes import *
-
-class RDAMessageType:
- ''' RDA Message Types
- '''
- START = 1 #: Setup / Start info
- DATA16 = 2 #: Block of 16-bit data
- STOP = 3 #: Data acquisition has been stopped
- DATA32 = 4 #: Block of 32-bit floating point data
- NEWSTATE = 5 #: Recorder state has been changed
- IMP_START = 6 #: Impedance measurement start
- IMP_DATA = 7 #: Impedance measurement data
- IMP_STOP = 8 #: Impedance measurement stop
- INFO = 9 #: Recorder info Header, sent after connection and when setup is changed
- KEEP_ALIVE = 10000 #: Sent periodically to check whether the connection is still alive
-
-
-class RDA_Server(ModuleBase):
- ''' Transmit EEG data over network via TCP/IP
- '''
-
- def __init__(self, *args, **keys):
- ''' Initialize module and create the accept thread
- '''
- ModuleBase.__init__(self, name="RDA Server", **keys)
- self.data = None
- self.dataavailable = False
-
- self._thServerLock = threading.Lock()
-
- self.params = None #: last channel configuration
- self.clients = [] #: list of connected clients
- self.blockcount = 0 #: number of received data blocks
- self.showClientErrors = False #: we don't want to see client performance problems
-
- # define message header structures
- self.GUID = unhexlify("8E45584396C9864CAF4A98BBF6C91450")
- self.hdr = "<16sLL" # generic header: GUID, nSize, nType
-
- # create server socket
- #self.HOST = 'localhost'
- self.HOST = '0.0.0.0'
- self.PORT = 51244 #: 32-Bit data port
- self.ADDR = (self.HOST, self.PORT)
- self.serversock = socket(AF_INET, SOCK_STREAM)
- try:
- self.serversock.bind(self.ADDR)
- self.serversock.setblocking(0)
- self.serversock.listen(2)
- except:
- raise Exception("RDA Server: another TCP/IP server is already running on this port: %d\r\n"%(self.PORT) +
- "Maybe there is already a running instance of BrainVision PyCorder "
- "or BrainVision Recoder.")
-
-
- # create server thread
- self.serverthread_running = True
- self.serverthread = threading.Thread(target=self._accept_thread)
- self.serverthread.start()
-
-
- def terminate(self):
- ''' Shut down server socket
- '''
- self.serverthread_running = False
- self.serverthread.join(5.0)
- # close client sockets
- for client in self.clients[:]:
- client.terminate()
- self.clients.remove(client)
- self.serversock.close()
-
-
- def _accept_thread(self):
- ''' Server socket accept client connections
- '''
- aliveMsg = self.build_message(RDAMessageType.KEEP_ALIVE)
- while self.serverthread_running:
-
- # wait until module is initialized
- if self.params == None:
- time.sleep(0.05)
- continue
-
- # waiting for connection
- rd, wr, err = select([self.serversock],[],[], 0.05)
- if len(rd) > 0:
- # get the client socket and create a connection object
- clientsock, addr = self.serversock.accept()
- client = ClientConnection(clientsock, addr)
- # init client
- try:
- sm = self.build_message(0)
- client.send(sm)
- si = self.build_message(RDAMessageType.INFO, self.params)
- client.send(si)
-
- if self.isRunning():
- if self.params.recording_mode == RecordingMode.IMPEDANCE:
- sm = self.build_message(RDAMessageType.IMP_START)
- st = self.build_message(RDAMessageType.NEWSTATE, 3)
- else:
- sm = self.build_message(RDAMessageType.START, self.params)
- st = self.build_message(RDAMessageType.NEWSTATE, 1)
- client.send(st)
- client.send(sm)
- else:
- st = self.build_message(RDAMessageType.NEWSTATE, 0)
- client.send(st)
- except:
- pass
-
- if client.connected:
- self._thServerLock.acquire()
- self.clients.append(client)
- self._thServerLock.release()
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
- "RDA Client connected: %s"%(str(addr))))
-
- # check connections
- self._thServerLock.acquire()
- for client in self.clients[:]:
- if not self.isRunning() or self.params.recording_mode == RecordingMode.IMPEDANCE:
- try:
- client.send(aliveMsg)
- except:
- pass
- if not client.connected:
- client.terminate()
- self.clients.remove(client)
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
- "RDA Client disconnected: %s"%(str(client.addr))))
- self._thServerLock.release()
-
-
-
- def process_input(self, datablock):
- ''' Build TCP/IP messages from data and send it to attached clients
- '''
- self.dataavailable = True
- self.data = datablock
- self.blockcount += 1
-
- # check for attached clients
- self._thServerLock.acquire()
- if len(self.clients) == 0:
- self._thServerLock.release()
- return
- self._thServerLock.release()
-
- # build impedance or data messages
- if self.data.recording_mode == RecordingMode.IMPEDANCE:
- # build impedance message
- dm = self.build_message(RDAMessageType.IMP_DATA, datablock)
- else:
- # build data message
- dm = self.build_message(RDAMessageType.DATA32, datablock)
-
- # send data to attached clients
- self._thServerLock.acquire()
- for client in self.clients:
- try:
- client.send(dm)
- except:
- if self.showClientErrors:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
- self._thServerLock.release()
-
-
- def process_output(self):
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
- def process_update(self, params):
- ''' Notify attached clients about channel configuration changes
- '''
- # copy settings
- self.params = copy.deepcopy(params)
-
- # notifiy attached clients
- self._thServerLock.acquire()
- if len(self.clients) > 0:
- si = self.build_message(RDAMessageType.INFO, self.params)
- for client in self.clients:
- try:
- client.send(si)
- except:
- if self.showClientErrors:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
- self._thServerLock.release()
-
- return params
-
-
- def process_start(self):
- ''' Notify attached clients about state change
- '''
- self.blockcount = 0
- # notifiy attached clients
- if self.params.recording_mode == RecordingMode.IMPEDANCE:
- sm = self.build_message(RDAMessageType.IMP_START)
- st = self.build_message(RDAMessageType.NEWSTATE, 3)
- else:
- sm = self.build_message(RDAMessageType.START, self.params)
- st = self.build_message(RDAMessageType.NEWSTATE, 1)
- self._thServerLock.acquire()
- for client in self.clients:
- try:
- client.send(st)
- client.send(sm)
- except:
- if self.showClientErrors:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
- self._thServerLock.release()
-
- def process_stop(self):
- ''' Notify attached clients about state change
- '''
- if self.params.recording_mode == RecordingMode.IMPEDANCE:
- sm = self.build_message(RDAMessageType.IMP_STOP)
- else:
- sm = self.build_message(RDAMessageType.STOP)
- st = self.build_message(RDAMessageType.NEWSTATE, 0)
- self._thServerLock.acquire()
- for client in self.clients:
- try:
- client.send(st)
- client.send(sm)
- except:
- if self.showClientErrors:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
- self._thServerLock.release()
-
-
-
- def build_message(self, type, data=None):
- ''' Build a message buffer according to message type
- @param type: RDAMessageType
- @param data: data object to send
- @return: binary message blob
- '''
- if type == RDAMessageType.START:
- channels = len(data.channel_properties)
- samplingInterval = 1.0e6 / data.sample_rate # sampling interval in us
-
- # create resolution byte array (we have a resolution of 1uV for all channels)
- res = [1.0] * channels
- resbyte = pack("<" + "d" * channels, *res)
-
- # create channel names byte array (null terminated strings)
- # use ansi code page 1252
- chn =[]
- for channel in data.channel_properties:
- chn.append(unicode(channel.name).encode("cp1252"))
- chnbyte = "\0".join(chn) + "\0"
-
- # create message header
- hdr_start = Struct(self.hdr+ "Ld") # start: nChannels, dSamplingInterval + data
- blocksize = hdr_start.size + len(resbyte) + len(chnbyte)
- hdrbyte = bytearray(hdr_start.pack(self.GUID, blocksize, type, channels, samplingInterval))
-
- # add data part
- hdrbyte.extend(resbyte)
- hdrbyte.extend(chnbyte)
- return hdrbyte
-
- elif type == RDAMessageType.STOP:
- # create message header
- hdr_start = Struct(self.hdr)
- blocksize = hdr_start.size
- hdrbyte = bytearray(hdr_start.pack(self.GUID, blocksize, type))
- return hdrbyte
-
- elif type == RDAMessageType.DATA32:
- # create data byte array
- nPoints = len(data.sample_channel[0])
- # convert data to float and write to data file
- d = data.eeg_channels.transpose()
- f = d.flatten().astype(np.float32)
- databyte = f.tostring()
-
- # create marker byte array
- nMarkers = len(data.markers)
- hdr_marker = Struct("= CHAMP_IMP_INVALID:
- nImpedance = -1
- else:
- nImpedance = (impedance + 500) / 1000
- return nImpedance
-
- def _packImpedance(self, number, value, name):
- fXPosition = (number % 10) * 0.05 + 0.5
- fYPosition = (number / 10) * 0.05 + 0.05
- hdr_imp = Struct(" 0:
- sent = self.sock.send(data[totalsent:])
- if sent == 0:
- raise RuntimeError, "socket connection broken"
- totalsent = totalsent + sent
-
- except Queue.Empty:
- time.sleep(0.002) # suspend thread (default = 2ms)
- except Exception as e:
- self.connected = False
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Remote Data Access (RDA) Server Module
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@version: 1.0
+'''
+
+from modbase import *
+from socket import *
+from select import *
+from struct import *
+from binascii import *
+from ctypes import *
+try:
+ import Queue as queue
+except ImportError:
+ import queue
+try:
+ unicode
+except NameError:
+ def unicode(obj, enc='utf-8'):
+ if isinstance(obj, bytes):
+ return obj.decode(enc)
+ return str(obj)
+
+class RDAMessageType:
+ ''' RDA Message Types
+ '''
+ START = 1 #: Setup / Start info
+ DATA16 = 2 #: Block of 16-bit data
+ STOP = 3 #: Data acquisition has been stopped
+ DATA32 = 4 #: Block of 32-bit floating point data
+ NEWSTATE = 5 #: Recorder state has been changed
+ IMP_START = 6 #: Impedance measurement start
+ IMP_DATA = 7 #: Impedance measurement data
+ IMP_STOP = 8 #: Impedance measurement stop
+ INFO = 9 #: Recorder info Header, sent after connection and when setup is changed
+ KEEP_ALIVE = 10000 #: Sent periodically to check whether the connection is still alive
+
+
+class RDA_Server(ModuleBase):
+ ''' Transmit EEG data over network via TCP/IP
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Initialize module and create the accept thread
+ '''
+ ModuleBase.__init__(self, name="RDA Server", **keys)
+ self.data = None
+ self.dataavailable = False
+
+ self._thServerLock = threading.Lock()
+
+ self.params = None #: last channel configuration
+ self.clients = [] #: list of connected clients
+ self.blockcount = 0 #: number of received data blocks
+ self.showClientErrors = False #: we don't want to see client performance problems
+
+ # define message header structures
+ self.GUID = unhexlify("8E45584396C9864CAF4A98BBF6C91450")
+ self.hdr = "<16sLL" # generic header: GUID, nSize, nType
+
+ # create server socket
+ #self.HOST = 'localhost'
+ self.HOST = '0.0.0.0'
+ self.PORT = 51244 #: 32-Bit data port
+ self.ADDR = (self.HOST, self.PORT)
+ self.serversock = socket(AF_INET, SOCK_STREAM)
+ try:
+ self.serversock.bind(self.ADDR)
+ self.serversock.setblocking(0)
+ self.serversock.listen(2)
+ except:
+ raise Exception("RDA Server: another TCP/IP server is already running on this port: %d\r\n"%(self.PORT) +
+ "Maybe there is already a running instance of BrainVision PyCorder "
+ "or BrainVision Recoder.")
+
+
+ # create server thread
+ self.serverthread_running = True
+ self.serverthread = threading.Thread(target=self._accept_thread)
+ self.serverthread.start()
+
+
+ def terminate(self):
+ ''' Shut down server socket
+ '''
+ self.serverthread_running = False
+ self.serverthread.join(5.0)
+ # close client sockets
+ for client in self.clients[:]:
+ client.terminate()
+ self.clients.remove(client)
+ self.serversock.close()
+
+
+ def _accept_thread(self):
+ ''' Server socket accept client connections
+ '''
+ aliveMsg = self.build_message(RDAMessageType.KEEP_ALIVE)
+ while self.serverthread_running:
+
+ # wait until module is initialized
+ if self.params == None:
+ time.sleep(0.05)
+ continue
+
+ # waiting for connection
+ rd, wr, err = select([self.serversock],[],[], 0.05)
+ if len(rd) > 0:
+ # get the client socket and create a connection object
+ clientsock, addr = self.serversock.accept()
+ client = ClientConnection(clientsock, addr)
+ # init client
+ try:
+ sm = self.build_message(0)
+ client.send(sm)
+ si = self.build_message(RDAMessageType.INFO, self.params)
+ client.send(si)
+
+ if self.isRunning():
+ if self.params.recording_mode == RecordingMode.IMPEDANCE:
+ sm = self.build_message(RDAMessageType.IMP_START)
+ st = self.build_message(RDAMessageType.NEWSTATE, 3)
+ else:
+ sm = self.build_message(RDAMessageType.START, self.params)
+ st = self.build_message(RDAMessageType.NEWSTATE, 1)
+ client.send(st)
+ client.send(sm)
+ else:
+ st = self.build_message(RDAMessageType.NEWSTATE, 0)
+ client.send(st)
+ except:
+ pass
+
+ if client.connected:
+ self._thServerLock.acquire()
+ self.clients.append(client)
+ self._thServerLock.release()
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
+ "RDA Client connected: %s"%(str(addr))))
+
+ # check connections
+ self._thServerLock.acquire()
+ for client in self.clients[:]:
+ if not self.isRunning() or self.params.recording_mode == RecordingMode.IMPEDANCE:
+ try:
+ client.send(aliveMsg)
+ except:
+ pass
+ if not client.connected:
+ client.terminate()
+ self.clients.remove(client)
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
+ "RDA Client disconnected: %s"%(str(client.addr))))
+ self._thServerLock.release()
+
+
+
+ def process_input(self, datablock):
+ ''' Build TCP/IP messages from data and send it to attached clients
+ '''
+ self.dataavailable = True
+ self.data = datablock
+ self.blockcount += 1
+
+ # check for attached clients
+ self._thServerLock.acquire()
+ if len(self.clients) == 0:
+ self._thServerLock.release()
+ return
+ self._thServerLock.release()
+
+ # build impedance or data messages
+ if self.data.recording_mode == RecordingMode.IMPEDANCE:
+ # build impedance message
+ dm = self.build_message(RDAMessageType.IMP_DATA, datablock)
+ else:
+ # build data message
+ dm = self.build_message(RDAMessageType.DATA32, datablock)
+
+ # send data to attached clients
+ self._thServerLock.acquire()
+ for client in self.clients:
+ try:
+ client.send(dm)
+ except:
+ if self.showClientErrors:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
+ self._thServerLock.release()
+
+
+ def process_output(self):
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+ def process_update(self, params):
+ ''' Notify attached clients about channel configuration changes
+ '''
+ # copy settings
+ self.params = copy.deepcopy(params)
+
+ # notifiy attached clients
+ self._thServerLock.acquire()
+ if len(self.clients) > 0:
+ si = self.build_message(RDAMessageType.INFO, self.params)
+ for client in self.clients:
+ try:
+ client.send(si)
+ except:
+ if self.showClientErrors:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
+ self._thServerLock.release()
+
+ return params
+
+
+ def process_start(self):
+ ''' Notify attached clients about state change
+ '''
+ self.blockcount = 0
+ # notifiy attached clients
+ if self.params.recording_mode == RecordingMode.IMPEDANCE:
+ sm = self.build_message(RDAMessageType.IMP_START)
+ st = self.build_message(RDAMessageType.NEWSTATE, 3)
+ else:
+ sm = self.build_message(RDAMessageType.START, self.params)
+ st = self.build_message(RDAMessageType.NEWSTATE, 1)
+ self._thServerLock.acquire()
+ for client in self.clients:
+ try:
+ client.send(st)
+ client.send(sm)
+ except:
+ if self.showClientErrors:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
+ self._thServerLock.release()
+
+ def process_stop(self):
+ ''' Notify attached clients about state change
+ '''
+ if self.params.recording_mode == RecordingMode.IMPEDANCE:
+ sm = self.build_message(RDAMessageType.IMP_STOP)
+ else:
+ sm = self.build_message(RDAMessageType.STOP)
+ st = self.build_message(RDAMessageType.NEWSTATE, 0)
+ self._thServerLock.acquire()
+ for client in self.clients:
+ try:
+ client.send(st)
+ client.send(sm)
+ except:
+ if self.showClientErrors:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "RDA Client input queue FULL, overrun!", severity=ErrorSeverity.NOTIFY))
+ self._thServerLock.release()
+
+
+
+ def build_message(self, type, data=None):
+ ''' Build a message buffer according to message type
+ @param type: RDAMessageType
+ @param data: data object to send
+ @return: binary message blob
+ '''
+ if type == RDAMessageType.START:
+ channels = len(data.channel_properties)
+ samplingInterval = 1.0e6 / data.sample_rate # sampling interval in us
+
+ # create resolution byte array (we have a resolution of 1uV for all channels)
+ res = [1.0] * channels
+ resbyte = pack("<" + "d" * channels, *res)
+
+ # create channel names byte array (null terminated strings)
+ # use ansi code page 1252
+ chn =[]
+ for channel in data.channel_properties:
+ chn.append(unicode(channel.name).encode("cp1252"))
+ chnbyte = "\0".join(chn) + "\0"
+
+ # create message header
+ hdr_start = Struct(self.hdr+ "Ld") # start: nChannels, dSamplingInterval + data
+ blocksize = hdr_start.size + len(resbyte) + len(chnbyte)
+ hdrbyte = bytearray(hdr_start.pack(self.GUID, blocksize, type, channels, samplingInterval))
+
+ # add data part
+ hdrbyte.extend(resbyte)
+ hdrbyte.extend(chnbyte)
+ return hdrbyte
+
+ elif type == RDAMessageType.STOP:
+ # create message header
+ hdr_start = Struct(self.hdr)
+ blocksize = hdr_start.size
+ hdrbyte = bytearray(hdr_start.pack(self.GUID, blocksize, type))
+ return hdrbyte
+
+ elif type == RDAMessageType.DATA32:
+ # create data byte array
+ nPoints = len(data.sample_channel[0])
+ # convert data to float and write to data file
+ d = data.eeg_channels.transpose()
+ f = d.flatten().astype(np.float32)
+ databyte = f.tostring()
+
+ # create marker byte array
+ nMarkers = len(data.markers)
+ hdr_marker = Struct("= CHAMP_IMP_INVALID:
+ nImpedance = -1
+ else:
+ nImpedance = (impedance + 500) / 1000
+ return nImpedance
+
+ def _packImpedance(self, number, value, name):
+ fXPosition = (number % 10) * 0.05 + 0.5
+ fYPosition = (number / 10) * 0.05 + 0.05
+ hdr_imp = Struct(" 0:
+ sent = self.sock.send(data[totalsent:])
+ if sent == 0:
+ raise RuntimeError("socket connection broken")
+ totalsent = totalsent + sent
+
+ except queue.Empty:
+ time.sleep(0.002) # suspend thread (default = 2ms)
+ except Exception as e:
+ self.connected = False
+
+
+
+
\ No newline at end of file
diff --git a/remote.py b/remote.py
index 30447ca..bc9e7c8 100644
--- a/remote.py
+++ b/remote.py
@@ -1,274 +1,284 @@
-# -*- coding: utf-8 -*-
-'''
-Remote Control Server
-
-PyCorder remote control server for use with E-Prime® or Presentation® stimulus control software
-
-------------------------------------------------------------
-
-Copyright (C) 2013, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-07-03 13:45:27 +0200 (Mi, 03 Jul 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 214 $
-'''
-from PyQt4 import Qt
-from socket import *
-from select import *
-import threading
-import time
-import sys
-import Queue
-
-from modbase import ModuleEvent
-from modbase import EventType
-from modbase import ErrorSeverity
-
-
-class RemoteControlServer(Qt.QObject):
- ''' Receive remote commands over network via TCP/IP
- It opens a TCP/IP server on port 6700 and listens for a string as a command.
- '''
-
- def __init__(self):
- ''' Initialize the server and create the accept thread
- '''
- Qt.QObject.__init__(self)
- self._object_name = "RemoteControlServer"
- self.clients = [] #: list of connected clients
- self.blockcount = 0 #: number of received data blocks
- self.feedbackEnabled = False #: enable feedback to attached client
-
- self._thServerLock = threading.Lock()
- self.postponed = []
-
- # create server socket
- self.HOST = '0.0.0.0'
- self.PORT = 6700
- self.ADDR = (self.HOST, self.PORT)
- self.serversock = socket(AF_INET, SOCK_STREAM)
- try:
- self.serversock.bind(self.ADDR)
- self.serversock.setblocking(0)
- self.serversock.listen(2)
- except:
- raise Exception("Remote Control Server: another TCP/IP server is already running on this port: %d\r\n"%(self.PORT) +
- "Maybe there is already a running instance of BrainVision PyCorder "
- "or Remote Control for BrainVision Recoder.\n"
- "Remote Control support will be not available for this session!")
-
- # create server thread
- self.serverthread_running = True
- self.serverthread = threading.Thread(target=self._accept_thread)
- self.serverthread.start()
-
- # recording started from remote control
- self.remoteRecording = False
-
- # initialize state variables
- self.resetControlState()
-
- def resetControlState(self):
- ''' Clear out the remote control state variables
- '''
- self.S_ConfigurationFile = ""
- self.S_SubjectID = ""
- self.S_ExperimentNr = ""
-
- def isInitialized(self):
- ret = (len(self.S_ConfigurationFile) > 0) &\
- (len(self.S_ExperimentNr) > 0) &\
- (len(self.S_SubjectID) > 0)
- return ret
-
- def isClientConnected(self):
- ''' Check for a client connection
- '''
- return len(self.clients) > 0
-
- def terminate(self):
- ''' Shut down server socket
- '''
- self.serverthread_running = False
- self.serverthread.join(5.0)
- # close client sockets
- for client in self.clients[:]:
- client.terminate()
- self.clients.remove(client)
- self.serversock.close()
-
- def send_event(self, event):
- ''' Send ModuleEvent objects to all connected slots.
- @param event: ModuleEvent object
- '''
- self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
-
- def send_feedback(self, feedback):
- ''' send feedback to attached client
- '''
- if not self.feedbackEnabled:
- return
- # prepare feedback string
- feedback = feedback.replace("\r", "")
- feedback = feedback.replace("\n", "")
- feedback = (feedback + "\r\n").encode("utf-8")
- for client in self.clients[:]:
- client.send(feedback)
-
- def postpone_feedback(self, cmd):
- self.postponed.append(cmd)
-
-
- def _accept_thread(self):
- ''' Server socket accept client connections
- '''
- while self.serverthread_running:
- # waiting for connection
- rd, wr, err = select([self.serversock],[],[], 0.2)
- if len(rd) > 0:
- # we want only one client connection
- if len(self.clients) > 0:
- addr = self.clients[0].addr
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "Another remote client %s is already connected"%(str(addr)),
- severity=ErrorSeverity.IGNORE
- )
- )
- clientsock, addr = self.serversock.accept()
- clientsock.close()
- else:
- # get the client socket and create a connection object
- clientsock, addr = self.serversock.accept()
- client = RemoteClientConnection(clientsock, addr, self)
-
- if client.connected:
- self._thServerLock.acquire()
- self.clients.append(client)
- self._thServerLock.release()
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
- "Client connected %s"%(str(addr))))
-
- # check connections
- self._thServerLock.acquire()
- for client in self.clients[:]:
- if not client.connected:
- client.terminate()
- self.clients.remove(client)
- self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
- "Client disconnected %s"%(str(client.addr))))
- self._thServerLock.release()
-
-
-class RemoteClientConnection(Qt.QObject):
- ''' Object holding a connected remote client
- '''
- def __init__(self, clientsock, addr, parent_server):
- ''' Create data transmit thread
- @param clientsock: client socket
- @param addr: client IP address
- '''
- Qt.QObject.__init__(self)
- self.ParentServer = parent_server
- self.sock = clientsock
- self.addr = addr
- self.transmit_queue = Queue.Queue(20)
- # start receive thread
- self.connected = True
- self.clientthread = threading.Thread(target=self._receive_thread)
- self.clientthread.start()
-
- def terminate(self):
- ''' Shut down client socket
- '''
- if self.connected:
- self.connected = False
- self.clientthread.join(5.0)
- self.sock.close()
-
- def send(self, message):
- ''' Put the message into the transmit queue
- '''
- if self.connected:
- self.transmit_queue.put(message, False)
-
-
- def _guessEncoding(self, data):
- ''' try different encodings and convert to unicode
- '''
- # these encodings should be in most likely order to save time
- encodings = [ "utf-8", "ascii", "cp1252", "utf_16", "utf_16_be", "utf_16_le"]
- for enc in encodings:
- try:
- ucode = unicode(data, enc)
- return ucode
- except:
- if enc == encodings[-1]:
- raise Exception("command character encoding failed")
-
- def _receive_thread(self):
- ''' Get data from client and send data to client
- '''
- while self.connected:
- try:
- # wait for data
- rd, wr, err = select([self.sock],[],[self.sock], 0.05)
- if len(err) > 0:
- # socket error
- self.connected = False
- elif len(rd) > 0:
- # data received
- data = self.sock.recv(2048)
- if len(data) == 0:
- # connection error
- self.connected = False
- else:
- try:
- cmd = self._guessEncoding(data).strip()
- # we don't want empty commands
- if len(cmd) > 0:
- self.ParentServer.send_event(ModuleEvent(self.ParentServer._object_name,
- EventType.COMMAND, "RemoteCommand", cmd_value=cmd))
- except Exception as e:
- self.ParentServer.send_event(ModuleEvent(self.ParentServer._object_name,
- EventType.ERROR, str(e), severity=ErrorSeverity.IGNORE))
- # send response to client
- if not self.transmit_queue.empty():
- try:
- # get data from queue
- data = self.transmit_queue.get(False)
- # send it to client
- totalsent = 0
- while totalsent < len(data):
- rd, wr, err = select([],[self.sock],[], 0.02)
- if len(wr) > 0:
- sent = self.sock.send(data[totalsent:])
- if sent == 0:
- raise RuntimeError, "socket connection broken"
- totalsent = totalsent + sent
- except Queue.Empty:
- time.sleep(0.002) # suspend thread (default = 2ms)
-
- except Exception as e:
- self.connected = False
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Remote Control Server
+
+PyCorder remote control server for use with E-Prime® or Presentation® stimulus control software
+
+------------------------------------------------------------
+
+Copyright (C) 2013, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-07-03 13:45:27 +0200 (Mi, 03 Jul 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 214 $
+'''
+from PyQt4 import Qt
+from socket import *
+from select import *
+import threading
+import time
+import sys
+try:
+ import Queue as queue
+except ImportError:
+ import queue
+try:
+ unicode
+except NameError:
+ def unicode(obj, enc='utf-8'):
+ if isinstance(obj, bytes):
+ return obj.decode(enc)
+ return str(obj)
+
+from modbase import ModuleEvent
+from modbase import EventType
+from modbase import ErrorSeverity
+
+
+class RemoteControlServer(Qt.QObject):
+ ''' Receive remote commands over network via TCP/IP
+ It opens a TCP/IP server on port 6700 and listens for a string as a command.
+ '''
+
+ def __init__(self):
+ ''' Initialize the server and create the accept thread
+ '''
+ Qt.QObject.__init__(self)
+ self._object_name = "RemoteControlServer"
+ self.clients = [] #: list of connected clients
+ self.blockcount = 0 #: number of received data blocks
+ self.feedbackEnabled = False #: enable feedback to attached client
+
+ self._thServerLock = threading.Lock()
+ self.postponed = []
+
+ # create server socket
+ self.HOST = '0.0.0.0'
+ self.PORT = 6700
+ self.ADDR = (self.HOST, self.PORT)
+ self.serversock = socket(AF_INET, SOCK_STREAM)
+ try:
+ self.serversock.bind(self.ADDR)
+ self.serversock.setblocking(0)
+ self.serversock.listen(2)
+ except:
+ raise Exception("Remote Control Server: another TCP/IP server is already running on this port: %d\r\n"%(self.PORT) +
+ "Maybe there is already a running instance of BrainVision PyCorder "
+ "or Remote Control for BrainVision Recoder.\n"
+ "Remote Control support will be not available for this session!")
+
+ # create server thread
+ self.serverthread_running = True
+ self.serverthread = threading.Thread(target=self._accept_thread)
+ self.serverthread.start()
+
+ # recording started from remote control
+ self.remoteRecording = False
+
+ # initialize state variables
+ self.resetControlState()
+
+ def resetControlState(self):
+ ''' Clear out the remote control state variables
+ '''
+ self.S_ConfigurationFile = ""
+ self.S_SubjectID = ""
+ self.S_ExperimentNr = ""
+
+ def isInitialized(self):
+ ret = (len(self.S_ConfigurationFile) > 0) &\
+ (len(self.S_ExperimentNr) > 0) &\
+ (len(self.S_SubjectID) > 0)
+ return ret
+
+ def isClientConnected(self):
+ ''' Check for a client connection
+ '''
+ return len(self.clients) > 0
+
+ def terminate(self):
+ ''' Shut down server socket
+ '''
+ self.serverthread_running = False
+ self.serverthread.join(5.0)
+ # close client sockets
+ for client in self.clients[:]:
+ client.terminate()
+ self.clients.remove(client)
+ self.serversock.close()
+
+ def send_event(self, event):
+ ''' Send ModuleEvent objects to all connected slots.
+ @param event: ModuleEvent object
+ '''
+ self.emit(Qt.SIGNAL('event(PyQt_PyObject)'), event)
+
+ def send_feedback(self, feedback):
+ ''' send feedback to attached client
+ '''
+ if not self.feedbackEnabled:
+ return
+ # prepare feedback string
+ feedback = feedback.replace("\r", "")
+ feedback = feedback.replace("\n", "")
+ feedback = (feedback + "\r\n").encode("utf-8")
+ for client in self.clients[:]:
+ client.send(feedback)
+
+ def postpone_feedback(self, cmd):
+ self.postponed.append(cmd)
+
+
+ def _accept_thread(self):
+ ''' Server socket accept client connections
+ '''
+ while self.serverthread_running:
+ # waiting for connection
+ rd, wr, err = select([self.serversock],[],[], 0.2)
+ if len(rd) > 0:
+ # we want only one client connection
+ if len(self.clients) > 0:
+ addr = self.clients[0].addr
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "Another remote client %s is already connected"%(str(addr)),
+ severity=ErrorSeverity.IGNORE
+ )
+ )
+ clientsock, addr = self.serversock.accept()
+ clientsock.close()
+ else:
+ # get the client socket and create a connection object
+ clientsock, addr = self.serversock.accept()
+ client = RemoteClientConnection(clientsock, addr, self)
+
+ if client.connected:
+ self._thServerLock.acquire()
+ self.clients.append(client)
+ self._thServerLock.release()
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
+ "Client connected %s"%(str(addr))))
+
+ # check connections
+ self._thServerLock.acquire()
+ for client in self.clients[:]:
+ if not client.connected:
+ client.terminate()
+ self.clients.remove(client)
+ self.send_event(ModuleEvent(self._object_name, EventType.LOGMESSAGE,
+ "Client disconnected %s"%(str(client.addr))))
+ self._thServerLock.release()
+
+
+class RemoteClientConnection(Qt.QObject):
+ ''' Object holding a connected remote client
+ '''
+ def __init__(self, clientsock, addr, parent_server):
+ ''' Create data transmit thread
+ @param clientsock: client socket
+ @param addr: client IP address
+ '''
+ Qt.QObject.__init__(self)
+ self.ParentServer = parent_server
+ self.sock = clientsock
+ self.addr = addr
+ self.transmit_queue = queue.Queue(20)
+ # start receive thread
+ self.connected = True
+ self.clientthread = threading.Thread(target=self._receive_thread)
+ self.clientthread.start()
+
+ def terminate(self):
+ ''' Shut down client socket
+ '''
+ if self.connected:
+ self.connected = False
+ self.clientthread.join(5.0)
+ self.sock.close()
+
+ def send(self, message):
+ ''' Put the message into the transmit queue
+ '''
+ if self.connected:
+ self.transmit_queue.put(message, False)
+
+
+ def _guessEncoding(self, data):
+ ''' try different encodings and convert to unicode
+ '''
+ # these encodings should be in most likely order to save time
+ encodings = [ "utf-8", "ascii", "cp1252", "utf_16", "utf_16_be", "utf_16_le"]
+ for enc in encodings:
+ try:
+ ucode = unicode(data, enc)
+ return ucode
+ except:
+ if enc == encodings[-1]:
+ raise Exception("command character encoding failed")
+
+ def _receive_thread(self):
+ ''' Get data from client and send data to client
+ '''
+ while self.connected:
+ try:
+ # wait for data
+ rd, wr, err = select([self.sock],[],[self.sock], 0.05)
+ if len(err) > 0:
+ # socket error
+ self.connected = False
+ elif len(rd) > 0:
+ # data received
+ data = self.sock.recv(2048)
+ if len(data) == 0:
+ # connection error
+ self.connected = False
+ else:
+ try:
+ cmd = self._guessEncoding(data).strip()
+ # we don't want empty commands
+ if len(cmd) > 0:
+ self.ParentServer.send_event(ModuleEvent(self.ParentServer._object_name,
+ EventType.COMMAND, "RemoteCommand", cmd_value=cmd))
+ except Exception as e:
+ self.ParentServer.send_event(ModuleEvent(self.ParentServer._object_name,
+ EventType.ERROR, str(e), severity=ErrorSeverity.IGNORE))
+ # send response to client
+ if not self.transmit_queue.empty():
+ try:
+ # get data from queue
+ data = self.transmit_queue.get(False)
+ # send it to client
+ totalsent = 0
+ while totalsent < len(data):
+ rd, wr, err = select([],[self.sock],[], 0.02)
+ if len(wr) > 0:
+ sent = self.sock.send(data[totalsent:])
+ if sent == 0:
+ raise RuntimeError("socket connection broken")
+ totalsent = totalsent + sent
+ except queue.Empty:
+ time.sleep(0.002) # suspend thread (default = 2ms)
+
+ except Exception as e:
+ self.connected = False
+
+
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..43e4c2d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+numpy
+scipy
+lxml
+PySide6
+pyqtgraph
diff --git a/res/frmActiChampOnline.py b/res/frmActiChampOnline.py
index 9b3fae7..c788d97 100644
--- a/res/frmActiChampOnline.py
+++ b/res/frmActiChampOnline.py
@@ -1,106 +1,104 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'frmActiChampOnline.ui'
-#
-# Created: Wed Jun 05 12:00:50 2013
-# by: PyQt4 UI code generator 4.5.4
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore, QtGui
-
-class Ui_frmActiChampOnline(object):
- def setupUi(self, frmActiChampOnline):
- frmActiChampOnline.setObjectName("frmActiChampOnline")
- frmActiChampOnline.resize(427, 233)
- frmActiChampOnline.setFrameShape(QtGui.QFrame.Panel)
- frmActiChampOnline.setFrameShadow(QtGui.QFrame.Raised)
- self.gridLayout_3 = QtGui.QGridLayout(frmActiChampOnline)
- self.gridLayout_3.setObjectName("gridLayout_3")
- self.groupBoxMode = QtGui.QGroupBox(frmActiChampOnline)
- self.groupBoxMode.setFlat(False)
- self.groupBoxMode.setCheckable(False)
- self.groupBoxMode.setObjectName("groupBoxMode")
- self.gridLayout_2 = QtGui.QGridLayout(self.groupBoxMode)
- self.gridLayout_2.setObjectName("gridLayout_2")
- self.gridLayout = QtGui.QGridLayout()
- self.gridLayout.setObjectName("gridLayout")
- self.horizontalLayout = QtGui.QHBoxLayout()
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.pushButtonStartDefault = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonStartDefault.setMinimumSize(QtCore.QSize(100, 40))
- self.pushButtonStartDefault.setStyleSheet("text-align: left; padding-left: 10px;")
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(":/icons/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon.addPixmap(QtGui.QPixmap(":/icons/play_green.png"), QtGui.QIcon.Normal, QtGui.QIcon.On)
- self.pushButtonStartDefault.setIcon(icon)
- self.pushButtonStartDefault.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonStartDefault.setCheckable(True)
- self.pushButtonStartDefault.setAutoExclusive(True)
- self.pushButtonStartDefault.setAutoDefault(False)
- self.pushButtonStartDefault.setObjectName("pushButtonStartDefault")
- self.horizontalLayout.addWidget(self.pushButtonStartDefault)
- self.pushButtonStartShielding = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonStartShielding.setMinimumSize(QtCore.QSize(100, 40))
- self.pushButtonStartShielding.setStyleSheet("text-align: left; padding-left: 10px;")
- self.pushButtonStartShielding.setIcon(icon)
- self.pushButtonStartShielding.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonStartShielding.setCheckable(True)
- self.pushButtonStartShielding.setAutoExclusive(True)
- self.pushButtonStartShielding.setObjectName("pushButtonStartShielding")
- self.horizontalLayout.addWidget(self.pushButtonStartShielding)
- self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
- self.pushButtonStop = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonStop.setMinimumSize(QtCore.QSize(100, 40))
- icon1 = QtGui.QIcon()
- icon1.addPixmap(QtGui.QPixmap(":/icons/stop.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon1.addPixmap(QtGui.QPixmap(":/icons/stop_green.png"), QtGui.QIcon.Normal, QtGui.QIcon.On)
- self.pushButtonStop.setIcon(icon1)
- self.pushButtonStop.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonStop.setCheckable(True)
- self.pushButtonStop.setAutoExclusive(True)
- self.pushButtonStop.setObjectName("pushButtonStop")
- self.gridLayout.addWidget(self.pushButtonStop, 2, 0, 1, 1)
- self.horizontalLayout_2 = QtGui.QHBoxLayout()
- self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.pushButtonStartImpedance = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonStartImpedance.setMinimumSize(QtCore.QSize(100, 40))
- self.pushButtonStartImpedance.setIcon(icon)
- self.pushButtonStartImpedance.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonStartImpedance.setCheckable(True)
- self.pushButtonStartImpedance.setAutoExclusive(True)
- self.pushButtonStartImpedance.setAutoDefault(False)
- self.pushButtonStartImpedance.setObjectName("pushButtonStartImpedance")
- self.horizontalLayout_2.addWidget(self.pushButtonStartImpedance)
- self.pushButtonStartTest = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonStartTest.setMinimumSize(QtCore.QSize(100, 40))
- self.pushButtonStartTest.setIcon(icon)
- self.pushButtonStartTest.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonStartTest.setCheckable(True)
- self.pushButtonStartTest.setChecked(False)
- self.pushButtonStartTest.setAutoExclusive(True)
- self.pushButtonStartTest.setObjectName("pushButtonStartTest")
- self.horizontalLayout_2.addWidget(self.pushButtonStartTest)
- self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
- self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 1)
- self.gridLayout_3.addWidget(self.groupBoxMode, 0, 0, 1, 1)
-
- self.retranslateUi(frmActiChampOnline)
- QtCore.QMetaObject.connectSlotsByName(frmActiChampOnline)
-
- def retranslateUi(self, frmActiChampOnline):
- frmActiChampOnline.setWindowTitle(QtGui.QApplication.translate("frmActiChampOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
- self.groupBoxMode.setTitle(QtGui.QApplication.translate("frmActiChampOnline", "Amplifier", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartDefault.setText(QtGui.QApplication.translate("frmActiChampOnline", "Default\n"
-"Mode", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartShielding.setText(QtGui.QApplication.translate("frmActiChampOnline", "Shielding\n"
-"Mode", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStop.setText(QtGui.QApplication.translate("frmActiChampOnline", "Stop", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartImpedance.setStyleSheet(QtGui.QApplication.translate("frmActiChampOnline", "text-align: left; padding-left: 10px;", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartImpedance.setText(QtGui.QApplication.translate("frmActiChampOnline", "Impedance\n"
-"Mode", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartTest.setStyleSheet(QtGui.QApplication.translate("frmActiChampOnline", "text-align: left; padding-left: 10px;", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonStartTest.setText(QtGui.QApplication.translate("frmActiChampOnline", "Test\n"
-"Mode", None, QtGui.QApplication.UnicodeUTF8))
-
-import resources_rc
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'frmActiChampOnline.ui'
+#
+# Created: Wed Jun 05 12:00:50 2013
+# by: PyQt4 UI code generator 4.5.4
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_frmActiChampOnline(object):
+ def setupUi(self, frmActiChampOnline):
+ frmActiChampOnline.setObjectName("frmActiChampOnline")
+ frmActiChampOnline.resize(427, 233)
+ frmActiChampOnline.setFrameShape(QtGui.QFrame.Panel)
+ frmActiChampOnline.setFrameShadow(QtGui.QFrame.Raised)
+ self.gridLayout_3 = QtGui.QGridLayout(frmActiChampOnline)
+ self.gridLayout_3.setObjectName("gridLayout_3")
+ self.groupBoxMode = QtGui.QGroupBox(frmActiChampOnline)
+ self.groupBoxMode.setFlat(False)
+ self.groupBoxMode.setCheckable(False)
+ self.groupBoxMode.setObjectName("groupBoxMode")
+ self.gridLayout_2 = QtGui.QGridLayout(self.groupBoxMode)
+ self.gridLayout_2.setObjectName("gridLayout_2")
+ self.gridLayout = QtGui.QGridLayout()
+ self.gridLayout.setObjectName("gridLayout")
+ self.horizontalLayout = QtGui.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.pushButtonStartDefault = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonStartDefault.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonStartDefault.setStyleSheet("text-align: left; padding-left: 10px;")
+ playIcon = QtGui.QIcon(":/icons/play.png")
+ self.pushButtonStartDefault.setIcon(playIcon)
+ self.pushButtonStartDefault.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonStartDefault.setCheckable(True)
+ self.pushButtonStartDefault.setAutoExclusive(True)
+ self.pushButtonStartDefault.setAutoDefault(False)
+ self.pushButtonStartDefault.setObjectName("pushButtonStartDefault")
+ self.horizontalLayout.addWidget(self.pushButtonStartDefault)
+ self.pushButtonStartShielding = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonStartShielding.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonStartShielding.setStyleSheet("text-align: left; padding-left: 10px;")
+ self.pushButtonStartShielding.setIcon(playIcon)
+ self.pushButtonStartShielding.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonStartShielding.setCheckable(True)
+ self.pushButtonStartShielding.setAutoExclusive(True)
+ self.pushButtonStartShielding.setObjectName("pushButtonStartShielding")
+ self.horizontalLayout.addWidget(self.pushButtonStartShielding)
+ self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
+ self.pushButtonStop = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonStop.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonStop.setIcon(QtGui.QIcon(":/icons/stop.png"))
+ self.pushButtonStop.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonStop.setCheckable(True)
+ self.pushButtonStop.setAutoExclusive(True)
+ self.pushButtonStop.setObjectName("pushButtonStop")
+ self.gridLayout.addWidget(self.pushButtonStop, 2, 0, 1, 1)
+ self.horizontalLayout_2 = QtGui.QHBoxLayout()
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ self.pushButtonStartImpedance = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonStartImpedance.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonStartImpedance.setIcon(playIcon)
+ self.pushButtonStartImpedance.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonStartImpedance.setCheckable(True)
+ self.pushButtonStartImpedance.setAutoExclusive(True)
+ self.pushButtonStartImpedance.setAutoDefault(False)
+ self.pushButtonStartImpedance.setObjectName("pushButtonStartImpedance")
+ self.horizontalLayout_2.addWidget(self.pushButtonStartImpedance)
+ self.pushButtonStartTest = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonStartTest.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonStartTest.setIcon(playIcon)
+ self.pushButtonStartTest.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonStartTest.setCheckable(True)
+ self.pushButtonStartTest.setChecked(False)
+ self.pushButtonStartTest.setAutoExclusive(True)
+ self.pushButtonStartTest.setObjectName("pushButtonStartTest")
+ self.horizontalLayout_2.addWidget(self.pushButtonStartTest)
+ self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
+ self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 1)
+ self.gridLayout_3.addWidget(self.groupBoxMode, 0, 0, 1, 1)
+
+ self.retranslateUi(frmActiChampOnline)
+ QtCore.QMetaObject.connectSlotsByName(frmActiChampOnline)
+
+ def retranslateUi(self, frmActiChampOnline):
+ frmActiChampOnline.setWindowTitle(QtGui.QApplication.translate("frmActiChampOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
+ self.groupBoxMode.setTitle(QtGui.QApplication.translate("frmActiChampOnline", "Amplifier", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartDefault.setText(QtGui.QApplication.translate("frmActiChampOnline", "Default\n"
+"Mode", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartShielding.setText(QtGui.QApplication.translate("frmActiChampOnline", "Shielding\n"
+"Mode", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStop.setText(QtGui.QApplication.translate("frmActiChampOnline", "Stop", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartImpedance.setStyleSheet(QtGui.QApplication.translate("frmActiChampOnline", "text-align: left; padding-left: 10px;", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartImpedance.setText(QtGui.QApplication.translate("frmActiChampOnline", "Impedance\n"
+"Mode", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartTest.setStyleSheet(QtGui.QApplication.translate("frmActiChampOnline", "text-align: left; padding-left: 10px;", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonStartTest.setText(QtGui.QApplication.translate("frmActiChampOnline", "Test\n"
+"Mode", None, QtGui.QApplication.UnicodeUTF8))
+
+try:
+ import resources_rc
+except ImportError:
+ from res import resources_rc
diff --git a/res/frmMain.py b/res/frmMain.py
index 653d370..4ba22bf 100644
--- a/res/frmMain.py
+++ b/res/frmMain.py
@@ -1,113 +1,112 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'frmMain.ui'
-#
-# Created: Wed Jun 05 12:00:50 2013
-# by: PyQt4 UI code generator 4.5.4
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore, QtGui
-
-class Ui_MainWindow(object):
- def setupUi(self, MainWindow):
- MainWindow.setObjectName("MainWindow")
- MainWindow.resize(862, 604)
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(":/icons/PyCorder.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- MainWindow.setWindowIcon(icon)
- self.centralwidget = QtGui.QWidget(MainWindow)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
- self.centralwidget.setSizePolicy(sizePolicy)
- self.centralwidget.setObjectName("centralwidget")
- self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.horizontalLayout_SignalPane = QtGui.QHBoxLayout()
- self.horizontalLayout_SignalPane.setContentsMargins(-1, 5, -1, -1)
- self.horizontalLayout_SignalPane.setObjectName("horizontalLayout_SignalPane")
- spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
- self.horizontalLayout_SignalPane.addItem(spacerItem)
- self.horizontalLayout.addLayout(self.horizontalLayout_SignalPane)
- self.scrollArea = QtGui.QScrollArea(self.centralwidget)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())
- self.scrollArea.setSizePolicy(sizePolicy)
- self.scrollArea.setMinimumSize(QtCore.QSize(300, 0))
- self.scrollArea.setFrameShape(QtGui.QFrame.NoFrame)
- self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
- self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self.scrollArea.setWidgetResizable(True)
- self.scrollArea.setObjectName("scrollArea")
- self.scrollAreaWidgetContents = QtGui.QWidget(self.scrollArea)
- self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 300, 533))
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth())
- self.scrollAreaWidgetContents.setSizePolicy(sizePolicy)
- self.scrollAreaWidgetContents.setMinimumSize(QtCore.QSize(300, 0))
- self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
- self.verticalLayout_OnlinePane = QtGui.QVBoxLayout(self.scrollAreaWidgetContents)
- self.verticalLayout_OnlinePane.setObjectName("verticalLayout_OnlinePane")
- self.pushButtonConfiguration = QtGui.QPushButton(self.scrollAreaWidgetContents)
- self.pushButtonConfiguration.setStyleSheet("text-align: left; padding-left: 10px; padding-top: 5px; padding-bottom: 5px")
- icon1 = QtGui.QIcon()
- icon1.addPixmap(QtGui.QPixmap(":/icons/process.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- self.pushButtonConfiguration.setIcon(icon1)
- self.pushButtonConfiguration.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonConfiguration.setObjectName("pushButtonConfiguration")
- self.verticalLayout_OnlinePane.addWidget(self.pushButtonConfiguration)
- spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
- self.verticalLayout_OnlinePane.addItem(spacerItem1)
- self.scrollArea.setWidget(self.scrollAreaWidgetContents)
- self.horizontalLayout.addWidget(self.scrollArea)
- MainWindow.setCentralWidget(self.centralwidget)
- self.menubar = QtGui.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect(0, 0, 862, 27))
- self.menubar.setObjectName("menubar")
- self.menuApplication = QtGui.QMenu(self.menubar)
- self.menuApplication.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
- self.menuApplication.setTearOffEnabled(False)
- self.menuApplication.setObjectName("menuApplication")
- MainWindow.setMenuBar(self.menubar)
- self.statusbar = QtGui.QStatusBar(MainWindow)
- self.statusbar.setObjectName("statusbar")
- MainWindow.setStatusBar(self.statusbar)
- self.actionQuit = QtGui.QAction(MainWindow)
- self.actionQuit.setObjectName("actionQuit")
- self.actionShow_Log = QtGui.QAction(MainWindow)
- self.actionShow_Log.setObjectName("actionShow_Log")
- self.actionLoad_Configuration = QtGui.QAction(MainWindow)
- self.actionLoad_Configuration.setObjectName("actionLoad_Configuration")
- self.actionSave_Configuration = QtGui.QAction(MainWindow)
- self.actionSave_Configuration.setObjectName("actionSave_Configuration")
- self.actionDefault_Configuration = QtGui.QAction(MainWindow)
- self.actionDefault_Configuration.setObjectName("actionDefault_Configuration")
- self.menuApplication.addAction(self.actionLoad_Configuration)
- self.menuApplication.addAction(self.actionSave_Configuration)
- self.menuApplication.addAction(self.actionDefault_Configuration)
- self.menuApplication.addSeparator()
- self.menuApplication.addAction(self.actionShow_Log)
- self.menuApplication.addSeparator()
- self.menuApplication.addAction(self.actionQuit)
- self.menubar.addAction(self.menuApplication.menuAction())
-
- self.retranslateUi(MainWindow)
- QtCore.QMetaObject.connectSlotsByName(MainWindow)
-
- def retranslateUi(self, MainWindow):
- MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "PyCorder", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonConfiguration.setText(QtGui.QApplication.translate("MainWindow", "Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
- self.menuApplication.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8))
- self.actionQuit.setText(QtGui.QApplication.translate("MainWindow", "Quit", None, QtGui.QApplication.UnicodeUTF8))
- self.actionShow_Log.setText(QtGui.QApplication.translate("MainWindow", "Show Log", None, QtGui.QApplication.UnicodeUTF8))
- self.actionLoad_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Load Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
- self.actionSave_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Save Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
- self.actionDefault_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Reset Configuration", None, QtGui.QApplication.UnicodeUTF8))
-
-import resources_rc
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'frmMain.ui'
+#
+# Created: Wed Jun 05 12:00:50 2013
+# by: PyQt4 UI code generator 4.5.4
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(862, 604)
+ MainWindow.setWindowIcon(QtGui.QIcon(":/icons/PyCorder.ico"))
+ self.centralwidget = QtGui.QWidget(MainWindow)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
+ self.centralwidget.setSizePolicy(sizePolicy)
+ self.centralwidget.setObjectName("centralwidget")
+ self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.horizontalLayout_SignalPane = QtGui.QHBoxLayout()
+ self.horizontalLayout_SignalPane.setContentsMargins(-1, 5, -1, -1)
+ self.horizontalLayout_SignalPane.setObjectName("horizontalLayout_SignalPane")
+ spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
+ self.horizontalLayout_SignalPane.addItem(spacerItem)
+ self.horizontalLayout.addLayout(self.horizontalLayout_SignalPane)
+ self.scrollArea = QtGui.QScrollArea(self.centralwidget)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())
+ self.scrollArea.setSizePolicy(sizePolicy)
+ self.scrollArea.setMinimumSize(QtCore.QSize(300, 0))
+ self.scrollArea.setFrameShape(QtGui.QFrame.NoFrame)
+ self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.scrollArea.setWidgetResizable(True)
+ self.scrollArea.setObjectName("scrollArea")
+ self.scrollAreaWidgetContents = QtGui.QWidget(self.scrollArea)
+ self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 300, 533))
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth())
+ self.scrollAreaWidgetContents.setSizePolicy(sizePolicy)
+ self.scrollAreaWidgetContents.setMinimumSize(QtCore.QSize(300, 0))
+ self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
+ self.verticalLayout_OnlinePane = QtGui.QVBoxLayout(self.scrollAreaWidgetContents)
+ self.verticalLayout_OnlinePane.setObjectName("verticalLayout_OnlinePane")
+ self.pushButtonConfiguration = QtGui.QPushButton(self.scrollAreaWidgetContents)
+ self.pushButtonConfiguration.setStyleSheet("text-align: left; padding-left: 10px; padding-top: 5px; padding-bottom: 5px")
+ self.pushButtonConfiguration.setIcon(QtGui.QIcon(":/icons/process.png"))
+ self.pushButtonConfiguration.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonConfiguration.setObjectName("pushButtonConfiguration")
+ self.verticalLayout_OnlinePane.addWidget(self.pushButtonConfiguration)
+ spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ self.verticalLayout_OnlinePane.addItem(spacerItem1)
+ self.scrollArea.setWidget(self.scrollAreaWidgetContents)
+ self.horizontalLayout.addWidget(self.scrollArea)
+ MainWindow.setCentralWidget(self.centralwidget)
+ self.menubar = QtGui.QMenuBar(MainWindow)
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 862, 27))
+ self.menubar.setObjectName("menubar")
+ self.menuApplication = QtGui.QMenu(self.menubar)
+ self.menuApplication.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
+ self.menuApplication.setTearOffEnabled(False)
+ self.menuApplication.setObjectName("menuApplication")
+ MainWindow.setMenuBar(self.menubar)
+ self.statusbar = QtGui.QStatusBar(MainWindow)
+ self.statusbar.setObjectName("statusbar")
+ MainWindow.setStatusBar(self.statusbar)
+ self.actionQuit = QtGui.QAction(MainWindow)
+ self.actionQuit.setObjectName("actionQuit")
+ self.actionShow_Log = QtGui.QAction(MainWindow)
+ self.actionShow_Log.setObjectName("actionShow_Log")
+ self.actionLoad_Configuration = QtGui.QAction(MainWindow)
+ self.actionLoad_Configuration.setObjectName("actionLoad_Configuration")
+ self.actionSave_Configuration = QtGui.QAction(MainWindow)
+ self.actionSave_Configuration.setObjectName("actionSave_Configuration")
+ self.actionDefault_Configuration = QtGui.QAction(MainWindow)
+ self.actionDefault_Configuration.setObjectName("actionDefault_Configuration")
+ self.menuApplication.addAction(self.actionLoad_Configuration)
+ self.menuApplication.addAction(self.actionSave_Configuration)
+ self.menuApplication.addAction(self.actionDefault_Configuration)
+ self.menuApplication.addSeparator()
+ self.menuApplication.addAction(self.actionShow_Log)
+ self.menuApplication.addSeparator()
+ self.menuApplication.addAction(self.actionQuit)
+ self.menubar.addAction(self.menuApplication.menuAction())
+
+ self.retranslateUi(MainWindow)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "PyCorder", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonConfiguration.setText(QtGui.QApplication.translate("MainWindow", "Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
+ self.menuApplication.setTitle(QtGui.QApplication.translate("MainWindow", "File", None, QtGui.QApplication.UnicodeUTF8))
+ self.actionQuit.setText(QtGui.QApplication.translate("MainWindow", "Quit", None, QtGui.QApplication.UnicodeUTF8))
+ self.actionShow_Log.setText(QtGui.QApplication.translate("MainWindow", "Show Log", None, QtGui.QApplication.UnicodeUTF8))
+ self.actionLoad_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Load Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
+ self.actionSave_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Save Configuration ...", None, QtGui.QApplication.UnicodeUTF8))
+ self.actionDefault_Configuration.setText(QtGui.QApplication.translate("MainWindow", "Reset Configuration", None, QtGui.QApplication.UnicodeUTF8))
+
+try:
+ import resources_rc
+except ImportError:
+ from res import resources_rc
diff --git a/res/frmMainConfiguration.py b/res/frmMainConfiguration.py
index 692e263..ef1c299 100644
--- a/res/frmMainConfiguration.py
+++ b/res/frmMainConfiguration.py
@@ -1,44 +1,45 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'frmMainConfiguration.ui'
-#
-# Created: Wed Jun 05 12:00:50 2013
-# by: PyQt4 UI code generator 4.5.4
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore, QtGui
-
-class Ui_frmConfiguration(object):
- def setupUi(self, frmConfiguration):
- frmConfiguration.setObjectName("frmConfiguration")
- frmConfiguration.setWindowModality(QtCore.Qt.ApplicationModal)
- frmConfiguration.resize(861, 743)
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(":/icons/process.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- frmConfiguration.setWindowIcon(icon)
- self.gridLayout = QtGui.QGridLayout(frmConfiguration)
- self.gridLayout.setObjectName("gridLayout")
- self.tabWidget = QtGui.QTabWidget(frmConfiguration)
- self.tabWidget.setObjectName("tabWidget")
- self.tab1 = QtGui.QWidget()
- self.tab1.setObjectName("tab1")
- self.gridLayout1 = QtGui.QGridLayout(self.tab1)
- self.gridLayout1.setObjectName("gridLayout1")
- self.tabWidget.addTab(self.tab1, "")
- self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
- self.buttonBox = QtGui.QDialogButtonBox(frmConfiguration)
- self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
- self.buttonBox.setObjectName("buttonBox")
- self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1)
-
- self.retranslateUi(frmConfiguration)
- self.tabWidget.setCurrentIndex(0)
- QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), frmConfiguration.accept)
- QtCore.QMetaObject.connectSlotsByName(frmConfiguration)
-
- def retranslateUi(self, frmConfiguration):
- frmConfiguration.setWindowTitle(QtGui.QApplication.translate("frmConfiguration", "Configuration", None, QtGui.QApplication.UnicodeUTF8))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab1), QtGui.QApplication.translate("frmConfiguration", "Tab 1", None, QtGui.QApplication.UnicodeUTF8))
-
-import resources_rc
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'frmMainConfiguration.ui'
+#
+# Created: Wed Jun 05 12:00:50 2013
+# by: PyQt4 UI code generator 4.5.4
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_frmConfiguration(object):
+ def setupUi(self, frmConfiguration):
+ frmConfiguration.setObjectName("frmConfiguration")
+ frmConfiguration.setWindowModality(QtCore.Qt.ApplicationModal)
+ frmConfiguration.resize(861, 743)
+ frmConfiguration.setWindowIcon(QtGui.QIcon(":/icons/process.png"))
+ self.gridLayout = QtGui.QGridLayout(frmConfiguration)
+ self.gridLayout.setObjectName("gridLayout")
+ self.tabWidget = QtGui.QTabWidget(frmConfiguration)
+ self.tabWidget.setObjectName("tabWidget")
+ self.tab1 = QtGui.QWidget()
+ self.tab1.setObjectName("tab1")
+ self.gridLayout1 = QtGui.QGridLayout(self.tab1)
+ self.gridLayout1.setObjectName("gridLayout1")
+ self.tabWidget.addTab(self.tab1, "")
+ self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
+ self.buttonBox = QtGui.QDialogButtonBox(frmConfiguration)
+ self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
+ self.buttonBox.setObjectName("buttonBox")
+ self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1)
+
+ self.retranslateUi(frmConfiguration)
+ self.tabWidget.setCurrentIndex(0)
+ QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), frmConfiguration.accept)
+ QtCore.QMetaObject.connectSlotsByName(frmConfiguration)
+
+ def retranslateUi(self, frmConfiguration):
+ frmConfiguration.setWindowTitle(QtGui.QApplication.translate("frmConfiguration", "Configuration", None, QtGui.QApplication.UnicodeUTF8))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab1), QtGui.QApplication.translate("frmConfiguration", "Tab 1", None, QtGui.QApplication.UnicodeUTF8))
+
+try:
+ import resources_rc
+except ImportError:
+ from res import resources_rc
diff --git a/res/frmRdaClientOnline.py b/res/frmRdaClientOnline.py
index ba6548e..312716f 100644
--- a/res/frmRdaClientOnline.py
+++ b/res/frmRdaClientOnline.py
@@ -1,92 +1,92 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'frmRdaClientOnline.ui'
-#
-# Created: Wed Jun 05 12:00:50 2013
-# by: PyQt4 UI code generator 4.5.4
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore, QtGui
-
-class Ui_frmRdaClientOnline(object):
- def setupUi(self, frmRdaClientOnline):
- frmRdaClientOnline.setObjectName("frmRdaClientOnline")
- frmRdaClientOnline.resize(314, 136)
- frmRdaClientOnline.setFrameShape(QtGui.QFrame.Panel)
- frmRdaClientOnline.setFrameShadow(QtGui.QFrame.Raised)
- self.gridLayout = QtGui.QGridLayout(frmRdaClientOnline)
- self.gridLayout.setObjectName("gridLayout")
- self.groupBoxMode = QtGui.QGroupBox(frmRdaClientOnline)
- self.groupBoxMode.setFlat(False)
- self.groupBoxMode.setCheckable(False)
- self.groupBoxMode.setObjectName("groupBoxMode")
- self.gridLayout_2 = QtGui.QGridLayout(self.groupBoxMode)
- self.gridLayout_2.setObjectName("gridLayout_2")
- self.horizontalLayout = QtGui.QHBoxLayout()
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.pushButtonConnect = QtGui.QPushButton(self.groupBoxMode)
- self.pushButtonConnect.setMinimumSize(QtCore.QSize(150, 40))
- self.pushButtonConnect.setStyleSheet("text-align: left; padding-left: 10px;")
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(":/icons/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon.addPixmap(QtGui.QPixmap(":/icons/play_green.png"), QtGui.QIcon.Normal, QtGui.QIcon.On)
- self.pushButtonConnect.setIcon(icon)
- self.pushButtonConnect.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonConnect.setCheckable(True)
- self.pushButtonConnect.setAutoExclusive(True)
- self.pushButtonConnect.setAutoDefault(False)
- self.pushButtonConnect.setObjectName("pushButtonConnect")
- self.horizontalLayout.addWidget(self.pushButtonConnect)
- self.labelMessage = QtGui.QLabel(self.groupBoxMode)
- self.labelMessage.setAlignment(QtCore.Qt.AlignCenter)
- self.labelMessage.setObjectName("labelMessage")
- self.horizontalLayout.addWidget(self.labelMessage)
- self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
- self.horizontalLayout_2 = QtGui.QHBoxLayout()
- self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.comboBoxServerIP = QtGui.QComboBox(self.groupBoxMode)
- self.comboBoxServerIP.setEditable(True)
- self.comboBoxServerIP.setInsertPolicy(QtGui.QComboBox.NoInsert)
- self.comboBoxServerIP.setObjectName("comboBoxServerIP")
- self.comboBoxServerIP.addItem(QtCore.QString())
- self.horizontalLayout_2.addWidget(self.comboBoxServerIP)
- self.pushButtonAdd = QtGui.QPushButton(self.groupBoxMode)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.pushButtonAdd.sizePolicy().hasHeightForWidth())
- self.pushButtonAdd.setSizePolicy(sizePolicy)
- self.pushButtonAdd.setMaximumSize(QtCore.QSize(30, 16777215))
- self.pushButtonAdd.setObjectName("pushButtonAdd")
- self.horizontalLayout_2.addWidget(self.pushButtonAdd)
- self.pushButtonRemove = QtGui.QPushButton(self.groupBoxMode)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.pushButtonRemove.sizePolicy().hasHeightForWidth())
- self.pushButtonRemove.setSizePolicy(sizePolicy)
- self.pushButtonRemove.setMinimumSize(QtCore.QSize(0, 0))
- self.pushButtonRemove.setMaximumSize(QtCore.QSize(30, 16777215))
- self.pushButtonRemove.setObjectName("pushButtonRemove")
- self.horizontalLayout_2.addWidget(self.pushButtonRemove)
- self.gridLayout_2.addLayout(self.horizontalLayout_2, 0, 0, 1, 1)
- self.gridLayout.addWidget(self.groupBoxMode, 0, 1, 1, 1)
-
- self.retranslateUi(frmRdaClientOnline)
- self.comboBoxServerIP.setCurrentIndex(0)
- QtCore.QMetaObject.connectSlotsByName(frmRdaClientOnline)
-
- def retranslateUi(self, frmRdaClientOnline):
- frmRdaClientOnline.setWindowTitle(QtGui.QApplication.translate("frmRdaClientOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
- self.groupBoxMode.setTitle(QtGui.QApplication.translate("frmRdaClientOnline", "RDA Client", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonConnect.setText(QtGui.QApplication.translate("frmRdaClientOnline", "Connect", None, QtGui.QApplication.UnicodeUTF8))
- self.labelMessage.setText(QtGui.QApplication.translate("frmRdaClientOnline", "disconnected", None, QtGui.QApplication.UnicodeUTF8))
- self.comboBoxServerIP.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "RDA Server IP or Name", None, QtGui.QApplication.UnicodeUTF8))
- self.comboBoxServerIP.setItemText(0, QtGui.QApplication.translate("frmRdaClientOnline", "localhost", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonAdd.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "Add IP to List", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonAdd.setText(QtGui.QApplication.translate("frmRdaClientOnline", "+", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonRemove.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "Remove IP from List", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonRemove.setText(QtGui.QApplication.translate("frmRdaClientOnline", "-", None, QtGui.QApplication.UnicodeUTF8))
-
-import resources_rc
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'frmRdaClientOnline.ui'
+#
+# Created: Wed Jun 05 12:00:50 2013
+# by: PyQt4 UI code generator 4.5.4
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_frmRdaClientOnline(object):
+ def setupUi(self, frmRdaClientOnline):
+ frmRdaClientOnline.setObjectName("frmRdaClientOnline")
+ frmRdaClientOnline.resize(314, 136)
+ frmRdaClientOnline.setFrameShape(QtGui.QFrame.Panel)
+ frmRdaClientOnline.setFrameShadow(QtGui.QFrame.Raised)
+ self.gridLayout = QtGui.QGridLayout(frmRdaClientOnline)
+ self.gridLayout.setObjectName("gridLayout")
+ self.groupBoxMode = QtGui.QGroupBox(frmRdaClientOnline)
+ self.groupBoxMode.setFlat(False)
+ self.groupBoxMode.setCheckable(False)
+ self.groupBoxMode.setObjectName("groupBoxMode")
+ self.gridLayout_2 = QtGui.QGridLayout(self.groupBoxMode)
+ self.gridLayout_2.setObjectName("gridLayout_2")
+ self.horizontalLayout = QtGui.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.pushButtonConnect = QtGui.QPushButton(self.groupBoxMode)
+ self.pushButtonConnect.setMinimumSize(QtCore.QSize(150, 40))
+ self.pushButtonConnect.setStyleSheet("text-align: left; padding-left: 10px;")
+ self.pushButtonConnect.setIcon(QtGui.QIcon(":/icons/play.png"))
+ self.pushButtonConnect.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonConnect.setCheckable(True)
+ self.pushButtonConnect.setAutoExclusive(True)
+ self.pushButtonConnect.setAutoDefault(False)
+ self.pushButtonConnect.setObjectName("pushButtonConnect")
+ self.horizontalLayout.addWidget(self.pushButtonConnect)
+ self.labelMessage = QtGui.QLabel(self.groupBoxMode)
+ self.labelMessage.setAlignment(QtCore.Qt.AlignCenter)
+ self.labelMessage.setObjectName("labelMessage")
+ self.horizontalLayout.addWidget(self.labelMessage)
+ self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
+ self.horizontalLayout_2 = QtGui.QHBoxLayout()
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ self.comboBoxServerIP = QtGui.QComboBox(self.groupBoxMode)
+ self.comboBoxServerIP.setEditable(True)
+ self.comboBoxServerIP.setInsertPolicy(QtGui.QComboBox.NoInsert)
+ self.comboBoxServerIP.setObjectName("comboBoxServerIP")
+ self.comboBoxServerIP.addItem(QtCore.QString())
+ self.horizontalLayout_2.addWidget(self.comboBoxServerIP)
+ self.pushButtonAdd = QtGui.QPushButton(self.groupBoxMode)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.pushButtonAdd.sizePolicy().hasHeightForWidth())
+ self.pushButtonAdd.setSizePolicy(sizePolicy)
+ self.pushButtonAdd.setMaximumSize(QtCore.QSize(30, 16777215))
+ self.pushButtonAdd.setObjectName("pushButtonAdd")
+ self.horizontalLayout_2.addWidget(self.pushButtonAdd)
+ self.pushButtonRemove = QtGui.QPushButton(self.groupBoxMode)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.pushButtonRemove.sizePolicy().hasHeightForWidth())
+ self.pushButtonRemove.setSizePolicy(sizePolicy)
+ self.pushButtonRemove.setMinimumSize(QtCore.QSize(0, 0))
+ self.pushButtonRemove.setMaximumSize(QtCore.QSize(30, 16777215))
+ self.pushButtonRemove.setObjectName("pushButtonRemove")
+ self.horizontalLayout_2.addWidget(self.pushButtonRemove)
+ self.gridLayout_2.addLayout(self.horizontalLayout_2, 0, 0, 1, 1)
+ self.gridLayout.addWidget(self.groupBoxMode, 0, 1, 1, 1)
+
+ self.retranslateUi(frmRdaClientOnline)
+ self.comboBoxServerIP.setCurrentIndex(0)
+ QtCore.QMetaObject.connectSlotsByName(frmRdaClientOnline)
+
+ def retranslateUi(self, frmRdaClientOnline):
+ frmRdaClientOnline.setWindowTitle(QtGui.QApplication.translate("frmRdaClientOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
+ self.groupBoxMode.setTitle(QtGui.QApplication.translate("frmRdaClientOnline", "RDA Client", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonConnect.setText(QtGui.QApplication.translate("frmRdaClientOnline", "Connect", None, QtGui.QApplication.UnicodeUTF8))
+ self.labelMessage.setText(QtGui.QApplication.translate("frmRdaClientOnline", "disconnected", None, QtGui.QApplication.UnicodeUTF8))
+ self.comboBoxServerIP.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "RDA Server IP or Name", None, QtGui.QApplication.UnicodeUTF8))
+ self.comboBoxServerIP.setItemText(0, QtGui.QApplication.translate("frmRdaClientOnline", "localhost", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonAdd.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "Add IP to List", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonAdd.setText(QtGui.QApplication.translate("frmRdaClientOnline", "+", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonRemove.setToolTip(QtGui.QApplication.translate("frmRdaClientOnline", "Remove IP from List", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonRemove.setText(QtGui.QApplication.translate("frmRdaClientOnline", "-", None, QtGui.QApplication.UnicodeUTF8))
+
+try:
+ import resources_rc
+except ImportError:
+ from res import resources_rc
diff --git a/res/frmStorageVisionOnline.py b/res/frmStorageVisionOnline.py
index 2cdbee6..ef3c5b5 100644
--- a/res/frmStorageVisionOnline.py
+++ b/res/frmStorageVisionOnline.py
@@ -1,176 +1,176 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'frmStorageVisionOnline.ui'
-#
-# Created: Wed Jun 05 12:00:50 2013
-# by: PyQt4 UI code generator 4.5.4
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore, QtGui
-
-class Ui_frmStorageVisionOnline(object):
- def setupUi(self, frmStorageVisionOnline):
- frmStorageVisionOnline.setObjectName("frmStorageVisionOnline")
- frmStorageVisionOnline.resize(393, 222)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(frmStorageVisionOnline.sizePolicy().hasHeightForWidth())
- frmStorageVisionOnline.setSizePolicy(sizePolicy)
- frmStorageVisionOnline.setFrameShape(QtGui.QFrame.Panel)
- frmStorageVisionOnline.setFrameShadow(QtGui.QFrame.Raised)
- self.gridLayout_2 = QtGui.QGridLayout(frmStorageVisionOnline)
- self.gridLayout_2.setObjectName("gridLayout_2")
- self.groupBox = QtGui.QGroupBox(frmStorageVisionOnline)
- self.groupBox.setObjectName("groupBox")
- self.formLayout = QtGui.QFormLayout(self.groupBox)
- self.formLayout.setContentsMargins(-1, -1, -1, 5)
- self.formLayout.setObjectName("formLayout")
- self.verticalLayout = QtGui.QVBoxLayout()
- self.verticalLayout.setObjectName("verticalLayout")
- self.gridLayout = QtGui.QGridLayout()
- self.gridLayout.setContentsMargins(-1, -1, -1, 5)
- self.gridLayout.setHorizontalSpacing(6)
- self.gridLayout.setObjectName("gridLayout")
- self.label = QtGui.QLabel(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
- self.label.setSizePolicy(sizePolicy)
- self.label.setMinimumSize(QtCore.QSize(0, 34))
- self.label.setMargin(2)
- self.label.setObjectName("label")
- self.gridLayout.addWidget(self.label, 2, 0, 1, 1)
- self.label_3 = QtGui.QLabel(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth())
- self.label_3.setSizePolicy(sizePolicy)
- self.label_3.setMargin(2)
- self.label_3.setObjectName("label_3")
- self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1)
- self.label_4 = QtGui.QLabel(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())
- self.label_4.setSizePolicy(sizePolicy)
- self.label_4.setMargin(2)
- self.label_4.setObjectName("label_4")
- self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1)
- self.lineEditPath = QtGui.QLineEdit(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.lineEditPath.sizePolicy().hasHeightForWidth())
- self.lineEditPath.setSizePolicy(sizePolicy)
- self.lineEditPath.setMaximumSize(QtCore.QSize(16777215, 16777215))
- palette = QtGui.QPalette()
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
- self.lineEditPath.setPalette(palette)
- self.lineEditPath.setReadOnly(True)
- self.lineEditPath.setObjectName("lineEditPath")
- self.gridLayout.addWidget(self.lineEditPath, 1, 1, 1, 1)
- self.lineEditFile = QtGui.QLineEdit(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.lineEditFile.sizePolicy().hasHeightForWidth())
- self.lineEditFile.setSizePolicy(sizePolicy)
- self.lineEditFile.setMaximumSize(QtCore.QSize(16777215, 16777215))
- palette = QtGui.QPalette()
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
- self.lineEditFile.setPalette(palette)
- self.lineEditFile.setReadOnly(True)
- self.lineEditFile.setObjectName("lineEditFile")
- self.gridLayout.addWidget(self.lineEditFile, 0, 1, 1, 1)
- self.horizontalLayout = QtGui.QHBoxLayout()
- self.horizontalLayout.setSpacing(12)
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.lineEditDiskSpace = QtGui.QLineEdit(self.groupBox)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.lineEditDiskSpace.sizePolicy().hasHeightForWidth())
- self.lineEditDiskSpace.setSizePolicy(sizePolicy)
- self.lineEditDiskSpace.setMinimumSize(QtCore.QSize(80, 0))
- self.lineEditDiskSpace.setMaximumSize(QtCore.QSize(80, 16777215))
- palette = QtGui.QPalette()
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
- brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
- brush.setStyle(QtCore.Qt.SolidPattern)
- palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
- self.lineEditDiskSpace.setPalette(palette)
- self.lineEditDiskSpace.setFrame(True)
- self.lineEditDiskSpace.setAlignment(QtCore.Qt.AlignCenter)
- self.lineEditDiskSpace.setReadOnly(True)
- self.lineEditDiskSpace.setObjectName("lineEditDiskSpace")
- self.horizontalLayout.addWidget(self.lineEditDiskSpace)
- self.label_2 = QtGui.QLabel(self.groupBox)
- self.label_2.setObjectName("label_2")
- self.horizontalLayout.addWidget(self.label_2)
- self.progressBar = QtGui.QProgressBar(self.groupBox)
- self.progressBar.setEnabled(False)
- sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.progressBar.sizePolicy().hasHeightForWidth())
- self.progressBar.setSizePolicy(sizePolicy)
- self.progressBar.setProperty("value", QtCore.QVariant(0))
- self.progressBar.setTextVisible(True)
- self.progressBar.setInvertedAppearance(False)
- self.progressBar.setObjectName("progressBar")
- self.horizontalLayout.addWidget(self.progressBar)
- self.gridLayout.addLayout(self.horizontalLayout, 2, 1, 1, 1)
- self.verticalLayout.addLayout(self.gridLayout)
- self.pushButtonRecord = QtGui.QPushButton(self.groupBox)
- self.pushButtonRecord.setMinimumSize(QtCore.QSize(100, 40))
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(":/icons/record_grey.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon.addPixmap(QtGui.QPixmap(":/icons/record.png"), QtGui.QIcon.Normal, QtGui.QIcon.On)
- self.pushButtonRecord.setIcon(icon)
- self.pushButtonRecord.setIconSize(QtCore.QSize(32, 32))
- self.pushButtonRecord.setCheckable(True)
- self.pushButtonRecord.setObjectName("pushButtonRecord")
- self.verticalLayout.addWidget(self.pushButtonRecord)
- self.formLayout.setLayout(0, QtGui.QFormLayout.FieldRole, self.verticalLayout)
- self.gridLayout_2.addWidget(self.groupBox, 0, 0, 1, 1)
-
- self.retranslateUi(frmStorageVisionOnline)
- QtCore.QMetaObject.connectSlotsByName(frmStorageVisionOnline)
-
- def retranslateUi(self, frmStorageVisionOnline):
- frmStorageVisionOnline.setWindowTitle(QtGui.QApplication.translate("frmStorageVisionOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
- self.groupBox.setTitle(QtGui.QApplication.translate("frmStorageVisionOnline", "Data Storage", None, QtGui.QApplication.UnicodeUTF8))
- self.label.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Available\n"
-"Disk Space", None, QtGui.QApplication.UnicodeUTF8))
- self.label_3.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Path", None, QtGui.QApplication.UnicodeUTF8))
- self.label_4.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "File", None, QtGui.QApplication.UnicodeUTF8))
- self.label_2.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "[d:h:m]", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButtonRecord.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Record", None, QtGui.QApplication.UnicodeUTF8))
-
-import resources_rc
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'frmStorageVisionOnline.ui'
+#
+# Created: Wed Jun 05 12:00:50 2013
+# by: PyQt4 UI code generator 4.5.4
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_frmStorageVisionOnline(object):
+ def setupUi(self, frmStorageVisionOnline):
+ frmStorageVisionOnline.setObjectName("frmStorageVisionOnline")
+ frmStorageVisionOnline.resize(393, 222)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(frmStorageVisionOnline.sizePolicy().hasHeightForWidth())
+ frmStorageVisionOnline.setSizePolicy(sizePolicy)
+ frmStorageVisionOnline.setFrameShape(QtGui.QFrame.Panel)
+ frmStorageVisionOnline.setFrameShadow(QtGui.QFrame.Raised)
+ self.gridLayout_2 = QtGui.QGridLayout(frmStorageVisionOnline)
+ self.gridLayout_2.setObjectName("gridLayout_2")
+ self.groupBox = QtGui.QGroupBox(frmStorageVisionOnline)
+ self.groupBox.setObjectName("groupBox")
+ self.formLayout = QtGui.QFormLayout(self.groupBox)
+ self.formLayout.setContentsMargins(-1, -1, -1, 5)
+ self.formLayout.setObjectName("formLayout")
+ self.verticalLayout = QtGui.QVBoxLayout()
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.gridLayout = QtGui.QGridLayout()
+ self.gridLayout.setContentsMargins(-1, -1, -1, 5)
+ self.gridLayout.setHorizontalSpacing(6)
+ self.gridLayout.setObjectName("gridLayout")
+ self.label = QtGui.QLabel(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
+ self.label.setSizePolicy(sizePolicy)
+ self.label.setMinimumSize(QtCore.QSize(0, 34))
+ self.label.setMargin(2)
+ self.label.setObjectName("label")
+ self.gridLayout.addWidget(self.label, 2, 0, 1, 1)
+ self.label_3 = QtGui.QLabel(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth())
+ self.label_3.setSizePolicy(sizePolicy)
+ self.label_3.setMargin(2)
+ self.label_3.setObjectName("label_3")
+ self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1)
+ self.label_4 = QtGui.QLabel(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())
+ self.label_4.setSizePolicy(sizePolicy)
+ self.label_4.setMargin(2)
+ self.label_4.setObjectName("label_4")
+ self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1)
+ self.lineEditPath = QtGui.QLineEdit(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.lineEditPath.sizePolicy().hasHeightForWidth())
+ self.lineEditPath.setSizePolicy(sizePolicy)
+ self.lineEditPath.setMaximumSize(QtCore.QSize(16777215, 16777215))
+ palette = QtGui.QPalette()
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
+ self.lineEditPath.setPalette(palette)
+ self.lineEditPath.setReadOnly(True)
+ self.lineEditPath.setObjectName("lineEditPath")
+ self.gridLayout.addWidget(self.lineEditPath, 1, 1, 1, 1)
+ self.lineEditFile = QtGui.QLineEdit(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.lineEditFile.sizePolicy().hasHeightForWidth())
+ self.lineEditFile.setSizePolicy(sizePolicy)
+ self.lineEditFile.setMaximumSize(QtCore.QSize(16777215, 16777215))
+ palette = QtGui.QPalette()
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
+ self.lineEditFile.setPalette(palette)
+ self.lineEditFile.setReadOnly(True)
+ self.lineEditFile.setObjectName("lineEditFile")
+ self.gridLayout.addWidget(self.lineEditFile, 0, 1, 1, 1)
+ self.horizontalLayout = QtGui.QHBoxLayout()
+ self.horizontalLayout.setSpacing(12)
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.lineEditDiskSpace = QtGui.QLineEdit(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.lineEditDiskSpace.sizePolicy().hasHeightForWidth())
+ self.lineEditDiskSpace.setSizePolicy(sizePolicy)
+ self.lineEditDiskSpace.setMinimumSize(QtCore.QSize(80, 0))
+ self.lineEditDiskSpace.setMaximumSize(QtCore.QSize(80, 16777215))
+ palette = QtGui.QPalette()
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
+ brush = QtGui.QBrush(QtGui.QColor(240, 240, 240))
+ brush.setStyle(QtCore.Qt.SolidPattern)
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
+ self.lineEditDiskSpace.setPalette(palette)
+ self.lineEditDiskSpace.setFrame(True)
+ self.lineEditDiskSpace.setAlignment(QtCore.Qt.AlignCenter)
+ self.lineEditDiskSpace.setReadOnly(True)
+ self.lineEditDiskSpace.setObjectName("lineEditDiskSpace")
+ self.horizontalLayout.addWidget(self.lineEditDiskSpace)
+ self.label_2 = QtGui.QLabel(self.groupBox)
+ self.label_2.setObjectName("label_2")
+ self.horizontalLayout.addWidget(self.label_2)
+ self.progressBar = QtGui.QProgressBar(self.groupBox)
+ self.progressBar.setEnabled(False)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.progressBar.sizePolicy().hasHeightForWidth())
+ self.progressBar.setSizePolicy(sizePolicy)
+ self.progressBar.setProperty("value", QtCore.QVariant(0))
+ self.progressBar.setTextVisible(True)
+ self.progressBar.setInvertedAppearance(False)
+ self.progressBar.setObjectName("progressBar")
+ self.horizontalLayout.addWidget(self.progressBar)
+ self.gridLayout.addLayout(self.horizontalLayout, 2, 1, 1, 1)
+ self.verticalLayout.addLayout(self.gridLayout)
+ self.pushButtonRecord = QtGui.QPushButton(self.groupBox)
+ self.pushButtonRecord.setMinimumSize(QtCore.QSize(100, 40))
+ self.pushButtonRecord.setIcon(QtGui.QIcon(":/icons/record.png"))
+ self.pushButtonRecord.setIconSize(QtCore.QSize(32, 32))
+ self.pushButtonRecord.setCheckable(True)
+ self.pushButtonRecord.setObjectName("pushButtonRecord")
+ self.verticalLayout.addWidget(self.pushButtonRecord)
+ self.formLayout.setLayout(0, QtGui.QFormLayout.FieldRole, self.verticalLayout)
+ self.gridLayout_2.addWidget(self.groupBox, 0, 0, 1, 1)
+
+ self.retranslateUi(frmStorageVisionOnline)
+ QtCore.QMetaObject.connectSlotsByName(frmStorageVisionOnline)
+
+ def retranslateUi(self, frmStorageVisionOnline):
+ frmStorageVisionOnline.setWindowTitle(QtGui.QApplication.translate("frmStorageVisionOnline", "Frame", None, QtGui.QApplication.UnicodeUTF8))
+ self.groupBox.setTitle(QtGui.QApplication.translate("frmStorageVisionOnline", "Data Storage", None, QtGui.QApplication.UnicodeUTF8))
+ self.label.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Available\n"
+"Disk Space", None, QtGui.QApplication.UnicodeUTF8))
+ self.label_3.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Path", None, QtGui.QApplication.UnicodeUTF8))
+ self.label_4.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "File", None, QtGui.QApplication.UnicodeUTF8))
+ self.label_2.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "[d:h:m]", None, QtGui.QApplication.UnicodeUTF8))
+ self.pushButtonRecord.setText(QtGui.QApplication.translate("frmStorageVisionOnline", "Record", None, QtGui.QApplication.UnicodeUTF8))
+
+try:
+ import resources_rc
+except ImportError:
+ from res import resources_rc
diff --git a/res/resources_rc.py b/res/resources_rc.py
index 8014eaf..f5870a9 100644
--- a/res/resources_rc.py
+++ b/res/resources_rc.py
@@ -1,3689 +1,3718 @@
-# -*- coding: utf-8 -*-
-
-# Resource object code
-#
-# Created: Fr 13. Jan 16:46:39 2012
-# by: The Resource Compiler for PyQt (Qt v4.5.2)
-#
-# WARNING! All changes made in this file will be lost!
-
-from PyQt4 import QtCore
-
-qt_resource_data = "\
-\x00\x00\x10\x2a\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x30\x04\x7b\
-\x02\x60\x8a\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
-\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
-\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x0f\xb9\x49\x44\x41\x54\
-\x78\xda\xad\x5a\x5b\x8c\x5d\x57\x79\xfe\xf7\xe5\x5c\xe7\xcc\xcc\
-\xb9\x8c\x67\xc6\xe3\x4c\x73\xb3\x8b\x49\x4b\x0a\xc8\x40\xfd\x10\
-\x51\x11\xc1\x13\x52\x11\x02\x81\x78\xe1\x99\x48\xbc\x44\x28\xe5\
-\x01\xa9\x2f\x3c\x20\x84\x78\x41\x6a\x1f\xfa\x84\x84\x28\xa8\x11\
-\x12\x02\x41\x5b\x48\x42\x92\x4a\xae\x13\x92\xd0\xc6\x0d\x36\x18\
-\x27\xae\x3d\xf6\x8c\xe7\x3e\x73\xae\xfb\xda\xef\xfb\xd7\x5a\xe7\
-\xec\x33\x63\xa8\x13\x58\xa3\x3d\xfb\xbe\xd6\xf7\xdf\x2f\xfb\x78\
-\x2f\xbf\xf4\x92\x1c\x1d\x9e\xe7\x49\x10\x04\x92\xe7\xb9\x64\x59\
-\x26\xbe\xef\xeb\x9e\xe7\x3c\xe6\xfd\x34\x4d\xf5\x9c\x03\xf7\xca\
-\x78\xfe\xdd\x38\xfc\xab\x72\xb9\xfc\x08\xee\x3f\x84\xf3\x39\xec\
-\x67\xec\xfd\x5e\x92\x24\x07\x78\xfe\xda\x68\x34\x7a\x03\x97\xfe\
-\x0b\xe7\xbf\xc6\x5c\x91\x9b\x83\xf3\xba\xf5\x1c\x06\xae\xc1\xbd\
-\x9d\x43\xee\x36\x42\x79\x07\x83\x0b\x61\xe3\xbb\x8f\x55\xab\xd5\
-\x4f\xd5\xea\xf5\xc7\xeb\xf5\xfa\xe9\x5a\xb5\x1a\x94\x2b\x15\x29\
-\x95\x4a\x12\x90\x50\xdf\x2d\x9e\x2b\x98\x38\x8a\x64\x38\x1a\xc9\
-\xa0\xdf\x4f\x7b\xbd\xde\xd5\x7e\xbf\xff\xcc\x70\x38\x7c\x1a\x73\
-\xbd\x88\x2d\x79\x27\x58\xbc\xb7\x23\x01\x6e\xb8\x17\x62\x7c\x06\
-\x80\xbf\x38\xdf\x6c\x7e\x68\x76\x76\x56\x70\x3c\xf5\xfe\x28\x8a\
-\x25\x8e\x63\x80\x8d\xf4\xbc\x5a\x29\x4b\x19\x44\x95\xcb\xa5\x09\
-\x13\x30\xd7\x61\xb7\x2b\x07\xfb\xfb\xb2\xb7\xb7\x77\x11\x04\x7d\
-\x0b\x52\xf9\x3e\x09\xe1\xfa\xf7\x2a\x81\x7b\x26\x80\x93\x61\xff\
-\x31\x00\xfe\x6a\xa7\xd3\xf9\xc0\x7c\x73\x1e\xcf\x18\x01\x6e\xed\
-\xee\xc9\x9b\x37\x6e\xc9\x75\x6c\x77\xb6\x77\x64\xff\xb0\x2b\x11\
-\x08\x48\x53\xb3\x28\xa5\x41\xf0\xf3\xb3\x0d\x59\x5c\x68\xcb\x03\
-\xf7\xad\xc8\x03\xab\x2b\xb2\xd0\x6a\x1a\x82\x21\x95\x9d\x9d\x1d\
-\xd9\xda\xdc\x7c\xf9\xf0\xf0\xf0\x2b\x58\xf3\xdf\xdd\x9a\x7f\x34\
-\x01\x16\x78\x03\xba\xfd\xb5\xc5\xc5\xc5\x27\xda\x9d\x0e\x45\xa0\
-\x6a\x71\xe9\xca\x55\x79\xe5\xd2\xaf\x01\x7e\x4d\x7a\xfd\x01\xdf\
-\x94\x30\x0c\xa4\x84\x77\xf9\x7e\x71\x71\x6e\x31\xe6\x4a\x92\x94\
-\xfc\x97\x46\xbd\x06\x42\x4e\xc9\xb9\x47\x1f\x91\xbf\x78\xd7\xc3\
-\xe2\xe3\xd9\xc1\x60\x20\x77\xee\xdc\xc9\xef\x6c\x6c\xfc\x43\x14\
-\x45\x5f\xc6\x3b\xdd\x3f\x8a\x00\xbb\xf0\xd9\x66\xab\xf5\xed\x93\
-\xcb\xcb\x1f\x9c\x69\x34\xf4\xfe\xeb\x57\x7e\x27\xcf\x5f\x7c\x45\
-\x6e\xac\xad\x13\xb3\xd4\xa0\xf7\xd0\x7f\xa9\x58\x55\x09\x02\xdf\
-\xd8\x80\x5b\xdc\x4a\x32\xa1\x1d\x40\x32\xa3\x51\x2c\xfd\xe1\x50\
-\x06\xc3\x91\xde\x5f\x5d\x59\x92\xbf\xf9\xeb\x73\xf2\x1e\x10\xc2\
-\xb1\xbd\xb5\x25\x6b\xb7\x6e\xbd\xb4\xbf\xb7\xf7\x79\x9c\x5e\xe6\
-\x3c\x6f\x9b\x00\x8a\x10\xe3\x3c\xd4\xe5\xe9\xe5\x93\x27\x57\x20\
-\x01\xd9\x83\x6a\xfc\xe4\xd9\xff\x50\x02\xc8\xb1\xc6\x4c\x5d\xb7\
-\x3a\xc0\x87\xa5\x50\xaf\xfd\xa1\xe1\x3c\x0e\xf7\x71\x92\x80\x80\
-\xa1\x1c\xf6\xfa\xd2\xed\xf6\x21\x93\x5c\xfe\xf2\xcf\x1f\x96\x8f\
-\x3f\xfe\x98\xaa\x1a\x54\x49\xd6\x6e\xde\xbc\xb5\xbd\xbd\xfd\x29\
-\x80\xbf\xe0\xde\xbd\x27\x02\x38\xa0\x26\xe7\x4f\x9c\x38\xf1\x63\
-\x80\x6f\x53\x1a\x57\xdf\xba\x29\x3f\xfc\xf9\xf3\xb2\xbd\xbb\x2f\
-\xb3\x8d\x19\x99\xc3\x22\x54\x83\x30\xf0\x55\x75\x1c\xe1\x8e\xeb\
-\xde\x11\x62\xac\xe7\x3a\x46\x08\x55\xb4\x0b\xf5\xa3\xdd\x1c\x76\
-\x7b\xd2\x69\xcd\xcb\x27\x3e\xfa\x61\x39\xfd\xc0\x2a\xbd\x95\xfc\
-\xef\x8d\x1b\x3b\x9b\x77\xee\x7c\x1c\xcf\x5d\xf0\xee\xc2\xa0\xdf\
-\x27\x81\x77\x2d\x2d\x2d\x3d\x73\x72\x65\xe5\x14\xc1\xff\xf7\xe5\
-\xab\xf2\xa3\x9f\xbf\x20\x29\x16\x24\x77\x9a\xf0\x3c\xe5\x32\x0d\
-\xd8\x87\xa4\x0c\x68\x72\x9f\x6e\xd3\x93\xe3\x04\xe4\x06\xed\x78\
-\x9f\x29\x31\x99\xda\x91\x23\x2c\x82\xe7\xda\x03\xd7\xf7\x0f\xba\
-\x3a\xe7\xdf\x82\x88\x47\xcf\x9e\x16\xb8\x5a\xb9\xfe\xd6\x5b\x6b\
-\x9b\x9b\x9b\x8f\xe3\xed\x2b\xc7\x18\x7d\x17\xaa\x1a\x30\xd4\xef\
-\x2e\x2d\x2f\x2b\xf8\x4b\xbf\xf9\x9d\xfc\xe8\x99\x17\xc5\x87\xe1\
-\x2e\xcc\xcd\x2a\xe7\x79\x9d\x8b\xf8\x1e\x7d\xbd\x6f\xc0\x7b\x8e\
-\x90\x31\x17\x8a\xec\x1f\xef\x48\x06\xf7\x19\x08\xc8\x33\x43\x0c\
-\xa5\xc0\x39\x2b\xf0\x54\x55\xa8\xea\xee\xc1\x21\xa4\xfd\x82\xb2\
-\xe2\x3d\x20\x62\x75\x75\xf5\x14\x5c\xec\x77\x77\x77\x77\x3f\x8c\
-\x4b\xdd\x29\x02\x8e\x8a\xb9\xd1\x68\x7c\x7d\x79\x79\xf9\xfd\x0c\
-\x46\x6f\xde\xbc\x25\x3f\xfd\xc5\x05\x35\xcc\x36\x44\x3b\xd7\x68\
-\xa8\x81\x12\xb8\x12\x60\x0d\xb5\x48\xc0\x84\x21\x45\xc6\x4c\x54\
-\xc6\xd0\x62\x8c\x9a\x87\x24\x22\xa5\xc3\xa0\x44\xd2\x00\xeb\x34\
-\x25\x00\xb3\x76\xf7\xf6\xe5\x27\xcf\x5f\x50\x1b\x7b\x10\x2e\x77\
-\x65\x65\xe5\xfd\x08\x7a\x5f\x87\x44\x9e\x28\x32\xdd\x27\x08\x07\
-\x04\xa0\x3f\xb2\xb8\xb4\xf4\x85\x5a\xad\x26\x07\xd0\xc7\x7f\x7b\
-\xe1\x22\xae\xfb\xb2\xd0\x6e\x49\x1b\x7e\x9f\x84\x94\x42\x6c\x30\
-\xd8\x52\x58\xdc\x4a\xe3\x63\x75\xa3\x7a\x1c\x98\xe7\x8e\x3c\x5b\
-\x2e\xf1\x99\x50\xa3\xf5\xf8\x79\x5c\x2b\xdb\x6b\x8c\x17\x5c\xab\
-\x83\x35\x89\xe9\x5f\x5f\xf8\x4f\x60\xe9\x0b\xb4\x42\xe0\xc6\xbf\
-\x00\x49\x7d\xa4\x68\xd0\xa1\xf5\x36\xe4\x4e\xa5\xd9\x6c\x7e\x03\
-\x9b\x9e\xff\xe2\xe2\x6b\x6a\x5c\x1d\x70\xa4\xdd\x9c\xb3\x9c\x37\
-\x5c\x37\x69\x42\x41\xff\xa9\xf9\x96\x09\x64\xce\xc4\x0e\xa6\x34\
-\x68\xa2\x3e\x36\xaf\xca\xad\x3d\x98\x73\x04\xcc\xcc\x1c\x07\x7e\
-\xae\x44\x70\xd0\x69\x3c\x77\xe1\x15\xd8\xc4\x63\x02\xe6\xca\xc1\
-\xc1\xc1\x37\xa0\x4a\xe7\x81\x63\x34\xb6\x01\x4e\x04\xea\x3f\x09\
-\x97\xf9\x3e\x02\xbc\x7a\xfd\x26\x82\xd3\x6d\x69\xce\xcd\x61\xa2\
-\x59\xe5\x9a\xc6\x06\x82\x1e\xab\xd0\x44\x8d\x0c\x61\xd6\x80\xbd\
-\x89\x01\x3b\x42\xd4\x7c\x0b\xfa\x5f\x04\x9e\x59\xd0\x99\x23\x00\
-\xf6\x90\x82\x80\x00\xf3\x91\x71\xbc\x77\x0d\xaa\xfc\x5b\x60\x3a\
-\x73\xff\x7d\x02\xcf\xf8\xbe\x6e\xb7\xfb\x49\xd8\xc4\x3f\x73\x9d\
-\xd0\x4e\x16\x22\x45\x78\x92\x81\x8a\xe1\xff\xd5\xff\xf9\xad\x06\
-\xa5\xe6\x5c\x03\x79\x4c\x65\x1c\xd8\x02\x02\x0d\xfc\xb1\x14\x1c\
-\x78\xcf\x19\xb4\x67\xd8\xee\x80\x8f\xcd\xc0\x82\x2f\xba\x53\x67\
-\xc0\x2e\x4a\xa7\x96\x90\x94\x73\x71\x9f\x7a\x52\xc3\xfc\x4d\x38\
-\x8e\x18\xd1\xfb\x95\x4b\x57\xe4\x21\xd8\x42\xab\xdd\x96\xad\xad\
-\xad\x27\x11\x1f\xfe\x05\xeb\x27\x6a\xc4\x00\x77\x1e\x89\xd9\x39\
-\x02\x7d\x6b\xed\x96\xec\xec\x1f\xc0\x60\x4d\x90\x52\xe0\x16\xb0\
-\x46\x58\xf5\x40\x13\xee\x87\xf6\x7c\x62\xaa\x77\x1f\xde\xf4\x3f\
-\xe3\x4e\x99\x5e\x20\xa0\xa9\x21\x3b\x42\x00\x3c\x01\x13\x7d\x2f\
-\xd3\x6b\x8d\x99\x9a\x26\x87\xc4\x44\xad\x38\x7d\xff\x29\x81\x9a\
-\x9f\x43\x02\x78\x9e\x59\xac\xaa\x50\xbd\x56\xfb\xec\xcc\x8c\xa6\
-\xee\x50\x9f\x35\x75\x65\x0d\x64\x98\x65\x8d\xae\x06\x38\x8d\xcd\
-\x10\x62\x00\x7b\x56\xff\x0f\x90\x51\xf6\xc3\x81\x24\xcb\x23\x49\
-\x67\xb2\xa3\x8c\x1f\x83\x2d\xaa\x91\x5e\x4a\x72\x69\xad\xb5\xa5\
-\x59\x6e\x2a\x01\xbe\x7a\xa1\xcc\x32\x27\x55\x22\x3c\xcd\xc3\x3c\
-\x99\x05\x23\x99\x1c\x52\xb5\x49\xc0\xfc\xfc\xbc\xc0\xd1\x7c\x16\
-\x1e\xe9\x45\x4a\xa0\x0c\xf0\x1f\xa5\x67\xd8\x87\xe7\xd9\xde\x3b\
-\x90\xfb\x90\x9b\x04\x56\x3d\xc8\x61\x07\xde\xa9\x4f\xc1\x85\xc9\
-\x77\xee\xff\x27\xb9\x78\xf8\x9c\xa4\x8d\x81\x04\xe5\x40\xca\xd9\
-\x8c\x94\x3c\x93\x36\xc7\x19\xf2\x1e\xaf\x27\x19\x54\x20\xec\x55\
-\x25\x8f\x01\x74\x50\x92\x0c\xfb\xea\x8d\x79\x39\xf3\xe8\x19\x79\
-\xaa\xfb\xf7\x92\xa0\x14\x70\x76\xa5\xa0\xd5\xa6\x20\x1d\x4d\xa9\
-\x13\x44\x7e\xa4\x2b\x88\xfa\x1b\x9b\xdb\xea\x1d\x1b\x60\x36\x31\
-\x23\xf9\x2b\xfb\x7e\x10\x3c\x82\x82\xe4\x41\x2e\xb8\xb1\xb5\xa3\
-\x2f\x2f\xb4\x9b\x32\x8f\x74\x81\xe0\xe9\xe2\x0c\x11\xe1\x14\x78\
-\x37\x86\xcd\x81\xe4\x7e\x36\x36\x5c\x90\x2a\x95\x1c\x29\x06\xfe\
-\x82\x1c\xa0\x32\x80\xc2\x96\x79\x09\xc0\xe1\x99\x04\x92\xc3\x96\
-\x06\xa9\x44\x95\xe8\xd8\x7c\xaa\xb2\xa1\x59\x93\x1a\xc0\x73\xc6\
-\x9f\x13\xc0\x44\x6c\xeb\xc4\x88\x6b\x20\xe0\x41\xe0\x79\x04\x01\
-\x36\x7c\x6f\xa5\x52\x09\x9d\xcb\xaa\xd5\xaa\xea\x9f\xa9\x7b\x95\
-\x4a\x49\x73\x9d\x00\x12\x90\x23\x01\x9b\xa7\x9c\x30\xc8\xe0\xa1\
-\x7c\x63\xbc\x3c\x26\x27\x79\x13\xa4\xe3\x1f\xae\x79\x65\xec\xa1\
-\xe3\x95\x58\x12\x02\x0e\x73\x66\x20\x6a\xf0\xb5\xb8\x6a\x5c\xef\
-\x11\x22\xa8\x36\x2a\x75\xac\x4d\x75\xa6\x0a\x91\x81\x50\x75\xd9\
-\xda\xd9\xd7\x67\xc0\x74\xd0\x18\xbc\xd7\xaf\x94\xcb\x67\x19\xa0\
-\xe8\xce\xba\xfd\xa1\xcc\x40\x54\x81\x35\x50\x1e\x7b\xd0\xf9\x63\
-\x06\x69\xe3\x81\x26\x63\x1e\x8d\x50\xa4\xb2\x5b\x57\x5f\x1e\xe7\
-\xb1\x0c\xa4\x07\x1d\x4e\x60\x84\x48\x9f\xbd\xa1\x71\xd3\xc3\xda\
-\xb1\x79\x22\x3f\x51\xfb\x50\xd5\x39\xbe\x88\x72\x9f\x8e\x84\x04\
-\x91\x98\x99\x7a\x55\xba\xbd\x81\xce\x07\xa6\x23\xe8\x95\xcf\x86\
-\x08\x40\x0f\x91\xc3\x51\x9c\x6a\xbe\x3e\x5b\xab\x8e\xd5\x01\xc4\
-\xe9\xc3\xf4\x02\x8e\x53\xee\x1e\xbd\xc7\xa8\x34\x02\xc0\x91\x04\
-\x71\x09\xaa\xd4\x13\x0f\xcc\x4d\xa1\xf3\xa5\xac\x8a\xeb\x91\x01\
-\x0f\x63\xcc\xd3\x5c\x12\x01\xf7\x63\xcc\xb9\x33\x63\xbc\x0e\x9e\
-\x8d\x40\x6a\x1f\x35\x01\x19\x46\x95\xe1\xcc\x59\x21\x53\xad\xd8\
-\xc8\xac\x55\x21\xee\x12\x34\x33\x56\xba\x7a\x32\x9d\xcd\x03\xd6\
-\xb7\x73\x94\xb9\x89\x80\xbe\xa6\x00\x2e\xfc\x70\xd4\xf1\x12\x6b\
-\x09\x72\x94\x2a\x93\xda\xcc\x91\xc4\x46\xb5\x91\xf8\xc8\x5f\x72\
-\x14\x59\xe1\xa8\x22\x49\x40\xc0\xb8\xee\xf7\xcd\x0c\x99\xf1\xf7\
-\xe4\x3e\x0d\x98\x9e\x27\x2d\x81\x70\x78\xad\xd2\x61\x43\xf2\xc0\
-\x44\xe5\x18\xcc\x53\x9d\x07\xd8\xc0\xae\x41\x35\xae\x55\x2b\x63\
-\x82\x08\x8a\xe9\x06\x31\x24\x90\xac\x62\x2d\x95\xe6\x98\x4a\xd4\
-\x0d\x47\x53\x9b\x13\x39\x43\xf5\xc6\xa2\xa4\x3d\xb0\xdc\x23\xb7\
-\xa2\x38\x51\xee\xf0\x6e\x25\x2e\x03\x34\x08\x1b\x85\x92\xc2\xcd\
-\x05\xdd\xaa\xda\x43\x5c\x1d\xe8\xab\x04\x4e\xce\x13\x7c\x30\x2a\
-\xab\xf7\x89\xca\x43\x09\x86\x25\xb2\x5a\x68\x26\x04\x44\xe7\x4b\
-\x86\xa4\xc3\x4c\x0d\x97\xc0\x59\x24\xc9\x91\x22\xc6\xa9\x12\x09\
-\x2e\xc1\x3e\x89\x7d\xca\xad\x84\x85\x3a\x76\x32\x72\x7d\xb1\x6e\
-\x55\x8b\xe2\x53\x55\xf2\x35\x67\x30\x59\x64\x9c\x19\x50\xc8\x4e\
-\x92\x3c\x12\x6f\x10\xe8\xc6\x63\xe5\x3c\xb6\xcc\x4a\xa0\xdc\xaf\
-\x4b\xa9\x5f\x13\x3f\x81\x67\x1b\x95\x4c\x3c\xb1\x39\xb8\xce\xcd\
-\xb8\x54\xad\xea\xb5\x63\x81\xd1\xba\x75\x42\x74\xf7\x98\x4a\xa8\
-\xbc\xcb\x25\xe3\x32\x8b\xc0\xdd\x48\x21\x1d\x8a\xba\x85\xb0\x4e\
-\xbd\x3c\x44\x91\x91\xe7\x36\x30\x51\xf5\x00\x24\x4d\x46\x70\x93\
-\xc8\x28\xe3\xf0\x28\xfd\x50\x7d\x10\x05\xf0\x7e\x14\x22\x26\xe0\
-\xf9\xcc\x38\x06\x2f\xf5\x4d\x90\xcb\x0d\x93\x18\xfd\xeb\x95\xaa\
-\x16\xff\x41\x6e\xd2\x74\x07\xc3\x09\xc3\x65\xb1\x36\x25\xe9\x87\
-\xec\x98\xa9\xff\xf5\x03\xdd\x5c\x95\xa4\xbe\xce\x1a\x95\xaa\x17\
-\x8d\x98\xf5\x02\xbc\x42\xad\x5a\x46\x2d\x3b\x80\x09\x0e\x25\xf6\
-\x23\xf1\x87\xa1\xc4\xd1\xc0\x70\xed\x48\xa8\xa0\x81\xd7\xe2\x1a\
-\xf4\x3a\x96\x70\x00\xb5\x28\xd4\xe6\x89\x1f\x2b\x3e\x72\x9c\xf3\
-\xd2\x65\x27\x49\xa6\x4c\x89\x69\x57\x61\x30\xc5\x09\xe2\x32\xee\
-\x35\x00\xc3\x60\x87\xc0\x4e\x52\xae\x69\xeb\x24\xf0\x55\x02\xf4\
-\x38\x54\x4f\xdf\x26\x5c\x71\x9a\x8c\xc5\xe7\x32\x49\x72\xe1\x44\
-\xdb\x18\xd8\xec\x68\x5e\xf2\xfe\x86\x94\xba\x35\xa3\x62\xa5\xd8\
-\xd1\xae\x60\x7d\x48\x84\x6a\x26\xe4\x7a\x01\xbc\x17\x07\x52\x2f\
-\xd7\x64\x79\xa1\x63\x9c\x03\x0c\xd3\xf4\x91\x72\xb5\x3f\xbe\xc3\
-\x0e\x06\xfd\xbf\x4e\x65\xbb\x7b\xf4\x8c\xb4\x83\xd1\x50\x6d\xf1\
-\x5a\x18\x45\xd1\x65\x50\xa2\x2e\x6a\xa6\x56\x91\xde\x60\xa8\x49\
-\x54\x89\xc6\x42\xc3\xa2\x38\xfd\xc0\x88\xd0\xcb\xc7\xcd\x2e\x3a\
-\xd5\x2a\x8c\xad\xe1\xcf\x29\xa1\x41\x64\xd2\x87\x2c\xcd\x27\x3e\
-\x1d\x6a\x40\x09\x04\x30\x72\x46\xe3\xa4\x06\x9b\xf0\x8d\x2e\xa4\
-\x88\x01\xb5\x52\x5d\xbb\x76\x6c\x37\x1a\xc9\xdb\x6c\xd5\xd4\x6a\
-\x9a\x0f\x01\xb6\x4a\x26\x55\x02\x32\xc5\xc8\xf9\x23\xbc\x03\x02\
-\x2f\x87\xb0\xfe\x5f\x81\x88\x84\xd1\x78\x0e\xe9\xc3\xc6\xf6\x9e\
-\x82\xce\x20\x2a\x12\x66\xd4\x30\x2f\xe4\xf2\x32\x5e\x88\x13\x32\
-\x8f\xe1\x7a\x7e\x6c\xc4\x5d\xb6\xfb\x50\x3d\xcd\xb4\x43\x08\x7b\
-\x15\x49\xab\xb1\xa1\x0d\x36\xc1\x5c\x89\x73\xb8\x52\x33\x1f\x17\
-\xfe\x13\x13\x24\x06\xdf\x2f\x19\x4c\x20\x82\x18\x39\x50\x5e\x02\
-\x7a\xfa\xab\x10\x05\xc4\x1b\x70\x91\x6f\xa2\x1e\x38\xd3\x44\xc1\
-\x4e\x2b\x8f\xe1\x2a\xe9\x67\x53\x1b\x1b\xc6\xd5\x93\xda\x01\xd3\
-\x5e\xba\x5b\xd3\x59\x48\x60\xa2\xd4\xfd\xac\x94\x4e\x81\x8d\x8e\
-\x9c\x1f\x1d\x39\xe9\xf3\xb3\x42\x77\x22\x53\x26\x69\x8d\x90\x67\
-\x63\x62\x88\x01\xfc\x55\x4c\x94\x04\x6b\x14\x3e\xdb\xeb\xf5\xde\
-\x84\x26\xbc\xc1\x74\x3a\x02\x01\x3f\xc3\xc9\x99\x3a\xc4\x43\x4f\
-\xc0\x66\x93\xaf\xdd\x35\xe3\xca\xc6\xdc\xb1\x44\xf8\xb6\xa2\x62\
-\x8a\xfb\xe4\xd6\xdf\xc9\xe1\x99\x03\xa9\xa5\x35\x6b\x2a\xb6\x2a\
-\x93\xe3\x7d\x21\xb7\xe7\x36\x5c\x1c\x49\xe3\x7a\x43\xa2\xb9\x64\
-\x52\xe4\xb8\xa2\xdf\x82\x77\xef\x44\x90\x42\x04\xa3\x25\xf7\xeb\
-\x50\x5b\xb6\x5a\xb0\xfd\x8c\xb7\x98\x5a\xb1\x81\xf4\xbd\xe1\x60\
-\xf0\x44\x1d\x69\xea\x52\xa7\xa5\x1d\x38\x72\x85\xc6\x45\x8f\x90\
-\x33\x0b\xcd\x6d\x64\x25\xe7\x3d\xdb\x4d\xc0\xc5\x66\x6b\x5e\x16\
-\x82\xb6\x6d\xb1\x78\xd3\x75\xb1\x37\x51\x85\xb1\x0a\xb2\x68\xb1\
-\x60\xb5\xe5\x0e\xce\x66\xae\x3a\xb3\xfd\x22\xd3\x76\x99\x10\xe4\
-\xaa\xb6\xa5\x13\x2d\x9d\xab\x7b\x78\xc8\xc0\xfa\x3d\x2d\x29\x3d\
-\xa3\x67\x17\x50\x2c\xff\x12\x04\x9c\xeb\xb4\xe6\x64\x66\xa3\xaa\
-\xde\xc8\xf7\x2b\xba\xb4\xa7\x93\xb3\x78\xb7\xa5\xa0\x6f\x5c\x0c\
-\x53\x65\x46\x45\xd7\x45\xf6\x6c\x39\xa9\xfe\xdb\x12\x32\x09\xa6\
-\xf9\xd8\x45\x67\x76\x1e\xe3\x10\x8c\xca\x64\x8e\x41\x8e\x10\x5b\
-\xf8\x67\xb6\x0d\x39\x83\x40\xba\x80\x42\x9f\x36\x81\x6a\xec\x97\
-\xec\xd4\xd9\x8e\x8a\x96\x89\xc9\x61\xb7\xfb\xcd\xd1\x70\xa8\x3a\
-\xbf\xba\xbc\xa8\x5d\x64\xfd\x0a\x63\x63\x81\xe3\x48\x96\xbb\x42\
-\xdc\x76\x13\xb4\x0c\xcc\xa7\x8a\xf3\x24\x33\xef\x26\xd6\x8b\x8d\
-\x8f\xed\xfd\xe2\xfb\xfa\x67\x25\x33\xc5\x7d\xbb\xae\xbe\x0f\x2c\
-\xab\xcb\x27\x34\xd8\x81\xd1\x82\xa2\xfe\x9b\x70\xaf\x89\x96\xb7\
-\x4e\x6f\xe3\x28\xfa\x01\x28\x7b\x8d\xe7\x27\xda\xf3\xd2\x69\xce\
-\xc2\x78\xa2\x09\xd7\x74\x71\xc7\xb9\x6c\xcc\x49\x02\xc8\xa7\x40\
-\x99\x6d\x5c\xe3\xda\xcd\x01\x26\xb1\xb9\x23\xc4\x4a\x60\x72\x2e\
-\xe3\x16\x8b\xbb\xc7\x58\x40\x2c\x2c\x68\xc8\xfd\xed\xad\xad\xd7\
-\x70\xed\x07\x2e\xe5\xf1\x9d\x7e\x01\xd0\x68\x77\x6f\xef\x4b\xfd\
-\x5e\x4f\x6f\xb0\x03\xc0\x56\x1f\x27\x70\x44\xa4\x4e\x7f\x1d\xf7\
-\x52\x03\x28\xd5\xc5\x52\x0b\x30\x1b\xb7\x4b\x26\x92\xc9\xed\x75\
-\x23\x41\x47\x5c\x96\xb9\xee\x44\xa6\x35\x45\xea\xda\x2d\xb9\x09\
-\x5c\x04\xcc\xe4\x8e\x58\x38\x76\xb6\xb7\xa9\x3e\x5f\x22\x56\x87\
-\xdb\x2f\x76\x8d\x93\x38\x7e\x76\x73\x73\xf3\x1f\xf9\x22\x03\xcc\
-\xc3\x78\x51\x8b\x14\x9c\x67\x85\x7e\x4e\x3a\x65\x70\x96\xd3\x96\
-\xc0\x2c\x77\xe0\x52\xcb\xd5\xa3\xc7\x79\xc1\x68\x2d\xf3\xb4\x3e\
-\xc8\xac\x24\xf2\x31\x78\xce\xfd\xf0\x9f\x9d\x52\x2c\x70\x9b\xfc\
-\xf8\x41\x6c\xcf\x16\xbd\x99\x5f\xec\x69\xd2\x1e\xf0\xe0\x53\xdb\
-\xdb\xdb\xaf\xf2\x66\x6b\x7e\x56\xa9\x67\x61\xad\x92\xb0\xc6\x55\
-\xec\xe5\xa4\x05\x5b\x50\xe2\xb2\xe2\xe6\x54\x68\x72\x9e\x4d\x11\
-\x59\x60\xc8\x11\xf0\x5c\x93\x6b\xb7\xe7\x8d\x2a\xaf\xdf\xbe\xfd\
-\x2a\x5c\xe7\x53\xfe\xb8\x03\x68\xb6\xf0\x2e\x1f\x0e\xba\xd0\xb3\
-\xcf\x21\xe3\x7b\xa6\xdd\xe9\x9c\xa2\x5b\xa5\xb6\x5d\xbb\x79\xdb\
-\x14\x1e\x21\xdb\x2a\xa4\x7e\xe2\x6d\xd4\xdd\xea\xa1\x99\xeb\x0f\
-\x35\xb6\xa4\x10\x71\xb3\x82\xff\xcf\xac\xcb\x4c\xe8\x56\x2d\xe7\
-\xb9\x36\x8d\x78\x7d\x7d\x7d\x6d\x67\x67\xe7\x73\xc4\x76\x34\xdd\
-\xbf\xeb\x67\x56\x4c\x78\x65\x6b\x6b\xeb\xd3\x28\x37\x7f\xdc\x6a\
-\xb5\xda\x8b\x98\x88\x29\xec\xb5\x1b\xeb\xfa\xa9\x34\x2c\xb1\x98\
-\x70\x18\xc9\xc1\x42\x77\x5a\x0a\x9d\xf5\x7c\x6a\xce\x49\x20\x2b\
-\xa8\x80\x6b\xb9\xd3\x9e\xa8\xaa\xe5\xb0\x24\x0f\xae\x9e\x52\xce\
-\x2b\xf8\xdb\xb7\x77\xc0\xd0\x4f\x13\xd3\xdd\xb0\x7a\x97\x5e\x7f\
-\xfd\xf8\x45\x6d\x21\x6a\x03\xeb\xfc\x42\xa7\xf3\x74\xab\xdd\x5e\
-\xe1\x35\x7e\x36\xbd\xbe\xb6\x2e\x7b\x07\x5d\x6d\xe6\xea\x87\x3c\
-\xdf\x3b\x56\x2f\x7b\x93\x89\x1c\xfa\x29\x7a\x8a\xb9\x8f\xaa\x4d\
-\x62\x6c\x84\x69\xc2\x03\xa7\x96\x55\xe7\xa9\xb2\x1b\xeb\xeb\xb7\
-\xc0\x48\xfd\xc4\x54\xfc\xb0\x7e\x4f\x04\x04\x2e\x8d\x4d\xd3\xb3\
-\xed\x76\xfb\xdb\x9d\x85\x85\x0f\xba\xd4\x76\x6b\x77\x5f\x6e\xdf\
-\xd9\xd6\x12\xd3\xb3\xfd\x52\xcf\xf6\x46\x3d\x07\x3c\x9f\x56\x21\
-\xf1\x8e\x48\xc1\xd9\x0e\x8e\x99\x1e\x9c\x5c\xec\xc8\x42\xcb\x74\
-\xa4\xf9\x69\x69\x7d\x63\xe3\xa5\xdd\x9d\x9d\xcf\x63\xfe\xcb\x62\
-\x82\xed\xdb\x27\x40\xf3\x7b\x66\x83\x41\xd0\xa8\x55\xab\x5f\x03\
-\x11\x4f\xcc\xcc\xcc\x28\x2c\x2e\xbe\xb3\x7f\xa8\xc4\xb0\x0d\xaf\
-\xb9\xbc\x37\xf9\xdc\x34\xd5\x07\xb5\x6d\x45\xa7\xf3\x64\x3f\x73\
-\x7a\x7e\x63\x63\x74\x65\x07\xdc\x7d\x4c\x87\x9b\xd4\xcf\xac\xa3\
-\xd1\xe8\xcb\xe0\x7a\xd7\xb5\x6f\xde\x31\x01\x99\xfd\xfc\x43\x0f\
-\x02\x60\x1f\x9b\x9b\x9f\xff\x6a\xb3\xd9\xfc\x40\x95\x45\xb7\x1d\
-\xfc\x5c\x7a\xc0\xaf\x8d\x20\x84\xc7\xac\xe0\xdc\xb7\x30\x0e\x67\
-\xec\xec\x78\xb0\x60\x27\xf0\x59\xfd\xba\x59\x19\x4b\x04\xd1\x95\
-\x7e\xfe\x65\x44\xda\xaf\x00\xb8\x7e\xe8\x36\xa9\xf4\x9f\x88\x00\
-\xe7\x42\xf9\x53\x03\x54\x64\x9f\x41\xde\xf4\x45\xa4\xe0\x1f\x22\
-\x21\xbc\xef\x46\xb1\xef\xcf\x45\x39\x4c\x5b\x72\xf2\x2d\xc1\x0d\
-\xea\x39\x33\xcb\x83\xfd\xfd\x8b\x48\x65\xbe\x85\x38\xf4\x7d\xf7\
-\x9b\x09\xae\xfd\x27\x27\xc0\x75\xd1\x38\x20\x11\xa6\xe2\x8f\xd5\
-\xf8\x63\x8f\x5a\xed\xf1\x6a\xad\x76\xba\x5c\x2e\x07\x04\xcb\x7c\
-\x4a\xbc\xe3\xe9\x74\x3a\xfe\xd0\x3d\x4a\xa1\xe7\xfa\x63\x0f\x64\
-\x95\xfa\x63\x0f\xcc\x95\x38\xd5\xd4\xb6\xcd\x3d\x12\xf0\x8e\x7e\
-\xad\x62\x5d\x26\x67\x7c\x6e\x30\x1c\x3e\x87\xe0\x47\xe4\xef\x06\
-\x77\xf5\xe7\x36\xc2\x9f\xdb\xf8\xfe\x1c\x3c\x95\xf9\xb9\x4d\x9a\
-\xf6\x00\xfe\x20\xb3\x3f\xb7\x81\x24\xc7\x3f\xb7\x29\xcc\x77\x57\
-\x80\xff\xdf\xf8\x3f\xb6\x68\x91\x32\x93\x47\xe0\x2e\x00\x00\x00\
-\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x10\x32\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
-\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
-\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
-\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
-\x46\x00\x00\x0f\xb8\x49\x44\x41\x54\x78\xda\xac\x5a\x4b\xac\x1c\
-\x57\x99\xfe\xfe\x73\x4e\x3d\xba\xaa\xdf\x7d\x5f\xbe\xce\x1d\x27\
-\x8e\x99\x38\x61\x78\x2a\xc0\x44\xa3\x28\x08\x13\x34\x8b\xcc\x30\
-\x42\x41\x20\x36\xac\x89\xc4\x06\x21\xc4\x82\x25\x0b\x84\x10\x1b\
-\x24\x58\xb3\x61\x40\x13\x21\x21\x22\x66\x06\xf2\x20\xc9\x48\x1e\
-\x27\x38\x61\x26\x9e\x60\x07\xc7\x8e\xe5\xf7\xbd\x7d\xef\xed\xdb\
-\xb7\xbb\xfa\x51\xe7\x9c\x7f\x16\xe7\x54\x75\xf7\xb5\x9d\x71\xc2\
-\x94\x74\xba\xab\xbb\xab\xfb\xfc\xcf\xef\xff\xfe\xbf\x9a\x5e\x7d\
-\xe5\x15\x1c\x3c\x88\x08\x52\x4a\x30\x33\xac\xb5\x10\x42\xc0\x5a\
-\x0b\x66\x86\x10\x02\x44\x04\x63\x0c\x98\x19\x00\x60\xad\x0d\xa5\
-\x94\x0f\x02\xf8\x48\x18\x86\x0f\x11\xd1\x51\x29\x65\x9d\x88\x52\
-\xff\xf9\x50\x6b\xdd\x67\xe6\x0b\x93\xc9\xe4\x4d\x00\xff\xa5\xb5\
-\xfe\x93\x10\x62\x5a\xfc\x86\x10\xa2\xdc\xaf\x90\xc1\x18\x03\x22\
-\x2a\xf6\xc0\xed\x0e\x85\xf7\x71\x30\x33\x98\x59\x01\x78\x34\x8e\
-\xe3\x27\x2b\x49\x72\x22\x49\x92\x63\x95\x38\x96\x61\x14\x21\x08\
-\x02\x48\x21\x40\xa2\xd8\x9c\x61\x8c\x41\x3e\x9d\x62\x3c\x99\x60\
-\x94\x65\x66\x38\x1c\x9e\xcf\xb2\xec\xb9\xf1\x78\xfc\x34\x33\xbf\
-\xcc\xcc\xfa\xfd\xc8\x42\xef\xc5\x03\xd6\x5a\x10\x91\x52\x4a\x7d\
-\x29\x49\x92\xaf\x37\x9a\xcd\x4f\xd5\x6a\x35\x24\x49\xb2\xf0\xfd\
-\xc9\x34\x47\x9e\xe7\x18\x4f\xa6\x00\x80\x38\x0a\x11\x06\x01\xc2\
-\x30\x98\x19\xc1\x5a\xec\x0f\x06\xe8\xef\xed\xa1\xd7\xeb\x9d\x1a\
-\x0e\x87\x3f\xd2\x5a\xff\x82\x99\x35\x11\xdd\xb5\x07\xee\x5a\x01\
-\x63\x0c\x84\x10\x9f\xab\xd5\x6a\xdf\xed\x74\x3a\x9f\x68\x34\x1b\
-\x90\xd2\x39\xb0\xbb\xdb\xc3\xc5\xcb\xd7\x70\xe9\xf2\x35\x6c\x6e\
-\xef\x60\x6f\x7f\x80\x69\x9e\xc3\x18\xb7\xa9\x14\x02\x61\x18\xa0\
-\x51\xab\x62\x65\xa9\x8d\x7b\xef\x59\xc7\xbd\x1b\xeb\x58\x6a\x35\
-\x9d\xc2\x93\x09\x76\x76\x76\xd0\xdd\xda\x7a\x75\x7f\x7f\xff\x3b\
-\xd6\xda\xdf\x16\x7b\xfe\xc5\x0a\x78\xc1\xab\x61\x18\x7e\x6f\x65\
-\x65\xe5\xa9\x76\xa7\x43\x4a\x29\x58\xcb\x38\x73\xee\x3c\x4e\x9f\
-\xf9\x13\x2e\x5e\xbe\x8a\x61\x36\x02\x40\x50\x4a\x22\x90\x12\x52\
-\xca\x85\xcd\xad\xb5\xc8\x8d\x81\xd6\x06\x00\xa3\x9a\x54\x70\xef\
-\x3d\x87\xf1\xf0\x87\x1f\xc2\x07\x1f\xb8\x1f\x82\x08\xa3\xd1\x08\
-\x9b\x9b\x9b\xbc\x79\xf3\xe6\x8f\xa7\xd3\xe9\xb7\xad\xb5\x83\xbf\
-\x48\x01\xbf\xf1\xf1\x66\xab\xf5\xd3\x43\x6b\x6b\x9f\x4c\xab\x55\
-\x00\xc0\x1b\xe7\xde\xc6\x8b\xa7\x4e\xe3\xf2\xd5\x1b\x00\x01\x95\
-\x28\x42\x25\x8e\x11\xf9\x50\x91\x52\xb8\x1c\x28\x36\xf7\x9e\xd4\
-\xc6\x20\xcf\x73\x4c\x26\x39\xb2\xf1\x18\xa3\xf1\x04\x00\xb0\xb1\
-\xbe\x8a\x4f\xff\xed\xc3\xf8\xd0\x03\xf7\x03\x00\xb6\xbb\x5d\x5c\
-\xbd\x76\xed\x95\xbd\x5e\xef\xab\x00\xce\x12\xd1\x7b\x57\x40\x08\
-\x01\x00\x8f\x74\x3a\x9d\xa7\xd7\x0e\x1d\x5a\x0f\xc3\x10\xbd\xfd\
-\x01\x7e\xf3\xfc\x7f\xe0\x8d\x73\x6f\x43\x10\xa1\x9a\x26\xa8\xa6\
-\x09\x92\x38\x86\x0a\x14\x84\x17\xf8\xdd\x92\xbf\x78\xce\xb5\xc6\
-\x68\x3c\xc6\xfe\x30\xc3\x60\x90\x81\xc1\xf8\x9b\xbf\xbe\x1f\x4f\
-\x9c\x78\x14\x8d\x5a\x15\xfb\xfb\xfb\xb8\x7a\xe5\xca\xb5\xed\xed\
-\xed\x27\xad\xb5\x27\x8b\xef\xde\x15\x0a\x31\x33\x88\xe8\x91\xe5\
-\xe5\xe5\x67\xd6\x0e\x1d\x6a\x4b\x29\x71\xfe\x9d\x2b\xf8\xd5\xb3\
-\x2f\x62\x7b\x77\x0f\xf5\x5a\x15\xf5\x5a\x15\xd5\xa4\x02\x25\x05\
-\x00\x2a\x15\x2f\xac\x4e\x07\x94\xf1\xc8\x55\x9e\x2b\xa5\x10\x47\
-\x11\xea\xd5\x2a\x06\xb5\x11\xf6\xf6\x07\x38\xf3\xd6\x05\x5c\xdf\
-\xda\xc6\x3f\x3d\xfe\x18\x8e\xdd\xbb\x81\x23\x47\x8e\xac\x93\x10\
-\xcf\x6c\x6d\x6e\x3e\x61\xad\x3d\x49\xb7\x31\xd0\x9d\x3c\xf0\xc0\
-\xea\xea\xea\x73\x87\xd6\xd7\x0f\x4b\x29\xf1\xdf\x67\xcf\xe3\xd7\
-\xcf\xbe\x04\xc3\x8c\x46\xad\x8a\x66\xad\x86\x30\x54\x00\x04\x84\
-\x70\x42\x0b\x22\x90\x20\x10\x6e\x55\x80\x9d\xd4\xe5\xb3\x65\x06\
-\xb3\x85\xb5\x5c\x2a\x36\x9d\xe6\xe8\xed\xef\x63\xaf\x3f\x80\x10\
-\x84\xcf\x3f\xfe\x18\x3e\x7c\xfc\x18\xb2\x2c\xc3\xa5\x77\xde\xb9\
-\xba\xb5\xb5\x75\x02\xc0\xb9\x5b\x3c\x70\x1b\xad\xaa\xed\x4e\xe7\
-\x67\xab\x6b\x6b\x87\xa5\x94\x38\xf3\xd6\xdb\xf8\xf5\x73\x2f\x43\
-\x28\x85\xa5\x7a\x0d\xf5\x5a\x15\x52\x4a\x08\x41\x10\x24\x40\x42\
-\x38\xe1\xa9\x50\xa4\xb4\xc2\xbc\xf9\xcb\x27\x06\x83\x19\xb0\x6c\
-\xc1\xd6\x29\x63\x8c\x81\x94\x12\x51\x18\x20\x0e\x43\xec\xf6\xf7\
-\xf1\xab\x67\x5f\x02\x01\xf8\xd0\xf1\x63\xd8\xd8\xd8\x38\xac\xb5\
-\xfe\xd9\xee\xee\xee\x63\x00\x06\x77\x0c\x21\x66\x46\xb5\x5a\xfd\
-\xfe\xda\xda\xda\xc7\x83\x20\xc0\xc5\x2b\xd7\xf0\xaf\xbf\x3f\x89\
-\x30\x08\xd0\x6e\x35\x50\xaf\x56\x21\xa5\x80\x20\x67\xf9\xa2\x2a\
-\xcf\x2b\x30\x33\xc8\xbc\x61\x78\x2e\x8c\xdc\x6b\x6b\x2d\x18\x00\
-\x5b\x86\x91\x12\x96\x2d\xac\x91\x68\xb7\x9a\x90\x4a\x61\xb7\xb7\
-\x87\xdf\xbc\x78\x12\xd5\x34\xc1\x7d\x1b\xeb\x58\x5f\x5f\xff\xf8\
-\x78\x3c\xfe\x7e\x96\x65\x4f\xcd\x1b\x5d\x08\x21\x4a\x41\x82\x20\
-\xf8\xcc\xca\xea\xea\xd7\x2a\x95\x0a\xfa\x83\x21\xfe\xfd\xa5\x53\
-\x20\x12\x58\x6a\xb7\xd0\x6e\x36\x10\x06\x01\x02\x15\x20\x08\x14\
-\x02\x35\xbf\x82\xf2\x5c\x29\xe9\xcf\xa5\xbb\xee\xc0\xb5\x61\xa0\
-\xa0\x94\x42\x10\x04\xb3\xeb\x03\x85\xd0\xbf\x17\x86\x01\xda\xcd\
-\x06\x3a\xed\x16\x88\x08\xff\xf6\xd2\x7f\xa2\x3f\xc8\xd0\xee\x74\
-\xb0\xb2\xb2\xf2\x35\x29\xe5\x67\xe6\x13\x5a\x79\xb4\x01\x33\x47\
-\xcd\x66\xf3\x07\xcd\xa6\x2b\x2e\xbf\x3f\xf5\x3a\x06\xd9\x08\x9d\
-\x56\x13\xed\x66\xdd\x5b\xde\x59\xdd\xd1\x84\xb9\xf8\x07\x81\xbc\
-\x11\x88\x30\x97\x07\x0b\x11\x34\x0b\x1f\xcf\xab\xd8\xe7\x83\x7b\
-\x6d\x21\xac\x3b\x97\x82\xd1\x6e\x36\x1c\xa4\xee\xee\xe1\x85\x93\
-\xa7\xf1\xf9\xc7\x1f\xc5\xca\xea\x2a\xfa\xfd\xfe\x0f\x76\x77\x77\
-\x1f\x11\x42\x4c\xca\x1c\x60\x66\x04\x41\xf0\x85\x4e\xa7\xf3\x31\
-\x21\x04\xce\x5f\xba\x82\x8b\x97\xaf\xa3\x59\xaf\xa3\xdd\xac\x21\
-\x0c\x94\xab\x0d\x42\x40\x94\x21\x34\x0b\x23\xa7\x98\x4f\x60\x9a\
-\x25\x70\xa1\x08\x83\x81\xb9\xf8\x9f\x17\xdc\x7a\xa1\x6d\xa1\x80\
-\x31\x30\x82\x21\x05\xa1\xdd\xac\xc3\x5a\x8b\x0b\x57\xae\xe1\xcf\
-\x97\xae\xe0\x03\x47\xee\xc1\xf2\xf2\xf2\xc7\x06\x83\xc1\x17\xb4\
-\xd6\xff\x4c\x44\x50\xfe\xc7\x54\xad\x56\xfb\x46\x5a\xad\xc2\x18\
-\x8b\xd7\xfe\xe7\xcf\x88\xa2\x10\xcd\x7a\x15\x71\x14\x95\x85\x4d\
-\x12\x39\x05\xbc\x17\x0a\xe1\xa9\x48\x68\x72\x66\x2f\x04\x2f\xd3\
-\xc0\x0b\x3f\x0f\xa7\x45\x02\x17\x55\xda\x78\x45\x8c\x20\x08\x6b\
-\x61\x0c\xa1\x22\x04\x9a\xf5\x1a\x72\x6d\x70\xfa\xcc\x39\x1c\xdd\
-\x58\x47\xab\xdd\x46\xb7\xdb\xfd\xc6\xf6\xf6\xf6\xbf\x08\x21\xb4\
-\x02\x00\x29\xe5\x23\x8d\x66\xf3\x61\x22\xc2\x3b\x57\xaf\x61\x67\
-\xaf\x8f\x7a\xd5\x15\x29\x29\x25\xa4\x17\x58\x4a\xe1\x11\x68\x66\
-\x7d\xe5\x5f\xcf\x52\xf5\x0e\xac\x71\xf1\xc1\xc1\xa9\xb5\xc8\xb5\
-\x76\x89\x5c\x28\x62\x08\xda\x58\x08\xb2\x30\xd6\xa2\x9a\x56\x30\
-\x99\xe6\xd8\xd9\xeb\xe3\xe2\xe5\xeb\x38\x76\xe4\x30\x9a\xcd\xe6\
-\xc3\xbd\x5e\xef\x11\x66\x7e\x59\x11\x11\x92\x4a\xe5\xcb\x69\x9a\
-\x02\x00\xce\x5f\xba\x8a\x38\x0c\x51\x4d\x12\x84\x81\x82\x20\x27\
-\xb8\x52\x85\x22\x4e\x60\xf2\xf1\xdf\x1f\x0c\x30\x99\x4c\x21\x04\
-\xcd\x0a\xda\x02\xf6\x14\x35\x80\x17\x3d\x61\x2d\xa2\x28\x46\xbd\
-\x5e\x03\x5b\x86\x60\x0b\x6b\xac\x37\x8e\x81\x36\x16\x64\x0c\x84\
-\x20\xd4\xd2\x04\xd3\x3c\xc7\xf9\x4b\x57\x70\xec\xc8\x61\x34\x1a\
-\x0d\x54\x2a\x95\x2f\x67\x59\xf6\xb2\x02\x10\xa6\x69\xfa\xb8\x52\
-\x0a\x7b\x83\x21\xb6\x7b\x7d\xdc\xb3\xbe\xea\x42\x84\x08\x4a\xca\
-\x52\xf8\x22\x7c\xe6\x20\x0c\xa7\x5e\x3d\x8d\xb7\xce\xbf\x8d\x38\
-\x8e\x21\xa5\x72\xca\x49\xe9\x38\x90\x31\xe5\x32\x56\xbb\x67\xff\
-\x7a\x3c\xca\xf0\xe0\xf1\xe3\xf8\xc7\x7f\x78\x02\x9a\x75\x99\x57\
-\x64\x8c\xcf\x29\x8b\x9c\x08\xc6\x68\xd4\xaa\x09\x92\xa4\x82\x9b\
-\x5b\xdb\xe8\x0f\x86\xa8\xa6\x29\xd2\x34\x7d\x7c\x34\x1a\x85\x42\
-\x48\xf9\x50\x25\x49\xee\x03\x80\x9b\xdd\x1d\x08\x22\x2c\xb5\x9b\
-\x68\x54\x53\x28\xe9\x20\xce\x29\xa1\x16\x84\xc7\x41\x2b\x17\x76\
-\x17\x02\xa2\xf0\x92\x83\xa4\xf2\xf3\x22\x81\x8b\x7a\x70\x3b\x7a\
-\x23\xa5\x84\x54\x6e\xcf\x30\x50\x90\x52\xa2\x5e\xad\x62\xb9\xdd\
-\x84\x20\xc2\x8d\xee\x0e\x84\x94\x48\xd3\xf4\x3e\x21\xc4\x43\x42\
-\x29\xf5\xd1\x28\x8a\x54\x01\x59\x95\x4a\x8c\x40\x29\x54\xd3\x0a\
-\xa2\x28\x80\x92\x02\x52\xc9\xc5\xba\xe4\xc5\x15\xbe\xf2\xd2\x81\
-\x42\x46\x1e\x81\x6e\x79\xff\xc0\x2a\x60\xf8\x20\x17\x10\xc2\xd1\
-\x72\x29\x05\xe2\x30\x44\x2d\x4d\xa0\x94\x42\x52\xa9\xa0\xbb\xb3\
-\x07\x00\xa8\x24\x89\x92\x52\x7e\x54\x45\x61\x78\x3c\x0c\x02\x58\
-\xcb\x18\x64\x63\xa4\x49\x05\xd2\x27\x68\x9a\x54\x30\x9e\xe4\xb7\
-\x67\xab\x1e\x7e\x67\x7c\xc6\x82\x59\x80\x8d\x81\x61\x06\xe0\x28\
-\x30\xcf\x7d\x7e\xf0\xb0\xd6\x02\xbe\xcf\x2e\x2a\xf3\xdc\x26\x90\
-\x52\x22\xae\x84\x10\x82\x20\x21\x90\x26\x31\x06\xc3\x11\x98\x19\
-\x51\x14\x21\x0c\xc3\xe3\x82\x84\x38\x2a\x95\x84\x36\x06\xda\x18\
-\xc4\x51\x58\xe2\x78\x14\x86\x88\x42\x55\xb2\xc8\xc2\xea\x04\x38\
-\x3a\x3c\x99\xc0\x68\x5d\x0a\x68\xad\x8b\x71\x63\x34\x8c\xc9\x5d\
-\xfc\x7b\x8c\x67\xcb\xe0\xb2\x80\x59\xdf\x2c\x59\x64\xe3\x09\x72\
-\xad\x4b\xa3\xcc\xd3\x9a\x68\xae\x0d\x15\x20\x44\x51\x84\xdc\x18\
-\x18\x63\x11\x06\x01\x88\xe8\xa8\x52\x4a\xd5\x0b\x6b\x49\x21\x10\
-\x28\x59\x96\x1f\x00\x48\xa2\x08\xd6\x02\xda\x68\x08\x22\x18\xcf\
-\x1c\xb5\x27\x60\x0c\x27\x98\x6b\x38\x08\x00\x43\xd8\x59\x23\xe3\
-\x84\x9d\x2d\xf0\xac\x98\x15\x9c\x28\xcf\x8d\x8b\xf9\x30\x80\xf4\
-\x7b\x04\x4a\xa1\x12\x47\xb0\x45\xa2\x10\x10\x2a\xd7\x73\x68\x6b\
-\x9c\xac\x41\x50\x57\x42\x88\x04\x00\x72\x6d\x7c\xe2\x89\xc5\xa4\
-\x24\x42\x35\xad\x60\x34\x1a\x21\x1b\x4f\x30\xcd\x9d\xc5\x9d\x37\
-\x9c\x9e\xd6\x3a\x64\xf1\x7d\x04\x98\x0a\x7a\x52\x84\x90\x2d\x0b\
-\x96\x0b\x2b\x5b\x5e\x2b\x88\x60\x01\x68\x63\x60\xc6\x16\x61\xe0\
-\x04\x4f\xe2\xf8\x96\x2c\x17\x82\x20\xa5\x40\x9e\x1b\x04\x51\x00\
-\x21\x44\xb2\xc0\x46\xd5\x5c\x1f\x3b\x8f\x33\x42\x08\x24\x95\x18\
-\xa3\xc9\x14\xc6\x58\x48\xe1\xaa\x2f\x88\x3c\x8b\x34\xb0\xd6\x80\
-\x98\xbd\x01\xec\xc2\xf4\x61\x5e\x01\xb6\xee\xda\xc2\xc3\xe4\x7f\
-\x8b\xad\x0b\x29\x0a\x09\x49\x1c\x83\x04\xc1\x1a\x3e\x98\x7c\x5e\
-\xc6\x19\xfa\x29\x66\xce\x00\x20\x0c\x1c\x64\xde\x0e\x20\x8d\x76\
-\xb1\xdc\xaa\xd7\x10\x05\x01\xf6\xb3\xac\xcc\x0b\x14\x96\x35\x16\
-\x20\x06\x91\x3d\xd8\xde\x79\xda\xe0\x2c\x6f\x8c\xc5\x42\x7b\xe8\
-\x61\x55\x08\x42\xbd\x9a\x20\x89\x62\xe4\xc6\x40\xb2\xcf\x09\x5e\
-\x24\x84\x05\x8b\xf5\x70\x9c\x29\xad\x75\xdf\x8d\x3e\x24\xa4\x90\
-\x25\xaa\x00\xa2\x8c\xe3\x5c\xbb\xe2\x42\xcc\xa8\xa6\x09\x2a\x71\
-\x88\xfd\xe1\x08\x53\xad\x1d\x05\xf0\x05\x6a\xb1\x1f\xc0\x2c\x8c\
-\x0a\x25\xac\x59\x10\x9e\x7d\x97\x96\xc4\x31\xaa\x69\x02\x25\x05\
-\xb4\x76\x8a\xe6\x06\x50\x4a\x2e\x18\xd4\xb5\xa2\x6e\xe2\x61\x74\
-\x0e\xad\x75\x5f\x81\xf9\x82\x31\x06\x42\x0a\x04\x81\xc2\x64\x9a\
-\xc3\x32\x20\xfc\xa6\xb9\xd1\xa5\xfb\x8a\xe4\x0b\x82\x00\xcb\x6d\
-\x97\x60\x81\x92\xd0\x5a\x97\x63\x94\x79\x25\x66\x45\x8b\xcb\xa4\
-\x9d\x17\x3e\x0c\x03\xac\x2d\x75\x1c\x38\x58\xe3\xe7\x48\x1e\x82\
-\xd9\x22\xcf\x73\x28\xa5\x16\xa6\x7b\x51\x18\x42\x4a\x81\xc9\x58\
-\x83\x99\x2f\x88\xe9\x74\x7a\x56\x6b\x87\x30\x69\x25\x72\xc9\x64\
-\xad\x83\x4a\x6f\x59\x94\x55\x93\xcb\x61\x97\xb5\x8c\x38\x8a\x10\
-\x85\xc1\xad\x30\xaa\xb5\x5b\x3e\x37\x5c\x0d\xe0\x83\x91\x85\x28\
-\x08\x10\x47\xa1\xcb\x23\x3b\x43\x27\xe7\x17\x86\x36\x16\xb9\x36\
-\x2e\x8c\x7d\x8e\xa4\x95\x08\x04\x60\x3a\x99\x20\xcf\xf3\xb3\x42\
-\x1b\xf3\xc7\xe9\x74\xaa\x01\xa0\x5e\x4d\xcb\x61\x96\x65\x86\xd6\
-\x7a\xd6\x88\x60\x91\x02\x14\x38\x3e\xdf\x9c\x94\x8a\xf8\x55\x86\
-\xcf\xed\x16\x1c\x03\x2d\x72\x62\x26\xbc\xd7\xd5\xeb\xab\xb5\x2e\
-\xfb\x66\x6b\x19\xf5\xaa\x23\x9d\xe3\xf1\x58\x1b\x63\xfe\x28\xac\
-\x31\x6f\x8e\x46\xa3\x8b\x00\xd0\xac\x55\xa1\xa4\x44\x9e\x6b\x4c\
-\x73\x17\xdf\xf3\xfc\xc5\xfa\x98\xb6\x96\xe7\x30\x9e\x4a\x66\xfa\
-\x5e\x97\xe3\x47\x85\xf5\x2d\x18\xbe\x47\x28\x14\x07\x60\xac\xc5\
-\x74\xaa\x91\xe7\x1a\x4a\x0a\x34\xeb\x55\x30\x5b\x0c\x87\xc3\x8b\
-\xd6\xda\x37\x15\x11\x4d\x47\xa3\xd1\xef\xac\xb5\x1f\x48\x2a\x11\
-\xea\xd5\x04\xfb\xc3\x0c\x42\x0a\x48\x41\xce\x18\xf3\x4a\x10\x41\
-\xf8\x8e\x6a\x9a\xe7\xf8\xfb\xcf\x9e\xc0\xa7\x1f\xfd\xbb\x32\xe1\
-\x0a\x02\x77\x90\xe1\xcc\xcf\x84\x98\x19\x5a\x5b\xc4\x71\x54\xd6\
-\x95\x79\x92\x57\x8c\x60\x8a\xef\x4c\xb5\xc6\x54\xe7\xa8\x57\x53\
-\x24\x71\x84\x2c\xcb\x90\x65\xd9\xef\x00\x4c\x15\x03\x18\x65\xd9\
-\xcf\xc7\xa3\xd1\x53\x49\x9a\x62\xb5\xd3\x42\x6f\x7f\x00\x6b\xd9\
-\xf1\x1d\x6b\xc1\x42\xb8\x3c\xb0\x0c\x26\x06\x93\x9f\x26\x80\xd1\
-\x6c\x35\xa0\x64\xdb\x8f\x58\x68\xb1\x2f\xa6\x59\x28\x94\xed\xa4\
-\xb5\x30\x5e\x58\x63\x0c\xf2\x5c\x97\x1e\x9d\x79\xb5\xa8\xe2\xb3\
-\x1e\xda\x5a\x8b\xd5\xe5\x16\x00\x60\xb0\xbf\x8f\xd1\x68\xf4\x73\
-\x22\x82\x22\x17\x67\x27\xfb\xfd\xfe\x1f\x92\x34\x7d\xb8\xd3\xaa\
-\x23\xbd\x19\x63\x32\xcd\x21\x44\x04\x80\x41\xcc\xb0\x4c\x10\xf0\
-\x70\x28\x00\xc0\x42\x58\x57\x15\xe7\x21\x94\xe0\xf1\xdb\x2b\x32\
-\x43\xcd\x19\xf1\xb3\xfe\x77\x0a\x32\xe8\x72\x06\xb3\xf7\x78\xc6\
-\x9b\xac\x1f\x43\xa6\x95\x18\x4b\xcd\x06\xb4\xd6\xe8\xf5\x7a\x7f\
-\x30\xc6\x9c\xf4\x13\x15\x01\x29\xa5\xde\x1f\x0c\x7e\x38\x19\x8f\
-\x21\x85\xc0\xc6\xda\x0a\xb4\x76\x82\x71\x31\x9c\x2d\xa7\x08\x45\
-\x23\xce\x25\x2d\x30\x86\x17\x9a\x73\xed\xd1\x48\x9b\x82\xdc\x99\
-\x12\xdd\x0e\x7e\xdf\xa2\x10\xde\x2e\x5a\xdf\xef\xeb\x50\xcd\x60\
-\x63\x6d\x19\x42\x10\xfa\xfd\x3e\x06\x83\xc1\x0f\x95\x52\x5a\x4a\
-\xe9\xaa\x15\x11\x21\x9f\x4e\x7f\xd9\xeb\xf5\x5e\x07\x80\xe5\x76\
-\x03\x9d\x66\x0d\xd3\xe9\x74\x66\x35\x6b\xbd\x65\x66\x6c\xd2\x82\
-\x61\x61\x67\x89\x3d\xb7\xca\x1e\xd7\xaf\x42\x60\x63\x3c\xf9\x63\
-\xeb\x9b\xfa\xf9\xd7\x28\x47\x2c\xc5\x67\x79\x9e\xa3\xd3\xac\x61\
-\xb9\xdd\x84\xd6\x1a\xdb\xdd\xee\xeb\x79\x9e\xff\xb2\xa8\x35\xa2\
-\xe4\x28\xcc\x93\xdd\x5e\xef\x9b\xd9\x70\x08\x00\x38\xba\xb1\x8e\
-\x28\x0c\x90\xe7\x79\xa9\x84\x29\xe2\xb7\xb0\x9e\x71\x02\x19\x76\
-\x55\xd6\x7a\x58\x2c\xea\xc4\xcc\x33\x05\xe4\x3a\x0f\x1a\x3b\xbb\
-\x66\x46\x33\xe0\x7e\x87\xd9\x2b\xe2\x60\x3c\x0c\x14\x8e\x6e\xac\
-\x03\x00\x76\xb6\xb7\xd1\xeb\xf5\xbe\xc9\xcc\x93\x42\x6e\x31\x3f\
-\x35\xd6\x79\xfe\xfc\xd6\xd6\xd6\x4f\xb4\xd6\x88\xa3\x10\xf7\x6f\
-\xac\x83\xd9\x4d\x0e\xec\xdc\x3c\xc7\x2c\x24\x9c\xb7\xb4\x57\xd0\
-\x96\xc4\xcd\x94\xf5\x60\xf1\x9c\xe7\x92\xd6\x96\x4d\x8f\xf1\x5c\
-\xa9\xb0\xbc\xd6\x1a\xcc\x16\xf7\xff\xd5\x61\xc4\x51\x88\xe1\x70\
-\x88\xcd\xcd\xcd\x9f\x68\xad\x9f\x9f\x47\x33\xb1\xd8\xe2\x09\x0c\
-\x87\xc3\x6f\x6d\x6f\x6f\xbf\xc6\xcc\x68\x35\x6a\x38\xba\xb1\x0e\
-\x63\xb4\xf3\x04\xcf\xb8\x7f\x31\xcb\x31\x73\xb9\x60\x99\xcb\xf7\
-\x8c\x0f\x23\x33\x77\x5d\x69\x79\x9e\x79\xa0\x34\xc8\x01\xe1\x8d\
-\xd1\x38\xba\xb1\x8e\x76\xc3\x85\xf2\x8d\xeb\xd7\x5f\xcb\xb2\xec\
-\x5b\xa2\x9c\x00\xba\xa5\x6e\x73\xe3\x60\xb0\xdd\xed\x7e\x25\x50\
-\xea\xb9\x76\xa7\x73\x78\xb5\xd3\x02\x01\xb8\x70\xe5\xba\x6b\x3c\
-\x94\x84\x10\x0c\xe6\x19\xda\x08\x22\x30\x01\x44\xbc\x30\x91\xbb\
-\xdd\x60\x0b\x73\x15\xd7\xce\xe1\xbf\xf5\x90\xa9\x73\x0d\xeb\x2d\
-\xbf\xda\x69\xc1\x18\x83\x1b\x37\x6e\x5c\xdd\xd9\xd9\xf9\x0a\x80\
-\xc1\x41\xb2\x78\xa7\x1b\x1c\xe7\xba\xdd\xee\x17\x49\x88\x67\x5a\
-\xad\x56\x7b\xa5\xd3\x42\xa0\x14\x2e\x5c\xbe\x81\x7c\x3a\x85\x0a\
-\x02\x14\x03\x0a\x02\xc3\xf0\x81\x86\xfe\x96\xc1\xd0\x81\x42\x86\
-\x03\xd3\x09\x30\xac\x71\xa1\x1a\xaa\x00\xf7\x6d\x1c\x46\xbb\x51\
-\x73\xc2\x5f\xbf\xbe\xb3\xdd\xed\x7e\x91\x99\xcf\xdd\xf5\x1d\x1a\
-\x22\x82\x65\x3e\xb9\xb5\xb5\xf5\x04\x5b\xfb\x74\xab\xdd\x5e\x6f\
-\x35\x6a\xf8\x60\x1c\xe1\xd2\xd5\x1b\xe8\xf5\x07\xb0\x0e\x7e\x7d\
-\xf1\x82\x6f\x66\x16\x07\x5b\x07\xa7\xbb\xbc\x70\xb7\x66\x6e\x46\
-\xaa\x5d\x8e\xb4\xea\x55\xdc\x7b\x78\x0d\x71\x14\x22\xcf\x73\xdc\
-\xbc\x71\xe3\x5a\xb7\xdb\x7d\x92\x99\x4f\x16\x33\xdc\x5b\x64\x3d\
-\xf3\xc6\x1b\xb7\xbf\xc9\x57\xd0\x58\x63\x8e\xb7\xdb\xed\x9f\x76\
-\x96\x96\x3e\x59\x50\xdb\xee\xee\x1e\xae\x6f\x6e\x23\x1b\x4f\x5c\
-\xee\x48\xd7\x8a\x96\x63\xa0\xa2\x11\x99\xf7\x04\x1d\xf0\x42\x91\
-\x3b\xcc\x48\xe2\x08\x87\x56\x3a\x58\x6a\xb9\x89\xf4\x28\xcb\x70\
-\xe3\xe6\xcd\x57\x76\x77\x76\xbe\x4a\x44\x67\x0b\x52\x77\x3b\x05\
-\xde\xf5\x4e\xbd\xb7\xe8\xd9\xde\xde\xde\x89\xc9\x64\xf2\xbd\xce\
-\xd2\xd2\x53\x69\x9a\xd2\x52\xab\x81\x76\xa3\x86\x9d\xbd\x7d\x74\
-\x77\xf7\x30\xc8\x46\xd0\x3a\xf7\xd5\xb7\xc8\x8d\x5b\x92\xa0\x8c\
-\x79\x30\x43\x4a\x81\x7a\x35\xc1\x52\xb3\x81\x76\xb3\x56\x8e\x56\
-\x7a\xbd\x1e\x6f\xde\xbc\xf9\xe3\xc9\x64\xf2\xed\x22\xe6\xef\x74\
-\x83\xef\xff\xf4\x00\x11\xc1\xfa\xe9\x83\xb1\x16\x82\xe8\x73\xf5\
-\x46\xe3\xbb\xcd\x66\xf3\x13\x71\x1c\x97\xd7\x8f\xc6\x13\xf4\x87\
-\x19\x06\xd9\x08\xa3\xf1\xc4\x71\x78\x1f\x1e\xf0\xa3\x18\x10\x21\
-\x50\x12\x95\x38\x42\x35\xa9\xa0\x96\x26\x48\xe2\xa8\xf4\xc8\x60\
-\x30\xc0\xce\xf6\xf6\xab\xfd\x7e\xff\x3b\xc6\x98\xdf\x0a\x21\xa0\
-\xb5\x2e\xff\x43\x71\x27\x0f\xdc\xb5\x02\x05\x84\x12\x91\x0a\x82\
-\xe0\x4b\x49\x9a\x7e\xbd\x56\xab\x7d\xca\xcd\x44\x67\xad\xdf\xfc\
-\xdc\xbf\xe8\x27\xdc\x58\x72\x76\x2f\xa1\x38\xf2\x3c\x47\x96\x65\
-\xe8\xef\xed\x9d\xda\x1f\x0c\x7e\xa4\xf3\xfc\x17\xc5\x7f\x26\x88\
-\xe8\xff\x5f\x81\x62\x8a\xe6\x3a\x24\xab\x88\xe8\xd1\x4a\x1c\x3f\
-\x59\xa9\x54\x4e\xc4\x95\xca\xb1\x30\x0c\xa5\x52\x0a\x52\x88\xc5\
-\x9b\x7c\xc0\x1c\xfb\xcc\x31\x99\x4c\xcc\x28\xcb\xce\x67\x59\xf6\
-\xdc\x68\x34\x7a\x9a\x99\x5f\x26\x22\x5d\x4c\xeb\x8a\x91\xcb\xdd\
-\x28\xf0\xbe\xfe\xad\xe2\x21\x53\x83\xf9\x85\xd1\x78\xfc\xc2\x70\
-\x38\x0c\xa5\x52\x0f\x0a\xa2\x8f\x84\x61\xf8\x10\x88\x8e\x4a\x21\
-\xea\x24\x44\xea\x81\x60\x68\x8c\xe9\x5b\xff\x77\x1b\xb6\xb6\xfc\
-\xbb\xcd\xfc\xfd\xe5\x77\x8b\xf5\x3b\x1d\xff\x3b\x00\x71\x48\xea\
-\xdb\x9a\xd7\x91\x4c\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\
-\x82\
-\x00\x00\x68\x46\
-\x00\
-\x00\x01\x00\x08\x00\x30\x30\x00\x00\x00\x00\x08\x00\xa8\x0e\x00\
-\x00\x86\x00\x00\x00\x20\x20\x00\x00\x00\x00\x08\x00\xa8\x08\x00\
-\x00\x2e\x0f\x00\x00\x18\x18\x00\x00\x00\x00\x08\x00\xc8\x06\x00\
-\x00\xd6\x17\x00\x00\x10\x10\x00\x00\x00\x00\x08\x00\x68\x05\x00\
-\x00\x9e\x1e\x00\x00\x30\x30\x00\x00\x00\x00\x20\x00\xa8\x25\x00\
-\x00\x06\x24\x00\x00\x20\x20\x00\x00\x00\x00\x20\x00\xa8\x10\x00\
-\x00\xae\x49\x00\x00\x18\x18\x00\x00\x00\x00\x20\x00\x88\x09\x00\
-\x00\x56\x5a\x00\x00\x10\x10\x00\x00\x00\x00\x20\x00\x68\x04\x00\
-\x00\xde\x63\x00\x00\x28\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\
-\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\
-\x00\x7a\x52\x1a\x00\x7c\x53\x1b\x00\x85\x5a\x1d\x00\x89\x5c\x1e\
-\x00\x8d\x5e\x1e\x00\x90\x61\x1f\x00\x87\x5e\x26\x00\x82\x5d\x29\
-\x00\x89\x60\x27\x00\x8d\x62\x26\x00\x88\x60\x29\x00\x8f\x67\x2d\
-\x00\x94\x63\x20\x00\x92\x65\x27\x00\x9b\x68\x21\x00\x9e\x6a\x22\
-\x00\x94\x67\x28\x00\x96\x6a\x2d\x00\x9c\x6c\x29\x00\x9c\x6d\x2d\
-\x00\x82\x61\x33\x00\x8a\x66\x34\x00\x81\x63\x3a\x00\x83\x64\x3b\
-\x00\x85\x66\x3b\x00\x86\x67\x3c\x00\x89\x69\x3d\x00\x8c\x6b\x3d\
-\x00\x95\x6d\x36\x00\x9c\x6f\x32\x00\x98\x6f\x37\x00\x9f\x71\x31\
-\x00\x99\x70\x37\x00\x99\x71\x38\x00\x99\x74\x3f\x00\xa0\x6b\x22\
-\x00\xa2\x6d\x23\x00\xa5\x6f\x24\x00\xa0\x6e\x2a\x00\xa7\x70\x24\
-\x00\xa8\x71\x24\x00\xac\x73\x25\x00\xae\x75\x25\x00\xa4\x71\x2c\
-\x00\xa8\x75\x2f\x00\xb1\x77\x26\x00\xb5\x79\x27\x00\xb9\x7c\x28\
-\x00\xbc\x7e\x28\x00\xa0\x72\x32\x00\xa1\x74\x34\x00\xa6\x76\x35\
-\x00\xab\x77\x30\x00\xaf\x7d\x37\x00\xa2\x76\x39\x00\xa0\x77\x3e\
-\x00\xa4\x78\x3b\x00\xa2\x79\x3f\x00\xa4\x79\x3c\x00\xb1\x7c\x31\
-\x00\x8b\x70\x4b\x00\x8d\x72\x4d\x00\x93\x72\x43\x00\x95\x72\x42\
-\x00\x9a\x75\x41\x00\x99\x75\x45\x00\x96\x77\x4b\x00\x95\x77\x4e\
-\x00\x9d\x79\x49\x00\x99\x7a\x4e\x00\x9c\x7c\x4f\x00\x90\x76\x53\
-\x00\x92\x78\x55\x00\x95\x7a\x54\x00\x9d\x7d\x50\x00\x94\x7c\x5b\
-\x00\x94\x7d\x5d\x00\x98\x7f\x5c\x00\xa2\x7b\x44\x00\xa3\x7c\x45\
-\x00\xa2\x7c\x49\x00\xa5\x7f\x4c\x00\xa0\x7f\x53\x00\xbf\x80\x29\
-\x00\xbf\x85\x34\x00\xc1\x81\x29\x00\xc4\x83\x2a\x00\xc6\x85\x2b\
-\x00\xc8\x86\x2b\x00\xcb\x88\x2c\x00\xcd\x89\x2c\x00\xd1\x8c\x2d\
-\x00\xc6\x8a\x36\x00\xd3\x8f\x30\x00\xd4\x90\x33\x00\xd4\x91\x35\
-\x00\xd5\x93\x38\x00\xd5\x94\x3a\x00\xd5\x95\x3d\x00\x9c\x82\x5f\
-\x00\xab\x83\x49\x00\xa8\x84\x53\x00\xae\x87\x50\x00\xa9\x85\x55\
-\x00\xaf\x89\x56\x00\xa1\x84\x5d\x00\xb7\x8e\x55\x00\xb8\x8f\x56\
-\x00\x9c\x83\x60\x00\x9f\x86\x62\x00\x93\x83\x6d\x00\x9d\x87\x68\
-\x00\x98\x87\x6f\x00\x9a\x88\x6f\x00\x97\x87\x70\x00\x9d\x8a\x70\
-\x00\x9e\x8e\x77\x00\x99\x8e\x7f\x00\x9d\x90\x7e\x00\xa0\x86\x62\
-\x00\xa0\x87\x64\x00\xac\x8d\x62\x00\xa3\x8b\x6a\x00\xa2\x8c\x6e\
-\x00\xa9\x8f\x6a\x00\xac\x93\x6f\x00\xb3\x92\x63\x00\xb6\x93\x64\
-\x00\xbc\x97\x62\x00\xb9\x96\x64\x00\xbe\x98\x64\x00\xbc\x9a\x6b\
-\x00\xa0\x8c\x70\x00\xa6\x90\x71\x00\xab\x93\x72\x00\xa0\x90\x7a\
-\x00\xa2\x92\x7d\x00\xaf\x9a\x7d\x00\xb1\x96\x72\x00\xb4\x98\x72\
-\x00\xb4\x9c\x79\x00\xb2\x9c\x7d\x00\xb6\x9e\x7c\x00\xbe\xa4\x7f\
-\x00\xc1\x91\x4d\x00\xd0\x93\x40\x00\xd6\x97\x40\x00\xd6\x98\x42\
-\x00\xd7\x9a\x45\x00\xd8\x9b\x47\x00\xd8\x9c\x49\x00\xd9\x9e\x4c\
-\x00\xd2\x9c\x51\x00\xda\xa0\x4f\x00\xda\xa1\x51\x00\xdb\xa2\x54\
-\x00\xdb\xa4\x57\x00\xdb\xa5\x59\x00\xdc\xa5\x59\x00\xdc\xa6\x5c\
-\x00\xdd\xa8\x5e\x00\xc4\xa0\x6f\x00\xca\xa1\x69\x00\xdd\xa9\x62\
-\x00\xde\xaa\x64\x00\xde\xac\x66\x00\xdf\xad\x69\x00\xd8\xab\x6e\
-\x00\xde\xaf\x6d\x00\xc3\xa2\x73\x00\xe0\xb0\x6d\x00\xe0\xb2\x71\
-\x00\xe2\xb4\x76\x00\xe2\xb6\x78\x00\xe3\xb8\x7c\x00\xe4\xba\x7f\
-\x00\x9e\x94\x86\x00\xa1\x94\x82\x00\xa5\x98\x86\x00\xa8\x99\x83\
-\x00\xaf\x9c\x81\x00\xb1\x9d\x81\x00\xbc\xa7\x8a\x00\xc4\xac\x8c\
-\x00\xca\xaf\x89\x00\xc8\xb0\x8e\x00\xce\xb3\x8e\x00\xd5\xb8\x8f\
-\x00\xe4\xba\x80\x00\xe5\xbc\x85\x00\xe6\xbf\x89\x00\xe6\xc1\x8d\
-\x00\xe8\xc3\x91\x00\xe8\xc4\x93\x00\xe8\xc5\x95\x00\xe9\xc6\x98\
-\x00\xea\xc9\x9b\x00\xea\xca\x9d\x00\xeb\xcc\xa1\x00\xec\xce\xa5\
-\x00\xec\xcf\xa8\x00\xed\xd1\xaa\x00\xed\xd2\xad\x00\xee\xd4\xaf\
-\x00\xee\xd4\xb0\x00\xef\xd6\xb4\x00\xf0\xd9\xb9\x00\xf1\xdb\xbc\
-\x00\xf1\xdc\xbf\x00\xf2\xdd\xc1\x00\xf2\xdf\xc4\x00\xf3\xe1\xc7\
-\x00\xf3\xe1\xc9\x00\xf5\xe6\xd1\x00\xe1\x00\xf0\x00\xf0\x11\xff\
-\x00\xf2\x31\xff\x00\xf4\x51\xff\x00\xf6\x71\xff\x00\xf7\x91\xff\
-\x00\xf9\xb1\xff\x00\xfb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x1b\x00\x2f\x00\x2d\x00\x50\x00\x3f\x00\x70\x00\x52\x00\x90\
-\x00\x63\x00\xb0\x00\x76\x00\xcf\x00\x88\x00\xf0\x00\x99\x11\xff\
-\x00\xa6\x31\xff\x00\xb4\x51\xff\x00\xc2\x71\xff\x00\xcf\x91\xff\
-\x00\xdc\xb1\xff\x00\xeb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x08\x00\x2f\x00\x0e\x00\x50\x00\x15\x00\x70\x00\x1b\x00\x90\
-\x00\x21\x00\xb0\x00\x26\x00\xcf\x00\x2c\x00\xf0\x00\x3e\x11\xff\
-\x00\x58\x31\xff\x00\x71\x51\xff\x00\x8c\x71\xff\x00\xa6\x91\xff\
-\x00\xbf\xb1\xff\x00\xda\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x87\
-\x52\x3b\x2a\x2a\x2a\x2a\x3b\x52\x7b\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x3b\x31\x9c\
-\xbe\xc6\xd3\xd3\xd3\xd3\xc6\xbe\x9c\x54\x37\x86\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x5c\xbf\xd0\xce\
-\xc2\xad\xa0\x9c\x9c\xa0\xad\xc0\xce\xd0\xbf\x5c\x32\x75\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x57\xbe\xce\xc2\xa0\x5c\
-\x5c\x5f\x5c\x5f\x5f\x5c\x5f\x5c\x5f\x9c\xc2\xce\xbe\x57\x47\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x3b\x63\xc9\xc2\x98\x5c\x5f\x5f\
-\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x98\xc2\xc9\x63\x23\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x37\xa1\xc6\xa5\x5b\x5b\x5c\x5c\x5c\
-\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x5f\x5c\x5f\x5c\x5b\xa9\xc6\xa1\
-\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x37\x9a\xbf\x96\x57\x5a\x5b\x5c\x98\xaf\
-\xbf\xc2\xc2\xc2\xc2\xc2\xc2\xbf\x9c\x5c\x5c\x5b\x5b\x57\x96\xbf\
-\x9a\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x7a\x57\xad\x63\x54\x57\x57\x5a\x60\xb0\x9c\
-\x57\x31\x31\x31\x31\x31\x31\x98\xaf\x60\x5b\x5b\x57\x57\x54\x63\
-\xaf\x57\x43\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x25\xa0\x63\x31\x31\x54\x57\x57\xa0\x98\x11\
-\x00\x00\x00\x00\x00\x00\x00\x12\xa0\x9c\x5a\x57\x57\x54\x31\x31\
-\x94\xa0\x25\x77\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x1d\x63\x98\x2e\x2f\x31\x31\x54\x63\xa0\x25\x00\
-\x00\x00\x00\x00\x00\x00\x00\xb8\x31\xa1\x5c\x57\x54\x31\x31\x2f\
-\x2e\x98\x63\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x2a\x9c\x54\x2e\x2e\x2f\x31\x54\xa0\x5c\x3f\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x23\x94\x98\x54\x31\x31\x2f\x2e\
-\x2e\x57\x9c\x2a\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x42\x60\x95\x2a\x2a\x2a\x2e\x2f\x63\x9c\x0d\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2a\xa1\x57\x31\x2f\x2e\x2e\
-\x2a\x2a\x95\x60\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x0d\xa0\x57\x28\x2a\x2a\x2a\x31\x9c\x57\x4a\x00\x00\
-\x00\x00\x00\xb5\x00\x00\x00\x00\x00\x1d\x96\x95\x2f\x2e\x2a\x2a\
-\x2a\x2a\x57\xa0\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x2e\xa0\x25\x25\x2a\x2a\x2a\x60\x9c\x05\x00\x00\x00\
-\x00\x00\x8e\x07\x00\x00\x00\x00\x00\x00\x2f\xa1\x31\x2e\x2a\x2a\
-\x28\x25\x2a\xa0\x2f\x73\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x7d\x5b\x95\x25\x25\x25\x2a\x2a\xa0\x57\x4a\x00\x00\x00\
-\x00\x00\x0c\x10\x6f\x00\x00\x00\x00\x00\x0c\x9a\x94\x2a\x2a\x28\
-\x28\x25\x25\x95\x5b\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x47\x63\x98\x5c\x5b\x31\x25\x5f\x9c\x03\x00\x00\x00\x00\
-\x00\x8a\x2e\x94\x08\x00\x00\x00\x00\x00\xb7\x2e\xa1\x2f\x28\x25\
-\x2f\x57\x5c\x98\x63\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x16\xa0\xad\xa5\xa1\xa1\xa0\xa9\x57\x48\x00\x00\x00\x00\
-\x00\x0a\x9c\xa5\x25\x6f\x00\x00\x00\x00\x00\x08\x9c\x95\x63\x9c\
-\xa0\xa1\xa5\xad\x9c\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x02\xb0\xab\xa5\xa5\xa5\xa9\xab\x03\x00\x00\x00\x00\x00\
-\x4e\x57\xa1\x98\x63\x18\x00\x00\x00\x00\x00\x8e\x60\xaf\xa0\xa1\
-\xa1\xa5\xa5\xab\xad\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x02\xbe\xad\xa9\xa5\xa5\xb0\x5c\x18\x77\xb1\x00\x00\x77\
-\x03\xab\xa5\xa1\xad\x0d\x77\x00\x00\x00\x00\x00\x0d\xaf\xab\xa1\
-\xa5\xa5\xa9\xad\xaf\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x3f\xad\xaf\xab\xa9\xad\xb0\x31\x28\x2a\x2a\x2a\x2a\x2a\
-\x60\xaf\x9c\x9c\xad\x63\x18\x00\x00\x00\x00\x00\x7e\x63\xb0\xa5\
-\xa9\xa9\xab\xb0\x9c\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x4b\xa0\xb0\xad\xab\xbe\xbe\xbe\xbe\xb0\xb0\xb0\xb0\xb0\
-\xb0\xab\xa0\xa0\xa5\xb0\x0e\x00\x00\x00\x00\x00\x00\x25\xbe\xad\
-\xa9\xab\xab\xb0\xa0\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x8e\x63\xbe\xad\xad\xab\xa9\xa9\xa5\xa5\xa5\xa5\xa5\xa1\
-\xa1\xa1\xa1\xa1\xa1\xb0\x5c\x3e\x00\x00\x00\x00\x00\x53\x9c\xb0\
-\xab\xad\xad\xbe\x60\x4c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x57\xc2\xaf\xad\xad\xab\xab\xa9\xa9\xa9\xa5\xa5\xa5\
-\xa5\xa5\xa5\xa5\xa5\xaf\xbe\x0d\x00\x00\x00\x00\x00\x00\x57\xc0\
-\xb0\xad\xaf\xc2\x31\xb3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x0e\xc6\xbf\xaf\xad\xad\xad\xab\xab\xab\xa9\xa9\xa9\
-\xa9\xa5\xa5\xa5\xa5\xa9\xc4\x63\x4a\x00\x00\x00\x00\x00\x1d\xbe\
-\xc0\xaf\xbf\xc2\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x7a\xa1\xc8\xb0\xaf\xaf\xad\xad\xad\xad\xab\xab\xab\
-\xab\xa9\xab\xa9\xa9\xab\xbf\xbf\x0a\x00\x00\x00\x00\x00\x00\x60\
-\xc8\xbe\xc8\xa1\x43\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x31\xcb\xc2\xb0\xb0\xaf\xaf\xad\xad\xad\xad\xad\
-\xad\xab\xab\xad\xab\xab\xaf\xcb\x5f\x4c\x00\x00\x00\x00\x00\x14\
-\xc6\xcb\xcb\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x68\xad\xce\xbe\xb0\xb0\xb0\xb0\xaf\xaf\xad\xad\
-\xad\xad\xad\xad\xad\xad\xad\xc4\xc2\x0e\x00\x00\x00\x00\x00\x90\
-\xa5\xce\xa5\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x2a\xcb\xc9\xbe\xbe\xbe\xb0\xb0\xb0\xb0\xaf\
-\xaf\xaf\xad\xad\xaf\xaf\xaf\xb0\xce\x5b\x74\x00\x00\x00\x00\x00\
-\x2a\xc9\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xbb\x5c\xd0\xc8\xbe\xbe\xbe\xb0\xb0\xb0\xb0\
-\xb0\xb0\xb0\xb0\xb0\xb0\xb0\xb0\xc8\xbe\x12\x00\x00\x00\x00\x00\
-\x80\x25\x8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x80\x63\xd0\xc8\xbe\xbe\xbe\xbe\xbe\xbe\
-\xbe\xb0\xb0\xb0\xb0\xb0\xb0\xbe\xc0\xd0\x57\x75\x00\x00\x00\x00\
-\x00\x69\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x65\x94\xd0\xcb\xbf\xbe\xbe\xbe\xbe\
-\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xcb\xb0\x42\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x81\x60\xcb\xd0\xc6\xbf\xbf\xbf\
-\xbf\xbf\xbf\xbe\xbe\xbe\xbf\xbf\xbf\xc4\xd0\x2f\xb2\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbb\x2a\xaf\xd4\xd0\xc8\xc2\
-\xbf\xbf\xbf\xbf\xbf\xbf\xbf\xbf\xc2\xc9\xd4\xaf\x50\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x6c\x54\xb0\xd3\xd6\
-\xd5\xd0\xce\xce\xce\xce\xd0\xd5\xd6\xd3\xb0\x54\x68\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x3c\x94\
-\xab\xc6\xc8\xd0\xd0\xc8\xc6\xab\x94\x2d\x84\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\xbc\x35\
-\x8b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\xbb\x83\x6c\x6c\x6b\x6b\x81\xb9\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x33\x36\x00\x00\x00\x00\x00\x91\
-\x2a\x2c\x78\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x50\x2a\x2f\x00\x00\x00\x00\x00\x00\x00\
-\x55\xa5\x5f\x33\x64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x50\x31\x9c\x5f\xa2\x00\x00\x00\x00\x00\x00\x00\
-\x00\x5d\xad\xbe\x60\x27\x47\x89\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x86\x33\x54\xa0\xc0\x63\xa3\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x99\xa0\xc8\xbf\x9c\x31\x14\x42\x70\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb5\x6a\x1e\
-\x2e\x94\xaf\xc2\xbf\x5f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\xa9\x95\xc6\xc8\xc0\xb0\x98\x5b\x2e\x10\x23\x23\x6d\
-\x6d\x6d\x89\xb3\x00\x00\x7b\x78\x6d\x47\x42\x14\x25\x57\x95\xa9\
-\xc0\xc2\xc6\xa9\x94\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x63\xa5\xce\xc8\xc2\xbe\xad\xa5\x98\x95\x5b\
-\x5c\x5c\x54\x31\x31\x31\x5b\x5c\x5c\x63\x96\xa1\xab\xb0\xbf\xc4\
-\xcb\xbe\x95\xa9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\xc4\x94\xad\xcb\xcb\xc6\xc0\xb0\xad\xa5\
-\xa0\x9c\xa0\xa0\xa0\xa0\x9c\xa0\xa5\xab\xb0\xbe\xc2\xc8\xce\xbf\
-\x96\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\xc4\x95\xa5\xc2\xd0\xce\xcb\xc8\
-\xc4\xc0\xbe\xb0\xb0\xbe\xbf\xc2\xc6\xc8\xcb\xd0\xc9\xad\x94\xa0\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\x63\x9c\xaf\xc2\
-\xc8\xd0\xd3\xd3\xd3\xd3\xd3\xc8\xc8\xbe\xa5\x95\x9c\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc2\xaf\
-\xa5\x95\x63\x63\x63\x63\x63\xa1\xa1\xc0\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\
-\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xc0\
-\x0f\xff\xff\x00\x00\xff\xfe\x00\x01\xff\xff\x00\x00\xff\xfc\x00\
-\x00\x7f\xff\x00\x00\xff\xf0\x00\x00\x3f\xff\x00\x00\xff\xe0\x00\
-\x00\x1f\xff\x00\x00\xff\xc0\x00\x00\x0f\xff\x00\x00\xff\x80\x00\
-\x00\x07\xff\x00\x00\xff\x00\x00\x00\x03\xff\x00\x00\xff\x00\x1f\
-\xc0\x01\xff\x00\x00\xfe\x00\x3f\xc0\x01\xff\x00\x00\xfe\x00\x3f\
-\xe0\x00\xff\x00\x00\xfc\x00\x7f\xf0\x00\xff\x00\x00\xfc\x00\x7d\
-\xf0\x00\xff\x00\x00\xfc\x00\xf9\xf8\x00\x7f\x00\x00\xf8\x00\xf8\
-\xf8\x00\x7f\x00\x00\xf8\x01\xf0\xf8\x00\x7f\x00\x00\xf8\x01\xf0\
-\x7c\x00\x7f\x00\x00\xf8\x03\xe0\x7c\x00\x7f\x00\x00\xf8\x00\xc0\
-\x3e\x00\x7f\x00\x00\xf8\x00\x00\x3e\x00\x7f\x00\x00\xf8\x00\x00\
-\x3f\x00\x7f\x00\x00\xf8\x00\x00\x1f\x00\x7f\x00\x00\xfc\x00\x00\
-\x1f\x80\x7f\x00\x00\xfc\x00\x00\x0f\x80\xff\x00\x00\xfc\x00\x00\
-\x0f\xc0\xff\x00\x00\xfe\x00\x00\x07\xc1\xff\x00\x00\xfe\x00\x00\
-\x07\xc1\xff\x00\x00\xff\x00\x00\x03\xe3\xff\x00\x00\xff\x00\x00\
-\x03\xe3\xff\x00\x00\xff\x80\x00\x01\xf7\xff\x00\x00\xff\xc0\x00\
-\x01\xff\xff\x00\x00\xff\xe0\x00\x00\xff\xff\x00\x00\xff\xf0\x00\
-\x00\xff\xff\x00\x00\xff\xfc\x00\x00\xff\xff\x00\x00\xff\xff\x00\
-\x03\xff\xf7\x00\x00\x8f\xff\xe0\x1f\xff\xe7\x00\x00\xc3\xff\xff\
-\xff\xff\x8f\x00\x00\xe0\xff\xff\xff\xfe\x0f\x00\x00\xf0\x1f\xff\
-\xff\xf0\x1f\x00\x00\xf8\x03\xff\xff\x00\x7f\x00\x00\xfc\x00\x01\
-\x80\x00\xff\x00\x00\xff\x00\x00\x00\x01\xff\x00\x00\xff\x80\x00\
-\x00\x07\xff\x00\x00\xff\xe0\x00\x00\x1f\xff\x00\x00\xff\xfc\x00\
-\x00\xff\xff\x00\x00\xff\xff\x80\x07\xff\xff\x00\x00\x28\x00\x00\
-\x00\x20\x00\x00\x00\x40\x00\x00\x00\x01\x00\x08\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95\x6f\x3b\x00\x9b\x74\x3f\
-\x00\xa3\x6d\x23\x00\xa4\x6e\x23\x00\xa6\x6f\x24\x00\xa7\x70\x24\
-\x00\xa9\x72\x24\x00\xac\x73\x25\x00\xaf\x75\x25\x00\xac\x76\x2b\
-\x00\xb1\x76\x25\x00\xb4\x78\x26\x00\xb8\x7b\x27\x00\xb9\x7c\x27\
-\x00\xb3\x79\x29\x00\xb5\x7a\x28\x00\xb7\x7c\x29\x00\xba\x7d\x29\
-\x00\xbd\x7f\x28\x00\xba\x7f\x2e\x00\xa8\x76\x34\x00\xa4\x78\x3a\
-\x00\xa7\x7b\x3f\x00\xae\x7d\x38\x00\xab\x7e\x3e\x00\xb1\x7c\x32\
-\x00\x85\x6b\x47\x00\x87\x6f\x4c\x00\x9c\x79\x47\x00\x93\x75\x4a\
-\x00\x90\x74\x4e\x00\x95\x77\x4d\x00\x9a\x79\x4c\x00\x9e\x7d\x4e\
-\x00\x89\x75\x58\x00\x92\x76\x51\x00\x94\x78\x51\x00\x9b\x7c\x52\
-\x00\x9f\x7e\x50\x00\x92\x7b\x5c\x00\xa2\x7a\x43\x00\xa6\x7c\x43\
-\x00\xac\x7f\x42\x00\x8d\x7e\x69\x00\x8b\x7e\x6c\x00\x93\x7e\x62\
-\x00\xbf\x80\x29\x00\xbf\x85\x35\x00\xb2\x80\x3a\x00\xb5\x81\x39\
-\x00\xb5\x83\x3e\x00\xbf\x87\x3a\x00\xc2\x82\x2a\x00\xc7\x85\x2a\
-\x00\xc1\x83\x2c\x00\xcd\x89\x2c\x00\xd2\x8d\x2d\x00\xc2\x87\x36\
-\x00\xc7\x89\x33\x00\xc4\x88\x35\x00\xc9\x89\x32\x00\xcd\x8c\x31\
-\x00\xc1\x8a\x3e\x00\xcb\x8d\x38\x00\xc8\x8e\x3e\x00\xcd\x90\x3b\
-\x00\xd0\x91\x3b\x00\xd4\x93\x39\x00\xd5\x94\x3b\x00\xd0\x92\x3d\
-\x00\xd3\x94\x3c\x00\xd5\x95\x3e\x00\xa7\x81\x4e\x00\xa8\x81\x4a\
-\x00\xaf\x85\x4b\x00\xa8\x83\x4f\x00\xae\x85\x4c\x00\xb9\x86\x42\
-\x00\xbb\x88\x42\x00\xbc\x89\x42\x00\xbb\x8b\x47\x00\xbd\x8a\x44\
-\x00\xb4\x88\x4b\x00\xb0\x88\x4f\x00\xb6\x8b\x4f\x00\xb8\x89\x48\
-\x00\xba\x8c\x4b\x00\xbc\x8c\x4a\x00\xba\x8b\x4d\x00\xbb\x8d\x4d\
-\x00\xa4\x83\x54\x00\xa8\x84\x52\x00\xab\x8b\x5f\x00\xb1\x8a\x54\
-\x00\xb9\x8e\x51\x00\xb2\x8e\x5c\x00\xbd\x90\x53\x00\xbe\x93\x57\
-\x00\xbd\x93\x59\x00\xbe\x94\x59\x00\xbd\x94\x5d\x00\x97\x80\x61\
-\x00\x96\x82\x67\x00\x9c\x84\x63\x00\x98\x83\x66\x00\x94\x82\x69\
-\x00\x91\x83\x6f\x00\x94\x84\x6d\x00\x9a\x85\x68\x00\x98\x86\x6d\
-\x00\x97\x87\x72\x00\x97\x89\x76\x00\x9e\x8d\x76\x00\x92\x88\x79\
-\x00\x95\x8a\x7b\x00\x91\x88\x7c\x00\x98\x8c\x7c\x00\xa1\x85\x60\
-\x00\xa0\x87\x65\x00\xa7\x8b\x66\x00\xac\x8c\x61\x00\xa6\x8d\x6a\
-\x00\xb0\x8f\x60\x00\xbd\x97\x65\x00\xbf\x98\x61\x00\xbb\x98\x65\
-\x00\xa8\x90\x70\x00\xad\x94\x72\x00\xa5\x92\x79\x00\xaa\x96\x79\
-\x00\xbb\xa0\x7c\x00\xc2\x8e\x43\x00\xc5\x8e\x41\x00\xc3\x8e\x44\
-\x00\xc8\x8f\x42\x00\xc5\x90\x46\x00\xcd\x92\x42\x00\xcf\x95\x43\
-\x00\xc1\x90\x4b\x00\xc6\x92\x4b\x00\xcf\x97\x48\x00\xd1\x95\x43\
-\x00\xd4\x96\x42\x00\xd1\x97\x46\x00\xd4\x97\x44\x00\xd7\x9b\x47\
-\x00\xd1\x97\x48\x00\xd2\x99\x49\x00\xd5\x9a\x4b\x00\xd7\x9c\x49\
-\x00\xd8\x9b\x48\x00\xd8\x9d\x4a\x00\xd8\x9f\x4f\x00\xc0\x91\x50\
-\x00\xc5\x96\x56\x00\xcb\x98\x51\x00\xce\x9b\x53\x00\xcc\x9b\x56\
-\x00\xce\x9c\x57\x00\xc3\x97\x5a\x00\xcf\x9d\x59\x00\xcd\x9e\x5c\
-\x00\xd7\xa0\x54\x00\xd9\xa0\x51\x00\xd9\xa2\x56\x00\xdb\xa4\x57\
-\x00\xd1\xa1\x5f\x00\xda\xa3\x58\x00\xdb\xa5\x5a\x00\xdc\xa5\x5b\
-\x00\xdc\xa7\x5d\x00\xdd\xa8\x5e\x00\xce\xa2\x64\x00\xcd\xa5\x6f\
-\x00\xd6\xa5\x61\x00\xdd\xa9\x61\x00\xdd\xaa\x64\x00\xde\xac\x66\
-\x00\xd3\xa6\x69\x00\xd6\xa8\x69\x00\xd5\xaa\x6f\x00\xda\xab\x6a\
-\x00\xde\xad\x69\x00\xda\xad\x6f\x00\xde\xae\x6c\x00\xc7\xa3\x71\
-\x00\xcc\xa7\x73\x00\xcd\xa9\x76\x00\xd3\xab\x74\x00\xd5\xad\x76\
-\x00\xd5\xaf\x7c\x00\xdf\xb5\x7a\x00\xdb\xb3\x7e\x00\xdc\xb3\x7c\
-\x00\xe0\xaf\x6c\x00\xe0\xb0\x6e\x00\xe0\xb1\x71\x00\xe0\xb3\x74\
-\x00\xe1\xb4\x75\x00\xe2\xb6\x79\x00\xe1\xb7\x7c\x00\xe3\xb8\x7b\
-\x00\xe3\xb8\x7d\x00\x91\x89\x80\x00\x92\x8c\x83\x00\xa8\x9a\x87\
-\x00\xa9\x9b\x88\x00\xb9\xa3\x84\x00\xb2\xa0\x88\x00\xce\xaf\x84\
-\x00\xc0\xa9\x8a\x00\xc8\xb0\x8e\x00\xd5\xb2\x82\x00\xda\xb6\x85\
-\x00\xd7\xb6\x88\x00\xd9\xbc\x94\x00\xe4\xba\x81\x00\xe4\xbc\x83\
-\x00\xe4\xbd\x86\x00\xe2\xbd\x8b\x00\xe5\xbe\x89\x00\xe6\xc0\x8b\
-\x00\xe6\xc0\x8d\x00\xe7\xc2\x90\x00\xe8\xc5\x95\x00\xe5\xc4\x98\
-\x00\xe9\xc8\x9a\x00\xea\xc9\x9d\x00\xea\xcc\xa1\x00\xeb\xce\xa6\
-\x00\xec\xce\xa5\x00\xed\xd2\xac\x00\xee\xd4\xb0\x00\xef\xd6\xb4\
-\x00\xf0\xd9\xba\x00\xcf\x91\xff\x00\xdc\xb1\xff\x00\xeb\xd1\xff\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x08\x00\x2f\x00\x0e\x00\x50\
-\x00\x15\x00\x70\x00\x1b\x00\x90\x00\x21\x00\xb0\x00\x26\x00\xcf\
-\x00\x2c\x00\xf0\x00\x3e\x11\xff\x00\x58\x31\xff\x00\x71\x51\xff\
-\x00\x8c\x71\xff\x00\xa6\x91\xff\x00\xbf\xb1\xff\x00\xda\xd1\xff\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\xcf\x6d\x24\x1f\x25\x69\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\
-\x9b\xca\xdf\xe5\xe4\xe3\xde\xb6\x61\x68\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7a\xa0\xdd\
-\xd9\xa6\x92\x45\x44\x48\x98\xb1\xdd\xc2\x53\x75\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x79\xc3\xc9\x45\
-\x39\x39\x39\x39\x39\x39\x39\x39\x39\xa4\xdd\x9c\x2e\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5d\xb6\xa5\x3d\x38\
-\x44\xb0\xc4\xc5\xc4\xc5\xc3\x48\x39\x38\x40\xc3\x9e\x6b\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x82\x90\x8e\x13\x35\x3e\
-\xac\x52\x5f\x64\x64\x62\x52\xac\x3e\x36\x35\x37\xa5\x32\x73\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x8a\x11\x0e\x2f\x8f\
-\x3f\x00\x00\x00\x00\x00\x00\x89\x43\x35\x2f\x0d\x12\x90\x02\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7b\x8a\x0c\x0b\x10\x37\x90\
-\x22\x00\x00\x00\x00\x00\x00\x54\x95\x2f\x0d\x0c\x09\x3c\x3a\x73\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x46\x07\x08\x0c\x89\x32\
-\x00\x00\x00\x00\x00\x00\x00\x00\x84\x3b\x0c\x09\x08\x0f\x91\x1c\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3b\x1a\x04\x06\x14\x91\x28\
-\x00\x00\x00\x21\x2d\x00\x00\x00\x5e\x95\x10\x08\x06\x04\x87\x16\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x47\x3d\x12\x07\x42\x1a\x00\
-\x00\x00\x00\x0a\x01\x00\x00\x00\x00\x41\x30\x06\x08\x12\x46\x32\
-\x00\x00\x00\x00\x00\x00\x00\x00\x80\xb7\xb2\xac\xb0\x9d\x25\x00\
-\x00\x00\x27\xa6\x34\x72\x00\x00\x00\x2a\xa4\x98\xaa\xb0\xc3\x5a\
-\xcd\x00\x00\x00\x00\x00\x00\x00\x78\xc8\xc3\xb1\xc4\x33\x2c\x00\
-\x00\x70\x51\xac\xac\x1b\x00\x00\x00\xd3\xa2\xb1\xb0\xb2\xc3\x64\
-\xcc\x00\x00\x00\x00\x00\x00\x00\xd0\xc5\xc5\xc4\xca\x86\x50\x50\
-\x50\x88\xb2\xa6\xb7\x52\x74\x00\x00\x00\x5f\xc7\xb2\xc3\xc7\x59\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\xc7\xc5\xc6\xc4\xc4\xc3\
-\xc3\xb7\xb2\xab\xac\xaf\x20\x00\x00\x00\x83\xb9\xc5\xc4\xca\x4e\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\xdb\xc7\xc5\xc4\xc3\xb7\
-\xb2\xb2\xb1\xb1\xb0\xc8\x51\x00\x00\x00\x00\x8b\xda\xc8\xde\x1d\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5e\xdf\xca\xc7\xc6\xc5\xc4\
-\xc3\xc3\xb7\xb7\xb7\xc7\xc8\x23\x00\x00\x00\x7e\xda\xdb\xcb\x6a\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xe0\xcb\xc8\xc8\xc7\
-\xc7\xc5\xc5\xc5\xc4\xc6\xe1\x58\x00\x00\x00\x00\xae\xe6\x55\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x65\xe6\xdd\xcb\xca\xca\
-\xc8\xc7\xc7\xc7\xc7\xc7\xda\xc1\x66\x00\x00\x00\x7d\xd6\x7a\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9f\xe5\xde\xd9\xd9\
-\xd9\xcb\xcb\xcb\xcb\xcb\xcb\xe3\x59\x00\x00\x00\x00\x5b\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa1\xe8\xe0\xda\
-\xda\xd9\xd9\xd9\xd9\xd9\xd9\xe1\xc0\x6c\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\xe2\xe6\
-\xe1\xde\xdd\xdd\xdb\xdb\xdd\xe0\xea\x4d\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xba\xb5\
-\xe9\xeb\xe8\xe5\xe5\xe7\xe9\xeb\xe5\x52\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\xd2\xb3\xbf\xd7\xd8\xd5\xbd\x7c\xd1\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x7f\x00\x00\x00\xbb\x29\x6e\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\xce\x17\x60\x00\x00\x00\x00\xa7\x95\x4b\x67\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x81\x56\
-\x94\x64\x00\x00\x00\x00\x00\x00\xb8\xc4\xb5\x56\x1e\x6f\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x71\x26\x9a\xb4\xb9\
-\xad\x00\x00\x00\x00\x00\x00\x00\x00\xde\xc5\xe0\xdf\xa8\x85\x18\
-\x19\x49\x4c\x76\x77\x76\x5c\x4a\x2b\x31\x85\xb1\xde\xdd\xb9\xd4\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc7\xc8\xe5\xe1\xc8\
-\xb2\x99\x97\x93\x8d\x93\x96\xa4\xb2\xcb\xe0\xe5\xc6\xb8\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcb\xca\xe0\
-\xe3\xe3\xe4\xe1\xe1\xe3\xe1\xe4\xe1\xdf\xc9\xbc\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\xde\xc7\xb9\xb2\xb2\xb2\xc3\xc6\xdc\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xf0\x3f\xff\xff\xc0\x07\
-\xff\xff\x00\x01\xff\xfe\x00\x00\xff\xfc\x00\x00\x7f\xf8\x00\x00\
-\x3f\xf8\x0f\xc0\x3f\xf0\x0f\xc0\x1f\xf0\x1f\xe0\x1f\xf0\x1c\xe0\
-\x1f\xf0\x3c\xf0\x1f\xe0\x38\x70\x0f\xe0\x30\x70\x0f\xe0\x00\x38\
-\x1f\xf0\x00\x38\x1f\xf0\x00\x3c\x1f\xf0\x00\x1c\x1f\xf8\x00\x1e\
-\x3f\xf8\x00\x0e\x3f\xfc\x00\x0f\x7f\xfe\x00\x07\xff\xff\x00\x07\
-\xff\xff\x80\x07\xff\xff\xe0\x0f\xfb\x8f\xff\xff\xe3\xc3\xff\xff\
-\x87\xe0\x7f\xfc\x0f\xf0\x00\x00\x1f\xfc\x00\x00\x7f\xff\x00\x01\
-\xff\xff\xe0\x0f\xff\x28\x00\x00\x00\x18\x00\x00\x00\x30\x00\x00\
-\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x9e\x74\x3a\x00\x9a\x72\x3c\x00\xa9\x71\x24\x00\xac\x73\x25\
-\x00\xae\x75\x25\x00\xaa\x75\x2c\x00\xb5\x79\x27\x00\xb9\x7d\x28\
-\x00\xbd\x7e\x28\x00\xa0\x78\x3f\x00\x96\x73\x42\x00\x99\x79\x4d\
-\x00\x9f\x7c\x4c\x00\x94\x77\x50\x00\x96\x79\x51\x00\x94\x7a\x55\
-\x00\x9c\x7e\x54\x00\x94\x7b\x59\x00\x86\x7a\x69\x00\x8b\x7f\x70\
-\x00\xbc\x80\x2c\x00\xb6\x84\x3f\x00\xbb\x85\x3a\x00\xbe\x88\x3d\
-\x00\xc2\x82\x2a\x00\xc7\x86\x2d\x00\xc9\x86\x2b\x00\xd1\x8d\x2d\
-\x00\xc6\x87\x30\x00\xc7\x88\x31\x00\xc6\x8b\x39\x00\xc1\x8a\x3d\
-\x00\xc9\x8d\x3c\x00\xca\x90\x3f\x00\xd3\x94\x3d\x00\xd6\x97\x3f\
-\x00\xaf\x82\x45\x00\xa7\x82\x4f\x00\xaa\x81\x4a\x00\xb5\x84\x40\
-\x00\xb0\x84\x47\x00\xb8\x87\x45\x00\xbe\x8b\x45\x00\xb1\x85\x49\
-\x00\xb7\x8a\x4b\x00\xb4\x88\x4d\x00\xb9\x8b\x4b\x00\xbd\x8f\x4f\
-\x00\xa1\x81\x54\x00\xa8\x85\x54\x00\xb0\x89\x52\x00\xb9\x8e\x52\
-\x00\xb9\x93\x5c\x00\xbc\x95\x5f\x00\x95\x80\x62\x00\x98\x81\x62\
-\x00\x9a\x87\x6c\x00\x95\x89\x78\x00\x93\x8a\x7c\x00\xad\x8f\x65\
-\x00\xa8\x8d\x68\x00\xaf\x91\x68\x00\xa2\x8d\x70\x00\xa8\x92\x73\
-\x00\xaa\x95\x77\x00\xc1\x8e\x47\x00\xca\x90\x41\x00\xcd\x93\x42\
-\x00\xcf\x95\x44\x00\xcc\x95\x49\x00\xd3\x96\x41\x00\xd2\x96\x44\
-\x00\xd7\x9c\x4a\x00\xd6\x9d\x4d\x00\xd8\x9d\x4a\x00\xcb\x98\x52\
-\x00\xc4\x98\x5a\x00\xca\x9c\x5e\x00\xda\xa2\x54\x00\xd1\xa0\x5c\
-\x00\xdc\xa6\x5b\x00\xd9\xa5\x5d\x00\xdd\xa8\x5e\x00\xc0\x9a\x66\
-\x00\xca\x9e\x62\x00\xcf\xa2\x64\x00\xc7\xa1\x6d\x00\xcf\xa5\x6b\
-\x00\xdb\xa8\x61\x00\xdd\xa9\x61\x00\xde\xab\x64\x00\xde\xac\x66\
-\x00\xd0\xa4\x68\x00\xda\xaa\x68\x00\xde\xad\x69\x00\xda\xad\x6f\
-\x00\xdf\xaf\x6c\x00\xc4\xa2\x73\x00\xc7\xa4\x74\x00\xcc\xa7\x73\
-\x00\xcd\xa8\x75\x00\xdb\xaf\x73\x00\xd2\xac\x78\x00\xd6\xaf\x79\
-\x00\xd9\xb2\x7c\x00\xe0\xaf\x6d\x00\xe0\xb0\x6e\x00\xe0\xb1\x71\
-\x00\xe1\xb4\x75\x00\xe2\xb6\x79\x00\xe2\xb7\x7c\x00\xe3\xb8\x7d\
-\x00\x95\x8d\x82\x00\x9b\x91\x83\x00\xc7\xab\x85\x00\xcf\xae\x81\
-\x00\xcf\xb1\x86\x00\xca\xb3\x92\x00\xe0\xb8\x80\x00\xe4\xba\x81\
-\x00\xe4\xbc\x83\x00\xe4\xbc\x85\x00\xe5\xbf\x89\x00\xe6\xc0\x8c\
-\x00\xe7\xc3\x91\x00\xe8\xc4\x93\x00\xe8\xc6\x96\x00\xe8\xc7\x99\
-\x00\xea\xca\x9e\x00\xec\xd0\xaa\x00\xed\xd2\xad\x00\x90\x74\x00\
-\x00\xb0\x8e\x00\x00\xcf\xa9\x00\x00\xf0\xc3\x00\x00\xff\xd2\x11\
-\x00\xff\xd8\x31\x00\xff\xdd\x51\x00\xff\xe4\x71\x00\xff\xea\x91\
-\x00\xff\xf0\xb1\x00\xff\xf6\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x2f\x14\x00\x00\x50\x22\x00\x00\x70\x30\x00\x00\x90\x3e\x00\
-\x00\xb0\x4d\x00\x00\xcf\x5b\x00\x00\xf0\x69\x00\x00\xff\x79\x11\
-\x00\xff\x8a\x31\x00\xff\x9d\x51\x00\xff\xaf\x71\x00\xff\xc1\x91\
-\x00\xff\xd2\xb1\x00\xff\xe5\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x2f\x03\x00\x00\x50\x04\x00\x00\x70\x06\x00\x00\x90\x09\x00\
-\x00\xb0\x0a\x00\x00\xcf\x0c\x00\x00\xf0\x0e\x00\x00\xff\x20\x12\
-\x00\xff\x3e\x31\x00\xff\x5c\x51\x00\xff\x7a\x71\x00\xff\x97\x91\
-\x00\xff\xb6\xb1\x00\xff\xd4\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x2f\x00\x0e\x00\x50\x00\x17\x00\x70\x00\x21\x00\x90\x00\x2b\
-\x00\xb0\x00\x36\x00\xcf\x00\x40\x00\xf0\x00\x49\x00\xff\x11\x5a\
-\x00\xff\x31\x70\x00\xff\x51\x86\x00\xff\x71\x9c\x00\xff\x91\xb2\
-\x00\xff\xb1\xc8\x00\xff\xd1\xdf\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x2f\x00\x20\x00\x50\x00\x36\x00\x70\x00\x4c\x00\x90\x00\x62\
-\x00\xb0\x00\x78\x00\xcf\x00\x8e\x00\xf0\x00\xa4\x00\xff\x11\xb3\
-\x00\xff\x31\xbe\x00\xff\x51\xc7\x00\xff\x71\xd1\x00\xff\x91\xdc\
-\x00\xff\xb1\xe5\x00\xff\xd1\xf0\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x2c\x00\x2f\x00\x4b\x00\x50\x00\x69\x00\x70\x00\x87\x00\x90\
-\x00\xa5\x00\xb0\x00\xc4\x00\xcf\x00\xe1\x00\xf0\x00\xf0\x11\xff\
-\x00\xf2\x31\xff\x00\xf4\x51\xff\x00\xf6\x71\xff\x00\xf7\x91\xff\
-\x00\xf9\xb1\xff\x00\xfb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x1b\x00\x2f\x00\x2d\x00\x50\x00\x3f\x00\x70\x00\x52\x00\x90\
-\x00\x63\x00\xb0\x00\x76\x00\xcf\x00\x88\x00\xf0\x00\x99\x11\xff\
-\x00\xa6\x31\xff\x00\xb4\x51\xff\x00\xc2\x71\xff\x00\xcf\x91\xff\
-\x00\xdc\xb1\xff\x00\xeb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x08\x00\x2f\x00\x0e\x00\x50\x00\x15\x00\x70\x00\x1b\x00\x90\
-\x00\x21\x00\xb0\x00\x26\x00\xcf\x00\x2c\x00\xf0\x00\x3e\x11\xff\
-\x00\x58\x31\xff\x00\x71\x51\xff\x00\x8c\x71\xff\x00\xa6\x91\xff\
-\x00\xbf\xb1\xff\x00\xda\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x57\
-\x64\x57\x35\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x41\x56\x7c\x5c\x4b\x24\x4b\x5c\x7c\x56\x38\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x6e\x4a\x1c\x1c\x1c\
-\x1c\x1c\x1c\x1c\x4a\x6e\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x3e\x5b\x1e\x1b\x49\x59\x50\x50\x50\x5c\x1c\x1b\x1e\x5b\x0c\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x22\x15\x08\x1a\x46\x00\x00\
-\x00\x00\x2d\x47\x19\x09\x15\x22\x71\x00\x00\x00\x00\x00\x00\x00\
-\x2c\x1f\x05\x07\x48\x0d\x00\x00\x00\x00\x00\x45\x09\x07\x05\x1f\
-\x0a\x00\x00\x00\x00\x00\x00\x00\x18\x04\x03\x08\x20\x00\x00\x00\
-\x72\x00\x00\x2e\x21\x05\x04\x05\x18\x00\x00\x00\x00\x00\x00\x00\
-\x23\x08\x03\x44\x0c\x00\x00\x3d\x02\x00\x00\x00\x45\x04\x03\x07\
-\x23\x3b\x00\x00\x00\x00\x00\x00\x6a\x5b\x5a\x42\x00\x00\x00\x20\
-\x44\x71\x00\x00\x2c\x4f\x51\x5a\x61\x13\x00\x00\x00\x00\x00\x00\
-\x6e\x5f\x6d\x06\x0f\x0f\x01\x5f\x5c\x0b\x00\x00\x00\x5c\x5b\x5f\
-\x6b\x14\x00\x00\x00\x00\x00\x00\x5c\x6c\x6d\x6d\x6c\x6c\x6b\x5a\
-\x53\x4c\x00\x00\x00\x30\x6c\x6c\x5a\x00\x00\x00\x00\x00\x00\x00\
-\x4e\x6e\x6d\x6b\x61\x5c\x5c\x5b\x5b\x70\x0c\x00\x00\x73\x7a\x6e\
-\x4d\x00\x00\x00\x00\x00\x00\x00\x54\x7f\x6e\x6d\x6c\x6c\x6b\x61\
-\x61\x6d\x55\x00\x00\x00\x5d\x81\x32\x00\x00\x00\x00\x00\x00\x00\
-\x00\x69\x7b\x70\x6e\x6e\x6d\x6d\x6d\x6d\x80\x10\x00\x00\x62\x68\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x75\x7f\x7c\x78\x78\x70\x78\
-\x70\x70\x7c\x55\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x63\x7d\x80\x7a\x7a\x7a\x79\x7a\x7a\x81\x37\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x58\x82\x83\x81\
-\x81\x81\x83\x83\x2f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x67\x65\x74\x65\x65\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x4e\x2a\x3a\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x17\x00\x00\x00\x00\x60\
-\x6b\x34\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x39\x34\
-\x5e\x50\x00\x00\x00\x00\x00\x00\x80\x6d\x7f\x59\x42\x16\x25\x27\
-\x31\x26\x29\x28\x2b\x52\x7c\x70\x65\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x7a\x70\x81\x7e\x70\x6b\x61\x6a\x6e\x7c\x81\x79\x66\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x79\x6f\x70\
-\x70\x70\x6e\x77\x76\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\
-\x00\xff\x01\xff\x00\xfc\x00\x7f\x00\xf8\x00\x3f\x00\xf0\x00\x1f\
-\x00\xf0\x78\x0f\x00\xe0\x7c\x0f\x00\xe0\xec\x0f\x00\xe0\xce\x07\
-\x00\xe1\xc6\x07\x00\xe0\x07\x07\x00\xe0\x07\x0f\x00\xe0\x03\x0f\
-\x00\xe0\x03\x8f\x00\xf0\x01\x9f\x00\xf0\x01\xdf\x00\xf8\x00\xff\
-\x00\xfe\x00\xff\x00\xff\x83\xff\x00\x8f\xff\xf3\x00\xc3\xff\x87\
-\x00\xe0\x00\x0f\x00\xf8\x00\x3f\x00\xff\x00\xff\x00\x28\x00\x00\
-\x00\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x08\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x71\x24\x00\xae\x74\x25\
-\x00\xbd\x7f\x29\x00\x94\x73\x45\x00\xa4\x7e\x4a\x00\xa1\x7e\x4e\
-\x00\x95\x7f\x60\x00\xbd\x81\x2e\x00\xbe\x83\x32\x00\xc2\x82\x29\
-\x00\xc3\x87\x35\x00\xc3\x88\x36\x00\xc5\x89\x35\x00\xc3\x89\x38\
-\x00\xc4\x8a\x3a\x00\xca\x8f\x3d\x00\xd2\x8f\x32\x00\xbb\x88\x43\
-\x00\xb7\x8a\x4d\x00\xbb\x8c\x4a\x00\xa2\x82\x56\x00\xa6\x85\x56\
-\x00\xb0\x88\x51\x00\xb7\x92\x5f\x00\xbb\x93\x5a\x00\xbd\x94\x5a\
-\x00\x98\x83\x66\x00\x9d\x88\x6b\x00\x9b\x8b\x75\x00\xa3\x89\x65\
-\x00\xa4\x8a\x67\x00\xa7\x90\x6f\x00\xaa\x91\x6e\x00\xa1\x8e\x72\
-\x00\xaf\x97\x75\x00\xae\x99\x7a\x00\xcf\x95\x44\x00\xc1\x91\x4e\
-\x00\xd2\x96\x43\x00\xd5\x9d\x4f\x00\xd8\x9d\x4a\x00\xd9\x9e\x4d\
-\x00\xc5\x96\x54\x00\xcd\x9b\x57\x00\xcd\x9c\x57\x00\xc4\x99\x5e\
-\x00\xcf\x9e\x59\x00\xd5\x9e\x53\x00\xd8\xa4\x5b\x00\xdc\xa5\x5a\
-\x00\xd8\xa5\x5f\x00\xdc\xa8\x5e\x00\xce\xa4\x6a\x00\xd0\xa2\x61\
-\x00\xde\xab\x65\x00\xde\xac\x67\x00\xd6\xa9\x69\x00\xde\xae\x69\
-\x00\xd8\xab\x6c\x00\xdf\xaf\x6c\x00\xc2\xa1\x72\x00\xcd\xab\x7b\
-\x00\xd1\xa9\x73\x00\xdc\xb1\x74\x00\xdc\xb3\x7a\x00\xda\xb2\x7d\
-\x00\xe0\xaf\x6d\x00\xe0\xb0\x6e\x00\xe1\xb2\x72\x00\xe1\xb4\x76\
-\x00\xe2\xb7\x7a\x00\xe3\xb8\x7e\x00\xe4\xba\x7f\x00\xac\x9f\x8d\
-\x00\xcb\xae\x85\x00\xd4\xb3\x84\x00\xc9\xb1\x90\x00\xd2\xb6\x91\
-\x00\xd3\xba\x98\x00\xe3\xba\x80\x00\xe4\xba\x80\x00\xe4\xbc\x83\
-\x00\xe3\xbc\x86\x00\xe7\xc2\x8f\x00\xe7\xc3\x92\x00\xe7\xc4\x93\
-\x00\xe6\xc4\x95\x00\xe8\xc5\x94\x00\xe9\xc7\x99\x00\xea\xca\x9e\
-\x00\xdd\xc5\xa2\x00\xde\xc8\xa8\x00\xec\xd0\xa8\x00\xd4\xff\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x14\x2f\x00\x00\x22\x50\x00\
-\x00\x30\x70\x00\x00\x3d\x90\x00\x00\x4c\xb0\x00\x00\x59\xcf\x00\
-\x00\x67\xf0\x00\x00\x78\xff\x11\x00\x8a\xff\x31\x00\x9c\xff\x51\
-\x00\xae\xff\x71\x00\xc0\xff\x91\x00\xd2\xff\xb1\x00\xe4\xff\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x26\x2f\x00\x00\x40\x50\x00\
-\x00\x5a\x70\x00\x00\x74\x90\x00\x00\x8e\xb0\x00\x00\xa9\xcf\x00\
-\x00\xc2\xf0\x00\x00\xd1\xff\x11\x00\xd8\xff\x31\x00\xde\xff\x51\
-\x00\xe3\xff\x71\x00\xe9\xff\x91\x00\xef\xff\xb1\x00\xf6\xff\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x26\x00\x00\x50\x41\x00\
-\x00\x70\x5b\x00\x00\x90\x74\x00\x00\xb0\x8e\x00\x00\xcf\xa9\x00\
-\x00\xf0\xc3\x00\x00\xff\xd2\x11\x00\xff\xd8\x31\x00\xff\xdd\x51\
-\x00\xff\xe4\x71\x00\xff\xea\x91\x00\xff\xf0\xb1\x00\xff\xf6\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x14\x00\x00\x50\x22\x00\
-\x00\x70\x30\x00\x00\x90\x3e\x00\x00\xb0\x4d\x00\x00\xcf\x5b\x00\
-\x00\xf0\x69\x00\x00\xff\x79\x11\x00\xff\x8a\x31\x00\xff\x9d\x51\
-\x00\xff\xaf\x71\x00\xff\xc1\x91\x00\xff\xd2\xb1\x00\xff\xe5\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x03\x00\x00\x50\x04\x00\
-\x00\x70\x06\x00\x00\x90\x09\x00\x00\xb0\x0a\x00\x00\xcf\x0c\x00\
-\x00\xf0\x0e\x00\x00\xff\x20\x12\x00\xff\x3e\x31\x00\xff\x5c\x51\
-\x00\xff\x7a\x71\x00\xff\x97\x91\x00\xff\xb6\xb1\x00\xff\xd4\xd1\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x00\x0e\x00\x50\x00\x17\
-\x00\x70\x00\x21\x00\x90\x00\x2b\x00\xb0\x00\x36\x00\xcf\x00\x40\
-\x00\xf0\x00\x49\x00\xff\x11\x5a\x00\xff\x31\x70\x00\xff\x51\x86\
-\x00\xff\x71\x9c\x00\xff\x91\xb2\x00\xff\xb1\xc8\x00\xff\xd1\xdf\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x00\x20\x00\x50\x00\x36\
-\x00\x70\x00\x4c\x00\x90\x00\x62\x00\xb0\x00\x78\x00\xcf\x00\x8e\
-\x00\xf0\x00\xa4\x00\xff\x11\xb3\x00\xff\x31\xbe\x00\xff\x51\xc7\
-\x00\xff\x71\xd1\x00\xff\x91\xdc\x00\xff\xb1\xe5\x00\xff\xd1\xf0\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2c\x00\x2f\x00\x4b\x00\x50\
-\x00\x69\x00\x70\x00\x87\x00\x90\x00\xa5\x00\xb0\x00\xc4\x00\xcf\
-\x00\xe1\x00\xf0\x00\xf0\x11\xff\x00\xf2\x31\xff\x00\xf4\x51\xff\
-\x00\xf6\x71\xff\x00\xf7\x91\xff\x00\xf9\xb1\xff\x00\xfb\xd1\xff\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x1b\x00\x2f\x00\x2d\x00\x50\
-\x00\x3f\x00\x70\x00\x52\x00\x90\x00\x63\x00\xb0\x00\x76\x00\xcf\
-\x00\x88\x00\xf0\x00\x99\x11\xff\x00\xa6\x31\xff\x00\xb4\x51\xff\
-\x00\xc2\x71\xff\x00\xcf\x91\xff\x00\xdc\xb1\xff\x00\xeb\xd1\xff\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x08\x00\x2f\x00\x0e\x00\x50\
-\x00\x15\x00\x70\x00\x1b\x00\x90\x00\x21\x00\xb0\x00\x26\x00\xcf\
-\x00\x2c\x00\xf0\x00\x3e\x11\xff\x00\x58\x31\xff\x00\x71\x51\xff\
-\x00\x8c\x71\xff\x00\xa6\x91\xff\x00\xbf\xb1\xff\x00\xda\xd1\xff\
-\x00\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4a\x35\x3c\x3c\x3c\x39\x1f\
-\x00\x00\x00\x00\x00\x00\x00\x00\x23\x31\x11\x29\x2a\x2a\x11\x28\
-\x19\x00\x00\x00\x00\x00\x00\x00\x10\x03\x25\x24\x00\x4b\x27\x0a\
-\x0b\x05\x00\x00\x00\x00\x00\x12\x02\x08\x16\x00\x00\x00\x2e\x03\
-\x02\x0e\x00\x00\x00\x00\x00\x0d\x01\x0e\x00\x00\x04\x00\x00\x0f\
-\x01\x09\x1b\x00\x00\x00\x00\x43\x37\x06\x00\x22\x30\x00\x00\x1a\
-\x32\x38\x07\x00\x00\x00\x00\x3c\x45\x2f\x2d\x33\x34\x15\x00\x00\
-\x44\x45\x1c\x00\x00\x00\x00\x35\x46\x44\x3c\x38\x38\x3b\x00\x00\
-\x35\x51\x00\x00\x00\x00\x00\x4e\x54\x47\x46\x45\x45\x48\x1e\x00\
-\x4f\x3d\x00\x00\x00\x00\x00\x00\x3f\x55\x51\x51\x48\x49\x42\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4c\x57\x5a\x58\x59\x5d\x22\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5c\x5b\x00\x00\
-\x00\x00\x00\x20\x00\x00\x41\x18\x1d\x00\x00\x00\x00\x00\x00\x00\
-\x00\x21\x2c\x00\x00\x00\x00\x00\x50\x40\x2b\x14\x17\x13\x26\x36\
-\x51\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x56\x52\x48\x48\x53\x4d\
-\x00\x00\x00\x00\x00\xff\xff\x00\x00\xf0\x1f\x00\x00\xe0\x0f\x00\
-\x00\xe1\x07\x00\x00\xc3\x87\x00\x00\xc6\xc3\x00\x00\xc4\xc3\x00\
-\x00\xc0\x63\x00\x00\xc0\x67\x00\x00\xc0\x27\x00\x00\xe0\x3f\x00\
-\x00\xf0\x1f\x00\x00\xfe\x7d\x00\x00\x8f\xf3\x00\x00\xe0\x07\x00\
-\x00\xf8\x1f\x00\x00\x28\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\
-\x00\x01\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x0b\x00\x00\x00\
-\x11\x00\x00\x00\x15\x00\x00\x00\x18\x00\x00\x00\x1a\x00\x00\x00\
-\x1a\x00\x00\x00\x18\x00\x00\x00\x15\x00\x00\x00\x11\x00\x00\x00\
-\x0b\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
-\x0a\x00\x00\x00\x16\x00\x00\x00\x23\x00\x00\x00\x30\x00\x00\x00\
-\x3b\x00\x00\x00\x44\x00\x00\x00\x4a\x00\x00\x00\x4c\x00\x00\x00\
-\x4c\x00\x00\x00\x4a\x00\x00\x00\x44\x00\x00\x00\x3b\x00\x00\x00\
-\x30\x00\x00\x00\x23\x00\x00\x00\x16\x00\x00\x00\x0a\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x18\x00\x00\x00\
-\x2d\x4e\x34\x11\x65\x77\x50\x1a\x9c\x8e\x5f\x1f\xcb\x98\x66\x21\
-\xe1\xa7\x70\x24\xff\xa7\x70\x24\xff\xa7\x70\x24\xff\xa7\x70\x24\
-\xff\x98\x66\x21\xe2\x8c\x5e\x1e\xce\x70\x4b\x18\xa4\x42\x2d\x0f\
-\x77\x00\x00\x00\x4f\x00\x00\x00\x41\x00\x00\x00\x2d\x00\x00\x00\
-\x18\x00\x00\x00\x0a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x05\x00\x00\x00\x14\x00\x00\x00\x2b\x70\x4b\x18\x8a\x98\x66\x21\
-\xdf\xbc\x7e\x28\xff\xdb\xa2\x54\xff\xe5\xbd\x85\xff\xea\xca\x9e\
-\xff\xf2\xde\xc2\xff\xf2\xde\xc2\xff\xf2\xde\xc2\xff\xf2\xde\xc2\
-\xff\xea\xca\x9e\xff\xe5\xbd\x85\xff\xdb\xa2\x54\xff\xbc\x7e\x28\
-\xff\x96\x64\x20\xe2\x65\x44\x16\x9c\x00\x00\x00\x55\x00\x00\x00\
-\x43\x00\x00\x00\x2b\x00\x00\x00\x14\x00\x00\x00\x05\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\
-\x1c\x6b\x48\x17\x77\x99\x67\x21\xe9\xd0\x8c\x2d\xff\xe6\xbf\x8a\
-\xff\xf1\xdb\xbc\xff\xef\xd5\xb2\xff\xe7\xc2\x8f\xff\xe2\xb4\x75\
-\xff\xdc\xa6\x5b\xff\xda\xa1\x51\xff\xda\xa1\x51\xff\xdc\xa6\x5b\
-\xff\xe2\xb4\x75\xff\xe7\xc2\x8f\xff\xef\xd5\xb2\xff\xf1\xdb\xbc\
-\xff\xe6\xbf\x8a\xff\xd0\x8c\x2d\xff\x98\x66\x21\xec\x57\x3a\x13\
-\x92\x00\x00\x00\x52\x00\x00\x00\x39\x00\x00\x00\x1c\x00\x00\x00\
-\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x07\x3a\x27\x0d\x2c\x8e\x5f\x1f\
-\xc3\xc1\x81\x29\xff\xe5\xbc\x84\xff\xef\xd6\xb4\xff\xe8\xc4\x93\
-\xff\xdb\xa4\x57\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xdb\xa4\x57\
-\xff\xe8\xc4\x93\xff\xef\xd6\xb4\xff\xe5\xbc\x84\xff\xc1\x81\x29\
-\xff\x86\x5a\x1d\xce\x19\x11\x06\x64\x00\x00\x00\x40\x00\x00\x00\
-\x1e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x07\x38\x26\x0c\x2c\x93\x63\x20\xdb\xd5\x93\x38\
-\xff\xec\xcf\xa8\xff\xe8\xc4\x93\xff\xd9\x9e\x4c\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd9\x9e\x4d\xff\xe8\xc4\x93\xff\xec\xcf\xa8\
-\xff\xd5\x93\x38\xff\x8e\x5f\x1f\xe2\x17\x0f\x05\x65\x00\x00\x00\
-\x40\x00\x00\x00\x1e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x05\x3b\x28\x0d\x29\x91\x61\x1f\xdb\xdd\xa8\x5e\xff\xea\xca\x9e\
-\xff\xdd\xa9\x61\xff\xcb\x88\x2c\xff\xce\x8a\x2c\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd0\x8c\x2d\xff\xcb\x88\x2c\xff\xdf\xad\x69\
-\xff\xea\xca\x9e\xff\xdd\xa8\x5e\xff\x8c\x5e\x1e\xe2\x17\x0f\x05\
-\x65\x00\x00\x00\x3f\x00\x00\x00\x1b\x00\x00\x00\x05\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x47\x30\x10\
-\x22\x90\x61\x1f\xda\xda\xa0\x4f\xff\xe6\xbf\x8a\xff\xd8\x9b\x47\
-\xff\xc4\x83\x2a\xff\xc8\x86\x2b\xff\xcd\x89\x2c\xff\xd0\x8c\x2d\
-\xff\xd8\x9c\x4a\xff\xe2\xb5\x77\xff\xe6\xbf\x8a\xff\xe8\xc3\x91\
-\xff\xe8\xc3\x91\xff\xe8\xc3\x91\xff\xe8\xc3\x91\xff\xe8\xc3\x91\
-\xff\xe8\xc3\x91\xff\xe6\xbf\x8a\xff\xda\xa1\x51\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xcd\x89\x2c\xff\xc9\x87\x2b\xff\xc4\x83\x2a\
-\xff\xd8\x9b\x47\xff\xe6\xbf\x8a\xff\xda\xa0\x4f\xff\x8b\x5d\x1e\
-\xe2\x17\x0f\x05\x63\x00\x00\x00\x37\x00\x00\x00\x12\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x87\x5b\x1d\
-\xaf\xc4\x83\x2a\xff\xe2\xb4\x75\xff\xd5\x94\x3a\xff\xbe\x7f\x29\
-\xff\xc1\x81\x29\xff\xc6\x85\x2b\xff\xc9\x87\x2b\xff\xd4\x92\x36\
-\xff\xe3\xb8\x7c\xff\xda\xa1\x51\xff\xc6\x85\x2b\xff\xba\x7d\x28\
-\xff\xba\x7d\x28\xff\xba\x7d\x28\xff\xba\x7d\x28\xff\xba\x7d\x28\
-\xff\xba\x7d\x28\xff\xd8\x9c\x48\xff\xe2\xb5\x77\xff\xd4\x91\x35\
-\xff\xce\x8a\x2c\xff\xc9\x87\x2b\xff\xc6\x85\x2b\xff\xc3\x83\x2a\
-\xff\xbe\x7f\x29\xff\xd5\x94\x3b\xff\xe2\xb5\x77\xff\xc4\x83\x2a\
-\xff\x77\x50\x1a\xc5\x00\x00\x00\x50\x00\x00\x00\x29\x00\x00\x00\
-\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x02\x78\x51\x1a\x60\xa0\x6b\x22\
-\xff\xdc\xa5\x59\xff\xd6\x96\x3e\xff\xb7\x7b\x27\xff\xba\x7d\x28\
-\xff\xbe\x7f\x29\xff\xc3\x83\x2a\xff\xc6\x85\x2b\xff\xdc\xa6\x5b\
-\xff\xd8\x9c\x4a\xff\x90\x61\x1f\xf4\x68\x46\x17\x70\x7c\x53\x1b\
-\x4c\x87\x5b\x1d\x45\x8b\x5d\x1e\x44\x8b\x5d\x1e\x44\x8b\x5d\x1e\
-\x44\x8b\x5d\x1e\x55\x91\x61\x1f\xf0\xdc\xa5\x59\xff\xda\xa1\x51\
-\xff\xc9\x87\x2b\xff\xc6\x85\x2b\xff\xc3\x83\x2a\xff\xbf\x80\x29\
-\xff\xbc\x7e\x28\xff\xb7\x7b\x27\xff\xd6\x96\x3e\xff\xdc\xa6\x5b\
-\xff\xa0\x6b\x22\xff\x44\x2e\x0f\x88\x00\x00\x00\x42\x00\x00\x00\
-\x18\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x8b\x5d\x1e\xe5\xd5\x94\x3b\
-\xff\xd8\x9c\x48\xff\xb1\x77\x26\xff\xb4\x79\x27\xff\xb7\x7b\x27\
-\xff\xbc\x7e\x28\xff\xbf\x80\x29\xff\xd5\x93\x38\xff\xdc\xa6\x5c\
-\xff\xa2\x6d\x23\xff\x5e\x3f\x14\x7a\x00\x00\x00\x15\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x8c\x5e\x1e\x82\xba\x7d\x28\xff\xdd\xa8\x5e\
-\xff\xce\x8a\x2c\xff\xc3\x83\x2a\xff\xbf\x80\x29\xff\xbc\x7e\x28\
-\xff\xb9\x7c\x28\xff\xb5\x79\x27\xff\xb2\x77\x26\xff\xd8\x9c\x48\
-\xff\xd5\x94\x3b\xff\x87\x5b\x1d\xec\x00\x00\x00\x54\x00\x00\x00\
-\x2c\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x01\x7c\x53\x1b\x6d\xa8\x71\x24\xff\xdb\xa4\x57\
-\xff\xbf\x80\x29\xff\xaf\x76\x26\xff\xb2\x77\x26\xff\xb5\x79\x27\
-\xff\xb9\x7c\x28\xff\xbf\x80\x29\xff\xdc\xa5\x59\xff\xce\x8a\x2c\
-\xff\x7f\x55\x1b\xd3\x00\x00\x00\x24\x00\x00\x00\x06\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x06\x00\x00\x00\
-\x03\x00\x00\x00\x00\x8e\x5f\x1f\x10\x8c\x5e\x1e\xe0\xd6\x97\x40\
-\xff\xd9\x9e\x4d\xff\xbf\x80\x29\xff\xbc\x7e\x28\xff\xb9\x7c\x28\
-\xff\xb5\x79\x27\xff\xb2\x77\x26\xff\xaf\x76\x26\xff\xc1\x81\x29\
-\xff\xdb\xa4\x57\xff\xa8\x71\x24\xff\x56\x3a\x13\x9b\x00\x00\x00\
-\x3f\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x04\x86\x5a\x1d\xd5\xd4\x90\x33\xff\xd7\x98\x42\
-\xff\xa8\x71\x24\xff\xac\x73\x25\xff\xaf\x76\x26\xff\xb2\x77\x26\
-\xff\xb5\x79\x27\xff\xd5\x93\x38\xff\xdb\xa3\x56\xff\x91\x61\x1f\
-\xff\x36\x24\x0c\x51\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\
-\x00\x00\x00\x00\x04\x00\x00\x00\x14\x00\x00\x00\x1b\x00\x00\x00\
-\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x89\x5c\x1e\x61\xa8\x71\x24\
-\xff\xdd\xa8\x5e\xff\xc4\x83\x2a\xff\xb9\x7c\x28\xff\xb5\x79\x27\
-\xff\xb2\x77\x26\xff\xaf\x76\x26\xff\xad\x74\x25\xff\xaa\x72\x25\
-\xff\xd7\x98\x42\xff\xd4\x90\x33\xff\x78\x51\x1a\xd8\x00\x00\x00\
-\x4d\x00\x00\x00\x20\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x6a\x47\x17\x29\x8e\x5f\x1f\xff\xdc\xa5\x59\xff\xc6\x85\x2b\
-\xff\xa7\x70\x24\xff\xaa\x72\x25\xff\xac\x73\x25\xff\xaf\x76\x26\
-\xff\xb7\x7b\x27\xff\xdb\xa4\x57\xff\xc4\x83\x2a\xff\x73\x4d\x19\
-\xbc\x00\x00\x00\x22\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\
-\x00\x4a\x32\x10\x1d\x6d\x49\x18\x8a\x00\x00\x00\x3a\x00\x00\x00\
-\x1a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x87\x5b\x1d\
-\xe1\xd8\x9b\x47\xff\xd7\x98\x42\xff\xb5\x79\x27\xff\xb2\x77\x26\
-\xff\xaf\x76\x26\xff\xad\x74\x25\xff\xaa\x72\x25\xff\xa7\x70\x24\
-\xff\xc6\x85\x2b\xff\xdc\xa5\x59\xff\x8e\x5f\x1f\xff\x28\x1b\x09\
-\x6b\x00\x00\x00\x2d\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x78\x51\x1a\x69\xb2\x77\x26\xff\xdc\xa5\x59\xff\xa2\x6d\x23\
-\xff\xa3\x6d\x23\xff\xa7\x70\x24\xff\xaa\x72\x25\xff\xac\x73\x25\
-\xff\xd4\x90\x33\xff\xdb\xa3\x56\xff\x8c\x5e\x1e\xff\x35\x24\x0c\
-\x51\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x04\x77\x50\x1a\x8f\x82\x57\x1c\xf4\x00\x00\x00\x52\x00\x00\x00\
-\x2e\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x82\x57\x1c\
-\x72\xb4\x79\x27\xff\xdd\xa8\x5f\xff\xbc\x7e\x28\xff\xaf\x76\x26\
-\xff\xad\x74\x25\xff\xaa\x72\x25\xff\xa7\x70\x24\xff\xa5\x6f\x24\
-\xff\xa7\x70\x24\xff\xdc\xa5\x59\xff\xb2\x77\x26\xff\x52\x37\x12\
-\x99\x00\x00\x00\x38\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7c\x53\x1b\xa6\xc9\x87\x2b\xff\xd7\x98\x42\xff\x9e\x6a\x22\
-\xff\xa2\x6d\x23\xff\xa3\x6d\x23\xff\xa7\x70\x24\xff\xaf\x76\x26\
-\xff\xdc\xa5\x59\xff\xc3\x83\x2a\xff\x6f\x4a\x18\xbc\x00\x00\x00\
-\x22\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x01\x47\x30\x10\
-\x1d\x81\x57\x1c\xf2\x9b\x68\x21\xff\x4f\x35\x11\x9c\x00\x00\x00\
-\x44\x00\x00\x00\x19\x00\x00\x00\x02\x00\x00\x00\x00\x82\x57\x1c\
-\x10\x81\x57\x1c\xf0\xda\xa0\x4f\xff\xd6\x96\x3e\xff\xac\x73\x25\
-\xff\xaa\x72\x25\xff\xa7\x70\x24\xff\xa5\x6f\x24\xff\xa2\x6d\x23\
-\xff\xa0\x6b\x22\xff\xd7\x98\x42\xff\xc9\x87\x2b\xff\x6a\x47\x17\
-\xc3\x00\x00\x00\x3f\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7c\x53\x1b\xc5\xd6\x96\x3d\xff\xd8\x9c\x4a\xff\xd2\x8d\x2d\
-\xff\xcb\x88\x2c\xff\xb9\x7c\x28\xff\xa3\x6d\x23\xff\xd3\x8f\x30\
-\xff\xdb\xa4\x57\xff\x87\x5b\x1d\xff\x33\x22\x0b\x51\x00\x00\x00\
-\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x06\x72\x4d\x19\
-\x90\xb2\x77\x26\xff\xd6\x97\x40\xff\x78\x51\x1a\xec\x00\x00\x00\
-\x54\x00\x00\x00\x2c\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7d\x54\x1b\x82\xb2\x77\x26\xff\xdd\xa8\x5f\xff\xb4\x79\x27\
-\xff\xa7\x70\x24\xff\xa3\x6d\x23\xff\xb4\x79\x27\xff\xc4\x83\x2a\
-\xff\xd2\x8d\x2d\xff\xd8\x9c\x4a\xff\xd6\x96\x3d\xff\x70\x4b\x18\
-\xd8\x00\x00\x00\x44\x00\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7c\x53\x1b\xe2\xdc\xa5\x59\xff\xe1\xb2\x70\xff\xde\xaa\x63\
-\xff\xdd\xa8\x5f\xff\xdd\xa8\x5e\xff\xdc\xa6\x5c\xff\xdf\xad\x69\
-\xff\xc3\x83\x2a\xff\x6a\x47\x17\xbd\x00\x00\x00\x28\x00\x00\x00\
-\x0c\x00\x00\x00\x06\x00\x00\x00\x07\x4f\x35\x11\x32\x84\x59\x1d\
-\xf3\xda\xa1\x52\xff\xde\xaa\x63\xff\xa0\x6b\x22\xff\x4e\x34\x11\
-\x9b\x00\x00\x00\x40\x00\x00\x00\x16\x00\x00\x00\x02\x00\x00\x00\
-\x00\x7d\x54\x1b\x10\x7c\x53\x1b\xf0\xdb\xa2\x54\xff\xd7\x9a\x45\
-\xff\xd5\x94\x3b\xff\xdb\xa2\x54\xff\xdc\xa6\x5c\xff\xdd\xa8\x5f\
-\xff\xde\xaa\x63\xff\xe1\xb2\x70\xff\xdb\xa4\x57\xff\x72\x4d\x19\
-\xe2\x00\x00\x00\x4a\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7a\x52\x1a\xff\xe3\xb8\x7d\xff\xe0\xb0\x6e\xff\xde\xac\x66\
-\xff\xde\xaa\x63\xff\xdd\xa9\x61\xff\xdf\xae\x6b\xff\xe0\xb0\x6e\
-\xff\x84\x59\x1d\xff\x26\x1a\x09\x66\x00\x00\x00\x35\x00\x00\x00\
-\x27\x00\x00\x00\x26\x00\x00\x00\x27\x6d\x49\x18\xb3\xc1\x81\x29\
-\xff\xdd\xa8\x5e\xff\xd9\x9e\x4c\xff\xd5\x93\x38\xff\x6b\x48\x17\
-\xd8\x00\x00\x00\x51\x00\x00\x00\x28\x00\x00\x00\x08\x00\x00\x00\
-\x00\x00\x00\x00\x00\x78\x51\x1a\x91\xd4\x91\x35\xff\xe2\xb5\x77\
-\xff\xdc\xa6\x5c\xff\xdd\xa8\x5e\xff\xdd\xa8\x5f\xff\xde\xaa\x63\
-\xff\xde\xac\x66\xff\xe0\xb0\x6d\xff\xe2\xb4\x75\xff\x7a\x52\x1a\
-\xff\x00\x00\x00\x49\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7c\x53\x1b\xff\xe4\xba\x80\xff\xe1\xb2\x70\xff\xdf\xad\x69\
-\xff\xde\xac\x66\xff\xde\xab\x64\xff\xe3\xb7\x7a\xff\xce\x8a\x2c\
-\xff\x6d\x49\x18\xd8\x3b\x28\x0d\x86\x3f\x2a\x0e\x80\x3f\x2a\x0e\
-\x7e\x3f\x2a\x0e\x7e\x49\x31\x10\x89\x86\x5a\x1d\xff\xe0\xb0\x6e\
-\xff\xde\xaa\x63\xff\xdd\xa8\x5f\xff\xe2\xb5\x77\xff\x8e\x5f\x1f\
-\xff\x3b\x28\x0d\x86\x00\x00\x00\x3d\x00\x00\x00\x13\x00\x00\x00\
-\x01\x00\x00\x00\x00\x77\x50\x1a\x21\x8e\x5f\x1f\xff\xe2\xb6\x78\
-\xff\xdf\xae\x6b\xff\xdd\xa8\x5f\xff\xde\xaa\x63\xff\xde\xac\x66\
-\xff\xdf\xad\x68\xff\xe1\xb2\x72\xff\xe2\xb6\x78\xff\x7c\x53\x1b\
-\xff\x00\x00\x00\x43\x00\x00\x00\x15\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7d\x54\x1b\xd2\xe2\xb4\x75\xff\xe2\xb6\x78\xff\xe0\xb0\x6d\
-\xff\xdf\xad\x69\xff\xe1\xb2\x70\xff\xe4\xba\x7f\xff\xba\x7d\x28\
-\xff\xa7\x70\x24\xff\xa7\x70\x24\xff\xa8\x71\x24\xff\xa8\x71\x24\
-\xff\xa8\x71\x24\xff\xa7\x70\x24\xff\xd4\x91\x35\xff\xe2\xb6\x78\
-\xff\xdb\xa4\x57\xff\xdb\xa2\x54\xff\xe2\xb4\x75\xff\xd5\x94\x3b\
-\xff\x70\x4b\x18\xd8\x00\x00\x00\x4f\x00\x00\x00\x23\x00\x00\x00\
-\x06\x00\x00\x00\x00\x00\x00\x00\x00\x7d\x54\x1b\xa1\xd6\x96\x3d\
-\xff\xe3\xb7\x7a\xff\xde\xab\x64\xff\xde\xac\x66\xff\xdf\xad\x69\
-\xff\xe0\xb0\x6d\xff\xe2\xb6\x78\xff\xdb\xa4\x57\xff\x70\x4b\x18\
-\xd7\x00\x00\x00\x3d\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7f\x55\x1b\xc3\xdc\xa6\x5b\xff\xe3\xb8\x7c\xff\xe1\xb2\x70\
-\xff\xe0\xb0\x6d\xff\xe4\xba\x7f\xff\xe4\xba\x80\xff\xe4\xba\x80\
-\xff\xe4\xba\x80\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
-\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe0\xb0\x6d\
-\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xde\xac\x66\xff\xe3\xb8\x7c\
-\xff\x94\x63\x20\xff\x24\x18\x08\x70\x00\x00\x00\x38\x00\x00\x00\
-\x11\x00\x00\x00\x01\x00\x00\x00\x00\x7d\x54\x1b\x31\xa5\x6f\x24\
-\xff\xe4\xba\x80\xff\xe1\xb2\x72\xff\xdf\xad\x69\xff\xe0\xb0\x6d\
-\xff\xe0\xb0\x6e\xff\xe3\xb8\x7c\xff\xdc\xa5\x59\xff\x73\x4d\x19\
-\xd6\x00\x00\x00\x35\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x81\x57\x1c\x93\xd6\x96\x3d\xff\xe4\xbb\x82\xff\xe1\xb3\x73\
-\xff\xe1\xb2\x70\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xad\x69\
-\xff\xde\xac\x66\xff\xde\xac\x66\xff\xde\xaa\x63\xff\xde\xaa\x63\
-\xff\xdd\xa9\x61\xff\xdd\xa8\x5f\xff\xdd\xa8\x5f\xff\xdd\xa8\x5e\
-\xff\xdd\xa8\x5e\xff\xdd\xa8\x5e\xff\xdd\xa8\x5e\xff\xe4\xba\x7f\
-\xff\xd2\x8d\x2d\xff\x6a\x47\x17\xc5\x00\x00\x00\x4d\x00\x00\x00\
-\x22\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x82\x57\x1c\
-\xc1\xdb\xa3\x56\xff\xe3\xb8\x7d\xff\xe0\xb0\x6d\xff\xe1\xb2\x70\
-\xff\xe1\xb2\x72\xff\xe4\xbb\x82\xff\xd4\x90\x33\xff\x68\x46\x17\
-\xb4\x00\x00\x00\x2a\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x84\x59\x1d\x62\xc6\x85\x2b\xff\xe8\xc3\x91\xff\xe2\xb6\x78\
-\xff\xe1\xb3\x73\xff\xe1\xb2\x72\xff\xe0\xb0\x6e\xff\xe0\xb0\x6d\
-\xff\xdf\xae\x6b\xff\xdf\xad\x69\xff\xdf\xad\x68\xff\xde\xac\x66\
-\xff\xde\xab\x64\xff\xde\xab\x64\xff\xde\xaa\x63\xff\xde\xaa\x63\
-\xff\xde\xaa\x63\xff\xdd\xa9\x61\xff\xdd\xa9\x61\xff\xe2\xb5\x77\
-\xff\xe4\xba\x7f\xff\x90\x61\x1f\xff\x28\x1b\x09\x6f\x00\x00\x00\
-\x36\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x84\x59\x1d\
-\x51\xc1\x81\x29\xff\xe7\xc2\x8f\xff\xe3\xb7\x7a\xff\xe1\xb3\x73\
-\xff\xe2\xb6\x78\xff\xe8\xc3\x91\xff\xba\x7d\x28\xff\x52\x37\x12\
-\x83\x00\x00\x00\x1d\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x87\x5b\x1d\x10\x94\x63\x20\xff\xea\xca\x9d\xff\xe5\xbe\x87\
-\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb2\x72\
-\xff\xe0\xb0\x6e\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
-\xff\xdf\xad\x69\xff\xdf\xad\x68\xff\xdf\xad\x68\xff\xde\xac\x66\
-\xff\xde\xac\x66\xff\xde\xac\x66\xff\xde\xac\x66\xff\xdf\xad\x69\
-\xff\xe9\xc6\x96\xff\xd5\x94\x3b\xff\x6a\x47\x17\xba\x00\x00\x00\
-\x49\x00\x00\x00\x1e\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
-\x00\x87\x5b\x1d\xe1\xe4\xba\x7f\xff\xe7\xc2\x8e\xff\xe2\xb5\x77\
-\xff\xe5\xbe\x87\xff\xe8\xc4\x93\xff\x89\x5c\x1e\xff\x1e\x14\x07\
-\x47\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x89\x5c\x1e\xb2\xdd\xa8\x5f\xff\xeb\xcd\xa3\
-\xff\xe3\xb8\x7c\xff\xe2\xb6\x78\xff\xe2\xb5\x77\xff\xe2\xb4\x75\
-\xff\xe1\xb3\x73\xff\xe1\xb2\x72\xff\xe1\xb2\x70\xff\xe0\xb0\x6e\
-\xff\xe0\xb0\x6d\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
-\xff\xdf\xae\x6b\xff\xdf\xae\x6b\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
-\xff\xe6\xbf\x8a\xff\xe6\xbf\x8a\xff\x89\x5c\x1e\xf5\x17\x0f\x05\
-\x62\x00\x00\x00\x33\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\
-\x00\x89\x5c\x1e\x72\xd4\x92\x36\xff\xec\xcf\xa6\xff\xe4\xba\x80\
-\xff\xeb\xcd\xa3\xff\xdd\xa8\x5f\xff\x7a\x52\x1a\xc8\x00\x00\x00\
-\x27\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x8b\x5d\x1e\x41\xb9\x7c\x28\xff\xee\xd4\xaf\
-\xff\xe8\xc3\x91\xff\xe3\xb8\x7c\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\
-\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
-\xff\xe1\xb2\x72\xff\xe1\xb2\x70\xff\xe1\xb2\x70\xff\xe0\xb0\x6e\
-\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\
-\xff\xe2\xb5\x77\xff\xed\xd1\xab\xff\xd3\x8e\x2e\xff\x66\x44\x16\
-\xb1\x00\x00\x00\x47\x00\x00\x00\x1a\x00\x00\x00\x02\x00\x00\x00\
-\x00\x8c\x5e\x1e\x10\x96\x64\x20\xf0\xea\xca\x9e\xff\xed\xd1\xaa\
-\xff\xee\xd4\xaf\xff\xa7\x70\x24\xff\x52\x37\x12\x6e\x00\x00\x00\
-\x14\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x5f\x1f\xc1\xe1\xb2\x72\
-\xff\xee\xd4\xb0\xff\xe4\xbb\x82\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\
-\xff\xe3\xb8\x7c\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\xff\xe2\xb5\x77\
-\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
-\xff\xe1\xb3\x73\xff\xe1\xb3\x73\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
-\xff\xe1\xb3\x73\xff\xe9\xc7\x99\xff\xe8\xc5\x94\xff\x8e\x5f\x1f\
-\xf5\x00\x00\x00\x56\x00\x00\x00\x2e\x00\x00\x00\x0a\x00\x00\x00\
-\x00\x00\x00\x00\x00\x8e\x5f\x1f\x91\xdd\xa9\x61\xff\xef\xd6\xb4\
-\xff\xde\xac\x66\xff\x82\x57\x1c\xd2\x00\x00\x00\x25\x00\x00\x00\
-\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x61\x1f\x31\xa8\x71\x24\
-\xff\xed\xd1\xab\xff\xec\xcf\xa6\xff\xe4\xba\x80\xff\xe4\xba\x80\
-\xff\xe4\xba\x7f\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
-\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\xff\xe2\xb6\x78\xff\xe2\xb6\x78\
-\xff\xe2\xb5\x77\xff\xe2\xb5\x77\xff\xe2\xb5\x77\xff\xe2\xb5\x77\
-\xff\xe2\xb6\x78\xff\xe4\xba\x7f\xff\xef\xd6\xb4\xff\xcd\x89\x2c\
-\xff\x5b\x3d\x14\x9c\x00\x00\x00\x44\x00\x00\x00\x19\x00\x00\x00\
-\x02\x00\x00\x00\x00\x8e\x5f\x1f\x21\xa8\x71\x24\xff\xec\xcf\xa8\
-\xff\xa0\x6b\x22\xff\x4f\x35\x11\x59\x00\x00\x00\x0f\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\
-\x81\xd2\x8d\x2d\xff\xf0\xd9\xb9\xff\xec\xce\xa5\xff\xe5\xbc\x84\
-\xff\xe4\xbb\x82\xff\xe4\xba\x80\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
-\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
-\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
-\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xec\xce\xa5\xff\xe4\xba\x80\
-\xff\x8e\x5f\x1f\xec\x00\x00\x00\x54\x00\x00\x00\x2c\x00\x00\x00\
-\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x93\x63\x20\xb2\xa2\x6d\x23\
-\xff\x81\x57\x1c\x96\x00\x00\x00\x17\x00\x00\x00\x03\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x96\x64\x20\xb1\xd6\x96\x3d\xff\xf0\xd9\xba\xff\xec\xcf\xa6\
-\xff\xe5\xbd\x85\xff\xe5\xbc\x84\xff\xe4\xbb\x82\xff\xe4\xbb\x82\
-\xff\xe4\xba\x80\xff\xe4\xba\x80\xff\xe4\xba\x80\xff\xe4\xba\x7f\
-\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
-\xff\xe4\xba\x7f\xff\xe4\xba\x80\xff\xe7\xc2\x8f\xff\xf0\xd9\xb9\
-\xff\xc4\x83\x2a\xff\x5e\x3f\x14\x9b\x00\x00\x00\x40\x00\x00\x00\
-\x16\x00\x00\x00\x02\x00\x00\x00\x00\x96\x64\x20\x41\x96\x64\x20\
-\xc1\x00\x00\x00\x0d\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x99\x67\x21\x10\x99\x67\x21\xd0\xd6\x96\x3e\xff\xf0\xd9\xba\
-\xff\xed\xd1\xaa\xff\xe6\xbf\x8a\xff\xe5\xbd\x85\xff\xe5\xbd\x85\
-\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\
-\xff\xe5\xbc\x84\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe5\xbc\x84\
-\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xed\xd1\xab\
-\xff\xe3\xb7\x7a\xff\x89\x5c\x1e\xd8\x00\x00\x00\x51\x00\x00\x00\
-\x28\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x99\x67\x21\
-\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x9d\x69\x22\x10\x9b\x68\x21\xb1\xd4\x90\x33\
-\xff\xee\xd4\xaf\xff\xf0\xd9\xba\xff\xea\xca\x9e\xff\xe6\xbf\x89\
-\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\
-\xff\xe5\xbe\x87\xff\xe5\xbd\x85\xff\xe5\xbd\x85\xff\xe5\xbd\x85\
-\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe9\xc7\x98\
-\xff\xf1\xdb\xbc\xff\xb4\x79\x27\xff\x4c\x33\x11\x85\x00\x00\x00\
-\x39\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x03\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9e\x6a\x22\
-\x81\xad\x74\x25\xff\xe2\xb5\x77\xff\xf3\xe1\xc7\xff\xf1\xdc\xbf\
-\xff\xec\xcf\xa6\xff\xe8\xc3\x91\xff\xe6\xbf\x89\xff\xe6\xbf\x89\
-\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe6\xbf\x89\
-\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe8\xc3\x91\xff\xec\xcf\xa6\
-\xff\xf3\xe1\xc7\xff\xe2\xb5\x77\xff\x91\x61\x1f\xd3\x00\x00\x00\
-\x31\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x03\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\
-\x00\x00\x00\x00\x09\x00\x00\x00\x18\x00\x00\x00\x1a\x00\x00\x00\
-\x0e\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x9d\x69\x22\x21\xa0\x6b\x22\xc1\xbe\x7f\x29\xff\xe3\xb8\x7d\
-\xff\xf2\xdf\xc4\xff\xf5\xe6\xd1\xff\xf3\xe1\xc9\xff\xf1\xdb\xbc\
-\xff\xee\xd4\xb0\xff\xee\xd4\xaf\xff\xee\xd4\xaf\xff\xee\xd4\xb0\
-\xff\xf1\xdb\xbc\xff\xf3\xe1\xc9\xff\xf5\xe6\xd1\xff\xf2\xdf\xc4\
-\xff\xe3\xb8\x7d\xff\xbe\x7f\x29\xff\x99\x67\x21\xc9\x00\x00\x00\
-\x13\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x09\x00\x00\x00\
-\x19\x00\x00\x00\x1f\x00\x00\x00\x0f\x00\x00\x00\x02\x00\x00\x00\
-\x00\x9e\x6a\x22\x76\x33\x22\x0b\x33\x00\x00\x00\x3a\x00\x00\x00\
-\x32\x00\x00\x00\x1e\x00\x00\x00\x0d\x00\x00\x00\x03\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\x41\xa2\x6d\x23\
-\xa2\xad\x74\x25\xf0\xd6\x96\x3e\xff\xe0\xb0\x6d\xff\xea\xca\x9e\
-\xff\xeb\xcd\xa3\xff\xf1\xdc\xbf\xff\xf1\xdc\xbf\xff\xeb\xcd\xa3\
-\xff\xea\xca\x9e\xff\xe0\xb0\x6d\xff\xd6\x96\x3e\xff\xa3\x6d\x23\
-\xf1\x9b\x68\x21\xa9\x8b\x5d\x1e\x4c\x00\x00\x00\x07\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x02\x00\x00\x00\x08\x00\x00\x00\x15\x00\x00\x00\x2a\x86\x5a\x1d\
-\x9e\x45\x2e\x0f\x4d\x00\x00\x00\x12\x00\x00\x00\x01\x00\x00\x00\
-\x00\xad\x74\x25\x82\xa7\x70\x24\xf1\x82\x57\x1c\x9e\x1a\x12\x06\
-\x5b\x00\x00\x00\x48\x00\x00\x00\x32\x00\x00\x00\x1b\x00\x00\x00\
-\x0c\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xa5\x6f\x24\x41\xa3\x6d\x23\x83\xa3\x6d\x23\
-\xb3\xa3\x6d\x23\xc3\xa2\x6d\x23\xc4\xa2\x6d\x23\xc4\xa2\x6d\x23\
-\xc4\xa2\x6d\x23\xb5\x9d\x69\x22\x88\x93\x63\x20\x49\x00\x00\x00\
-\x06\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\
-\x15\x00\x00\x00\x28\x5c\x3e\x14\x6e\x9e\x6a\x22\xe9\xa8\x71\x24\
-\xe8\x00\x00\x00\x29\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
-\x00\xb9\x7c\x28\x10\xb4\x79\x27\xd1\xad\x74\x25\xff\xa0\x6b\x22\
-\xf3\x73\x4d\x19\xad\x00\x00\x00\x57\x00\x00\x00\x46\x00\x00\x00\
-\x32\x00\x00\x00\x1e\x00\x00\x00\x11\x00\x00\x00\x08\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
-\x04\x00\x00\x00\x0c\x00\x00\x00\x18\x00\x00\x00\x29\x5b\x3d\x14\
-\x6e\x91\x61\x1f\xd3\xaa\x72\x25\xff\xb5\x79\x27\xff\x81\x57\x1c\
-\x74\x00\x00\x00\x12\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xbc\x7e\x28\x31\xbc\x7e\x28\xf0\xde\xaa\x63\
-\xff\xd3\x8f\x30\xff\x98\x66\x21\xeb\x70\x4b\x18\xb0\x19\x11\x06\
-\x62\x00\x00\x00\x4a\x00\x00\x00\x3a\x00\x00\x00\x29\x00\x00\x00\
-\x1b\x00\x00\x00\x12\x00\x00\x00\x0b\x00\x00\x00\x05\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
-\x04\x00\x00\x00\x08\x00\x00\x00\x0f\x00\x00\x00\x17\x00\x00\x00\
-\x22\x00\x00\x00\x32\x56\x3a\x13\x73\x90\x61\x1f\xd4\xb7\x7b\x27\
-\xff\xdb\xa4\x57\xff\xd3\x8f\x30\xff\xa5\x6f\x24\xa7\x00\x00\x00\
-\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x83\x2a\x31\xc3\x83\x2a\
-\xf0\xe2\xb4\x75\xff\xe5\xbd\x85\xff\xd4\x92\x36\xff\x9d\x69\x22\
-\xf5\x7f\x55\x1b\xc4\x56\x3a\x13\x8f\x00\x00\x00\x54\x00\x00\x00\
-\x49\x00\x00\x00\x3c\x00\x00\x00\x30\x00\x00\x00\x24\x00\x00\x00\
-\x1b\x00\x00\x00\x16\x00\x00\x00\x12\x00\x00\x00\x0e\x00\x00\x00\
-\x0d\x00\x00\x00\x0c\x00\x00\x00\x09\x00\x00\x00\x07\x00\x00\x00\
-\x06\x00\x00\x00\x08\x00\x00\x00\x0a\x00\x00\x00\x0c\x00\x00\x00\
-\x0e\x00\x00\x00\x11\x00\x00\x00\x14\x00\x00\x00\x19\x00\x00\x00\
-\x20\x00\x00\x00\x2a\x00\x00\x00\x37\x36\x24\x0c\x5b\x6f\x4a\x18\
-\x9c\x98\x66\x21\xea\xbf\x80\x29\xff\xdc\xa6\x5c\xff\xe6\xc0\x8c\
-\xff\xd5\x93\x38\xff\xb4\x79\x27\xb1\x00\x00\x00\x13\x00\x00\x00\
-\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc9\x87\x2b\
-\x31\xc9\x87\x2b\xd1\xdc\xa5\x59\xff\xeb\xcc\xa1\xff\xe6\xbf\x8a\
-\xff\xda\xa1\x51\xff\xba\x7d\x28\xff\x99\x67\x21\xf5\x82\x57\x1c\
-\xce\x68\x46\x17\xa5\x3d\x29\x0d\x77\x00\x00\x00\x52\x00\x00\x00\
-\x4b\x00\x00\x00\x44\x00\x00\x00\x3d\x00\x00\x00\x36\x00\x00\x00\
-\x33\x00\x00\x00\x31\x00\x00\x00\x2b\x00\x00\x00\x27\x00\x00\x00\
-\x26\x00\x00\x00\x29\x00\x00\x00\x2e\x00\x00\x00\x32\x00\x00\x00\
-\x36\x00\x00\x00\x3b\x00\x00\x00\x41\x33\x22\x0b\x5f\x5b\x3d\x14\
-\x86\x7a\x52\x1a\xb4\x94\x63\x20\xea\xb1\x77\x26\xff\xd6\x96\x3d\
-\xff\xe2\xb5\x77\xff\xe9\xc6\x96\xff\xe5\xbe\x87\xff\xd3\x8e\x2e\
-\xff\xa3\x6d\x23\x79\x00\x00\x00\x0e\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\xd3\x8e\x2e\x10\xd0\x8c\x2d\xb1\xd7\x9a\x45\xff\xea\xc9\x9b\
-\xff\xeb\xcc\xa0\xff\xe7\xc2\x8f\xff\xe3\xb8\x7c\xff\xd8\x9c\x48\
-\xff\xcb\x88\x2c\xff\xaf\x76\x26\xff\x9b\x68\x21\xff\x8e\x5f\x1f\
-\xe2\x87\x5b\x1d\xd8\x70\x4b\x18\xaf\x72\x4d\x19\xad\x72\x4d\x19\
-\xac\x57\x3a\x13\x8c\x4c\x33\x11\x80\x4e\x34\x11\x7e\x4e\x34\x11\
-\x7e\x6b\x48\x17\x9f\x72\x4d\x19\xab\x72\x4d\x19\xac\x84\x59\x1d\
-\xcb\x89\x5c\x1e\xd6\x98\x66\x21\xf5\xa3\x6d\x23\xff\xc1\x81\x29\
-\xff\xd7\x98\x42\xff\xdf\xad\x69\xff\xe6\xc0\x8c\xff\xe8\xc5\x94\
-\xff\xea\xca\x9e\xff\xdf\xad\x68\xff\xcb\x88\x2c\xe6\x9e\x6a\x22\
-\x56\x00\x00\x00\x0a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8f\x30\x51\xd3\x8f\x30\
-\xf0\xde\xab\x64\xff\xee\xd4\xb0\xff\xeb\xcc\xa1\xff\xe8\xc3\x91\
-\xff\xe4\xbb\x82\xff\xe1\xb2\x72\xff\xde\xab\x64\xff\xd9\x9e\x4c\
-\xff\xd7\x99\x43\xff\xce\x8a\x2c\xff\xd0\x8c\x2d\xff\xd2\x8d\x2d\
-\xff\xbe\x7f\x29\xff\xb7\x7b\x27\xff\xb7\x7b\x27\xff\xb7\x7b\x27\
-\xff\xcd\x89\x2c\xff\xd2\x8d\x2d\xff\xd0\x8c\x2d\xff\xd5\x93\x38\
-\xff\xd8\x9b\x47\xff\xdd\xa8\x5e\xff\xe0\xb0\x6d\xff\xe3\xb8\x7d\
-\xff\xe6\xbf\x8a\xff\xe9\xc7\x98\xff\xed\xd1\xaa\xff\xe5\xbd\x85\
-\xff\xd7\x99\x43\xff\xc6\x85\x2b\xae\x6a\x47\x17\x21\x00\x00\x00\
-\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd5\x93\x38\
-\x10\xd4\x91\x35\x82\xd6\x96\x3e\xff\xe1\xb3\x73\xff\xed\xd2\xad\
-\xff\xee\xd4\xaf\xff\xea\xca\x9e\xff\xe6\xc0\x8c\xff\xe3\xb8\x7d\
-\xff\xe1\xb2\x72\xff\xde\xab\x64\xff\xdc\xa6\x5b\xff\xdb\xa2\x54\
-\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xdc\xa5\x59\
-\xff\xdb\xa4\x57\xff\xdc\xa5\x59\xff\xdd\xa9\x61\xff\xe0\xb0\x6d\
-\xff\xe3\xb7\x7a\xff\xe5\xbd\x85\xff\xe8\xc5\x94\xff\xeb\xcd\xa3\
-\xff\xef\xd7\xb5\xff\xe6\xbf\x8a\xff\xd8\x9b\x47\xff\xd2\x8d\x2d\
-\xc9\xb1\x77\x26\x50\x00\x00\x00\x08\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xd3\x8f\x31\x21\xd4\x91\x35\x82\xd4\x92\x36\
-\xf0\xde\xab\x64\xff\xe8\xc3\x91\xff\xf0\xd9\xb9\xff\xef\xd7\xb5\
-\xff\xed\xd1\xaa\xff\xeb\xcc\xa1\xff\xe9\xc6\x96\xff\xe7\xc2\x8e\
-\xff\xe5\xbc\x84\xff\xe4\xba\x7f\xff\xe3\xb8\x7d\xff\xe4\xba\x80\
-\xff\xe5\xbe\x87\xff\xe8\xc3\x91\xff\xea\xc9\x9b\xff\xec\xce\xa5\
-\xff\xee\xd4\xaf\xff\xf0\xd9\xba\xff\xec\xcf\xa8\xff\xe2\xb5\x77\
-\xff\xd6\x97\x40\xff\xd3\x8f\x30\xc7\xb5\x79\x27\x4e\x00\x00\x00\
-\x08\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\xd4\x92\x36\x61\xd4\x91\x35\xb2\xd5\x93\x38\xff\xdb\xa2\x54\
-\xff\xe2\xb5\x77\xff\xe8\xc3\x91\xff\xeb\xcc\xa1\xff\xf1\xdb\xbc\
-\xff\xf2\xde\xc2\xff\xf2\xdd\xc1\xff\xf2\xdd\xc1\xff\xf2\xdd\xc1\
-\xff\xf2\xde\xc2\xff\xeb\xcc\xa0\xff\xeb\xcc\xa1\xff\xe4\xba\x80\
-\xff\xde\xac\x66\xff\xd7\x98\x42\xff\xd3\x8f\x31\xd5\xc8\x86\x2b\
-\x7c\xa8\x71\x24\x2a\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x91\x35\
-\x41\xd4\x90\x33\x83\xd3\x8f\x31\xa4\xd4\x91\x35\xc3\xd5\x93\x38\
-\xf0\xd5\x93\x38\xff\xd5\x93\x38\xff\xd5\x93\x38\xff\xd5\x93\x38\
-\xff\xd5\x93\x38\xff\xd3\x8f\x31\xc5\xd3\x8f\x31\xc4\xce\x8a\x2c\
-\x89\xc6\x85\x2b\x59\xb5\x79\x27\x27\x00\x00\x00\x04\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x80\
-\x01\xff\xff\x08\x8e\xff\xfe\x00\x00\x7f\xff\x08\x8e\xff\xf8\x00\
-\x00\x1f\xff\x08\x8e\xff\xf0\x00\x00\x0f\xff\x08\x8e\xff\xe0\x00\
-\x00\x07\xff\x08\x8e\xff\xc0\x00\x00\x03\xff\x08\x8e\xff\x80\x00\
-\x00\x01\xff\x08\x8e\xff\x00\x00\x00\x00\xff\x08\x8e\xfe\x00\x00\
-\x00\x00\x7f\x08\x8e\xfe\x00\x00\x00\x00\x7f\x08\x8e\xfc\x00\x00\
-\x00\x00\x3f\x08\x8e\xfc\x00\x07\xc0\x00\x3f\x08\x8e\xf8\x00\x0c\
-\x40\x00\x1f\x08\x8e\xf8\x00\x08\x60\x00\x1f\x08\x8e\xf8\x00\x18\
-\x30\x00\x1f\x08\x8e\xf8\x00\x10\x30\x00\x1f\x08\x8e\xf8\x00\x20\
-\x10\x00\x1f\x08\x8e\xf8\x00\x20\x18\x00\x1f\x08\x8e\xf8\x00\x00\
-\x08\x00\x1f\x08\x8e\xf8\x00\x00\x0c\x00\x1f\x08\x8e\xf8\x00\x00\
-\x04\x00\x1f\x08\x8e\xf8\x00\x00\x06\x00\x1f\x08\x8e\xf8\x00\x00\
-\x02\x00\x1f\x08\x8e\xf8\x00\x00\x03\x00\x1f\x08\x8e\xf8\x00\x00\
-\x03\x00\x1f\x08\x8e\xf8\x00\x00\x01\x80\x3f\x08\x8e\xfc\x00\x00\
-\x01\x80\x3f\x08\x8e\xfc\x00\x00\x00\x80\x3f\x08\x8e\xfe\x00\x00\
-\x00\xc0\x7f\x08\x8e\xfe\x00\x00\x00\x40\x7f\x08\x8e\xff\x00\x00\
-\x00\x60\xff\x08\x8e\xff\x80\x00\x00\x21\xff\x08\x8e\xff\x80\x00\
-\x00\x33\xff\x08\x8e\xff\xc0\x00\x00\x3f\xff\x08\x8e\x8f\xf0\x00\
-\x00\x3f\xf0\x08\x8e\x83\xf8\x00\x00\x3f\xc0\x08\x8e\x80\xfe\x00\
-\x00\x7f\x00\x08\x8e\x80\x3f\xc0\x03\xfc\x01\x08\x8e\x80\x07\xff\
-\xff\xe0\x01\x08\x8e\xc0\x00\x7f\xfe\x00\x03\x08\x8e\xe0\x00\x00\
-\x00\x00\x07\x08\x8e\xf0\x00\x00\x00\x00\x0f\x08\x8e\xf8\x00\x00\
-\x00\x00\x1f\x08\x8e\xfe\x00\x00\x00\x00\x7f\x08\x8e\xff\x00\x00\
-\x00\x00\xff\x08\x8e\xff\xc0\x00\x00\x03\xff\x08\x8e\xff\xf8\x00\
-\x00\x0f\xff\x08\x8e\xff\xff\x00\x00\x7f\xff\x08\x8e\x28\x00\x00\
-\x00\x20\x00\x00\x00\x40\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x02\x00\x00\x00\x06\x00\x00\x00\x12\x00\x00\x00\x1c\x00\x00\x00\
-\x26\x00\x00\x00\x2a\x00\x00\x00\x2a\x00\x00\x00\x26\x00\x00\x00\
-\x1c\x00\x00\x00\x12\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\
-\x16\x22\x17\x07\x3d\x54\x38\x12\x80\x63\x42\x15\xa4\x6f\x4a\x18\
-\xc1\x6f\x4a\x18\xc3\x6c\x48\x17\xbd\x60\x40\x14\xa5\x40\x2b\x0e\
-\x76\x0e\x0a\x03\x4b\x00\x00\x00\x2d\x00\x00\x00\x16\x00\x00\x00\
-\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x02\x0b\x08\x02\x1a\x2d\x1e\x09\x59\x9b\x6c\x2b\
-\xc4\xc4\x94\x52\xf8\xe3\xb6\x7a\xff\xe6\xc1\x8d\xff\xea\xca\x9e\
-\xff\xea\xc9\x9c\xff\xe9\xc8\x9a\xff\xe6\xc0\x8b\xff\xda\xab\x6a\
-\xff\xba\x8b\x4a\xf2\x6c\x48\x17\xab\x24\x18\x07\x6b\x00\x00\x00\
-\x30\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x02\x1b\x12\x06\x17\x75\x4e\x19\xa3\xbe\x8f\x4d\xeb\xe5\xbf\x8a\
-\xff\xe4\xba\x80\xff\xda\xa2\x55\xff\xd7\x9b\x47\xff\xd5\x94\x3b\
-\xff\xd4\x93\x39\xff\xd6\x96\x3f\xff\xd8\x9d\x4a\xff\xdd\xaa\x63\
-\xff\xe5\xbe\x88\xff\xdc\xb3\x7b\xfd\xac\x7b\x37\xe5\x41\x2b\x0e\
-\x8a\x05\x03\x01\x3f\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x29\x1c\x09\
-\x2e\x8f\x64\x29\xbc\xdf\xaf\x6c\xff\xe1\xb6\x7b\xff\xd4\x93\x3a\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd8\xa0\x51\xff\xe4\xbd\x87\xff\xca\x96\x4d\
-\xf9\x61\x43\x1a\xae\x05\x03\x01\x41\x00\x00\x00\x17\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x22\x17\x07\x11\x8d\x62\x25\
-\xbb\xd7\xa8\x67\xfb\xd8\xa1\x56\xff\xca\x89\x31\xff\xce\x8a\x2c\
-\xff\xd4\x93\x39\xff\xdd\xa9\x62\xff\xe0\xb0\x6e\xff\xe0\xb1\x6f\
-\xff\xe0\xb1\x6f\xff\xe0\xb1\x6f\xff\xdf\xaf\x6c\xff\xd5\x95\x3d\
-\xff\xd2\x8d\x2d\xff\xcc\x89\x2c\xff\xcb\x8d\x38\xff\xdf\xae\x6b\
-\xff\xcb\x99\x52\xf9\x48\x2f\x0f\x98\x05\x03\x01\x3b\x00\x00\x00\
-\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x1f\x15\x06\x0d\x6c\x49\x17\x93\xd2\x97\x47\
-\xff\xd1\x95\x43\xff\xbd\x7f\x28\xff\xc3\x83\x2a\xff\xcc\x8c\x32\
-\xff\xdc\xa7\x5d\xff\xb8\x82\x37\xed\xa5\x6f\x23\xc7\xa9\x71\x24\
-\xc1\xaa\x72\x24\xc1\xaa\x72\x24\xc3\xb9\x81\x33\xe9\xdc\xa7\x5d\
-\xff\xcf\x8d\x31\xff\xc7\x85\x2a\xff\xc2\x82\x2a\xff\xc1\x83\x2c\
-\xff\xd7\xa0\x54\xff\xb4\x7c\x30\xf2\x3b\x27\x0c\x8a\x00\x00\x00\
-\x26\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x2c\x1d\x09\x42\xae\x77\x2b\xe8\xcf\x95\x43\
-\xff\xb7\x7c\x29\xff\xb9\x7c\x27\xff\xbf\x80\x29\xff\xd4\x96\x41\
-\xff\xc1\x8a\x3e\xff\x55\x39\x12\x7e\x27\x1a\x08\x23\x88\x5b\x1d\
-\x17\x8b\x5d\x1e\x17\x8b\x5d\x1e\x19\x8d\x5e\x1e\x79\xcc\x93\x43\
-\xff\xd1\x92\x3b\xff\xc2\x82\x2a\xff\xbe\x7f\x28\xff\xb8\x7b\x27\
-\xff\xbb\x7e\x2b\xff\xd1\x97\x45\xff\x8c\x5f\x21\xdc\x00\x00\x00\
-\x40\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x01\x91\x62\x20\xb5\xce\x93\x43\xff\xb4\x79\x26\
-\xff\xb0\x76\x25\xff\xb5\x79\x27\xff\xc1\x82\x2b\xff\xd0\x97\x47\
-\xff\x82\x57\x1b\xc5\x00\x00\x00\x15\x00\x00\x00\x01\x00\x00\x00\
-\x04\x00\x00\x00\x0c\x00\x00\x00\x04\x8e\x5f\x1f\x07\x9e\x6c\x26\
-\xce\xd3\x99\x49\xff\xbe\x7f\x28\xff\xb8\x7b\x27\xff\xb3\x77\x26\
-\xff\xaf\x75\x25\xff\xc4\x88\x35\xff\xc2\x87\x36\xff\x40\x2b\x0e\
-\x8b\x00\x00\x00\x27\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x40\x2b\x0d\x13\xa5\x72\x2d\xf6\xd0\x91\x3b\xff\xa8\x71\x24\
-\xff\xac\x73\x25\xff\xb2\x77\x26\xff\xcd\x92\x40\xff\xb1\x7a\x2e\
-\xf0\x35\x23\x0b\x67\x00\x00\x00\x07\x00\x00\x00\x00\x39\x26\x0c\
-\x2f\x18\x10\x05\x41\x00\x00\x00\x0f\x00\x00\x00\x00\x90\x60\x1e\
-\x64\xc1\x8b\x3e\xf8\xc7\x89\x33\xff\xb4\x78\x26\xff\xaf\x75\x25\
-\xff\xab\x73\x25\xff\xb3\x79\x29\xff\xd4\x97\x43\xff\x62\x42\x15\
-\xc2\x08\x06\x02\x3b\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x79\x51\x1a\x54\xc4\x88\x35\xff\xb3\x7d\x32\xff\xa3\x6d\x23\
-\xff\xa7\x70\x24\xff\xba\x7f\x2e\xff\xd4\x97\x44\xff\x62\x42\x15\
-\xb1\x0b\x08\x02\x1f\x00\x00\x00\x01\x14\x0e\x04\x08\x7f\x55\x1b\
-\xc8\x3f\x2a\x0d\x9a\x00\x00\x00\x29\x00\x00\x00\x05\x82\x57\x1b\
-\x1b\x98\x66\x20\xc2\xd4\x9a\x4b\xff\xb5\x7a\x28\xff\xab\x72\x24\
-\xff\xa7\x70\x24\xff\xa4\x6e\x23\xff\xc8\x8f\x42\xff\x99\x67\x21\
-\xe2\x1e\x14\x06\x5f\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7c\x53\x1b\x7c\xd3\x94\x3c\xff\xc8\x89\x33\xff\xb9\x7c\x28\
-\xff\xa8\x71\x24\xff\xcd\x90\x3b\xff\xb0\x7c\x33\xff\x2f\x1f\x0a\
-\x55\x00\x00\x00\x09\x00\x00\x00\x01\x42\x2c\x0e\x48\xac\x75\x29\
-\xfc\x87\x5c\x21\xe1\x00\x00\x00\x41\x00\x00\x00\x11\x82\x57\x1b\
-\x02\x7f\x55\x1b\x71\xc8\x8e\x3e\xff\xbf\x85\x35\xff\xa6\x6f\x24\
-\xff\xab\x73\x25\xff\xbb\x7d\x28\xff\xd0\x92\x3d\xff\xb0\x79\x2c\
-\xf0\x24\x18\x07\x72\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7b\x52\x1a\x9d\xdf\xad\x68\xff\xde\xac\x67\xff\xdd\xa8\x5f\
-\xff\xdd\xa8\x60\xff\xce\x9b\x53\xff\x71\x4c\x18\xc0\x00\x00\x00\
-\x25\x00\x00\x00\x12\x1d\x13\x06\x2b\x83\x58\x1c\xc4\xdb\xa4\x57\
-\xff\xbf\x87\x3a\xff\x3a\x27\x0c\x8c\x00\x00\x00\x2a\x00\x00\x00\
-\x03\x7d\x54\x1b\x07\x9a\x6b\x2a\xe0\xda\xa0\x51\xff\xd8\x9d\x4b\
-\xff\xdc\xa5\x5b\xff\xdd\xa9\x61\xff\xdf\xaf\x6b\xff\xba\x8b\x49\
-\xf9\x26\x1a\x08\x80\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7b\x52\x1a\xaa\xe2\xb6\x79\xff\xdf\xae\x6a\xff\xdd\xaa\x64\
-\xff\xe0\xb0\x6f\xff\xb4\x7e\x34\xf6\x49\x31\x10\x9f\x2a\x1c\x09\
-\x65\x2a\x1c\x09\x61\x4a\x31\x10\x92\xb8\x87\x43\xf7\xdd\xa7\x5e\
-\xff\xdd\xa8\x5e\xff\x64\x43\x15\xc8\x0d\x08\x02\x4b\x00\x00\x00\
-\x0a\x00\x00\x00\x00\x86\x5a\x1e\x84\xcd\x9e\x5c\xff\xdd\xaa\x63\
-\xff\xdd\xa8\x60\xff\xde\xab\x65\xff\xe0\xaf\x6d\xff\xbf\x94\x58\
-\xff\x29\x1b\x08\x83\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x7d\x54\x1b\x89\xe0\xb1\x70\xff\xe1\xb2\x71\xff\xe0\xb0\x6d\
-\xff\xe3\xb8\x7b\xff\xc3\x8e\x44\xff\xbb\x88\x42\xff\xbc\x89\x42\
-\xff\xbc\x89\x42\xff\xc5\x90\x46\xff\xde\xac\x67\xff\xdb\xa3\x57\
-\xff\xde\xac\x67\xff\xba\x86\x3e\xf6\x31\x21\x0a\x88\x00\x00\x00\
-\x1f\x00\x00\x00\x03\x7d\x54\x1b\x29\xad\x77\x2c\xd3\xe1\xb4\x75\
-\xff\xde\xac\x67\xff\xdf\xae\x6b\xff\xe1\xb4\x75\xff\xb7\x86\x42\
-\xf2\x25\x19\x08\x6e\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x80\x56\x1b\x6d\xdb\xa5\x5a\xff\xe1\xb5\x76\xff\xe0\xb2\x70\
-\xff\xe0\xb2\x72\xff\xe0\xb1\x70\xff\xe0\xb0\x6e\xff\xe0\xaf\x6c\
-\xff\xdf\xae\x6b\xff\xdf\xae\x69\xff\xde\xab\x65\xff\xdc\xa7\x5c\
-\xff\xdd\xa8\x5f\xff\xd6\xa5\x61\xff\x76\x4f\x19\xc5\x00\x00\x00\
-\x38\x00\x00\x00\x0c\x7d\x54\x1b\x05\x8b\x5d\x1e\x94\xdf\xae\x6b\
-\xff\xe0\xb1\x70\xff\xe0\xb1\x6f\xff\xe2\xb7\x7a\xff\xb3\x7c\x32\
-\xea\x23\x18\x07\x5e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x85\x59\x1d\x2f\xc6\x92\x4b\xff\xe4\xbc\x85\xff\xe1\xb3\x73\
-\xff\xe0\xb1\x70\xff\xe0\xb0\x6d\xff\xdf\xae\x6a\xff\xde\xad\x68\
-\xff\xde\xac\x66\xff\xde\xab\x65\xff\xde\xaa\x64\xff\xdd\xaa\x63\
-\xff\xdd\xaa\x62\xff\xe2\xb6\x79\xff\xbb\x8b\x47\xff\x29\x1b\x09\
-\x6f\x00\x00\x00\x21\x00\x00\x00\x01\x84\x59\x1d\x24\xc0\x8d\x46\
-\xf8\xe4\xbc\x83\xff\xe1\xb5\x77\xff\xe6\xbf\x8a\xff\x86\x5a\x1d\
-\xcf\x15\x0e\x04\x36\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x87\x5b\x1d\x04\xa8\x7a\x3a\xdd\xe6\xc1\x8d\xff\xe2\xb6\x79\
-\xff\xe1\xb4\x76\xff\xe1\xb2\x73\xff\xe0\xb1\x70\xff\xe0\xb0\x6d\
-\xff\xdf\xaf\x6c\xff\xdf\xae\x6a\xff\xde\xad\x69\xff\xde\xad\x69\
-\xff\xde\xad\x69\xff\xe1\xb4\x76\xff\xe2\xb6\x79\xff\x59\x3c\x13\
-\xb4\x05\x03\x01\x3b\x00\x00\x00\x07\x00\x00\x00\x00\xa3\x72\x2d\
-\xba\xe4\xbc\x83\xff\xe5\xbd\x86\xff\xe3\xb8\x7e\xff\x58\x3b\x13\
-\xa2\x03\x02\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x99\x66\x21\x6b\xd4\xab\x73\xf8\xe7\xc3\x91\
-\xff\xe3\xb7\x7c\xff\xe2\xb6\x79\xff\xe2\xb5\x77\xff\xe1\xb3\x74\
-\xff\xe1\xb3\x73\xff\xe1\xb2\x71\xff\xe0\xb1\x70\xff\xe0\xb1\x6f\
-\xff\xe0\xb1\x6f\xff\xe0\xb2\x71\xff\xe8\xc5\x95\xff\xb7\x84\x3d\
-\xed\x26\x19\x08\x75\x00\x00\x00\x17\x00\x00\x00\x02\x8f\x5f\x1e\
-\x4d\xca\xa0\x66\xef\xec\xce\xa5\xff\xb4\x87\x48\xf5\x24\x18\x08\
-\x3e\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x8f\x60\x1f\x20\xac\x79\x33\xca\xec\xce\xa6\
-\xff\xe5\xbe\x88\xff\xe3\xb9\x7e\xff\xe3\xb8\x7c\xff\xe2\xb7\x7a\
-\xff\xe2\xb6\x79\xff\xe1\xb5\x76\xff\xe1\xb5\x76\xff\xe1\xb4\x75\
-\xff\xe1\xb4\x75\xff\xe1\xb4\x75\xff\xe4\xbb\x81\xff\xdb\xb3\x7e\
-\xfe\x65\x44\x16\xac\x00\x00\x00\x2f\x00\x00\x00\x08\x8e\x5f\x1f\
-\x17\xab\x77\x2f\xc1\xda\xb6\x85\xff\x7c\x55\x20\xab\x00\x00\x00\
-\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\x39\xcb\x95\x4b\
-\xee\xeb\xcc\xa2\xff\xe6\xc0\x8b\xff\xe4\xbb\x81\xff\xe4\xba\x80\
-\xff\xe3\xb9\x7e\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\
-\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe9\xc8\x9a\
-\xff\xb8\x88\x48\xf7\x14\x0e\x04\x59\x00\x00\x00\x1a\x00\x00\x00\
-\x00\x94\x63\x20\x5e\x86\x5a\x1c\xbf\x1c\x13\x06\x2e\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9f\x6b\x23\
-\x79\xce\x9a\x53\xf5\xed\xd1\xac\xff\xe7\xc2\x8f\xff\xe4\xbc\x84\
-\xff\xe4\xbb\x83\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe4\xbb\x81\
-\xff\xe4\xba\x81\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe8\xc5\x95\
-\xff\xdf\xb5\x7a\xff\x51\x36\x11\x9c\x00\x00\x00\x30\x00\x00\x00\
-\x04\x96\x64\x20\x0e\x65\x44\x15\x34\x00\x00\x00\x03\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9d\x69\x22\
-\x04\x9b\x68\x21\x52\xc9\x93\x49\xe3\xe5\xc4\x98\xff\xeb\xce\xa6\
-\xff\xe8\xc5\x94\xff\xe5\xbf\x89\xff\xe5\xbe\x87\xff\xe5\xbe\x87\
-\xff\xe5\xbd\x86\xff\xe5\xbd\x86\xff\xe5\xbe\x87\xff\xe7\xc3\x90\
-\xff\xef\xd6\xb4\xff\xa3\x74\x33\xdf\x21\x16\x07\x59\x00\x00\x00\
-\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
-\x02\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x0e\x00\x00\x00\
-\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xa1\x6c\x22\x40\xaf\x7c\x35\xb2\xd7\xab\x6f\
-\xff\xed\xd2\xad\xff\xf0\xd8\xb9\xff\xed\xd3\xad\xff\xeb\xcd\xa2\
-\xff\xeb\xcd\xa2\xff\xec\xce\xa5\xff\xee\xd4\xb0\xff\xf0\xda\xbb\
-\xff\xea\xcc\xa1\xff\xb8\x82\x37\xee\x32\x21\x0a\x57\x00\x00\x00\
-\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x0e\x00\x00\x00\
-\x13\x00\x00\x00\x05\xa4\x6e\x23\x29\x72\x4c\x18\x75\x1f\x15\x06\
-\x52\x02\x02\x00\x33\x00\x00\x00\x15\x00\x00\x00\x06\x00\x00\x00\
-\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\
-\x41\xa8\x71\x24\x8f\xc7\x8e\x3f\xc7\xd0\xa5\x6a\xe0\xd4\xb0\x7e\
-\xeb\xd6\xb7\x8b\xeb\xd2\xac\x77\xea\xcc\x9e\x5f\xdc\xa4\x70\x2a\
-\xb8\x69\x46\x16\x82\x54\x38\x12\x23\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x05\x00\x00\x00\x0f\x26\x19\x08\x45\x72\x4c\x18\x9d\x1e\x14\x06\
-\x30\x00\x00\x00\x06\xb3\x78\x26\x12\xb1\x77\x26\xa4\x90\x60\x1e\
-\xd5\x59\x3c\x13\x9d\x00\x00\x00\x44\x00\x00\x00\x2b\x00\x00\x00\
-\x12\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xa4\x6e\x23\x1d\xa3\x6d\x23\x36\xa2\x6d\x23\
-\x41\xa2\x6d\x23\x41\xa2\x6d\x23\x40\x9e\x6a\x22\x32\x51\x36\x11\
-\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x0e\x14\x0d\x04\
-\x2f\x54\x38\x12\x81\x99\x67\x21\xdc\x98\x66\x20\xba\x00\x00\x00\
-\x13\x00\x00\x00\x02\x00\x00\x00\x00\xbc\x7e\x28\x16\xc5\x89\x35\
-\xc9\xd7\x9b\x4a\xfd\xa6\x78\x37\xe5\x5e\x3f\x15\xa5\x25\x19\x08\
-\x69\x09\x06\x02\x42\x00\x00\x00\x27\x00\x00\x00\x1a\x00\x00\x00\
-\x0d\x00\x00\x00\x08\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\
-\x04\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\
-\x04\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00\x0c\x00\x00\x00\
-\x16\x0c\x08\x02\x28\x29\x1b\x09\x57\x61\x41\x15\x91\xb3\x80\x3a\
-\xec\xd2\x9a\x4b\xff\xaa\x73\x26\xc0\x24\x18\x08\x32\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc5\x84\x2a\
-\x30\xcd\x90\x3b\xbb\xe0\xb0\x6e\xff\xd4\xa9\x6e\xfd\xb0\x7c\x35\
-\xe5\x76\x50\x19\xc8\x50\x36\x11\x97\x32\x21\x0a\x6b\x00\x00\x00\
-\x40\x00\x00\x00\x37\x00\x00\x00\x2d\x00\x00\x00\x27\x00\x00\x00\
-\x23\x00\x00\x00\x1d\x00\x00\x00\x1c\x00\x00\x00\x21\x00\x00\x00\
-\x26\x00\x00\x00\x2c\x0b\x07\x02\x39\x33\x22\x0b\x5b\x57\x3a\x12\
-\x93\x7b\x52\x1a\xc0\xba\x86\x3e\xe7\xd5\xa8\x69\xfd\xdf\xae\x6a\
-\xff\xc0\x86\x35\xc3\x28\x1a\x08\x30\x00\x00\x00\x03\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\xd3\x8e\x2e\x07\xd2\x90\x34\x90\xde\xab\x64\xe8\xe7\xc3\x90\
-\xff\xe6\xc1\x8e\xff\xda\xa3\x58\xff\xc6\x8f\x42\xff\xad\x7a\x34\
-\xf9\xa3\x71\x2b\xe7\x90\x60\x1f\xc9\x91\x62\x1f\xc8\x76\x4f\x19\
-\xb0\x70\x4b\x18\xa9\x79\x51\x1a\xb0\x8f\x60\x1f\xc4\x95\x65\x21\
-\xcf\xa1\x6f\x29\xe1\xb1\x7e\x37\xfb\xc5\x8e\x41\xff\xde\xaa\x64\
-\xff\xe6\xc0\x8c\xff\xe5\xbf\x89\xff\xdb\xa8\x62\xed\x94\x63\x20\
-\x81\x2e\x1f\x0a\x18\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xd3\x8f\x30\x09\xd3\x91\x34\x45\xd9\x9f\x4f\
-\xc7\xe2\xb5\x78\xff\xeb\xcc\xa2\xff\xe8\xc5\x94\xff\xe2\xb5\x78\
-\xff\xde\xab\x65\xff\xd8\x9f\x4f\xff\xd8\x9b\x48\xff\xd1\x97\x48\
-\xff\xcf\x97\x48\xff\xd1\x98\x48\xff\xd7\x9c\x49\xff\xd9\xa1\x53\
-\xff\xde\xab\x64\xff\xe3\xb8\x7d\xff\xe7\xc3\x91\xff\xea\xca\x9e\
-\xff\xe1\xb3\x74\xff\xcf\x94\x41\xc0\x6b\x49\x19\x58\x1f\x14\x06\
-\x09\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8f\x31\
-\x07\xd3\x90\x33\x41\xd6\x99\x43\xae\xdf\xad\x68\xdc\xe7\xc2\x90\
-\xff\xe9\xc6\x97\xff\xe9\xc8\x9b\xff\xea\xc9\x9c\xff\xe9\xc6\x97\
-\xff\xe8\xc4\x94\xff\xe8\xc6\x97\xff\xe9\xc6\x97\xff\xea\xc9\x9b\
-\xff\xe8\xc6\x97\xff\xe6\xc0\x8c\xfa\xdc\xac\x69\xdd\xb3\x7c\x2f\
-\xa7\x7f\x55\x1c\x50\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xd4\x92\x36\x0b\xd4\x91\x35\x32\xd5\x96\x3d\
-\x63\xd7\x9a\x46\x9e\xda\xa1\x53\xc9\xdd\xa8\x60\xeb\xde\xab\x65\
-\xff\xde\xab\x65\xff\xde\xab\x65\xff\xdc\xa6\x5b\xe5\xd9\xa0\x51\
-\xcb\xd0\x94\x41\x9b\x97\x67\x25\x63\x43\x2d\x0f\x35\x5d\x3e\x14\
-\x0a\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xff\xc0\x03\xff\xff\x80\x01\xff\xfe\x00\x00\
-\x7f\xfc\x00\x00\x3f\xf8\x00\x00\x1f\xf8\x00\x00\x1f\xf0\x00\x00\
-\x0f\xf0\x00\x00\x0f\xe0\x00\x00\x07\xe0\x04\x40\x07\xe0\x00\x00\
-\x07\xe0\x00\x00\x07\xe0\x00\x00\x07\xe0\x00\x10\x07\xe0\x00\x00\
-\x07\xe0\x00\x00\x07\xe0\x00\x00\x07\xe0\x00\x04\x0f\xf0\x00\x00\
-\x0f\xf0\x00\x00\x0f\xf8\x00\x02\x1f\xfc\x00\x00\x3f\x9c\x00\x01\
-\xf8\x0f\x00\x01\xf0\x01\xc0\x03\xc0\x00\x70\x0f\x00\x80\x00\x00\
-\x01\xc0\x00\x00\x03\xe0\x00\x00\x07\xf0\x00\x00\x0f\xfc\x00\x00\
-\x7f\xff\x00\x00\xff\x28\x00\x00\x00\x18\x00\x00\x00\x30\x00\x00\
-\x00\x01\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x08\x00\x00\x00\x19\x00\x00\x00\x29\x00\x00\x00\x32\x00\x00\x00\
-\x32\x00\x00\x00\x29\x00\x00\x00\x19\x00\x00\x00\x08\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x07\x1c\x12\x06\x36\x68\x46\x16\
-\x9c\xb1\x83\x44\xd9\xc6\x9f\x69\xf8\xcc\xa7\x73\xff\xc6\x9f\x69\
-\xf8\xaf\x82\x43\xdc\x65\x43\x15\xaa\x19\x11\x05\x58\x00\x00\x00\
-\x24\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x13\x0d\x04\x0e\x6e\x4a\x17\x95\xcf\xa1\x61\xfa\xe6\xc0\x8c\
-\xff\xde\xac\x66\xff\xd8\x9d\x4a\xff\xd6\x97\x3f\xff\xd8\x9d\x4a\
-\xff\xde\xac\x66\xff\xe6\xc0\x8c\xff\xcf\xa1\x61\xfa\x67\x45\x16\
-\xac\x06\x04\x01\x3e\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x0d\x04\
-\x0d\x8e\x64\x2a\xb8\xe2\xb5\x77\xff\xd6\x9d\x4d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
-\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd7\x9d\x4e\xff\xe2\xb6\x79\
-\xff\x83\x5d\x28\xca\x05\x03\x01\x40\x00\x00\x00\x0a\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x88\x5b\x1d\
-\xaa\xdd\xa9\x62\xff\xc6\x87\x30\xff\xc9\x86\x2b\xff\xd7\x9c\x4a\
-\xff\xda\xa6\x5f\xff\xd1\xa0\x5c\xff\xd1\xa0\x5c\xff\xd1\xa0\x5c\
-\xff\xde\xac\x66\xff\xd1\x8d\x2e\xff\xc9\x87\x2b\xff\xc7\x88\x31\
-\xff\xdd\xaa\x62\xff\x77\x4f\x19\xc2\x00\x00\x00\x30\x00\x00\x00\
-\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x2b\x0e\x54\xca\x90\x3f\
-\xff\xbc\x80\x2c\xff\xba\x7d\x28\xff\xc7\x86\x2d\xff\xcc\x95\x49\
-\xff\x55\x39\x12\x7d\x56\x3a\x12\x25\x8b\x5d\x1e\x22\x8b\x5d\x1e\
-\x26\xac\x78\x2f\xdc\xd3\x96\x41\xff\xc2\x82\x2a\xff\xbc\x7e\x28\
-\xff\xbd\x80\x2c\xff\xca\x90\x40\xff\x32\x22\x0b\x82\x00\x00\x00\
-\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x01\x9f\x6b\x23\xd0\xc6\x8b\x39\
-\xff\xaf\x75\x25\xff\xb5\x79\x27\xff\xd2\x96\x44\xff\x85\x59\x1c\
-\xc8\x00\x00\x00\x0f\x00\x00\x00\x01\x00\x00\x00\x0e\x00\x00\x00\
-\x04\x8b\x5d\x1e\x54\xcd\x93\x43\xff\xbe\x7f\x28\xff\xb5\x79\x27\
-\xff\xaf\x75\x25\xff\xc7\x8b\x39\xff\x92\x63\x21\xdc\x00\x00\x00\
-\x30\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x71\x4c\x18\x24\xbe\x88\x3d\xff\xac\x73\x25\
-\xff\xa9\x71\x24\xff\xb9\x7d\x29\xff\xc1\x8a\x3d\xff\x2a\x1c\x09\
-\x50\x00\x00\x00\x02\x40\x2b\x0e\x2c\x3b\x28\x0d\x82\x00\x00\x00\
-\x15\x00\x00\x00\x00\xa5\x71\x29\xd4\xc9\x8d\x3c\xff\xaf\x75\x25\
-\xff\xaa\x72\x24\xff\xae\x75\x25\xff\xbe\x88\x3d\xff\x1e\x14\x06\
-\x5a\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x7c\x53\x1b\x5b\xd3\x94\x3d\xff\xb7\x7b\x27\
-\xff\xa9\x71\x24\xff\xce\x93\x41\xff\x7b\x52\x1a\xc3\x00\x00\x00\
-\x0e\x00\x00\x00\x02\x7b\x52\x1a\xa8\x8e\x61\x23\xe2\x00\x00\x00\
-\x37\x00\x00\x00\x03\x80\x56\x1b\x60\xcf\x95\x44\xff\xac\x73\x25\
-\xff\xa8\x71\x24\xff\xb6\x7a\x27\xff\xd3\x94\x3d\xff\x36\x24\x0b\
-\x88\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x7b\x52\x1a\x78\xe0\xaf\x6d\xff\xdd\xaa\x62\
-\xff\xdd\xa9\x61\xff\xc1\x8e\x47\xff\x24\x18\x08\x60\x00\x00\x00\
-\x18\x2f\x1f\x0a\x45\xbf\x88\x3d\xfc\xcb\x91\x42\xff\x2e\x1f\x0a\
-\x81\x00\x00\x00\x12\x7d\x54\x1b\x04\xa8\x75\x2f\xe0\xda\xa2\x54\
-\xff\xdc\xa6\x5b\xff\xdd\xaa\x62\xff\xdf\xae\x6a\xff\x3b\x27\x0c\
-\x9d\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x7c\x53\x1b\x74\xe2\xb5\x77\xff\xdf\xad\x69\
-\xff\xe1\xb3\x73\xff\xa7\x70\x24\xf5\x72\x4c\x18\xc1\x73\x4d\x19\
-\xbe\x92\x63\x21\xe2\xde\xad\x68\xff\xdf\xac\x67\xff\x83\x59\x1f\
-\xd7\x00\x00\x00\x30\x00\x00\x00\x02\x80\x56\x1c\x70\xde\xac\x66\
-\xff\xdd\xaa\x63\xff\xdf\xad\x69\xff\xe0\xb0\x6e\xff\x3b\x27\x0c\
-\x96\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x80\x56\x1b\x56\xde\xab\x65\xff\xe0\xb1\x70\
-\xff\xe1\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb2\x71\xff\xe0\xb1\x70\
-\xff\xe0\xb1\x6f\xff\xdd\xa9\x60\xff\xdd\xa8\x5e\xff\xcb\x98\x52\
-\xff\x23\x17\x07\x6e\x00\x00\x00\x0e\x7d\x54\x1b\x0c\xb9\x88\x45\
-\xf0\xe0\xb1\x71\xff\xe0\xb1\x6f\xff\xdd\xaa\x62\xff\x36\x24\x0c\
-\x7a\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x85\x5a\x1d\x1c\xcb\x9d\x5e\xff\xe2\xb7\x7a\
-\xff\xe1\xb2\x72\xff\xe0\xb0\x6e\xff\xdf\xae\x6a\xff\xde\xac\x67\
-\xff\xde\xab\x65\xff\xde\xab\x64\xff\xdd\xaa\x63\xff\xe3\xb8\x7d\
-\xff\x7d\x55\x1e\xca\x00\x00\x00\x2b\x00\x00\x00\x01\x99\x67\x21\
-\x8c\xe5\xbd\x85\xff\xe2\xb7\x7a\xff\xc4\x98\x5a\xff\x1c\x12\x06\
-\x3e\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x77\x30\xbc\xe9\xc7\x97\
-\xff\xe2\xb6\x79\xff\xe1\xb4\x75\xff\xe1\xb2\x72\xff\xe0\xb1\x70\
-\xff\xe0\xb0\x6e\xff\xdf\xaf\x6c\xff\xdf\xaf\x6c\xff\xe1\xb4\x76\
-\xff\xcb\x9e\x60\xfc\x1f\x14\x06\x63\x00\x00\x00\x0a\x8a\x5d\x1e\
-\x20\xd0\xa3\x66\xfb\xea\xcb\x9f\xff\x94\x68\x2b\xcd\x00\x00\x00\
-\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8f\x60\x1f\x3c\xd9\xb2\x7c\
-\xff\xe5\xbf\x89\xff\xe3\xb9\x7d\xff\xe2\xb7\x7a\xff\xe2\xb6\x79\
-\xff\xe1\xb4\x76\xff\xe1\xb4\x75\xff\xe1\xb4\x75\xff\xe1\xb4\x75\
-\xff\xe9\xc7\x98\xff\x6d\x49\x17\xba\x00\x00\x00\x25\x00\x00\x00\
-\x00\xa8\x76\x30\xac\xd6\xaf\x79\xff\x34\x23\x0b\x58\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\x71\x24\
-\x8c\xe8\xc5\x95\xff\xe6\xc0\x8c\xff\xe4\xba\x81\xff\xe3\xb9\x7f\
-\xff\xe3\xb9\x7e\xff\xe3\xb9\x7d\xff\xe3\xb9\x7d\xff\xe3\xb9\x7d\
-\xff\xe6\xc0\x8c\xff\xc9\x9d\x60\xfa\x17\x0f\x05\x57\x00\x00\x00\
-\x08\x94\x63\x20\x3d\x6e\x4a\x17\x99\x00\x00\x00\x08\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x99\x67\x21\
-\x04\xa9\x73\x28\xa4\xe7\xc3\x91\xff\xe9\xc7\x99\xff\xe5\xbd\x86\
-\xff\xe5\xbd\x85\xff\xe5\xbd\x85\xff\xe4\xbc\x83\xff\xe5\xbc\x85\
-\xff\xe5\xbd\x85\xff\xea\xca\x9e\xff\x62\x42\x15\xab\x00\x00\x00\
-\x1e\x00\x00\x00\x00\x4c\x33\x10\x04\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x03\x00\x00\x00\x0f\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xa2\x6d\x23\x68\xcc\xa0\x62\xf0\xec\xd0\xa9\
-\xff\xed\xd2\xad\xff\xea\xcb\x9f\xff\xea\xc9\x9c\xff\xea\xcb\x9f\
-\xff\xed\xd2\xad\xff\xed\xd1\xab\xff\xb2\x7f\x38\xe6\x00\x00\x00\
-\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x03\x00\x00\x00\x10\x00\x00\x00\x06\xa5\x6f\x23\
-\x3e\x57\x3a\x12\x7f\x06\x04\x01\x3d\x00\x00\x00\x17\x00\x00\x00\
-\x04\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\x10\xa7\x70\x24\
-\x64\xbf\x88\x3c\xb0\xc6\x9c\x61\xdd\xc9\xa4\x71\xe2\xc6\x9c\x61\
-\xde\xb9\x84\x3b\xb4\x4f\x35\x11\x68\x45\x2e\x0f\x15\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
-\x12\x3e\x2a\x0d\x66\x5c\x3e\x14\x7f\x00\x00\x00\x07\xb9\x7c\x28\
-\x04\xb6\x7a\x27\xbc\xb1\x7c\x33\xe8\x42\x2c\x0e\x8e\x06\x04\x01\
-\x3f\x00\x00\x00\x1f\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x03\x00\x00\x00\x0a\x00\x00\x00\x19\x39\x26\x0c\x62\x9f\x6f\x2c\
-\xd0\xb5\x7a\x28\xe9\x20\x15\x07\x28\x00\x00\x00\x00\x00\x00\x00\
-\x00\xc3\x83\x2a\x0c\xcd\x91\x3d\xbc\xe0\xb0\x6d\xff\xb7\x87\x46\
-\xee\x6a\x47\x17\xb6\x3a\x27\x0c\x7e\x0f\x0a\x03\x47\x00\x00\x00\
-\x30\x00\x00\x00\x25\x00\x00\x00\x1f\x00\x00\x00\x18\x00\x00\x00\
-\x17\x00\x00\x00\x1e\x00\x00\x00\x24\x0c\x08\x02\x33\x35\x23\x0b\
-\x61\x5e\x3f\x14\x9f\xaf\x7e\x3b\xe1\xda\xaa\x68\xff\xcc\x93\x45\
-\xde\x2d\x1e\x09\x35\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xd3\x8e\x2e\x04\xd3\x91\x36\x80\xe1\xb3\x73\
-\xfb\xe8\xc6\x97\xff\xdb\xa8\x61\xff\xc2\x8e\x47\xff\xb1\x7c\x32\
-\xee\xa0\x6b\x22\xd6\x96\x64\x20\xce\x82\x57\x1c\xbf\x8f\x60\x1e\
-\xc7\xa1\x6c\x23\xd5\xae\x78\x2e\xe8\xbe\x8a\x43\xfc\xd8\xa4\x5c\
-\xff\xe6\xc1\x8d\xff\xe3\xb9\x7e\xff\xb6\x80\x35\xad\x34\x23\x0b\
-\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x92\x36\
-\x24\xd7\x9a\x45\xa8\xe3\xb8\x7d\xfb\xea\xc9\x9d\xff\xe8\xc4\x93\
-\xff\xe3\xb8\x7d\xff\xe0\xb1\x6f\xff\xdf\xaf\x6b\xff\xe0\xb0\x6d\
-\xff\xe2\xb6\x79\xff\xe7\xc2\x8e\xff\xea\xca\x9e\xff\xe4\xbc\x83\
-\xff\xd1\x98\x4a\xc5\x60\x41\x14\x49\x00\x00\x00\x02\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xd4\x92\x36\x18\xd4\x92\x36\x6c\xd9\x9e\x4c\
-\xb0\xde\xab\x66\xd9\xe3\xb7\x7b\xfb\xe3\xb8\x7c\xff\xe3\xb8\x7c\
-\xff\xdf\xad\x68\xe2\xd5\x9d\x4f\xb8\x97\x68\x26\x80\x7a\x52\x1a\
-\x2a\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\
-\x41\xfc\x00\x3f\x41\xf8\x00\x1f\x41\xf0\x00\x0f\x41\xe0\x00\x07\
-\x41\xe0\x00\x07\x41\xc0\x00\x03\x41\xc0\x04\x03\x41\xc0\x00\x03\
-\x41\xc0\x00\x03\x41\xc0\x00\x03\x41\xc0\x00\x03\x41\xc0\x00\x03\
-\x41\xe0\x00\x07\x41\xe0\x00\x87\x41\xf0\x00\x0f\x41\xf0\x00\x5f\
-\x41\x1c\x00\x78\x41\x06\x00\xe0\x41\x00\xff\x01\x41\x80\x00\x01\
-\x41\xc0\x00\x07\x41\xf0\x00\x0f\x41\xfc\x00\x3f\x41\x28\x00\x00\
-\x00\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x01\x0b\x07\x02\x17\x2d\x1e\x0a\
-\x54\x37\x25\x0c\x75\x33\x22\x0b\x6c\x13\x0d\x04\x3c\x00\x00\x00\
-\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x13\x0d\x04\x06\x5b\x41\x1c\x80\xca\x9e\x61\xee\xdf\xad\x69\
-\xff\xdf\xaf\x6b\xff\xdf\xaf\x6b\xff\xd6\xa8\x68\xfc\x86\x63\x34\
-\xbe\x11\x0b\x03\x43\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x10\x05\
-\x05\x87\x62\x2f\xa8\xd8\xa4\x5b\xff\xd2\x8f\x33\xff\xd8\x9d\x4a\
-\xff\xd9\x9f\x4e\xff\xd9\x9e\x4d\xff\xd2\x8f\x31\xff\xd5\x9d\x4f\
-\xff\xb5\x88\x49\xe7\x14\x0d\x04\x4b\x00\x00\x00\x03\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5f\x41\x16\
-\x73\xca\x8f\x3d\xff\xbe\x7f\x28\xff\xcf\x95\x44\xff\x76\x51\x1d\
-\x95\xa1\x6c\x22\x6c\xa4\x6f\x27\x8f\xd2\x96\x43\xff\xc2\x82\x29\
-\xff\xc3\x87\x35\xff\x93\x66\x28\xd6\x00\x00\x00\x1f\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x23\x17\x07\x05\xb5\x7e\x33\
-\xea\xae\x74\x25\xff\xbd\x81\x2e\xff\x8e\x63\x27\xc7\x00\x00\x00\
-\x07\x16\x0f\x05\x20\x1c\x13\x06\x07\xb5\x7f\x35\xca\xbc\x7f\x2a\
-\xff\xaf\x75\x25\xff\xc3\x88\x36\xff\x2b\x1d\x09\x6c\x00\x00\x00\
-\x02\x00\x00\x00\x00\x00\x00\x00\x00\x7a\x52\x1a\x34\xc5\x89\x35\
-\xff\xab\x72\x25\xff\xc3\x89\x38\xff\x27\x1a\x08\x4c\x1e\x14\x06\
-\x15\x7c\x54\x1c\xd0\x00\x00\x00\x20\x8a\x5d\x1e\x54\xc4\x8a\x3a\
-\xff\xa9\x71\x24\xff\xbe\x83\x32\xff\x63\x43\x17\xa8\x00\x00\x00\
-\x06\x00\x00\x00\x00\x00\x00\x00\x00\x7b\x52\x1a\x52\xe0\xaf\x6d\
-\xff\xde\xab\x64\xff\x8f\x65\x2c\xd5\x15\x0e\x04\x3f\x68\x49\x1d\
-\x9e\xd5\x9e\x53\xff\x2a\x1c\x09\x72\x19\x10\x05\x05\xb2\x81\x3d\
-\xd8\xdc\xa5\x5a\xff\xde\xad\x68\xff\x72\x55\x2c\xbf\x00\x00\x00\
-\x08\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x55\x1b\x3d\xdf\xaf\x6c\
-\xff\xe1\xb3\x73\xff\xcf\x9e\x59\xff\xcd\x9c\x57\xff\xd8\xa5\x5f\
-\xff\xdc\xa8\x5e\xff\x8e\x67\x31\xd1\x00\x00\x00\x1a\x97\x67\x23\
-\x65\xe0\xb0\x6e\xff\xe1\xb2\x72\xff\x6d\x4d\x21\xaa\x00\x00\x00\
-\x04\x00\x00\x00\x00\x00\x00\x00\x00\x85\x5a\x1c\x0d\xce\xa2\x66\
-\xf6\xe1\xb4\x75\xff\xe0\xb0\x6f\xff\xdf\xae\x6a\xff\xde\xac\x67\
-\xff\xde\xab\x66\xff\xd8\xab\x6c\xff\x22\x16\x07\x60\x2c\x1d\x09\
-\x0b\xcb\x9e\x5e\xec\xe4\xba\x81\xff\x3d\x29\x0d\x70\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb1\x82\x41\
-\x93\xe7\xc2\x8f\xff\xe2\xb7\x7a\xff\xe2\xb5\x77\xff\xe1\xb3\x73\
-\xff\xe1\xb2\x72\xff\xe3\xb9\x7f\xff\x87\x65\x36\xc3\x00\x00\x00\
-\x14\xab\x7c\x3b\x85\xbd\x98\x65\xe8\x0b\x07\x02\x15\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\
-\x0e\xc9\x9a\x59\xd7\xe7\xc3\x92\xff\xe4\xba\x81\xff\xe4\xba\x80\
-\xff\xe3\xb9\x7f\xff\xe4\xba\x7f\xff\xda\xb2\x7c\xfd\x19\x11\x05\
-\x50\x4a\x31\x10\x1c\x4a\x31\x10\x49\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\
-\x00\x9c\x68\x21\x15\xc3\x94\x53\xb5\xe6\xc4\x95\xff\xea\xca\x9e\
-\xff\xe8\xc5\x94\xff\xe9\xc7\x99\xff\xec\xd0\xa8\xff\x6b\x4b\x1f\
-\x9f\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x04\x00\x00\x00\x07\x98\x66\x20\x55\x43\x2d\x0e\x7e\x00\x00\x00\
-\x23\x00\x00\x00\x07\x00\x00\x00\x00\xa5\x6f\x23\x34\xc1\x8e\x48\
-\x7f\xc8\xa2\x6c\x96\xc3\x97\x59\x8e\x71\x4d\x1b\x53\x2e\x1f\x0a\
-\x09\x00\x00\x00\x00\x00\x00\x00\x05\x1d\x13\x06\x31\x72\x4d\x18\
-\x9e\x09\x06\x02\x13\xbb\x7e\x28\x05\xcc\x8f\x3b\xac\xae\x84\x4a\
-\xe1\x55\x3b\x16\x96\x20\x16\x07\x51\x00\x00\x00\x23\x00\x00\x00\
-\x17\x00\x00\x00\x12\x00\x00\x00\x11\x00\x00\x00\x17\x11\x0b\x03\
-\x2a\x37\x25\x0c\x64\x86\x63\x31\xb3\xc9\x93\x49\xeb\x45\x2e\x0f\
-\x49\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8e\x2e\x02\xd7\x9a\x46\
-\x72\xe2\xb6\x79\xf1\xdc\xb1\x74\xff\xc4\x93\x4f\xf7\xb4\x7f\x35\
-\xe4\xa1\x72\x30\xd6\xac\x79\x32\xdd\xbc\x88\x40\xeb\xd0\xa2\x61\
-\xfe\xe4\xba\x80\xff\xbe\x91\x51\xc1\x4d\x34\x10\x29\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\xd3\x90\x33\x12\xda\xa0\x51\x72\xe0\xb1\x70\xc0\xe3\xb7\x7a\
-\xed\xe3\xb8\x7d\xff\xe3\xb7\x7c\xf9\xdf\xb1\x71\xd9\xa7\x80\x4a\
-\x9c\x81\x58\x1f\x40\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\xf0\x0f\xac\x41\xe0\x07\xac\x41\xc0\x03\xac\
-\x41\xc0\x03\xac\x41\x80\x01\xac\x41\x80\x01\xac\x41\x80\x01\xac\
-\x41\x80\x01\xac\x41\x80\x03\xac\x41\xc0\x03\xac\x41\xc0\x07\xac\
-\x41\x20\x0c\xac\x41\x08\x10\xac\x41\x00\x01\xac\x41\x80\x03\xac\
-\x41\xe0\x07\xac\x41\
-\x00\x00\x10\xbf\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x2a\x04\xcb\
-\x2f\x9a\x51\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
-\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
-\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x10\x4e\x49\x44\x41\x54\
-\x78\xda\xad\x5a\x6b\x8c\x5c\xe5\x79\x7e\xcf\x65\xee\xb3\xbb\xb3\
-\xb3\xbb\xf6\x7a\xcd\xa6\xbe\x60\x70\x28\xa6\x24\x32\x26\x4e\x6b\
-\x48\xe2\x82\x94\x36\x6d\x23\x0b\x44\x14\x45\xca\x9f\x36\x15\x48\
-\x51\x5a\x8a\x52\x2a\x25\x7f\xaa\x44\xa2\x29\xca\x1f\xa4\xf6\x47\
-\xa5\x4a\xf9\xd1\x94\xa4\x28\x52\x4b\x44\x9b\x26\x40\xb0\x53\xb9\
-\x86\x00\x69\x31\xc4\x26\xc6\x98\xd8\x6b\xaf\xf7\x32\x3b\xbb\x33\
-\x3b\xf7\x73\x4e\x9f\xe7\x3d\xdf\x77\xe6\xec\xae\x93\x1a\x92\x33\
-\x3a\x3a\xf7\xef\x7b\xde\xfb\xe5\x1b\xe7\xa5\x17\x5f\x94\xcd\x9b\
-\xe3\x38\xe2\x79\x9e\x44\x51\x24\x61\x18\x8a\xeb\xba\x7a\xe4\x35\
-\xcf\xf9\x3c\x08\x02\xbd\xe6\x86\x67\x59\xbc\xff\x7e\x9c\xfe\x56\
-\x36\x9b\xbd\x05\xcf\xf7\xe0\x7a\x14\xc7\x92\x79\xbe\x3e\x18\x0c\
-\xd6\xf0\xfe\xf9\x6e\xb7\xfb\x06\x6e\xfd\x0f\xae\x7f\x8a\xb1\x7a\
-\x76\x0c\x8e\x6b\xe7\xb3\x18\x38\x07\x8f\x66\x0c\xb9\xd6\xe6\xcb\
-\x7b\xd8\x38\x11\x76\x7e\x7b\x24\x9f\xcf\xdf\x57\x28\x16\x8f\x16\
-\x8b\xc5\x1b\x0b\xf9\xbc\x97\xcd\xe5\x24\x93\xc9\x88\x47\x42\x5d\
-\x3b\x79\xa4\x60\xfa\xbd\x9e\x74\xba\x5d\x69\xb7\x5a\xc1\xfa\xfa\
-\xfa\xb9\x56\xab\xf5\x6c\xa7\xd3\x79\x0a\x63\x9d\xc0\x3e\x78\x2f\
-\x58\x9c\x77\x23\x01\xee\x78\xe6\x63\x7b\x00\x80\x3f\x3f\x56\xa9\
-\xdc\x39\x32\x32\x22\x38\xdf\xf0\x7d\xb7\xd7\x97\x7e\xbf\x0f\xb0\
-\x3d\xbd\xce\xe7\xb2\x92\x05\x51\xd9\x6c\x66\xc8\x04\x8c\xd5\x68\
-\x36\x65\x6d\x75\x55\xea\xf5\xfa\x29\x10\xf4\x04\xa4\xf2\x2d\x12\
-\xc2\xf9\xaf\x57\x02\xd7\x4d\x00\x07\xc3\xf1\x5e\x00\xfe\xca\xc4\
-\xc4\xc4\x1d\x63\x95\x31\xbc\x13\x0b\x70\x69\xa5\x2e\x6f\x5f\xbc\
-\x2c\xef\x60\x5f\x58\xae\xc9\x6a\xa3\x29\x3d\x10\x10\x04\xf1\xa4\
-\x94\x06\xc1\x8f\x8d\x94\x65\xdb\x64\x55\x76\xdd\x30\x23\xbb\x66\
-\x67\x64\x72\xbc\x12\x13\x0c\xa9\xd4\x6a\x35\x59\x5a\x5c\x7c\xa9\
-\xd1\x68\x7c\x09\x73\xfe\xa7\x9d\xf3\x57\x26\xc0\x00\x2f\x43\xb7\
-\x1f\xdb\xb6\x6d\xdb\x43\xd5\x89\x09\x8a\x40\xd5\xe2\xf4\xd9\x73\
-\xf2\xf2\xe9\x9f\x02\xfc\x9c\xac\xb7\xda\xfc\x52\x7c\xdf\x93\x0c\
-\xbe\xe5\xf7\xe9\xc9\xb9\xf7\x31\xd6\x60\x10\x90\xff\x52\x2e\x16\
-\x40\xc8\x4e\x39\x78\xdb\x2d\xf2\x9b\x37\xef\x15\x17\xef\xb6\xdb\
-\x6d\x59\x58\x58\x88\x16\xae\x5e\xfd\xbb\x5e\xaf\xf7\x28\xbe\x69\
-\xfe\x4a\x04\x98\x89\xf7\x57\xc6\xc7\xbf\xb1\x63\x7a\xfa\x50\xa9\
-\x5c\xd6\xe7\xaf\x9d\x7d\x4b\x5e\x38\xf5\xb2\x5c\x9c\x9b\x27\x66\
-\x29\x40\xef\xa1\xff\x92\x33\xaa\xe2\x79\x6e\x6c\x03\x76\x72\x23\
-\xc9\x01\xed\x00\x92\xe9\x76\xfb\xd2\xea\x74\xa4\xdd\xe9\xea\xf3\
-\xd9\x99\xed\xf2\x91\x0f\x1d\x94\x03\x20\x84\xdb\xf2\xd2\x92\xcc\
-\x5d\xbe\xfc\xe2\x6a\xbd\xfe\x59\x5c\x9e\xe1\x38\xef\x9a\x00\x8a\
-\x10\xdb\x61\xa8\xcb\x53\xd3\x3b\x76\xcc\x40\x02\x52\x87\x6a\x3c\
-\xf3\xdc\x8f\x94\x00\x72\xac\x5c\x2a\xea\x5e\x04\x78\x3f\xe3\xeb\
-\xbd\x5f\xb6\x59\x8f\xc3\x63\x7f\x30\x00\x01\x1d\x69\xac\xb7\xa4\
-\xd9\x6c\x41\x26\x91\xdc\x7a\xd3\x5e\xf9\xc4\xd1\x23\xaa\x6a\x50\
-\x25\x99\xbb\x74\xe9\xf2\xf2\xf2\xf2\x7d\x00\x7f\xd2\x7e\x7b\x5d\
-\x04\x70\x83\x9a\x1c\x9e\x9a\x9a\xfa\x2e\xc0\x57\x29\x8d\x73\x17\
-\x2e\xc9\xbf\xfe\xe0\x05\x59\x5e\x59\x95\x91\x72\x49\x46\x31\x09\
-\xd5\xc0\xf7\x5c\x55\x1d\x4b\xb8\xe5\xba\xb3\x89\x18\xe3\xb9\xb6\
-\x10\x42\x15\x6d\x42\xfd\x68\x37\x8d\xe6\xba\x4c\x8c\x8f\xc9\x27\
-\xef\xb9\x5b\x6e\xdc\x35\x4b\x6f\x25\x3f\xbf\x78\xb1\xb6\xb8\xb0\
-\xf0\x09\xbc\x77\xd2\xb9\x06\x83\x7e\x91\x04\x6e\xde\xbe\x7d\xfb\
-\xb3\x3b\x66\x66\x76\x12\xfc\xff\x9e\x39\x27\x4f\xff\xe0\xb8\x04\
-\x98\x90\xdc\xa9\xc0\xf3\x64\xb3\x34\x60\x17\x92\x8a\x41\x93\xfb\
-\x74\x9b\x8e\x6c\x25\x20\x8a\xd1\x26\xc7\x50\x89\x09\xd5\x8e\x2c\
-\x61\x3d\x78\xae\x3a\xb8\xbe\xba\xd6\xd4\x31\xff\x08\x44\xdc\xb6\
-\xff\x46\x81\xab\x95\x77\x2e\x5c\x98\x5b\x5c\x5c\x3c\x8a\xaf\xcf\
-\x6e\x61\xf4\x35\xa8\x2a\xc3\x50\xbf\xb9\x7d\x7a\x5a\xc1\x9f\x7e\
-\xf3\x2d\x79\xfa\xd9\x13\xe2\xc2\x70\x27\x47\x47\x94\xf3\xbc\xcf\
-\x49\x5c\x87\xbe\xde\x8d\xc1\x3b\x96\x90\x84\x0b\x69\xf6\x27\x07\
-\x92\xc1\x63\x08\x02\xa2\x30\x26\x86\x52\xe0\x98\x39\x78\xaa\x3c\
-\x54\x75\x65\xad\x01\x69\x1f\x57\x56\x1c\x00\x11\xb3\xb3\xb3\x3b\
-\xe1\x62\xbf\xb9\xb2\xb2\x72\x37\x6e\x35\x37\x10\xb0\x59\xcc\xe5\
-\x72\xf9\x6b\xd3\xd3\xd3\x1f\x64\x30\x7a\xfb\xd2\x65\xf9\xf7\x1f\
-\x9e\x54\xc3\xac\x42\xb4\xa3\xe5\xb2\x1a\x28\x81\x2b\x01\xc6\x50\
-\xd3\x04\x0c\x19\x92\x66\xcc\x50\x65\x62\x5a\x62\xa3\xe6\x29\x89\
-\x08\xe8\x30\x28\x91\xc0\xc3\x3c\x15\xf1\xc0\xac\x95\xfa\xaa\x3c\
-\xf3\xc2\x49\xb5\xb1\xdd\x70\xb9\x33\x33\x33\x1f\x44\xd0\xfb\x1a\
-\x24\xf2\x50\x9a\xe9\x2e\x41\x58\x20\x00\xfd\xb1\x6d\xdb\xb7\x3f\
-\x58\x28\x14\x64\x0d\xfa\xf8\xbd\xe3\xa7\x70\xdf\x95\xc9\xea\xb8\
-\x54\xe1\xf7\x49\x48\xc6\xc7\x0e\x83\xcd\xf8\xe9\x3d\x93\x9c\xab\
-\x1b\xd5\x73\x2f\x7e\x6f\xd3\xbb\xd9\x0c\xdf\xf1\x35\x5a\x27\xef\
-\xe3\x5e\xd6\xdc\x63\xbc\xe0\x5c\x13\x98\x93\x98\xfe\xe3\xf8\x7f\
-\x03\x4b\x4b\xa0\x15\x02\x37\xfe\x20\x24\xf5\xb1\xb4\x41\xfb\xc6\
-\xdb\x90\x3b\xb9\x4a\xa5\xf2\x38\x76\xbd\xfe\xe1\xa9\x57\xd5\xb8\
-\x26\xc0\x91\x6a\x65\xd4\x70\x3e\xe6\x7a\x9c\x26\xa4\xf4\x9f\x9a\
-\x6f\x98\x40\xe6\x0c\xed\x60\x83\x06\x0d\xd5\xc7\xe4\x55\x91\xb1\
-\x87\xf8\x1a\x01\x33\x8c\xcf\x3d\x37\x52\x22\xb8\xd1\x69\x3c\x7f\
-\xf2\x65\xd8\xc4\x11\x01\x73\x65\x6d\x6d\xed\x71\xa8\xd2\x61\xe0\
-\xe8\x26\x36\xc0\x81\x40\xfd\x31\xb8\xcc\x0f\x10\xe0\xb9\x77\x2e\
-\x21\x38\x5d\x91\xca\xe8\x28\x06\x1a\x51\xae\x69\x6c\x20\xe8\x44\
-\x85\xdc\xc4\x0e\x2c\xd7\xe9\xe7\x09\x33\xf1\x44\x86\x10\x35\xdf\
-\x94\xfe\xa7\x81\x87\x06\x74\x68\x09\xc0\x18\x01\x08\xf0\x30\x36\
-\x19\xc7\x67\xe7\xa1\xca\x3f\x03\xa6\x7d\xbf\x71\x83\xc0\x33\x7e\
-\xa0\xd9\x6c\x1e\x83\x4d\xfc\x33\xe7\xf1\xcd\x60\x3e\x52\x84\x87\
-\x19\xa8\x18\xfe\x5f\x79\xfd\x67\x1a\x94\x2a\xa3\x65\xe4\x31\xb9\
-\x24\xb0\x79\xe4\xb6\xe7\x26\x52\xd0\x63\xde\x95\xb3\xe3\x67\x64\
-\xa1\xb0\x20\x47\x1a\x47\x24\xdb\xca\x4b\xd8\x0f\x63\xbd\x4f\x44\
-\x60\x88\x48\xb9\x53\x6b\xc0\x36\x4a\x07\x86\x90\x80\x4c\xe1\x31\
-\x70\xa4\x80\xf1\x2b\x70\x1c\x7d\x44\xef\x97\x4f\x9f\x95\x3d\xb0\
-\x85\xf1\x6a\x55\x96\x96\x96\x1e\x46\x7c\xf8\x17\xcc\x3f\x50\xfd\
-\x01\xb8\xc3\x48\xcc\x0e\x12\xe8\x85\xb9\x2b\x52\x5b\x5d\x83\xc1\
-\xc6\x41\x8a\xc0\x55\x7f\x3d\xa3\xab\xd4\xd3\x4c\x66\xa8\xdf\x05\
-\x5f\x16\x8b\x0b\xf2\xed\x27\x9f\x94\x2f\xf7\xfe\x4a\xde\x1c\x39\
-\x2b\xb9\x7c\x4e\x0d\x71\xe8\x97\x87\x31\x42\x89\xc6\x58\xbe\xb5\
-\x8d\x64\xcc\xd8\x3e\x68\x0b\xbe\x17\x3f\xe3\x7b\xe5\x52\x01\x58\
-\x4a\x8a\x89\x5a\x91\x03\x43\xa1\xe6\x07\x89\x59\x8d\x98\x83\x16\
-\x0b\x85\x4f\x95\x4a\x9a\xba\x43\x7d\xe6\xd4\x95\x95\x91\x61\xaa\
-\xc1\x71\x32\xaa\x48\x76\xa3\x41\xd2\x10\x15\xcc\x00\xd7\x61\x56\
-\x42\x3f\x90\xda\x33\x75\x79\xfc\xf8\x63\xf2\x37\x85\xaf\xca\xdc\
-\xd4\x9c\x4e\x26\xc3\x04\x74\x6b\xbc\x71\x63\xc9\xfa\x7e\x6a\x6c\
-\x43\x8c\xef\xc7\x73\x93\xb8\x11\x30\x92\x98\xa8\xda\xdc\xc6\xc6\
-\xc6\x04\x8e\xe6\x53\x4a\x00\xf6\x2c\xc0\xdf\xc3\x0f\x56\xe1\x79\
-\x96\xeb\x6b\x72\x03\x72\x13\x46\x5b\xcf\xf5\x8c\xe7\x00\xc7\xbd\
-\x78\x50\x0f\xc4\x38\xee\xd0\x8d\x39\x91\x13\x6b\x0b\x0d\xba\x8c\
-\xf3\x96\x23\x6f\x3e\x75\x4e\xfe\xfa\xe4\x97\xe5\x1f\x27\xfe\x41\
-\x3a\xb9\x36\xfc\x7b\x56\x9c\x5f\x96\x66\x38\x71\x41\xe3\x67\x86\
-\x5e\xcc\x12\x41\x55\x1d\x81\x36\xcc\xce\x4c\x23\xeb\x5d\x55\xef\
-\x48\x66\x13\x33\xc6\xcc\xba\xae\xe7\xdd\x82\x82\x64\x37\xc7\xb9\
-\xba\x54\x53\x4f\x33\x59\xad\xc8\x18\x08\xf0\x8d\xda\xf8\x86\x4b\
-\xd6\x63\x6d\x89\x86\xf0\xdf\x09\x16\x0f\x4e\xa1\x02\x43\x9c\x0f\
-\xe5\xf8\x3f\x9d\x90\x47\x2e\x7f\x41\x9e\x9e\xfa\x37\x89\x0a\x81\
-\x06\xaa\xd8\xf0\x1d\xf9\x45\xe4\xa8\xad\x25\xdc\x8f\xd5\x88\xf1\
-\x67\x0a\x98\xf8\xdd\x3c\x31\xe2\x1e\x08\xd8\x0d\x3c\xb7\x20\xc0\
-\xfa\xb7\x43\xd4\xbe\x75\x59\x85\x42\x5e\x39\x40\xdd\xcb\xe5\x32\
-\x9a\xeb\x90\xeb\x9b\x67\x74\x62\xfd\x53\x6f\xe1\x86\x5b\x09\x73\
-\x33\xf8\xae\xe2\x4a\xef\xad\x81\x7c\xfb\x7b\x4f\xca\x23\xf2\xe7\
-\xf2\xc2\xf4\x09\xe9\x96\x50\xe4\x78\x92\x04\xc0\x6b\x25\x80\x24\
-\x92\x92\xa0\xeb\xa6\xea\x50\x85\xc8\x40\xa8\xba\x2c\xd5\x56\xf5\
-\x1d\x30\x1d\x34\x7a\xb7\xfb\x10\xef\x7e\xaa\x08\xdd\x59\xb3\xd5\
-\x91\x12\x12\x34\xcf\x44\x59\x9e\x77\x90\xfa\x6e\x91\xb8\xe1\xa0\
-\xe6\x30\xfd\x81\x34\x36\x46\xf7\xd8\xdb\xe4\x90\xff\x03\x6c\x90\
-\xc3\xf7\x90\x4a\xfd\xf5\x55\x79\xa2\xf4\xb7\xb2\xf7\xd6\x9b\xe5\
-\xd8\xe8\xfd\xb2\x6f\xfd\x66\xc9\x37\x72\x2a\x3d\xd7\xb8\xf2\x68\
-\xe3\x24\xca\xfd\x7c\x21\xab\x04\xe1\x2d\xe0\xc9\x4b\x73\xbd\xad\
-\xef\xd2\xbe\x90\x21\xef\xf7\x11\x80\xf6\x90\xc3\xbd\x7e\xa0\x7e\
-\x7c\x04\x12\xb0\xfa\x4a\xdd\xe5\xcb\x2c\x11\x2d\x68\xfb\x8c\xae\
-\xad\x3f\xe8\x63\x70\xf8\x7f\xd9\x48\xa4\x03\xce\x85\x79\xb8\xc7\
-\x1c\x9e\x94\x90\xa4\x8d\x2c\x4a\x36\xcc\x49\x34\x88\xe4\xe7\xaf\
-\x5c\x90\xc7\x4b\x5f\x95\x43\x37\xfd\xb6\xfc\xde\xb6\x3f\x94\xe9\
-\xd6\xb4\x14\x1a\x90\x3a\xb2\x1a\x8e\x1c\xa6\x32\xd5\x9c\x89\xcc\
-\x5a\x15\xe2\x29\x41\x33\x63\xa5\xab\x27\xd3\xd9\x3c\x60\x7d\x3b\
-\x4a\x85\x88\x23\xa0\xab\x29\x80\x0d\x3f\xdc\x8a\xf8\x88\xb5\xc4\
-\x20\x18\x28\xa7\x02\x93\x39\xc6\x41\x0b\xfa\x0f\x75\xf0\xa2\xd8\
-\x65\xfa\x3d\x54\x6a\xb9\x40\xc2\x4c\x20\xdd\x62\x5b\xda\xa3\x0d\
-\x69\x95\x1b\xe2\x64\x3d\x19\x78\x03\x89\xfa\x91\x2c\xe7\x2e\xcb\
-\xe4\xe2\x4e\x79\xfd\xb9\xd3\x72\x66\xc7\xeb\xf2\xe1\x03\x77\xc9\
-\xbd\x99\x8f\x4b\xa9\x5f\x04\x21\x05\x10\xe2\xe9\x1c\x54\xe3\x02\
-\xdc\xb1\x25\x88\xa0\xe8\x62\x89\x61\x10\x06\x31\xd6\x4c\x66\x94\
-\xa9\x44\xd1\x72\x34\xce\x89\xac\x3e\x3b\x89\x28\x69\x0f\x2c\xf7\
-\x5a\xa8\xa0\xa8\x32\xe4\x8e\x63\xfd\xbb\xeb\x6c\xca\x3c\x31\x56\
-\xae\x17\xdf\x62\xad\xe0\xa3\xb6\x2d\x76\x35\x6d\x8c\x20\x28\xbf\
-\x99\x55\xa2\x80\x52\xb2\x97\x0a\xf2\x5f\x6f\xfc\x48\x4e\x1e\x38\
-\x21\xf7\xdc\xf6\x71\xb9\xcb\xfd\xa8\x8c\x2d\xc1\x45\xe6\xb3\x5a\
-\x24\xc9\xa6\x22\x46\x55\x09\x63\xf6\xa1\x2d\x99\x1c\x1d\x82\x5b\
-\xdc\x60\x7d\x7e\xaa\x8e\x4d\x23\xe2\x87\x45\xa3\x5a\x14\x9f\xaa\
-\x92\xeb\x6c\x79\x37\x00\xe7\x23\xeb\x90\xf0\xbc\xef\xb5\xf5\xa8\
-\xce\xda\x8d\xf3\xa4\xb4\x33\xb0\x92\xe3\x96\xe9\x43\x89\x6a\x59\
-\x7d\x87\xe0\x39\xfe\x96\x1a\x8c\xa9\x83\x62\x94\xe4\x19\x53\x89\
-\x16\x4f\xb2\x99\xd8\x65\x6e\x60\xa5\x05\x06\xe9\x50\xc5\xc6\x11\
-\xd6\xa9\x97\x0d\x14\x19\x2a\x85\x6b\xf9\x76\xdc\xf7\x06\x48\x8f\
-\xa1\x72\x11\xb4\x2c\xd3\x05\x28\x10\xd5\xcf\x76\x25\xd3\x81\x1d\
-\x80\x01\x99\x0e\xc0\xae\xe4\x64\x7d\x77\x5d\x7e\x7f\xf7\x31\x39\
-\x2a\xbf\x2b\x63\x57\x47\x25\xef\xe7\xa0\xb2\x79\x2d\xfe\xbd\xc8\
-\x78\xa8\x28\x19\x36\x26\xd4\x64\xb1\x26\x25\x69\xf9\xec\x98\x29\
-\x37\x10\xb4\xb8\xdb\x2a\x29\x66\x5b\x6c\x54\xaa\x5e\x34\x62\xd6\
-\x0b\x70\x69\x14\x71\x63\x3d\x56\x29\xe6\x34\x96\x0c\x66\x26\xa1\
-\x17\xe8\xd1\xef\x21\x97\x6a\x4c\xc9\xaa\xb7\x28\x51\x27\xd4\xeb\
-\x08\xa2\xcf\xd7\xca\xe2\x00\xc4\xa1\x3b\x3f\x24\xc7\x6a\xf7\x4b\
-\xa5\x36\x0e\x2d\x83\x9a\xc2\x45\xd2\x75\x0e\x06\xa1\xf6\x8c\xf0\
-\xaa\x5e\xa7\x19\x4a\x5c\xb1\x7b\x85\x9d\xc0\x81\x10\x3b\x49\x39\
-\xaf\xad\x13\xcf\x55\x09\xd0\xe3\x00\x93\xb8\x26\xe1\xea\x07\x83\
-\x44\x7c\x36\x93\x24\x17\xa6\xaa\x39\xed\xfd\x74\x31\x53\xd6\xf5\
-\x13\x15\x52\x2d\xe9\xc2\x96\x7c\x80\x76\x32\x32\x16\x4d\x49\xab\
-\xb0\x26\x5e\x0b\xee\x10\x69\xc5\x4d\xfb\x0e\xc8\xe7\x2e\x3f\x28\
-\x33\xad\x1d\xe2\x97\x3c\x29\xb0\x93\x01\x8e\x06\x51\x68\xfa\x48\
-\x91\xea\x19\xb3\x53\x76\x30\x7c\x93\x53\xd9\xee\x1e\x3d\x23\xed\
-\xa0\xdb\x51\x5b\x3c\xef\xf7\x7a\xbd\x33\xa0\x44\x5d\x54\xa9\x00\
-\xb1\xb6\x3b\xc8\x0c\x21\x66\x1a\x0b\x53\x5b\x8a\xd3\xf5\x62\x11\
-\x3a\x51\xd2\xec\x22\x50\x66\xaa\x95\xb1\x9c\xec\xa9\xef\xd1\xe7\
-\x69\x8d\xf2\x5a\x19\x55\x17\x67\x15\x91\x14\x93\x4e\xdd\x31\x25\
-\x7f\xd2\xff\x53\xb9\xad\x77\xab\x64\x67\xe2\xd6\xa3\xb6\x6e\x82\
-\xb8\xdd\x12\x4b\xde\x64\xab\x71\xad\x86\xfb\xac\xda\x02\x0d\xa6\
-\x81\x12\x10\x2a\x46\x4e\xd3\xeb\x76\x49\xe0\x19\x1f\x1f\xff\x04\
-\x44\x0c\x18\x8d\x99\xf5\x5d\x5d\xae\x2b\xe8\x50\xc5\x39\x30\x06\
-\x13\xa5\x72\x79\x31\x35\x6d\x64\x6c\x03\x46\xee\x39\xb2\xc5\x1c\
-\x60\x59\x21\x82\xe6\xe8\x87\x4b\xf2\xf0\xc4\x5f\xc8\xe1\xd1\x43\
-\x0a\x98\x63\xc6\x75\xb0\x4d\xa3\x83\xa4\xd4\x4c\x82\x59\x34\x34\
-\x41\xbe\xef\x42\x74\x81\x21\x92\x18\xb9\xa1\xbc\x04\xf4\xe0\x27\
-\x3e\x0a\x88\x37\xe0\x22\xdf\x46\x3d\xb0\xaf\x82\x82\x9d\x56\xde\
-\x87\xab\x24\x87\x02\x13\x1b\x92\xea\x49\xed\x80\x13\xbb\x0a\x58\
-\xf9\x13\xc4\xc5\x79\xa2\xa9\x70\x8f\xce\x1a\xe2\xe6\xfe\x48\x3e\
-\x73\xfb\x67\xe4\x01\xff\x3e\x66\x8e\xda\x3e\xec\xf6\x7a\xc3\xb1\
-\x0c\x47\xc3\x54\x97\x82\xf0\xe3\xeb\x30\x21\x86\x18\xc0\x5f\xc5\
-\x44\x49\xb0\x46\xe1\xbb\xeb\xeb\xeb\x6f\x83\x01\x6f\xb0\x22\xeb\
-\x81\x80\xef\xe3\x62\x5f\x11\xe2\x61\x1d\xc0\x66\x93\xab\xdd\xb5\
-\xd8\x95\x25\xdc\x31\x44\xb8\x0a\x80\xee\x31\x56\xa7\x3e\xd2\x05\
-\x12\xe1\x36\xa0\x6a\x3b\x06\xf2\xd1\xa3\x77\xcb\xe7\xe4\x8f\x65\
-\xb2\x5c\x55\xd0\xad\x76\x7b\x23\x97\xad\x04\x2c\xd0\x54\x7b\x25\
-\x32\x13\x46\x32\xec\x1f\xf5\x20\x85\x1e\x8c\x96\xdc\x2f\x22\xb8\
-\xb1\xd5\x82\xfd\xfb\x7c\xe4\xf3\x95\x76\xab\xf5\x64\xa7\xdd\x7e\
-\xa8\x88\x34\x75\xfb\xc4\xb8\x76\xe0\x54\x35\x68\xb8\xac\x57\x99\
-\x85\x46\x71\x07\x21\xa2\x1d\x38\x92\x02\x14\x4a\x3b\x6c\x8b\xdb\
-\xf7\x64\xd7\x1f\xbc\x4f\x1e\x6d\xff\xa5\xec\xaa\xcc\x6a\xe7\x6d\
-\xbd\xd5\x4a\x7b\xd7\x0d\xdc\xb7\xe0\x43\x5b\x99\xa5\x25\x61\x54\
-\xd4\xce\x61\xab\xb6\xed\x53\xe3\x3a\x56\xb3\xd1\x60\x60\x7d\x52\
-\x4b\x4a\x27\xd6\xb3\x93\x28\x96\x7f\x0c\x02\x0e\x4e\x8c\x8f\x4a\
-\xe9\x6a\x5e\xbd\x91\xeb\xe6\x54\x19\x1d\x1d\x9c\xc5\xbb\x29\x05\
-\xd5\xc3\xc2\x63\x84\xae\x8a\xf6\x86\xce\x4e\xf9\xb3\x4f\x7e\x41\
-\x3e\xe2\xdd\x25\xce\x98\x63\x1a\xbd\x71\xde\x34\x0c\xa6\x51\xe2\
-\xa2\x43\x19\x72\xdd\x12\x13\x5a\x06\x59\x42\x4c\xe1\x1f\x9a\x36\
-\x64\x09\x81\x74\x12\x85\x3e\x6d\xa2\x5e\xaf\xff\x98\x9d\x3a\xd3\
-\x51\x51\x6f\x30\x68\x34\x9b\x5f\xef\x76\x3a\xaa\xf3\xb3\xd3\xdb\
-\xb4\x8b\xac\xab\x30\x26\x16\x58\x8e\x84\x91\x2d\xc4\x63\xf5\x61\
-\x6a\xb1\x33\x3b\x2b\x47\x9c\xdf\x51\x75\xe1\x02\xc6\x40\xbd\x57\
-\x68\x8e\xf1\xae\xe7\xa6\x78\x4f\x7f\xaf\xbf\x48\x0c\xe0\x14\xf7\
-\xcd\xbc\xfa\x3d\xb0\xcc\x4e\x4f\x69\x46\x00\x46\x0b\x8a\xfa\xaf\
-\xc3\xbd\x0e\xb4\xc1\x66\x39\xd5\xef\xf5\xbe\x03\xca\x5e\xe5\xf5\
-\x54\x75\x4c\x26\x2a\x23\x30\x9e\xde\x90\x6b\x61\x68\x38\x63\xc5\
-\x6c\x38\x29\xf4\xdf\x03\x78\x85\x9e\xf1\x2c\xc6\x40\x93\x62\x3d\
-\x34\xe7\xa6\x68\x0f\x62\xee\xc6\x5c\x37\x52\x49\xae\x25\x69\xb1\
-\xd8\x67\x8c\x05\xc4\xc2\x82\x86\xdc\x5f\x5e\x5a\x7a\x15\xf7\xbe\
-\x63\xb3\x00\xd7\xea\x17\x00\x75\x57\xea\xf5\x47\x5a\xeb\xeb\xfa\
-\x80\x1d\x00\x56\x50\x1c\xc0\x12\xa1\x60\x6c\x47\x81\xdf\x05\x31\
-\xa0\x40\x27\x0b\x0c\xc0\x30\x69\x97\xf0\x59\x42\x90\x25\x2e\x0a\
-\x13\xe2\x62\xf0\xc6\x1e\x42\x89\xc7\xd1\x3d\x0e\x5c\x04\xcc\xaa\
-\x8c\x58\xb8\xd5\x96\x97\xa9\x3e\x8f\x10\xab\xc5\xed\xa6\xbb\xc6\
-\x83\x7e\xff\xb9\xc5\xc5\xc5\xbf\xe7\x87\x5c\x16\xda\x8b\x0f\xc9\
-\x8d\xbe\xfa\xee\x61\x3f\x27\xd8\x60\x70\x86\xd3\x86\xc0\x30\xb2\
-\xe0\x02\xc3\xd5\xcd\xe7\x69\xc3\x35\xcc\x23\x81\x51\x68\x24\x11\
-\x25\xe0\x39\xf6\xde\xf7\xed\x54\x2c\x70\x9b\x5c\xfc\x20\xb6\xe7\
-\x44\x86\x0e\xc1\x4d\xf7\x34\x69\x0f\x78\xf1\x8b\xcb\xcb\xcb\xaf\
-\xf0\xe1\xf8\xd8\x88\x52\x4f\x15\x51\x49\x18\xe3\x4a\xf7\x72\x6c\
-\x3f\xc7\x4e\x6e\xef\x05\x46\x8d\x82\xd4\x7b\x09\xe7\xa3\xa1\x04\
-\x12\x86\x6c\x02\xcf\x39\x39\x77\x75\x2c\x56\xe5\xf9\x2b\x57\x5e\
-\x81\xeb\xfc\xa2\x9b\x74\x00\xe3\xdd\xbf\xc6\xc2\x41\x13\x7a\xf6\
-\x69\x64\x7c\xcf\x56\x27\x26\x76\xd2\xad\x52\xdb\xce\x5f\xba\xa2\
-\x79\x38\x93\x29\xd7\x25\xf5\x26\x5b\x34\x75\x6d\xa4\xa7\xf1\x58\
-\x49\xc9\xee\x24\x0e\x28\x69\x6c\x49\x2a\xe2\x0e\x5d\xa5\x95\x28\
-\xb5\x60\xa0\x04\x92\xf3\x9c\x9b\x46\x3c\x3f\x3f\x3f\x57\xab\xd5\
-\x3e\x4d\x6c\x9b\x33\xe0\x6b\x2e\xb3\x62\xc0\xb3\x4b\x4b\x4b\xf7\
-\xa3\xdc\xfc\xee\xf8\xf8\x78\x75\x1b\x06\x62\x0a\x7b\xfe\xe2\xbc\
-\x2e\x95\xfa\x19\x16\x13\x16\x23\x39\x98\xea\x4e\x4b\x2a\x27\x8a\
-\x36\x8c\x99\x1c\x6d\x90\x4a\x62\x09\x5d\x41\x10\xab\x6a\xd6\xcf\
-\xc8\xee\xd9\x9d\xca\x79\x05\x7f\xe5\x4a\x0d\x0c\xbd\x9f\x98\xae\
-\x85\xd5\x39\xfd\xda\x6b\x5b\x6f\x6a\x0b\x91\x9c\x76\x0f\x4f\x4e\
-\x4c\x3c\x35\x5e\xad\xce\xf0\x1e\x97\x4d\xdf\x99\x9b\x97\xfa\x5a\
-\x53\x9b\xb9\xba\x90\xe7\x3a\x5b\xea\x65\x67\x38\xd0\x30\x8a\xa5\
-\xe8\x49\x47\x65\x55\x9b\x41\x6c\x23\x4c\x13\x76\xed\x9c\x56\x9d\
-\xa7\xca\x5e\x9d\x9f\xbf\x0c\x46\xea\x12\x53\x7a\x61\xfd\xba\x08\
-\xb0\xad\x41\xe4\x4a\xfb\xab\xd5\xea\x37\x26\x26\x27\x0f\xd9\xd4\
-\x96\x0d\xa6\x2b\x0b\xcb\x5a\x0f\x38\xa6\x5f\xca\x52\x34\xee\x4c\
-\x1b\xe0\xd1\x46\x15\x12\x67\x93\x14\xac\xed\xe0\x9c\xe9\xc1\x8e\
-\x6d\x13\x32\x39\x1e\x77\xa4\xb9\xb4\x34\x7f\xf5\xea\x8b\x2b\xb5\
-\xda\x67\x31\xfe\x19\x9b\xd4\xbd\x6b\x02\xb4\x84\x64\x36\xe8\x79\
-\xe5\x42\x3e\xff\x18\x88\x78\xa8\x54\x2a\x29\x2c\x4e\x5e\x5b\x6d\
-\x28\x31\x6c\xc3\x6b\x2e\xef\x0c\x97\x9b\x92\xa2\x79\x68\x04\x89\
-\xce\x6b\xd5\x06\xa2\xb9\xc6\xc6\xe8\xca\x0e\xb8\x5d\x4c\x87\x9b\
-\xd4\x65\x56\x24\x7f\x8f\x82\xeb\x4d\xfb\x17\x84\xf7\x4c\x40\x68\
-\x96\x7f\xe8\x41\x00\xec\xde\xd1\xb1\xb1\xaf\x54\x2a\x95\x3b\xf2\
-\x2c\xba\xcd\xc6\xe5\xd2\x35\xae\x36\x82\x10\x9e\xb3\x82\xb3\x6b\
-\x61\xdc\xac\xb1\xb3\xe3\xc1\x4e\x03\x81\x8f\xe8\xea\x66\x2e\x91\
-\x08\xa2\x2b\xfd\xfc\x4b\x88\xb4\x5f\x02\x70\x5d\xe8\x8e\x53\xe9\
-\x5f\x13\x01\xd6\x85\xf2\xaf\x06\xa8\xc8\x1e\x40\xde\xf4\x79\xa4\
-\xe0\x77\x92\x10\x3e\xb7\x5b\xba\xef\xcf\x49\xb9\xc5\x6d\xc9\xd8\
-\x4d\xa7\x3b\x71\xd4\x73\x66\x96\x6b\xab\xab\xa7\x90\xca\x3c\x81\
-\x38\xf4\x2d\xfb\x9f\x09\xce\xfd\x6b\x27\x40\xcc\xbf\x55\xb8\x41\
-\x22\x4c\xc5\x8f\x14\xf8\x67\x8f\x42\xe1\x68\xbe\x50\xb8\x31\x9b\
-\xcd\x7a\xb6\x21\xbb\xb9\xc2\xb1\x4b\xaa\xf1\x42\x77\x37\x80\x9e\
-\xeb\x9f\x3d\x90\x55\xea\x9f\x3d\x30\xd6\xc0\xaa\xa6\x6d\x18\x5c\
-\x0f\x01\xef\xe9\xdf\x2a\xc6\x65\x72\xc4\xe7\xdb\x9d\xce\xf3\x08\
-\x7e\x44\xfe\x7e\x70\x57\xff\x6e\x23\xfc\xbb\x8d\xeb\x8e\xc2\x53\
-\xc5\x7f\xb7\x09\x82\x75\x80\x5f\x0b\xcd\xdf\x6d\x20\xc9\xe4\xef\
-\x36\xa9\xf1\xae\x09\xf0\xff\xdb\xfe\x0f\x0f\x1d\x3a\xca\xed\x25\
-\x35\x52\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x11\x31\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
-\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
-\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
-\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
-\x46\x00\x00\x10\xb7\x49\x44\x41\x54\x78\xda\xac\x9a\x59\xac\x24\
-\x67\x75\xc7\x7f\xdf\x52\x55\x5d\xdd\xd5\xcb\xed\xbe\xcb\xcc\x1d\
-\x5f\x3c\x9e\x19\x33\x8b\x8d\xc7\x46\x63\x4f\x2c\x61\x1c\x98\x18\
-\x29\x12\x12\x11\x32\x02\x21\x45\x3c\xe4\x09\x4b\x28\x12\x42\x88\
-\x07\x1e\x79\x40\x08\xf1\x82\x04\xcf\x48\x89\x81\xe0\x38\x02\x13\
-\x02\xd8\x86\x60\xa2\x4c\x6c\x30\x90\xd8\xe0\x19\x33\x9e\xf1\x30\
-\xfb\xdc\xbd\x6f\xef\xf5\x2d\x79\xa8\xe5\xf6\x9d\x05\x1b\x44\x4b\
-\xad\xae\xee\xae\xfa\xbe\xb3\xfc\xcf\x39\xff\x73\xaa\xc4\x2f\x5e\
-\x7a\x89\x1b\x5f\x42\x08\x94\x52\x78\xef\x71\xce\x21\xa5\xc4\x39\
-\x87\xf7\x1e\x29\x25\x42\x08\xac\xb5\x78\xef\x01\x70\xce\x85\x4a\
-\xa9\xc3\xc0\xd1\x30\x0c\x8f\x08\x21\xf6\x29\xa5\x1a\x42\x88\x5a\
-\xfe\x7f\xdf\x18\xd3\xf5\xde\x9f\x1d\x8f\xc7\xbf\x03\xfe\xd7\x18\
-\xf3\x9a\x94\x72\x52\xac\x21\xa5\x2c\xf7\x2b\x64\xb0\xd6\x22\x84\
-\x28\xf6\xe0\x56\x2f\xcd\x9f\xf1\xf2\xde\xe3\xbd\xd7\xc0\x23\x95\
-\x4a\xe5\xf1\xb8\x5a\x3d\x51\xad\x56\x0f\xc4\x95\x8a\x0a\xa3\x88\
-\x20\x08\x50\x52\x22\x64\xb1\xb9\xc7\x5a\x4b\x3a\x99\x30\x1a\x8f\
-\x19\x0e\x06\xb6\xdf\xef\x9f\x19\x0c\x06\xcf\x8f\x46\xa3\xa7\xbc\
-\xf7\x3f\xf7\xde\x9b\x3f\x47\x16\xfd\xa7\x0a\xee\x9c\xd3\x5a\xeb\
-\x8f\x26\x49\xf2\xa9\x66\xab\x75\xbc\x5e\xaf\x53\xad\x56\x77\x9c\
-\x37\x9e\xa4\x0c\xc7\x13\x46\xe3\x09\x00\x95\x28\x24\x0c\x02\x92\
-\x7a\x9d\xa4\x5e\x07\x50\xde\xb9\x83\x5b\xbd\xde\xc1\xee\xe6\xe6\
-\x13\x1b\x1b\x1b\x2f\xf6\xfb\xfd\xaf\x1a\x63\xbe\xed\xbd\x37\x85\
-\xd5\xff\xa2\x0a\x78\xef\x51\x4a\x7d\xa0\x5e\xaf\x7f\xa1\xd3\xe9\
-\x3c\xd8\x6c\x35\x51\x2a\xbb\x7c\x65\x7d\x83\x73\x17\x2e\x73\xfe\
-\xc2\x65\xae\xaf\xae\xb1\xb9\xd5\x63\x92\xa6\x58\x9b\xb9\x5d\x49\
-\x49\x18\x06\x34\xeb\x09\xf3\xb3\x6d\xf6\xde\xb1\xc8\xde\xa5\x45\
-\x66\x67\x5a\x34\x1a\x0d\xe6\xe6\xe7\x8f\xaf\xad\xad\x1d\x5f\x59\
-\x5e\xfe\xc7\xad\xad\xad\xcf\x3b\xe7\x7e\xfc\x76\x95\x10\x6f\x15\
-\x03\xd6\x5a\xa4\x94\x49\x18\x86\x5f\x9c\x9f\x9f\x7f\xa2\xdd\xe9\
-\x08\xad\x35\xce\x79\x5e\x3d\x7d\x86\x97\x5f\x7d\x8d\x73\x17\x2e\
-\xd1\x1f\x0c\x01\x81\xd6\x8a\x40\x29\x94\x52\x3b\xf0\xeb\x9c\x23\
-\xb5\x16\x63\x2c\xe0\x49\xaa\x31\x7b\xef\xd8\xc3\xb1\xfb\x8e\x70\
-\xcf\xc1\xfd\x48\x21\x18\x0e\x87\x5c\xbf\x7e\xdd\x5f\xbf\x76\xed\
-\x6b\x93\xc9\xe4\x73\xce\xb9\xde\x5b\xc5\xc0\x1f\x55\x20\xdf\xf8\
-\x50\x6b\x66\xe6\x1b\xbb\x77\xed\x7a\xa8\x96\x24\x00\xbc\x72\xfa\
-\x0d\x7e\xf6\xe2\xcb\x5c\xb8\x74\x15\x04\xc4\x51\x44\x5c\xa9\x10\
-\xe5\x50\x51\x4a\x66\x31\x50\x6c\x9e\x07\xa7\xb1\x96\x34\x4d\x19\
-\x8f\x53\x06\xa3\x11\xc3\xd1\x18\x80\xa5\xc5\x05\xfe\xfa\xaf\x8e\
-\xf1\xae\x83\xfb\x01\x58\x5d\x59\xe1\xd2\xe5\xcb\x2f\x6d\x6e\x6c\
-\x7c\x02\x38\x25\x84\xf8\xd3\x15\x90\x52\x02\x3c\xdc\xe9\x74\x9e\
-\xda\xb5\x7b\xf7\x62\x18\x86\x6c\x6c\xf5\xf8\xc1\x4f\xfe\x8b\x57\
-\x4e\xbf\x81\x14\x82\xa4\x56\x25\xa9\x55\xa9\x56\x2a\xe8\x40\x23\
-\xdf\xc2\xed\x45\xc6\xf1\xde\x93\x1a\xc3\x70\x34\x62\xab\x3f\xa0\
-\xd7\x1b\xe0\xf1\xdc\xfb\xce\xfd\x7c\xf0\xc4\x23\x34\xeb\x09\x5b\
-\x5b\x5b\x5c\xba\x78\xf1\xf2\xea\xea\xea\xe3\xce\xb9\x93\xc5\xb5\
-\x6f\x2b\x06\xbc\xf7\x08\x21\x1e\x9e\x9b\x9b\xfb\xfe\xae\xdd\xbb\
-\xdb\x4a\x29\xce\xbc\x79\x91\xef\x3e\xf7\x33\x56\xd7\x37\x69\xd4\
-\x13\x1a\xf5\x84\xa4\x1a\xa3\x95\x04\x44\xa9\x78\x61\xf5\x1b\x31\
-\x9c\x67\xae\xf2\x58\x6b\x4d\x25\x8a\x68\x24\x09\xbd\xfa\x90\xcd\
-\xad\x1e\xaf\xbe\x7e\x96\x2b\xcb\xab\xfc\xdd\x63\x8f\x72\x60\xef\
-\x12\x77\xde\x79\xe7\xa2\x90\xf2\xfb\xcb\xd7\xaf\x7f\xd0\x39\x77\
-\xf2\x56\x71\x71\x3b\x0f\x1c\x5c\x58\x58\x78\x7e\xf7\xe2\xe2\x1e\
-\xa5\x14\xff\x77\xea\x0c\xcf\x3c\xf7\x02\xd6\x7b\x9a\xf5\x84\x56\
-\xbd\x4e\x18\x6a\x40\x22\x65\x26\xb4\x14\x02\x21\x05\x82\x9b\x15\
-\xf0\x99\xd4\xe5\xa7\xf3\x1e\xef\x1d\xce\xf9\x52\xb1\xc9\x24\x65\
-\x63\x6b\x8b\xcd\x6e\x0f\x29\x05\x1f\x7a\xec\x51\xee\x3b\x74\x80\
-\xc1\x60\xc0\xf9\x37\xdf\xbc\xb4\xbc\xbc\x7c\x02\x38\x7d\x93\x07\
-\x6e\xa1\x55\xd2\xee\x74\x9e\x5c\xd8\xb5\x6b\x8f\x52\x8a\x57\x5f\
-\x7f\x83\x67\x9e\xff\x39\x52\x6b\x66\x1b\x75\x1a\xf5\x04\xa5\x14\
-\x52\x0a\xa4\x90\x08\x29\x33\xe1\x45\xa1\x48\x69\x85\x69\xf3\x97\
-\x1f\x1e\x8f\xf7\xe0\xbc\xc3\xbb\x4c\x19\x6b\x2d\x4a\x29\xa2\x30\
-\xa0\x12\x86\xac\x77\xb7\xf8\xee\x73\x2f\x20\x80\x77\x1d\x3a\xc0\
-\xd2\xd2\xd2\x1e\x63\xcc\x93\xeb\xeb\xeb\x8f\x02\xbd\xdb\x42\xc8\
-\x7b\x4f\x92\x24\x5f\xda\xb5\x6b\xd7\xbb\x83\x20\xe0\xdc\xc5\xcb\
-\xfc\xc7\x7f\x9e\x24\x0c\x02\xda\x33\x4d\x1a\x49\x82\x52\x12\x29\
-\x32\xcb\x17\x55\x79\x5a\x81\x6d\x83\x4c\x1b\xc6\x4f\xc1\x28\xfb\
-\xee\x9c\xc3\x03\xde\x79\xac\x52\x38\xef\x70\x56\xd1\x9e\x69\xa1\
-\xb4\x66\x7d\x63\x93\x1f\xfc\xec\x24\x49\xad\xca\x5d\x4b\x8b\x2c\
-\x2e\x2e\xbe\x7b\x34\x1a\x7d\x69\x30\x18\x3c\x31\x6d\x74\x29\xa5\
-\x2c\x05\x09\x82\xe0\xfd\xf3\x0b\x0b\x9f\x8c\xe3\x98\x6e\xaf\xcf\
-\x8f\x5e\x78\x11\x21\x24\xb3\xed\x19\xda\xad\x26\x61\x10\x10\xe8\
-\x80\x20\xd0\x04\x7a\xfa\x1d\x94\xc7\x5a\xab\xfc\x58\x65\xe7\xdd\
-\x70\x6e\x18\x68\xb4\xd6\x04\x41\xb0\x7d\x7e\xa0\x09\xf3\xdf\xc2\
-\x30\xa0\xdd\x6a\xd2\x69\xcf\x20\x84\xe0\x87\x2f\xfc\x0f\xdd\xde\
-\x80\x76\xa7\xc3\xfc\xfc\xfc\x27\x95\x52\xef\x9f\x0e\x68\x9d\x67\
-\x1b\xbc\xf7\x51\xab\xd5\xfa\x72\xab\xd5\x02\xe0\x3f\x5f\xfc\x35\
-\xbd\xc1\x90\xce\x4c\x8b\x76\xab\x91\x5b\x3e\xb3\x7a\x46\x13\xa6\
-\xf0\x8f\x40\xe4\x46\x10\x82\xa9\x38\xd8\x81\xa0\x6d\xf8\xe4\xbc\
-\xca\xe7\xf1\x90\x7d\x77\x48\x97\x1d\x2b\xe9\x69\xb7\x9a\x59\x4a\
-\x5d\xdf\xe4\xa7\x27\x5f\xe6\x43\x8f\x3d\xc2\xfc\xc2\x02\xdd\x6e\
-\xf7\xcb\xeb\xeb\xeb\x0f\x4b\x29\xc7\x65\x0c\x78\xef\x09\x82\xe0\
-\xc3\x9d\x4e\xe7\x01\x29\x25\x67\xce\x5f\xe4\xdc\x85\x2b\xb4\x1a\
-\x0d\xda\xad\x3a\x61\xa0\xb3\xda\x20\x25\xb2\x84\x90\x2c\xe3\xa0\
-\xb0\xba\xb1\x59\x91\x2a\x33\x51\xae\x88\xc7\xc3\x14\xfe\xa7\x05\
-\x77\xb9\xd0\xae\x50\xc0\x5a\xac\xf4\x28\x29\x68\xb7\x1a\x38\xe7\
-\x38\x7b\xf1\x32\xbf\x3f\x7f\x91\xbb\xef\xbc\x83\xb9\xb9\xb9\x07\
-\x7a\xbd\xde\x87\x8d\x31\xdf\x14\x42\x20\xf3\xc5\x74\xbd\x5e\xff\
-\x74\x2d\x49\xb0\xd6\xf1\xab\xdf\xfe\x9e\x28\x0a\x69\x35\x12\x2a\
-\x51\x84\x52\xaa\x74\x79\xa0\x33\x58\x84\x81\x26\x0c\x02\xe2\x4a\
-\xc4\x66\x77\x93\x37\xff\x70\x9e\xb8\x52\x21\x8e\xe3\x12\x52\x3a\
-\xc8\xde\x81\x0e\xb2\xcf\x20\x28\x61\x15\xea\xec\xfa\x30\x5f\x6b\
-\xfa\x9a\x40\x67\x95\x3c\x8e\x22\x5a\x8d\x3a\x51\x18\xf2\xf2\xab\
-\xa7\xb1\xce\x31\xd3\x6e\x53\xaf\xd7\x3f\xed\x9c\xd3\xde\x7b\x24\
-\x80\x52\xea\xe1\x66\xab\x75\x4c\x08\xc1\x9b\x97\xae\xb0\xb6\xd9\
-\xa5\x91\x64\x45\x4a\xa9\x1c\xd3\x2a\xc7\x6a\x10\x64\xb1\x90\x0b\
-\x12\x45\x21\xfd\x7e\x9f\x7f\x7d\xfa\xdf\xf8\xee\x33\xcf\xb0\xbc\
-\xbc\x4c\x54\x89\x50\x7a\x2a\x3f\x88\xed\x1a\x21\xa5\x44\x29\x95\
-\x0b\xaa\xa7\xd6\xcc\x8d\xa2\x35\x5a\x65\xff\x29\xa5\x48\x6a\x31\
-\x8d\xa4\xc6\xda\x66\x97\x73\x17\xae\x10\x45\x11\xad\x56\xeb\x98\
-\x52\xea\x61\x00\x29\x84\xa0\x1a\xc7\x1f\xab\xd5\x6a\x00\x9c\x39\
-\x7f\x89\x4a\x18\x92\x54\xab\x59\xc0\x29\x95\x41\x24\xdc\x19\x90\
-\x5a\xeb\xac\x5a\xe7\xb4\x43\x08\xc1\x2b\xbf\xfd\x2d\xff\xf4\xe4\
-\x37\x79\xf6\xb9\xe7\xe9\xf5\xfa\x44\x51\x54\x54\xf4\x5b\xd3\x00\
-\x99\x5d\xab\xf5\xd4\xda\xb9\x32\x5a\x67\x7b\x87\x41\x40\xbd\x56\
-\xa5\x12\x86\x9c\x39\x7f\x11\x80\x66\xb3\x49\x1c\xc7\x1f\x2b\xd2\
-\x68\x58\xab\xd5\x1e\xd3\x5a\xb3\xd9\xeb\xb3\xba\xd1\xe5\x8e\xc5\
-\x85\x0c\xef\x42\x94\x0a\x94\xf8\xbf\x8d\x40\x42\x08\xa2\x30\x24\
-\x4d\x53\xfe\xfb\xe4\x49\x5e\x3b\x75\x8a\xe3\x0f\x3d\xc8\xd1\xfb\
-\xee\xa3\x12\x45\x4c\xd2\x94\xdb\xd1\x01\x04\x65\x5c\x09\x6b\xf3\
-\x64\xe1\x48\x85\xc0\x5a\x43\x3d\xa9\x52\xad\xc6\x5c\x5b\x5e\xa5\
-\xdb\xeb\x93\xd4\x6a\xd4\x6a\xb5\xc7\x86\xc3\x61\x28\xa5\x52\x47\
-\xe2\x6a\xf5\x2e\x80\x6b\x2b\x6b\x48\x21\x98\x6d\xb7\x68\x26\x35\
-\x74\x0e\x1b\x9d\x5b\xe9\xb6\xc2\x4f\x1d\x4b\x29\x89\xa2\x88\xad\
-\xad\x2d\x7e\xf8\xa3\x67\xf9\xe7\x27\xbf\xc9\x6b\xa7\x4e\xa3\xa4\
-\x24\x0a\x83\x3c\xf0\x05\xb7\x63\x4d\x4a\x29\x54\x69\xfd\x0c\x46\
-\x8d\x24\x61\xae\xdd\x42\x0a\xc1\xd5\x95\x35\xa4\x52\xd4\x6a\xb5\
-\xbb\xa4\x94\x47\xa4\xd6\xfa\xfe\x28\x8a\x74\x91\xb2\xe2\xb8\x42\
-\xa0\x35\x49\x2d\x26\x8a\x02\xb4\x92\x28\xad\xb8\x71\x47\x91\xe1\
-\x0f\x25\x05\xb7\xe2\x28\x4a\x29\xe2\xb8\xc2\xf2\xca\x2a\xdf\xfb\
-\xf7\x1f\xf0\xf4\xf7\x9e\xe1\xdc\xf9\x0b\x59\x6e\x9a\xa2\x1f\xb7\
-\x22\x80\x52\x66\xb4\x5c\x29\x49\x25\x0c\xa9\xd7\xaa\x68\xad\xa9\
-\xc6\x31\x2b\x6b\x9b\x00\xc4\xd5\xaa\x56\x4a\xdd\xaf\xa3\x30\x3c\
-\x14\x06\x01\xce\x79\x7a\x83\x11\xb5\x6a\x8c\xca\xab\x6c\xad\x1a\
-\x33\x1a\xa7\xb7\x84\x8b\xc8\x2b\xeb\x24\x35\x4c\x26\xe9\x6d\x2c\
-\x19\x64\xf1\x21\x25\x17\x2e\x5e\xe2\xf2\x95\x6b\x1c\xd8\x77\x17\
-\x47\xef\x7b\x17\xb3\x9d\x4e\x26\xac\xc8\x0c\xe1\x0b\xae\xb4\xbd\
-\x09\x4a\x29\x2a\x71\x88\x94\x02\x85\xa4\x56\xad\xd0\xeb\x0f\xf1\
-\xde\x13\x45\x11\x61\x18\x1e\xd2\x42\xca\x7d\x4a\x2b\x26\xa9\xc5\
-\x58\x4b\x3d\xae\x94\x16\x8d\xc2\x10\xef\x3d\xe3\x49\x5a\x0a\x5d\
-\xfc\x97\x1a\x4b\x6a\x52\xa4\xd4\xd8\x1b\xb8\xba\x94\x12\xa5\x75\
-\x06\x85\x20\x40\x4a\x4d\x18\x81\xb3\x8e\xd7\xcf\xbc\xc1\x9b\x7f\
-\xf8\x03\x87\x0f\xbe\x93\x7b\xef\xb9\x87\x7a\x92\x80\xf0\x68\x29\
-\x11\x79\xef\x50\xd0\x8e\x28\xaf\xcc\xce\x39\x24\x22\x83\x66\xaf\
-\x8f\xb5\x8e\x30\x08\x10\x42\xec\xd3\x5a\xeb\x06\x88\xbc\x02\x4a\
-\x02\xad\xca\xf2\x03\x50\x8d\x22\x9c\x03\x63\x0d\x52\x08\x6c\xce\
-\x1c\xb3\xa2\x05\x5a\xed\xe4\x6d\x85\x02\x52\x69\xb4\x0e\xd0\x2a\
-\x40\x69\x8d\x90\x12\x67\x2d\x52\x29\xd2\xc9\x88\x5f\xbe\xfc\x32\
-\xaf\xbf\xfe\x3a\xf7\x1f\x3d\xca\xa1\x43\x87\x20\x0c\x4b\x48\x5a\
-\xef\x09\xb4\x26\xae\x44\xa5\x42\x08\x08\x75\xd6\x73\x18\x67\x33\
-\x59\x83\xa0\x21\xa5\x94\xd5\xc2\xa2\x19\x27\x92\x3b\x43\x53\x88\
-\x2c\x1e\x02\xcd\xc4\x18\x86\xa3\x31\xc6\xda\xdc\x1b\x59\x2a\xbc\
-\x49\x03\x51\x90\x3c\x85\x54\x0a\x1d\x04\xe8\x20\xcc\xbd\x21\x51\
-\x3a\x20\xaa\xc4\x6c\x6e\x76\x79\xf6\xb9\xe7\xf8\x97\xef\x7c\x87\
-\xdf\xbf\xf1\x06\xe3\x34\x65\x62\x2c\x61\xa0\x49\xaa\xf1\x4d\x81\
-\x2e\xa5\x40\x29\x49\x9a\xda\x9c\x09\xc8\xea\x8e\xb4\xa2\xa7\xfa\
-\xd8\x69\x26\x29\xa5\xa0\x9a\x43\xcb\x5a\x97\x09\x7f\x9b\xe0\xdd\
-\x01\xb5\xa9\x02\x76\x33\x5b\xdd\xd6\x5b\xe4\x6f\xeb\x1c\x42\x40\
-\xb5\x52\x41\x48\x81\xbf\x79\xf1\x5c\x46\xca\xff\xb4\xf7\x7e\x00\
-\x10\x06\x59\xca\xbc\x91\x02\x03\x58\x63\x71\xce\x31\xd3\xa8\x13\
-\x05\x01\x5b\x83\x41\xd1\xb5\xdd\x6e\x66\x84\xcf\x1b\x79\xe7\x2c\
-\xc6\x18\xa4\x74\x38\x9b\xad\x63\x8d\x61\x3c\x1a\xd1\x68\x34\x79\
-\xe0\xfe\xa3\x1c\x3a\x74\x30\xab\xea\x41\x40\x35\xaa\x90\x5a\x8b\
-\xf2\x79\x86\xf2\x3b\x09\x61\x41\x69\xf2\x7d\x06\xda\x18\xd3\xcd\
-\x46\x1f\x0a\x25\x55\xd9\x25\x91\xb1\x0c\x9c\xf7\x19\xbc\x84\x40\
-\x78\x4f\x52\xab\x12\x57\x42\xb6\xfa\x43\x06\xa3\x31\xde\xf9\x9b\
-\x5c\x9d\x75\x5b\x0e\xeb\x0c\xd2\x48\xbc\xcf\x3c\xe1\xac\x65\x34\
-\x1a\x11\x45\x01\x0f\x3d\x78\x8c\x23\x87\x0f\x93\xd4\x13\xb4\x14\
-\x24\x71\x8c\xd6\x0a\x63\x1c\xde\x39\x52\x0b\x5a\xab\x1d\x06\xcd\
-\x5a\xd1\x8c\x27\x59\x93\x62\x8c\xe9\x6a\xbc\x3f\x6b\xad\x45\x2a\
-\x49\x10\x68\xc6\x93\x14\xe7\x41\x7a\x8f\x77\x9e\xd4\x9a\xd2\x7d\
-\x05\x93\x0c\x82\x80\xb9\x76\x56\x5d\xc7\x69\x16\x50\xd3\xaf\x6c\
-\x24\x68\xc8\x73\x2d\xd2\x2a\xd2\xd4\xa0\xb4\xe2\x9d\x07\xf6\x73\
-\xff\x7d\xf7\x32\x37\x37\x8b\x56\x8a\x38\x0a\x09\xb5\xc6\x7a\x97\
-\xcf\x91\x3c\x20\x70\xde\x91\xa6\x29\x3a\xe7\x54\xc5\x74\x2f\x0a\
-\x43\x94\x92\x8c\x47\x06\xef\xfd\x59\x3d\x99\x4c\x4e\x19\x63\x88\
-\xa2\x88\x5a\x1c\xd1\x1f\x8e\xb0\xce\x11\x28\x49\x6a\x6d\xd6\xee\
-\x49\x95\xb9\x50\xf8\x72\x7e\x29\x80\x4a\x14\xd1\x6a\x46\x5c\x9f\
-\x69\xe5\xdb\x4e\x2b\x61\x00\xcf\x78\x3c\x46\x4a\xc5\xdd\xfb\xf7\
-\x71\xfc\xc1\x63\xec\x5d\xba\x83\x30\xcc\x46\x8f\x4a\x29\x9c\xcd\
-\xc6\x2d\x99\xe7\x73\x08\x66\xbd\x1a\xc6\x3a\x3c\x16\xad\x24\xd6\
-\x79\xac\x75\xd4\xe2\x08\x01\x4c\xc6\x63\xd2\x34\x3d\xa5\x8d\xb5\
-\xbf\x99\x4c\x26\x26\x8a\x22\xdd\x48\x6a\x5c\x5b\xdd\xc0\x5a\x8b\
-\xd3\x0a\x63\x4c\x1e\x30\x7e\x8a\xcb\x93\xf7\xb4\x3e\x8f\x0d\x7f\
-\x4b\x6a\x90\xa6\x29\xc3\xe1\x88\xfd\xfb\xf6\xf2\xb7\x8f\x9d\xe0\
-\xde\x23\x87\x51\x2a\x5b\x33\xeb\x83\x0b\x98\xd9\xb2\xd5\x2c\x8b\
-\x99\xdf\x0e\xc1\x2c\x7e\x82\x4c\x26\xe7\x69\x24\x19\xe9\x1c\x8d\
-\x46\xc6\x5a\xfb\x1b\xed\xac\xfd\xdd\x70\x38\x3c\x57\xaf\xd7\xef\
-\x6e\xd5\x13\xb4\xca\xdd\x2d\x25\x36\xaf\x0d\x65\xf7\x24\x04\xc2\
-\x3b\x9c\x93\x08\x01\x8e\x4c\x90\x69\x92\xe6\x9c\x63\x32\x99\x30\
-\x37\x3b\xcb\xdf\xbc\xef\x51\xde\xf3\xf0\x71\xe2\x38\x66\x3c\x1e\
-\x33\x9e\x4c\xb6\xd7\xca\x2d\xea\xa6\xa6\x14\x9e\xbc\xd1\xf1\xae\
-\x54\xc6\x3a\xc7\x64\x62\x48\x53\x83\x56\x92\x56\x23\xc1\x7b\x47\
-\xbf\xdf\x3f\xe7\x9c\xfb\x9d\x16\x42\x4c\x86\xc3\xe1\xb3\xce\xb9\
-\xbb\xab\x71\x44\x23\xa9\xb2\xd5\x1f\x20\x95\x44\xe5\xa9\xac\xb4\
-\x4e\xae\x84\xf4\x1e\xe7\x04\xc8\xa2\x9b\xf2\x79\xc5\x9e\x90\xd4\
-\x6a\xbc\xef\xbd\xef\xe1\xfd\x8f\xbe\x97\xd9\x4e\x9b\xf1\x64\xc2\
-\x60\x38\xdc\x69\xe5\xc2\x03\x85\xa0\x53\xe3\x15\xef\xb7\x47\x30\
-\x85\x61\x26\xc6\x30\x31\x29\x8d\xa4\x46\xb5\x12\x31\x18\x0c\x18\
-\x0c\x06\xcf\x02\x13\xed\x81\xe1\x60\xf0\xad\xd1\x70\xf8\x44\xb5\
-\x56\x63\xa1\x33\xc3\xc6\x56\xaf\x84\x86\x77\x0e\x2f\x65\xd6\x12\
-\x3a\x8f\x17\x1e\x2f\x98\x12\x28\x0b\x36\xe7\x1c\x0f\xbe\xfb\x01\
-\x3e\x70\xe2\x7d\xec\x7d\xc7\x12\xa9\x31\xf4\x07\x83\x1d\x93\x95\
-\x69\xeb\x17\xc2\x3b\xe7\x4b\x2f\x94\x9e\xc8\x21\x5a\xec\x51\xcc\
-\x56\x17\xe6\x66\x00\xe8\x6d\x6d\x31\x1c\x0e\xbf\x25\x84\x40\x8b\
-\x0c\x67\x27\xbb\xdd\xee\x2f\xab\xb5\xda\xb1\xce\x4c\x83\xda\xb5\
-\x0a\xe3\x49\x8a\x94\x51\xd6\xe3\x7a\x8f\xf3\x02\x49\x66\x2d\x27\
-\x01\x1c\x38\x49\x9a\x1a\x9a\xcd\x06\xff\xf0\x89\xbf\xe7\xde\x23\
-\x87\x11\x42\xe4\x83\xde\x2c\x75\x6e\xa3\xcb\x97\x29\xda\xb1\x6d\
-\xf5\x42\x19\x57\x18\xa8\x50\x24\x6f\xfc\x5d\x3e\x86\xac\xc5\x15\
-\x66\x5b\x4d\x8c\x31\x6c\x6c\x6c\xfc\xd2\x5a\x7b\x32\xaf\xc6\x12\
-\xa5\x94\xd9\xea\xf5\xbe\x32\x1e\x8d\x50\x52\xb2\xb4\x6b\x1e\x63\
-\xb2\x0c\xe4\x8b\xe1\x6c\x39\x45\x28\x1a\xf1\x0c\x3e\x93\xd4\x30\
-\xdb\x99\xe3\xf0\xa1\x83\x8c\xf3\x1b\x18\xc6\x5a\x6c\x9e\x5d\x6c\
-\xfe\x36\xd6\x62\xf3\xe6\x7d\xfa\x7a\x47\x21\xbc\xdb\x69\xfd\x7c\
-\x5f\x6b\x2d\xd6\x58\x96\x76\xcd\x21\xa5\xa0\xdb\xed\xd2\xeb\xf5\
-\xbe\xa2\xb5\x36\x4a\xa9\xac\x5a\x09\x21\x48\x27\x93\xa7\x37\x36\
-\x36\x7e\x0d\x30\xd7\x6e\xd2\x69\xd5\x99\x4c\x26\xdb\x56\x2b\xb0\
-\xee\x0a\x37\xe7\x96\xc4\x61\xad\x61\x34\x9a\xe4\x99\x25\x0f\x50\
-\x57\x64\x19\x97\x1f\x67\x6b\x58\x9b\x57\x69\x9f\x07\xb0\x9b\xfe\
-\x4e\x39\x62\x29\xfe\x4b\xd3\x94\x4e\xab\xce\x5c\xbb\x85\x31\x86\
-\xd5\x95\x95\x5f\xa7\x69\xfa\x74\xc1\x02\x64\x81\x2f\xef\xfd\x78\
-\x7d\x63\xe3\x33\x83\x7e\x1f\x80\x7d\x4b\x8b\x44\x61\x90\xe3\x3b\
-\x0f\x3c\xe7\xb0\xb9\x12\xce\x39\x9c\xcd\x04\xb2\xde\xe3\x5c\x4e\
-\x13\xac\x2b\xc7\x25\xd6\xfa\x6d\x85\x0a\xe5\xbc\x2b\x95\xcb\x84\
-\xcf\xe3\xc1\x91\xad\xe3\x7d\xae\x88\xc7\x18\x43\x18\x68\xf6\x2d\
-\x2d\x02\xb0\xb6\xba\xca\xc6\xc6\xc6\x67\xbc\xf7\xe3\x42\x6e\x39\
-\x3d\x35\x36\x69\xfa\x93\xe5\xe5\xe5\xaf\x1b\x63\xa8\x44\x21\xfb\
-\x97\x16\xb3\x20\x35\x26\x73\x73\xbe\x81\xdd\x11\x70\xb9\xa5\x73\
-\x05\x9d\xdf\xe6\x40\x99\x55\x6f\x3c\x9e\x0e\xdc\xdc\x78\xce\x63\
-\xbd\xcb\x3d\xe1\x4b\xe1\xbd\x77\xec\x7f\xc7\x1e\x2a\xf9\xe4\xe3\
-\xfa\xf5\xeb\x5f\x37\xc6\xfc\x64\x9a\x73\xc9\x69\x96\x28\xa5\xa4\
-\xdf\xef\x7f\x76\x75\x75\xf5\x57\xde\x7b\x66\x9a\x75\xf6\x2d\x2d\
-\x62\xad\xc9\x3c\xb1\x83\xa4\xb9\xac\x3a\xe6\xde\x28\x36\x2f\x7e\
-\xb3\x39\x8c\xec\xd4\x79\xa5\xe5\xfd\xb6\x07\x4a\x83\xdc\x20\xbc\
-\xb5\x86\x7d\x4b\x8b\xb4\x9b\x19\x94\xaf\x5e\xb9\xf2\xab\xc1\x60\
-\xf0\x59\x59\x4e\x00\xb3\xb7\xbe\xc5\xa4\xa0\xb7\xba\xb2\xf2\xf1\
-\x40\xeb\xe7\xdb\x9d\xce\x9e\x85\xce\x0c\x02\x38\x7b\xf1\x0a\x69\
-\x6a\xd1\x5a\x21\xa5\xc7\x17\x6c\x31\xef\x6b\xbd\x00\x21\xfc\x8e\
-\x89\x5c\x59\x9e\xf3\xa9\x5c\x79\x3c\x15\xa4\xd3\x23\x46\xef\xc1\
-\xa4\x06\x97\x5b\x7e\xa1\x33\x83\xb5\x96\xab\x57\xaf\x5e\x5a\x5b\
-\x5b\xfb\x38\xd0\xbb\x91\x01\xdf\xee\x06\xc7\xe9\x95\x95\x95\x8f\
-\x08\x29\xbf\x3f\x33\x33\xd3\x9e\xef\xcc\x10\x68\xcd\xd9\x0b\x57\
-\x49\x27\x93\xbc\x31\x29\x64\xf4\x58\x3f\xc5\xf7\xa7\x27\xeb\xfe\
-\xd6\x77\x67\xfc\x34\xed\xce\x95\x73\x36\x83\x6a\xa8\x03\xee\x5a\
-\xda\x43\xbb\x59\xcf\x84\xbf\x72\x65\x6d\x75\x65\xe5\x23\xde\xfb\
-\xd3\x6f\xfb\x0e\x8d\x10\x02\xe7\xfd\xc9\xe5\xe5\xe5\x0f\x7a\xe7\
-\x9e\x9a\x69\xb7\x17\x67\x9a\x75\xee\xa9\x44\x9c\xbf\x74\x95\x8d\
-\x6e\x0f\x97\x93\xb1\xac\xb1\x01\xe1\xa7\x67\xa2\x37\x74\x2c\x85\
-\xf0\x3b\x7a\x86\xa9\x19\xa9\xc9\x62\x64\xa6\x91\xb0\x77\xcf\x2e\
-\x2a\x51\x36\x5f\xba\x76\xf5\xea\xe5\x95\x95\x95\xc7\xbd\xf7\x27\
-\x8b\x19\xee\x4d\xb2\xbe\xfa\xca\x2b\xb7\xbe\xc9\x57\xd0\x58\x6b\
-\x0f\xb5\xdb\xed\x6f\x74\x66\x67\x1f\x2a\xa8\xed\xca\xfa\x26\x57\
-\xae\xaf\x32\x18\x8d\xb3\xd8\x51\x59\x2b\x9a\x37\x61\x94\x2d\xd3\
-\xb4\x27\xc4\x0d\x5e\x28\x62\xc7\x7b\xaa\x95\x88\xdd\xf3\x1d\x66\
-\x67\xb2\x89\xf4\x70\x30\xe0\xea\xb5\x6b\x2f\xad\xaf\xad\x7d\x42\
-\x08\x71\xaa\x20\x75\xb7\x52\xe0\x8f\xde\x27\xce\x2d\x7a\x6a\x63\
-\x73\xf3\xc4\x78\x3c\xfe\x62\x67\x76\xf6\x89\x5a\xad\x26\x66\x67\
-\x9a\xb4\x9b\x75\xd6\x36\xb7\x58\x59\xdf\xa4\x37\x18\x62\x4c\x0a\
-\x62\x7b\xde\x93\x6b\x32\x1d\x04\x25\xe6\xf1\x1e\xa5\x24\x8d\xa4\
-\xca\x6c\xab\x49\xbb\x55\x2f\x1f\x67\xd8\xd8\xd8\xf0\xd7\xaf\x5d\
-\xfb\xda\x78\x3c\xfe\x5c\x81\xf9\xdb\x4e\xf4\xde\xca\x03\x45\x17\
-\xa5\x94\xc2\x3a\x87\x14\xe2\x03\x8d\x66\xf3\x0b\xad\x56\xeb\xc1\
-\x4a\xa5\x52\x9e\x3f\x1c\x8d\xe9\xf6\x07\xf4\x06\x43\x86\xa3\x31\
-\xa9\xb1\xe5\xbd\xb0\x7c\x00\x0b\x42\x10\x68\x45\x5c\x89\x48\xaa\
-\x31\xf5\x5a\x95\x6a\x25\x2a\x3d\xd2\xeb\xf5\x58\x5b\x5d\xfd\x45\
-\xb7\xdb\xfd\xbc\xb5\xf6\xc7\x52\xca\x9c\x4a\x67\x6c\xf8\x76\x1e\
-\x78\xdb\x0a\x14\x29\x54\x08\xa1\x83\x20\xf8\x68\xb5\x56\xfb\x54\
-\xbd\x5e\x3f\x5e\xa9\x54\x50\x6a\xbb\xf5\x9b\x9e\xfb\x1b\x93\x75\
-\x73\xd9\x58\x32\x4b\xd3\xd3\x93\xb8\x34\x4d\x19\x0c\x06\x74\x37\
-\x37\x5f\xdc\xea\xf5\xbe\x6a\xd2\xf4\xdb\xc5\x33\x13\x42\x88\xbf\
-\xbc\x02\xe4\x4f\xab\x90\xf1\x74\x2d\x84\x78\x24\xae\x54\x1e\x8f\
-\xe3\xf8\x44\x25\x8e\x0f\x84\x61\xa8\xb4\xd6\x59\x8b\x79\x8b\xdb\
-\xac\xb6\xbc\xd1\x3d\xb6\xc3\xc1\xe0\xcc\x60\x30\x78\x7e\x38\x1c\
-\x3e\xe5\xbd\xff\xb9\x10\xc2\x14\xfd\x44\x31\x30\x78\x3b\x0a\xfc\
-\x59\x4f\xab\xe4\x29\xd3\xe0\xfd\x4f\x87\xa3\xd1\x4f\xfb\xfd\x7e\
-\xa8\xb4\x3e\x2c\x85\x38\x1a\x86\xe1\x11\x84\xd8\xa7\xa4\x6c\x08\
-\x29\x6b\x79\x22\xe8\x5b\x6b\xbb\x2e\x7f\xdc\xc6\x3b\x57\x3e\x6e\
-\x33\x7d\x7f\xf9\x8f\x61\xfd\x76\xaf\xff\x1f\x00\x54\x46\xd5\x89\
-\x5c\xa3\x2a\xa1\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\
-\x00\x00\x11\x6f\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
-\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
-\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
-\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
-\x46\x00\x00\x10\xf5\x49\x44\x41\x54\x78\xda\xac\x9a\x59\xac\x24\
-\x57\x79\xc7\x7f\x67\xa9\xaa\xae\xde\x97\x3b\xf7\xde\xb9\xe3\x8b\
-\xc7\xe3\xc1\x1b\x01\x0c\x31\x38\x7e\x70\x40\x1e\x8c\x95\x08\x44\
-\x64\x19\x41\x78\xe1\x35\x58\x42\x48\x16\x20\x2c\xa1\x28\x91\x90\
-\x82\x1c\x84\x84\x2c\x41\x78\xe4\x85\x80\xb0\x2c\x10\x0e\x24\x78\
-\xc1\x9b\x32\xd8\xc4\x06\xe2\x01\x3c\xf6\x78\xc6\x13\xcf\x7a\xd7\
-\xbe\xbd\x77\xd5\x59\xf2\x50\x55\x7d\xfb\x8e\xc7\xd8\x20\x4a\x3a\
-\xea\xaa\xee\xea\x3a\xdf\xff\xdb\xce\xf7\xfd\x4f\x89\x5f\x3e\xfb\
-\x2c\x97\x1e\x42\x08\x94\x52\x78\xef\x71\xce\x21\xa5\xc4\x39\x87\
-\xf7\x1e\x29\x25\x42\x08\xac\xb5\x78\xef\x01\x70\xce\x85\x4a\xa9\
-\xeb\x81\x77\x87\x61\x78\x83\x10\xe2\x90\x52\xaa\x2e\x84\xa8\xe4\
-\xbf\x0f\x8d\x31\x3d\xef\xfd\xc9\xe9\x74\xfa\x3b\xe0\x37\xc6\x98\
-\xdf\x4b\x29\x93\xe2\x19\x52\xca\xd9\x7c\x85\x0c\xd6\x5a\x84\x10\
-\xc5\x1c\x5c\xee\xd0\xfc\x09\x87\xf7\x1e\xef\xbd\x06\x6e\x2d\x95\
-\x4a\x77\xc5\xe5\xf2\x91\x72\xb9\x7c\x38\x2e\x95\x54\x18\x45\x04\
-\x41\x80\x92\x12\x21\x8b\xc9\x3d\xd6\x5a\xd2\x24\x61\x32\x9d\x32\
-\x1e\x8d\xec\x70\x38\x3c\x31\x1a\x8d\x1e\x9d\x4c\x26\x0f\x78\xef\
-\x9f\xf2\xde\x9b\x3f\x45\x16\xfd\xc7\x0a\xee\x9c\xd3\x5a\xeb\x4f\
-\x54\xab\xd5\xcf\x36\x9a\xcd\x9b\x6b\xb5\x1a\xe5\x72\x79\xcf\x7d\
-\xd3\x24\x65\x3c\x4d\x98\x4c\x13\x00\x4a\x51\x48\x18\x04\x54\x6b\
-\x35\xaa\xb5\x1a\x80\xf2\xce\x5d\xdb\x1f\x0c\xae\xed\xed\xec\xdc\
-\xdd\xed\x76\x9f\x19\x0e\x87\xf7\x1b\x63\xbe\xef\xbd\x37\x85\xd6\
-\xff\xac\x00\xbc\xf7\x28\xa5\x3e\x5c\xab\xd5\xbe\xd2\xe9\x74\xde\
-\xd7\x68\x36\x50\x2a\xfb\xfb\xc6\x76\x97\x53\xaf\x9d\xe3\xf4\x6b\
-\xe7\x58\xdb\xdc\x62\xa7\x3f\x20\x49\x53\xac\xcd\xcc\xae\xa4\x24\
-\x0c\x03\x1a\xb5\x2a\x8b\x0b\x6d\x0e\x5e\xb1\xc2\xc1\xd5\x15\x16\
-\x5a\x4d\xea\xf5\x3a\xfb\x16\x17\x6f\xde\xda\xda\xba\x79\x63\x7d\
-\xfd\x73\xfd\x7e\xff\xcb\xce\xb9\x9f\xbd\x55\x10\xe2\xcd\x62\xc0\
-\x5a\x8b\x94\xb2\x1a\x86\xe1\x57\x17\x17\x17\xef\x6e\x77\x3a\x42\
-\x6b\x8d\x73\x9e\x63\xc7\x4f\xf0\xdc\xb1\xdf\x73\xea\xb5\xb3\x0c\
-\x47\x63\x40\xa0\xb5\x22\x50\x0a\xa5\xd4\x1e\xff\x75\xce\x91\x5a\
-\x8b\x31\x16\xf0\x54\xcb\x31\x07\xaf\x38\xc0\x4d\xef\xba\x81\x77\
-\x5c\x7b\x35\x52\x08\xc6\xe3\x31\x6b\x6b\x6b\x7e\xed\xe2\xc5\x6f\
-\x26\x49\xf2\x25\xe7\xdc\xe0\xcd\x62\xe0\x0f\x02\xc8\x27\xbe\xae\
-\xd9\x6a\x7d\x67\xff\xf2\xf2\xfb\x2b\xd5\x2a\x00\x2f\x1c\x7f\x85\
-\x27\x9e\x79\x8e\xd7\xce\x5e\x00\x01\x71\x14\x11\x97\x4a\x44\xb9\
-\xab\x28\x25\xb3\x18\x28\x26\xcf\x83\xd3\x58\x4b\x9a\xa6\x4c\xa7\
-\x29\xa3\xc9\x84\xf1\x64\x0a\xc0\xea\xca\x12\x1f\xfc\xab\x9b\x78\
-\xe7\xb5\x57\x03\xb0\xb9\xb1\xc1\xd9\x73\xe7\x9e\xdd\xe9\x76\x3f\
-\x0d\xbc\x28\x84\xf8\xe3\x01\x48\x29\x01\x6e\xe9\x74\x3a\x0f\x2c\
-\xef\xdf\xbf\x12\x86\x21\xdd\xfe\x80\x9f\x3c\xf6\x34\x2f\x1c\x7f\
-\x05\x29\x04\xd5\x4a\x99\x6a\xa5\x4c\xb9\x54\x42\x07\x1a\xf9\x26\
-\x66\x2f\x32\x8e\xf7\x9e\xd4\x18\xc6\x93\x09\xfd\xe1\x88\xc1\x60\
-\x84\xc7\xf3\x17\xd7\x5c\xcd\x47\x8e\xdc\x4a\xa3\x56\xa5\xdf\xef\
-\x73\xf6\xcc\x99\x73\x9b\x9b\x9b\x77\x39\xe7\x8e\x16\xff\x7d\x4b\
-\x31\xe0\xbd\x47\x08\x71\xcb\xbe\x7d\xfb\x1e\x5a\xde\xbf\xbf\xad\
-\x94\xe2\xc4\xab\x67\xf8\xd1\x23\x4f\xb0\xb9\xbd\x43\xbd\x56\xa5\
-\x5e\xab\x52\x2d\xc7\x68\x25\x01\x31\x03\x5e\x68\xfd\x52\x1f\xce\
-\x33\xd7\xec\x5c\x6b\x4d\x29\x8a\xa8\x57\xab\x0c\x6a\x63\x76\xfa\
-\x03\x8e\xbd\x74\x92\xf3\xeb\x9b\xfc\xdd\xed\x1f\xe0\xf0\xc1\x55\
-\xae\xbc\xf2\xca\x15\x21\xe5\x43\xeb\x6b\x6b\x1f\x71\xce\x1d\xbd\
-\x5c\x5c\xbc\x91\x05\xae\x5d\x5a\x5a\x7a\x74\xff\xca\xca\x01\xa5\
-\x14\xff\xfb\xe2\x09\x7e\xfc\xc8\x93\x58\xef\x69\xd4\xaa\x34\x6b\
-\x35\xc2\x50\x03\x12\x29\x33\xa1\xa5\x10\x08\x29\x10\xbc\x1e\x80\
-\xcf\xa4\x9e\x7d\x3a\xef\xf1\xde\xe1\x9c\x9f\x01\x4b\x92\x94\x6e\
-\xbf\xcf\x4e\x6f\x80\x94\x82\x8f\xdd\xfe\x01\xde\x75\xdd\x61\x46\
-\xa3\x11\xa7\x5f\x7d\xf5\xec\xfa\xfa\xfa\x11\xe0\xf8\xeb\x2c\x70\
-\x19\x54\xd5\x76\xa7\xf3\xdd\xa5\xe5\xe5\x03\x4a\x29\x8e\xbd\xf4\
-\x0a\x3f\x7e\xf4\x29\xa4\xd6\x2c\xd4\x6b\xd4\x6b\x55\x94\x52\x48\
-\x29\x90\x42\x22\xa4\xcc\x84\x17\x05\x90\x99\x16\xe6\xd5\x3f\xfb\
-\xf0\x78\xbc\x07\xe7\x1d\xde\x65\x60\xac\xb5\x28\xa5\x88\xc2\x80\
-\x52\x18\xb2\xdd\xeb\xf3\xa3\x47\x9e\x44\x00\xef\xbc\xee\x30\xab\
-\xab\xab\x07\x8c\x31\xdf\xdd\xde\xde\xfe\x00\x30\x78\x43\x17\xf2\
-\xde\x53\xad\x56\xef\x5b\x5e\x5e\x7e\x6f\x10\x04\x9c\x3a\x73\x8e\
-\x9f\x3e\x7e\x94\x30\x08\x68\xb7\x1a\xd4\xab\x55\x94\x92\x48\x91\
-\x69\xbe\x58\x95\xe7\x01\xec\x2a\x64\x5e\x31\x7e\xce\x8d\xb2\x6b\
-\xe7\x1c\x1e\xf0\xce\x63\x95\xc2\x79\x87\xb3\x8a\x76\xab\x89\xd2\
-\x9a\xed\xee\x0e\x3f\x79\xe2\x28\xd5\x4a\x99\xab\x56\x57\x58\x59\
-\x59\x79\xef\x64\x32\xb9\x6f\x34\x1a\xdd\x3d\xaf\x74\x29\xa5\x9c\
-\x09\x12\x04\xc1\x6d\x8b\x4b\x4b\x9f\x89\xe3\x98\xde\x60\xc8\x7f\
-\x3d\xf9\x0c\x42\x48\x16\xda\x2d\xda\xcd\x06\x61\x10\x10\xe8\x80\
-\x20\xd0\x04\x7a\x7e\x04\xb3\x73\xad\x55\x7e\xae\xb2\xfb\x2e\xb9\
-\x37\x0c\x34\x5a\x6b\x82\x20\xd8\xbd\x3f\xd0\x84\xf9\x77\x61\x18\
-\xd0\x6e\x36\xe8\xb4\x5b\x08\x21\xf8\xcf\x27\x7f\x41\x6f\x30\xa2\
-\xdd\xe9\xb0\xb8\xb8\xf8\x19\xa5\xd4\x6d\xf3\x01\xad\xf3\x6c\x83\
-\xf7\x3e\x6a\x36\x9b\x5f\x6b\x36\x9b\x00\x3c\xfe\xcc\xaf\x18\x8c\
-\xc6\x74\x5a\x4d\xda\xcd\x7a\xae\xf9\x4c\xeb\x59\x99\x30\xe7\xff\
-\x08\x44\xae\x04\x21\x98\x8b\x83\x3d\x1e\xb4\xeb\x3e\x79\x5d\xe5\
-\xf3\x78\xc8\xae\x1d\xd2\x65\xe7\x4a\x7a\xda\xcd\x46\x96\x52\xb7\
-\x77\xf8\xf9\xd1\xe7\xf8\xd8\xed\xb7\xb2\xb8\xb4\x44\xaf\xd7\xfb\
-\xda\xf6\xf6\xf6\x2d\x52\xca\xe9\x2c\x06\xbc\xf7\x04\x41\x70\x67\
-\xa7\xd3\x79\x8f\x94\x92\x13\xa7\xcf\x70\xea\xb5\xf3\x34\xeb\x75\
-\xda\xcd\x1a\x61\xa0\xb3\xb5\x41\x4a\xe4\xcc\x85\x76\xdd\x28\x03\
-\x96\x07\xb0\x00\x21\x15\x48\x89\x50\x2a\x13\xdc\x5a\x70\x16\xef\
-\x5c\x16\x07\x73\x82\xbb\x5c\x68\x57\x00\xb0\x16\x2b\x3d\x4a\x0a\
-\xda\xcd\x3a\xce\x39\x4e\x9e\x39\xc7\xcb\xa7\xcf\xf0\xf6\x2b\xaf\
-\x60\xdf\xbe\x7d\xef\x19\x0c\x06\x77\x1a\x63\xfe\x5d\x08\x81\xce\
-\x1f\xa6\x6b\xb5\xda\x3d\x95\x6a\x15\x6b\x1d\xcf\xff\xf6\x65\xa2\
-\x28\xa4\x59\xaf\x52\x8a\xa2\xd9\xc2\xa6\x84\xc8\x00\xe4\x56\x28\
-\x84\x17\x79\x40\xa3\x15\xa2\x14\x43\x32\x45\xec\x74\xa1\xb7\x93\
-\x79\x7f\xbd\x81\x68\x34\x21\x8c\xf0\x93\x31\xce\x98\x59\x00\x17\
-\xab\xb4\xcd\x81\x58\x29\x90\xce\x61\xad\x20\x96\x92\x66\xbd\x46\
-\x6a\x2c\xcf\x1d\x3b\xce\xa1\xd5\x15\x5a\xed\x36\x1b\x1b\x1b\xf7\
-\x6c\x6e\x6e\xfe\x40\x4a\x69\x34\x80\x52\xea\x96\x46\xb3\x79\x93\
-\x10\x82\x57\xcf\x9e\x63\x6b\xa7\x47\xbd\x9a\x2d\x52\x4a\x29\x54\
-\x2e\xb0\x52\x32\xcf\x40\x7b\xb5\x8f\x10\x10\xc7\x88\xed\x6d\xd4\
-\x43\x0f\x22\xff\xfb\x49\xc4\xab\x27\x31\x93\x09\xa9\x80\xb4\x54\
-\xc2\x1d\x3c\x84\xb8\xe5\xaf\x89\x8e\xfc\x0d\xba\xd5\xc2\x8f\xc7\
-\x39\x88\x2c\x1b\xd9\x02\x88\x15\x18\xeb\x90\xc2\x61\x9d\xa3\x5a\
-\x89\x99\x26\x29\x5b\x3b\x3d\x4e\xbd\x76\x9e\xc3\x57\x1e\xa0\xd9\
-\x6c\xde\xd4\xed\x76\x6f\xf1\xde\x3f\xa5\x85\x10\x94\xe3\xf8\x93\
-\x95\x4a\x05\x80\x13\xa7\xcf\x52\x0a\x43\xaa\xe5\x32\x61\xa0\x91\
-\x22\x13\x5c\xeb\x02\x48\x06\xa0\x28\x95\x11\x02\x1f\x97\xd1\x4f\
-\x3d\x46\xf0\x6f\xdf\x40\x9e\x7c\x19\x2f\x15\x3e\x08\xd0\x52\xe0\
-\x00\x3b\x1e\x61\xd6\x2e\x32\xfd\xc5\xd3\x0c\x7e\xf8\x03\x6a\xff\
-\xf0\x39\x4a\xb7\xde\x06\xe3\x11\xca\x0b\x90\x20\xbd\xc3\x59\x97\
-\x2b\xc7\x62\xac\x43\x58\x8b\x94\x82\x5a\xa5\x4c\x92\xa6\x9c\x38\
-\x7d\x86\xc3\x57\x1e\xa0\xd1\x68\x10\xc7\xf1\x27\x47\xa3\xd1\x53\
-\x1a\x08\x2b\x95\xca\xed\x5a\x6b\x76\x06\x43\x36\xbb\x3d\xae\x58\
-\x59\xca\x5c\x44\x08\xb4\x52\x33\xe1\x0b\xf7\xd9\x73\xc4\x65\x82\
-\x07\xbf\x47\x78\xff\xbf\x42\x9a\x42\xa5\xba\x27\x89\x2a\x40\x09\
-\x90\x5a\xa3\x80\xf4\xff\x4e\xb1\xf1\x8f\x5f\xa0\xf1\xd9\x2f\x50\
-\xbf\xf3\x93\xb8\xd1\x10\x04\xb3\xb8\x12\xd6\xe6\x31\xe5\x48\x85\
-\xc0\x5a\x43\xad\x5a\xa6\x5c\x8e\xb9\xb8\xbe\x49\x6f\x30\xa4\x5a\
-\xa9\x50\xa9\x54\x6e\x1f\x8f\xc7\xa1\x94\x4a\xdd\x10\x97\xcb\x57\
-\x01\x5c\xdc\xd8\x42\x0a\xc1\x42\xbb\x49\xa3\x5a\x41\xab\x2c\xc5\
-\x65\x20\xf4\x65\x84\x8f\x51\x4f\x3e\x46\x78\xff\x7d\xe0\x1c\xa2\
-\x54\x42\x5c\xb2\x02\x88\x4b\x86\x8c\x4a\xe0\x1c\x1b\xf7\xdf\xc7\
-\xe0\xc9\x47\x91\x71\xbc\xe7\x91\x4a\x29\x94\xce\xe6\x0c\x03\x8d\
-\x52\x8a\x7a\xb5\xca\xbe\x76\x13\x29\x04\x17\x36\xb6\x90\x4a\x51\
-\xa9\x54\xae\x92\x52\xde\x20\xb5\xd6\x37\x46\x51\xa4\x8b\x94\x15\
-\xc7\x25\x02\xad\xa9\x56\x62\xa2\x28\x40\x2b\x89\xd2\x6a\xaf\x54\
-\xd9\x4c\xc8\xee\x36\xc1\xb7\xbf\x01\xa9\x81\x20\x78\xcb\x4d\x88\
-\x08\x02\xbc\x49\x59\xfb\xf6\x37\x70\xdd\xed\x59\xb6\xda\x5d\x9c\
-\xb2\xb2\x5c\x29\x49\x29\x0c\xa9\x55\xca\x68\xad\x29\xc7\x31\x1b\
-\x5b\x3b\xb9\xe1\xcb\x5a\x29\x75\xa3\x8c\xc2\xf0\xba\x30\x08\x70\
-\xce\x33\x18\x4d\xa8\x94\x63\x54\x1e\xa0\x95\x72\x9c\xa5\xc4\xcb\
-\x55\xab\x71\x8c\x7a\xe4\xa7\x70\xf2\x65\x7c\xa9\x34\x5b\x6f\xfd\
-\x6c\xdd\xdd\x5d\x83\xdd\x25\xbf\x79\x40\x44\x25\xc6\x27\x5f\x66\
-\xfb\xe1\x9f\xa2\xe3\xf8\x75\xfa\x21\xcf\x7c\xd5\x4a\x19\x29\x05\
-\x4a\x49\x2a\xe5\x12\x83\xe1\x18\xef\x3d\x51\x14\x11\x86\xe1\x75\
-\x52\x48\x79\x48\x69\x85\xb1\x16\x63\x2d\xa5\x28\x9c\x95\x03\x51\
-\x18\x12\x85\x7a\x56\x45\x0a\xc8\xcb\x06\x49\x3a\x18\x62\x9f\x7e\
-\x02\x94\x7a\x9d\x70\xf3\x82\xdb\xfc\xd3\x5d\x06\x08\x52\xb1\xf5\
-\xf4\xe3\x4c\x07\xc3\x59\x4d\x35\x5f\xd6\x44\xf9\xca\x0c\x20\x11\
-\x44\x51\x44\x6a\x2d\xd6\x3a\xc2\x20\x40\x08\x71\x48\x6a\xad\xeb\
-\x20\xf2\x15\x50\x12\x68\x95\x6b\x23\x13\xa5\x1c\x45\x04\x3a\x7f\
-\x88\x10\x38\xef\x99\xa4\x29\xd3\xf5\x35\xdc\xe9\x93\xb8\xdc\x75\
-\xfc\x65\x86\x05\x8c\x98\x03\xe1\x77\x7f\x73\x00\x41\xc0\xf8\xf4\
-\x49\x86\xeb\x6b\x4c\x52\x83\xf3\x1e\x95\x83\x08\xb4\x26\x2e\x45\
-\x7b\x82\x29\xd4\x59\xcf\x61\x9c\x45\x4a\x49\x10\x04\x75\x29\xa5\
-\x2c\x03\xa4\xc6\xe6\x35\x91\xdc\x5b\x8c\x09\x91\xc5\x43\xa0\x49\
-\x8c\x61\x3c\x99\x62\xbc\x47\xf4\x7b\x98\x64\x42\x22\x24\xe9\x9c\
-\x76\x0b\xad\xa7\x40\x2a\xc0\x14\x40\xe6\xac\x61\xf1\x19\x20\x21\
-\x71\xd3\x09\xae\xd7\xc3\x7a\xcf\x78\x32\x25\x31\x86\x30\xd0\x54\
-\xcb\xaf\x77\xab\xc2\x95\xd2\xd4\xe6\xe9\x56\x96\xf7\xa4\x15\x3d\
-\xd7\xc7\xce\x7b\xb1\x94\x82\x72\x5c\xca\xb9\x1a\x97\x3d\x58\x40\
-\x02\x24\x22\x1b\x53\xb2\x91\x00\xd3\xfc\xbb\x24\x17\x3c\x05\x8c\
-\xcf\x00\xcc\x40\xe4\xd7\x00\x85\xce\xac\x75\x08\x21\x28\x97\x4a\
-\x08\x29\xf0\x97\x89\x8b\x4c\xc6\x5d\x57\xd5\xde\xfb\x11\x40\x18\
-\x64\x29\xf3\xd2\x12\x18\xc0\x1a\x8b\x73\x8e\x56\xbd\x46\x14\x04\
-\xf4\xa7\x09\xae\x56\xc7\x44\x25\xfc\x64\x8c\x45\x22\xc5\x6e\xa2\
-\xb2\x73\x2e\x64\x81\x74\x4e\x78\x8b\x9f\x81\xf0\xde\x21\xa3\x12\
-\xb2\x56\x47\x02\xb5\x7a\x85\x72\x54\x22\xb5\x16\xe5\xb3\x32\xbd\
-\x10\xa3\x28\x08\x8b\x2a\x36\x2f\x06\x47\xda\x18\xd3\xcb\xa8\x0f\
-\x85\x92\x6a\xd6\x25\x81\x9c\x35\xe4\xa9\xc9\x16\x17\xe1\x3d\xd5\
-\x4a\x99\x38\x2e\xd1\x8f\x42\xb6\xde\x76\x15\xe6\xf9\x35\x94\xd2\
-\xc8\xcb\x64\x1e\x9b\x6b\xda\xe5\x96\x30\xf8\xec\x33\x07\xe4\x92\
-\x94\xf8\xca\xab\xa8\x2d\x2d\x53\x8d\x4b\x68\x29\x30\xc6\xe1\x9d\
-\x23\xb5\xa0\xb5\xda\xf3\xd4\xac\x15\xcd\x18\x0f\x6b\x52\x8c\x31\
-\x3d\x89\xf7\x27\xad\xb5\x48\x25\x09\x02\x8d\xb5\x36\x0b\x36\xef\
-\xf1\xce\x93\xa4\x66\xae\x97\xcd\xbe\x0f\x94\x62\x71\xff\x32\xed\
-\x0f\xdd\xc1\x24\xb5\x24\x1e\xa6\x1e\x92\x4b\x46\xea\xe7\x5c\x28\
-\x17\x3e\xcd\xc1\x38\xc0\x5a\xcb\x15\x1f\xba\x83\xc5\xe5\x25\x02\
-\x25\x71\x6e\x96\x64\x33\xc5\xa5\xe9\x1c\x7d\x99\x75\x6e\x05\xeb\
-\x61\x8c\xc1\x7b\x7f\x52\x26\x49\xf2\xa2\x31\x06\x29\x04\x95\x38\
-\xc2\x58\x8b\x75\x99\x9f\x67\x29\xcb\x82\x67\xd6\x49\x65\xd5\xab\
-\xc3\x8e\xc7\x2c\x7c\xf4\x4e\xc2\x6b\xae\x65\x32\x19\x93\xe6\xfe\
-\x9f\x14\x01\x3c\x1b\x3e\x1f\x99\xe6\x4d\x6e\x11\x3b\x19\x53\xbb\
-\xe6\x5a\x56\x3f\x7a\x27\x76\x32\xce\x2d\x9f\x2b\x2e\xcf\x55\xc6\
-\x3a\x52\x93\x45\x8a\x75\x1e\x6b\x1d\x95\x38\x42\x00\xc9\x74\x4a\
-\x9a\xa6\x2f\x4a\x63\xed\xaf\x93\x24\x31\x00\xf5\x6a\x65\x46\x66\
-\x39\xef\x31\xc6\xec\x36\x22\xf8\x59\x4b\xe8\x3d\x38\x93\x22\x9b\
-\x6d\xae\xb8\xe7\x5e\x6c\x10\x30\x4d\x93\x99\xc0\xc9\x4c\x68\xbf\
-\x0b\xc4\x33\xcb\x56\x2e\x4d\x11\x41\xc0\xf5\xf7\xdc\x8b\x6e\xb6\
-\xb1\x69\x3a\x27\xfc\xde\x05\xc5\x18\x33\xeb\x9b\x9d\xf3\xd4\xab\
-\x59\xd1\x39\x99\x4c\x8c\xb5\xf6\xd7\xd2\x59\xfb\xbb\xf1\x78\x7c\
-\x0a\xa0\x59\xab\xa2\x95\x22\x4d\x0d\x49\x6a\xb0\x73\x0d\x88\xf7\
-\x7e\x16\x78\x45\x9c\x98\xe1\x80\xe6\x6d\x77\x70\xf0\xde\x7f\x22\
-\x91\x82\xf1\x64\x34\xb3\xc2\x6c\xcc\xb9\x52\xa1\x79\x2f\x05\xef\
-\xb8\xf7\x9f\x59\xbc\xed\x0e\xd2\xd1\x20\x9f\xc3\xe1\xc9\x7b\x04\
-\xef\x66\x60\xac\x73\x24\x89\x21\x4d\x0d\x5a\x49\x9a\xf5\x2a\xde\
-\x3b\x86\xc3\xe1\x29\xe7\xdc\xef\xb4\x10\x22\x19\x8f\xc7\x0f\x3b\
-\xe7\xde\x5e\x8e\x23\xea\xd5\x32\xfd\xe1\x08\xa9\x24\x2a\x4f\x65\
-\x7b\x40\x08\x81\xf4\x1e\xe7\x04\x48\x47\x3a\xe8\xb3\xff\xef\x3f\
-\x4d\xb0\xb8\xcc\x4b\x5f\xff\x17\xfa\xc7\x7f\x0f\x4a\x21\x82\x10\
-\x2f\xb2\xff\x3b\xef\x71\x49\x82\x77\x96\xda\x35\xd7\x73\xfd\x3d\
-\xf7\xb2\x78\xe4\xc3\xa4\x83\xfe\x2c\xd6\x8a\xe7\x7b\xbf\x4b\xc1\
-\x14\xfe\x9f\x18\x43\x62\x52\xea\xd5\x0a\xe5\x52\xc4\x68\x34\x62\
-\x34\x1a\x3d\x0c\x24\xda\x03\xe3\xd1\xe8\x7b\x93\xf1\xf8\xee\x72\
-\xa5\xc2\x52\xa7\x45\xb7\x3f\xc0\x39\x8f\x14\x22\x6b\x03\xa5\xcc\
-\xe2\xc0\x79\xbc\xf0\x78\x51\x00\x12\x38\x67\x49\x7a\x3d\x3a\x1f\
-\xbc\x9d\xf7\xdd\xf8\x97\x9c\xfb\x8f\x1f\x72\xf1\xf1\x47\xe9\xbf\
-\xf2\x12\x76\x3c\x46\x00\x41\x1c\x53\xbd\xfa\x1a\x96\x3e\x78\x84\
-\x95\xbf\xfd\x18\xba\xd5\x26\xe9\xf7\x72\xb6\xbb\xd0\xfa\x2e\x5f\
-\x94\xd1\x2e\xbb\x80\x8a\xae\x6d\x69\x5f\x0b\x80\x41\xbf\xcf\x78\
-\x3c\xfe\x9e\x10\x02\xf1\xdb\x63\xc7\x70\xce\xe9\x85\x85\x85\xa3\
-\xcb\xfb\xf7\xdf\x64\x9d\xe3\x37\x2f\xbe\xc2\x34\x49\x89\xa2\x08\
-\x29\xd8\xd3\x3e\x16\xcd\x8c\x14\x39\x2f\x24\x04\x52\xe6\x05\x9e\
-\x0e\xd0\x71\x8c\x9f\x4e\x49\xbb\xdb\x24\x3b\xdb\xe0\x21\x6c\xb6\
-\x08\x9a\x2d\x44\x14\x61\xc6\x63\xac\x49\x67\x5a\x77\x79\x57\xe6\
-\x3c\xf8\xbc\x37\xde\xed\x93\x3d\xd6\x79\xa6\x49\x42\x29\x0c\xb8\
-\xf1\xba\xc3\x38\x67\x39\xf9\xca\x2b\xff\xb3\xb1\xb1\x71\x8b\x94\
-\xd2\x48\x29\x25\x4a\x29\xd3\x1f\x0c\xbe\x3e\x9d\x4c\x50\x52\xb2\
-\xba\xbc\x88\x31\x59\x06\x9a\xb9\x80\xdf\xcd\x40\xd9\x04\x39\x9b\
-\xe0\x1c\xd6\x66\xd7\x36\x4d\x48\xfa\x3d\x4c\x9a\x22\xeb\x0d\xca\
-\x07\xaf\x26\x3e\x78\x08\x59\x6f\x60\xd2\x94\x69\xaf\x87\x4d\x93\
-\x3d\xff\x77\xec\x0a\xbf\x47\xfb\xf9\xbc\xd6\x5a\xac\xb1\xac\x2e\
-\xef\x43\x4a\x41\xaf\xd7\x63\x30\x18\x7c\x5d\x6b\x6d\x94\x52\xd9\
-\xfa\x23\x84\x20\x4d\x92\x07\xbb\xdd\xee\xaf\x00\xf6\xb5\x1b\x74\
-\x9a\x35\x92\x24\x99\x05\x6c\xa1\x91\x4c\x73\x2e\x0f\x6a\x8f\xc3\
-\xcd\x02\xbb\x18\xd6\x5a\x4c\x92\x90\x4e\xc6\xa4\x93\x09\x26\x49\
-\xf2\x2c\x92\x81\x9d\x69\x3a\xb7\xc0\xee\x35\x33\x8a\xa5\xf8\x2d\
-\x4d\x53\x3a\xcd\x1a\xfb\xda\x4d\x8c\x31\x6c\x6e\x6c\xfc\x2a\x4d\
-\xd3\x07\x8b\x92\x47\x16\xfe\xe5\xbd\x9f\x6e\x77\xbb\x9f\x1f\x0d\
-\x87\x00\x1c\x5a\x5d\x21\x0a\x03\xd2\x34\x9d\x81\xb0\xce\x61\x73\
-\x10\xce\x65\x3d\xac\xb5\x1e\xeb\x3d\xce\x15\x02\xba\x19\x5d\x52\
-\x58\xc6\xe5\x39\xbc\x70\x97\xa2\x81\xcf\x84\x2f\x1a\x7b\xb2\xe7\
-\x78\x9f\x03\xc9\xd2\x78\x18\x68\x0e\xad\xae\x00\xb0\xb5\xb9\x49\
-\xb7\xdb\xfd\xbc\xf7\x7e\x5a\xc8\x2d\xe7\x59\x63\x93\xa6\x8f\xad\
-\xaf\xaf\x7f\xcb\x18\x43\x29\x0a\xb9\x7a\x75\x05\xef\x1d\xa9\x31\
-\xb3\xd5\xd9\x79\x3f\x9b\x28\xb3\x44\xce\x26\xe4\x00\x9d\x2f\x84\
-\xb3\xb9\x56\x2f\x3d\x9f\x0f\xdc\x5c\x79\xce\x63\xbd\xcb\x2d\xe1\
-\x67\xc2\x7b\xef\xb8\xfa\x6d\x07\x28\x45\x21\xc3\xe1\x90\xb5\xb5\
-\xb5\x6f\x19\x63\x1e\x9b\x67\xbb\xe5\x3c\xa7\x29\xa5\x64\x38\x1c\
-\x7e\x71\x73\x73\xf3\x79\xef\x3d\xad\x46\x8d\x43\xab\x2b\x58\x6b\
-\x32\x4b\xf8\xdc\xdc\x73\x5c\x8e\x9d\x8b\x85\x22\xe8\x76\x87\xcb\
-\xc7\xee\xb5\xdb\x03\x72\x4e\x21\x97\x08\x6f\xad\xe1\xd0\xea\x0a\
-\xed\x46\xe6\xca\x17\xce\x9f\x7f\x7e\x34\x1a\x7d\x51\xce\x18\xc0\
-\x6c\xe8\xcb\x6c\x1c\x0c\x36\x37\x36\x3e\x15\x68\xfd\x68\xbb\xd3\
-\x39\xb0\xd4\x69\x21\x80\x93\x67\xce\x93\xa6\x16\xad\x15\x52\x66\
-\x29\xb4\xe0\x84\xa4\x10\x78\x01\x42\x14\x9d\x9b\xd8\xcb\xef\xe6\
-\xac\xf4\xec\x7c\x2e\x48\xe7\x29\x46\xef\xc1\xa4\x06\x97\x6b\x7e\
-\xa9\xd3\xc2\x5a\xcb\x85\x0b\x17\xce\x6e\x6d\x6d\x7d\x0a\x18\x5c\
-\x5a\xee\xbf\xd1\x06\xc7\xf1\x8d\x8d\x8d\x8f\x0b\x29\x1f\x6a\xb5\
-\x5a\xed\xc5\x4e\x8b\x40\x6b\x4e\xbe\x76\x81\x34\x49\xd0\x41\x40\
-\x41\x50\x08\x3c\xd6\xcf\xb1\xd3\xf3\xcc\xba\xbf\xfc\xee\x8c\x9f\
-\x73\x81\x82\x72\x77\x36\x73\xd5\x50\x07\x5c\xb5\x7a\x80\x76\xa3\
-\x96\x09\x7f\xfe\xfc\xd6\xe6\xc6\xc6\xc7\xbd\xf7\xc7\xdf\xf2\x0e\
-\x8d\xc8\x5a\xc7\xa3\xeb\xeb\xeb\x1f\xf1\xce\x3d\xd0\x6a\xb7\x57\
-\x5a\x8d\x1a\xef\x28\x45\x9c\x3e\x7b\x81\x6e\x6f\x80\xcb\xd2\x6f\
-\xb6\xa9\x21\x40\x64\xbb\x3a\x7b\x89\xf5\x4b\xd8\x5d\xbf\x67\xb7\
-\x66\x8e\x23\x35\x59\x8c\xb4\xea\x55\x0e\x1e\x58\xa6\x14\x85\xa4\
-\x69\xca\xc5\x0b\x17\xce\x6d\x6c\x6c\xdc\xe5\xbd\x3f\x5a\x70\xb8\
-\xaf\x93\xf5\xd8\x0b\x2f\x5c\x7e\x93\x4f\x67\xd8\x9c\xb5\xd7\xb5\
-\xdb\xed\xef\x74\x16\x16\xde\xaf\x75\xb1\xad\xba\xc3\xf9\xb5\x4d\
-\x46\x93\x69\x16\x3b\x2a\x6b\x45\x45\xd1\xd4\x14\x8d\xc8\xbc\x25\
-\xc4\x25\x56\x28\x62\xc7\x7b\xca\xa5\x88\xfd\x8b\x1d\x16\x5a\x19\
-\x23\x3d\x1e\x8d\xb8\x70\xf1\xe2\xb3\xdb\x5b\x5b\x9f\x16\x42\xbc\
-\x58\x14\x75\x97\x03\xf0\x07\xf7\x89\x73\x8d\xbe\xd8\xdd\xd9\x39\
-\x32\x9d\x4e\xbf\xda\x59\x58\xb8\xbb\x52\xa9\x88\x85\x56\x83\x76\
-\xa3\xc6\xd6\x4e\x9f\x8d\xed\x1d\x06\xa3\x31\xc6\xa4\x20\x76\xb7\
-\x9b\x66\x7d\xe7\x6e\x10\xcc\x7c\x1e\xef\x51\x4a\x52\xaf\x96\x59\
-\x68\x36\x68\x37\x6b\xb3\xd7\x19\xba\xdd\xae\x5f\xbb\x78\xf1\x9b\
-\xd3\xe9\xf4\x4b\x85\xcf\xbf\xd1\x06\xdf\x9b\x5a\x40\x08\x81\xcb\
-\xb7\x7f\xac\x73\x48\x21\x3e\x5c\x6f\x34\xbe\xd2\x6c\x36\xdf\x57\
-\xca\xb9\x20\x80\xf1\x64\x4a\x6f\x38\x62\x30\x1a\x33\x9e\x4c\xb3\
-\x1a\x3e\x77\x8f\x82\xcd\x40\x08\x02\xad\x88\x4b\x11\xd5\x72\x4c\
-\xad\x52\xa6\x9c\xb3\x0e\xde\x7b\x06\x83\x01\x5b\x9b\x9b\xbf\xec\
-\xf5\x7a\x5f\xb6\xd6\xfe\x4c\xca\xac\x69\x29\xde\xa1\x78\x23\x0b\
-\xbc\x65\x00\x45\x0a\x15\x42\xe8\x20\x08\x3e\x51\xae\x54\x3e\x5b\
-\xab\xd5\x6e\x2e\x95\x4a\xa8\x39\x66\x6d\x9e\xf7\x2f\xfa\x89\x8c\
-\x96\x9c\x63\xb3\xf3\x23\x4d\x53\x46\xa3\x11\xbd\x9d\x9d\x67\xfa\
-\x83\xc1\xfd\x26\x4d\xbf\x5f\xbc\x33\x21\x84\xf8\xf3\x03\x20\x7f\
-\x5b\x25\xeb\x90\x9c\x16\x42\xdc\x1a\x97\x4a\x77\xc5\x71\x7c\xa4\
-\x14\xc7\x87\xc3\x30\x54\x5a\x6b\x54\x56\xdd\xbd\x6e\x9b\xd5\xce\
-\x36\xba\xa7\x76\x3c\x1a\x9d\x18\x8d\x46\x8f\x8e\xc7\xe3\x07\xbc\
-\xf7\x4f\x09\x21\x4c\xb1\x23\x9f\x6f\xf3\xbe\x25\x00\x7f\xd2\xdb\
-\x2a\x79\xca\x34\x78\xff\xf3\xf1\x64\xf2\xf3\xe1\x70\x18\x2a\xad\
-\xaf\x97\x42\xbc\x3b\x0c\xc3\x1b\x10\xe2\x90\x92\xb2\x2e\xa4\xac\
-\xe4\x89\x60\x68\xad\xed\xb9\xfc\x75\x1b\xef\xdc\xec\x75\x9b\xf9\
-\xfd\xe5\x3f\xe4\xeb\x6f\x74\xfc\xff\x00\x57\x74\x47\xd3\xc3\x02\
-\x5f\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x14\xc9\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
-\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
-\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
-\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
-\x46\x00\x00\x14\x4f\x49\x44\x41\x54\x78\xda\xb4\x5a\x79\x90\x5d\
-\x55\x9d\xfe\xce\x72\xef\x7d\xfb\xd2\x5b\xd2\xf4\x9a\xee\x4e\xc8\
-\xda\xdd\x61\x6b\xa2\x35\xc8\xa2\x23\x12\x12\x20\xea\xb8\xa2\x58\
-\xae\x85\x32\xe0\x8c\x16\xe8\x68\x39\x8c\x8e\x25\x35\x8b\x8e\xe8\
-\x08\xd6\xcc\xc8\x80\x0a\x5a\x25\x60\x10\x15\x23\x1a\x03\xa8\x91\
-\x48\xf6\xf4\x96\xde\x5e\xa7\x97\xa4\xdf\xd6\x6f\xbb\xeb\x39\x67\
-\xfe\xb8\xef\xbd\x74\x02\x0a\x3a\xf8\xaa\x5e\x77\x55\xf7\x7d\xf7\
-\xfe\xce\xf9\xfd\x7e\xdf\xef\xfb\xbe\xf3\xc8\x2d\x37\xbf\x1b\xaf\
-\xd6\x4b\x29\x05\x4d\xd3\xde\xdb\xd9\xd5\xf5\x51\xc6\x98\x20\x20\
-\x20\x94\x80\x52\x0a\xa5\x14\x9b\x38\x79\xf2\xeb\x8e\xe3\xfc\x2f\
-\x21\xe4\x55\x7b\x26\xff\x73\x3e\x24\xa5\x84\x10\xa2\x93\x31\x56\
-\xa1\x94\xa6\x57\x2e\xc0\x30\x8c\xee\xd7\x5d\x75\xd5\xa5\x89\x44\
-\x12\xc2\xf3\xa0\xa0\xea\x9f\x99\x9b\x9b\xeb\xb6\x2c\x0b\x8c\xb1\
-\xfa\xbd\x84\x10\x4d\x9e\xeb\x86\xb8\xa6\xa5\x56\xfe\xfd\x95\xbe\
-\xe8\x9f\xba\xc3\xae\xe3\x04\x74\x5d\xbf\x63\x70\xeb\xd6\x03\xc9\
-\x86\x86\x7b\x84\x10\x2b\x2f\x80\x52\xca\x65\x94\x21\x14\x0c\x42\
-\xd3\x34\x40\x01\xc2\xf3\x50\xbd\xce\x5d\x79\x3f\xd7\x75\xd1\xd6\
-\xde\x7e\xcf\x8d\x6f\x7e\xcb\x81\x68\x34\x7a\x87\x69\x9a\x01\x29\
-\xe5\x5f\x66\x01\x9e\xe7\x41\x29\xb5\xb3\xa7\xaf\xef\x37\x37\xee\
-\xda\xf5\xe5\x1b\x77\xed\x6a\x1e\xdc\xba\xf5\x66\xa5\xd4\x50\x3d\
-\x7e\x00\x20\x04\x94\x51\x50\xea\xbf\x19\x63\x60\x8c\x81\x33\x86\
-\x97\x28\x9c\xa1\x37\xfc\xf5\x1b\x6f\xfe\xab\x2b\xae\x68\xbe\xe3\
-\xef\x3f\xf1\xe5\xd7\xbf\xe1\x0d\xbf\x21\x84\xec\xb4\x6d\xfb\x55\
-\x2f\xa1\x4b\x56\xb7\xb6\x7e\x7a\x70\x70\xf0\xa6\x8b\x2f\xbd\x14\
-\x5d\x5d\x5d\x30\x8c\x00\x4c\xd3\xd2\x46\x86\x87\xbf\x74\x6a\x76\
-\xf6\x6a\x4d\xd3\x54\xfd\xa6\x9c\x83\x73\x0e\xc6\x18\xa4\x94\x50\
-\x4a\x81\x10\x82\x95\xb5\x6f\x59\x16\xb9\xf8\x92\x4b\xbe\xd4\xd3\
-\xdb\xa3\x1d\x3a\x74\x10\xb1\x68\x14\xd7\x6d\xdf\x3e\x38\x74\xf9\
-\xb6\x1f\xfe\xe8\x89\xdd\x8f\x8d\x8d\x8e\x7e\x51\x29\x75\xe0\xe5\
-\xfa\xe5\x65\x33\x20\x84\x40\x34\x1a\xbd\xf3\xda\x37\xbd\xe9\xa6\
-\x1b\x6f\xda\x85\xf5\xeb\x37\xc0\x30\x02\x90\x52\xa2\xbb\xbb\x1b\
-\x43\xdb\xb6\x5d\xa9\x69\xda\xdb\x95\xf2\xe3\x27\x00\x18\xe3\x60\
-\x1a\x07\xaf\xee\x7e\xed\x8d\x6a\x0e\xa4\x94\x08\x85\x42\x6f\x7f\
-\xe3\xb5\x6f\xba\xf2\xcc\x99\x33\xa0\x94\xa2\x58\x2a\xe1\xd0\xa1\
-\x43\x10\xc2\xc3\x6d\xb7\xdf\x7e\x53\x6b\x6b\xeb\x9d\x9e\xe7\xfd\
-\xff\x4b\x88\x31\x86\x7c\x3e\xff\x85\x89\x89\x09\x53\xac\xa8\x4f\
-\xa5\x14\x82\xc1\x20\x2e\xbe\xf8\x12\xac\xdf\xb8\xf1\xf3\xae\xe3\
-\x84\xa5\x94\xf0\x84\x70\x19\x63\xe0\x94\x81\x6b\x9a\x5f\x3e\x9c\
-\x83\x71\x0e\xcf\x73\x5d\x21\x04\x6c\xcb\x0a\x5f\x79\xd5\xd5\x9f\
-\x8f\xc5\xe3\x58\x5e\x5e\x06\x63\x0c\x94\xfa\xa1\x30\xce\x71\xf0\
-\x85\x83\xe6\xec\xec\xec\x17\x38\x7f\xf9\x02\x61\x83\x03\xfd\xaf\
-\xa4\x79\x4f\x67\x32\xe9\x64\x73\x4b\xcb\x6b\x3a\x3b\xbb\x6a\xb0\
-\x08\xa5\x14\xc2\xe1\x30\x34\x5d\x6b\x18\x1d\x19\x89\x72\xce\xfb\
-\xd6\xac\x59\x73\xf3\xe6\xcd\x5b\x5a\xc3\x91\x08\xa4\x92\x90\xd5\
-\xeb\x02\x81\x00\x96\x96\xd2\x4d\xcb\xcb\x79\x3d\x99\x4c\xfe\xcd\
-\xdb\xdf\xf9\xce\x6b\xe7\xe6\xe6\x20\x84\x00\x21\xc4\x47\xb0\x40\
-\x00\x5d\x9d\x5d\x78\xf0\x81\x6f\x7d\xb5\x50\x28\x3c\xf4\x4a\x50\
-\x89\xbc\xd4\x1c\x10\x42\xe0\xfc\x0f\x3b\x8e\xd3\xd4\x3f\x38\x78\
-\xe8\x3d\xb7\xdc\xd2\xd6\xba\xba\x15\x4a\x29\x78\x9e\x07\x42\x08\
-\xb2\xd9\x2c\x8e\x1c\x3e\x84\x60\x30\x84\x64\x32\x89\x40\x30\x88\
-\x78\x2c\x06\xdb\x71\xe0\xba\x2e\x6c\xcb\x42\x24\x1a\x45\x34\x1a\
-\x45\xb1\x50\x80\xeb\x3a\x30\x2d\x0b\xf3\x73\xf3\x20\xd4\x0f\xde\
-\x71\x1c\x6c\xd8\xb0\x11\x2f\xfc\xfe\xc0\xdc\xf7\x1e\xfe\xee\x60\
-\x30\x18\x3a\x07\x9e\x3d\xcf\x03\xe7\x1c\xe7\xf7\xc4\x4b\x95\xd0\
-\xd6\x44\x22\xf1\x1d\x21\xc4\xe0\x4a\x88\xe4\x9c\xa7\x47\x47\x46\
-\xee\x3e\x7c\xf0\x60\x3d\xf0\x5a\x26\xe2\xf1\x38\xd6\x5d\xb8\x1e\
-\xed\x1d\x1d\x68\x6c\x6a\x42\x3c\x16\x03\x21\x04\xbc\x5a\x3e\x9a\
-\xa6\xc1\xac\x54\x90\xc9\x64\xa0\x1b\x06\xc2\x91\x28\x32\x99\x0c\
-\x18\x63\xf5\xdd\x4f\x26\x1b\x00\xa5\xf0\xd4\x4f\x7e\x72\xb7\xae\
-\x1b\xe9\x95\x50\x2b\x84\x18\xec\xec\xea\xfa\x0e\x21\x64\xf0\x8f\
-\x96\x90\xe7\xba\x68\x59\xbd\xfa\xfe\xeb\x77\xec\x78\x4b\x34\x16\
-\x7b\x6f\x36\x93\x49\x54\x2a\x95\xc3\x84\x90\x32\xa5\x14\xae\x6d\
-\x1f\x2e\x96\x4a\x57\xf4\xf6\xf5\x76\x27\x93\xc9\xfa\xc3\x01\x20\
-\x18\x0a\x21\x1c\x89\x40\x37\x8c\x7a\xf6\x08\x25\x80\x52\x00\x01\
-\x84\x27\x60\x55\x2a\xc8\xe7\xf3\xc8\xe7\xb2\x80\x52\x90\x4a\x01\
-\x0a\x90\x4a\xa2\xb7\xb7\x0f\xbb\x1f\x7f\x6c\xef\xd8\xe8\xe8\xc7\
-\x0d\xc3\x50\x42\x08\x58\x96\xd5\xd2\xd9\xd5\xf5\x8f\xef\xbe\xf9\
-\xe6\xfb\xdf\xfa\xb6\xb7\x5f\x74\xf2\xe4\x78\xeb\xdc\xdc\xdc\x23\
-\x2b\x7b\x83\xaf\x4c\x13\x65\x6c\xfb\xc0\xd6\xad\x3b\x37\x6c\xdc\
-\x84\x8d\x9b\x36\x07\xd7\xaf\xdf\xf0\x89\xfd\xfb\x7f\xfb\xb6\x91\
-\x13\x27\xfe\xd9\xb2\xac\x6f\x02\x10\x85\xe5\xe5\xd9\xf9\xb9\x79\
-\xb4\x34\xb7\x20\x1c\x89\x9c\x6d\x3e\xc6\x60\x59\x16\xf2\xf9\x3c\
-\x0a\x85\x02\x1c\xc7\x06\x01\x39\xdb\xc4\x8c\x41\x37\x0c\x80\x90\
-\x7a\x89\x72\xa5\x60\x7b\x36\x5a\x5a\x5a\x10\x89\x44\x90\x4a\xa5\
-\x66\x01\x88\x4a\xa5\x42\x12\x89\xc4\x87\xde\xfc\x96\xb7\xfe\xc3\
-\xd0\xb6\x6d\x1d\x67\x4e\x9f\xc6\xc8\xc8\x30\x76\xde\x70\xc3\x0d\
-\xa3\x23\x23\xdb\xa5\x94\x4f\xd6\x9f\x5b\xcb\x80\xeb\xba\x81\xde\
-\xb5\x6b\x1f\xf9\xab\x2b\x5e\xb7\x2a\x99\x4c\x20\x10\x08\xa1\xad\
-\xed\x02\xf4\xad\x5d\x1b\x6f\x69\x59\x75\x7d\xa9\x54\xba\x82\x51\
-\x76\xcd\xce\x1b\x6e\xbc\xb9\xa1\xb1\x11\xd1\x68\x14\x81\x40\x00\
-\x84\x10\xb8\xae\x8b\xa9\xa9\x29\x8c\x8f\x8f\x21\x9d\x5e\xaa\x36\
-\x6d\x10\x01\xc3\x80\xa6\x6b\xf5\x81\x16\x08\x06\x10\x8b\xc5\x61\
-\xe8\x3a\x1c\xc7\x86\x10\x02\x52\x49\x84\x82\x21\xb4\x77\x74\x60\
-\xcb\x96\xfe\x81\x91\xe1\xe1\xde\xfe\x81\xc1\xdb\xde\xff\xc1\x0f\
-\xde\xd6\xda\xda\x1a\x1f\x1e\x19\xc6\xd2\xd2\x19\x14\x0b\x05\xac\
-\xe9\xe9\x85\xe3\x3a\x9b\xc7\xc7\xc6\x1e\xd0\x34\xcd\xab\x37\xb1\
-\x94\x12\x86\x61\xdc\xbe\xf3\xc6\x1b\xbf\xb2\x79\xcb\x16\xe8\xba\
-\x0e\x4d\xd3\xa1\xeb\x3a\x0c\x43\x87\x10\x12\x8b\x8b\x0b\x38\x73\
-\xfa\x0c\xa2\xd1\x28\xda\xdb\xdb\x91\x48\x26\x01\x00\x4b\x4b\x4b\
-\x38\x7e\xfc\x18\x00\xa0\xbd\xbd\x03\x4d\x4d\x4d\xd0\x75\x1d\x4a\
-\x29\x48\x29\x21\x85\x80\x27\x3c\x38\x8e\x0b\xe1\x79\x60\x8c\x21\
-\x1a\x8d\x02\x84\x60\x7e\x7e\x0e\xe9\xa5\x25\xd8\x8e\x83\xa6\xa6\
-\x66\x6c\xd9\xb2\x05\xa6\xe9\x97\xd9\xcc\xcc\x0c\xb2\x99\x4c\xbd\
-\xcf\x84\x10\x88\x44\xfc\x67\x7f\xf9\xdf\xfe\xf5\x8e\x62\xb1\xf8\
-\x1f\x8c\x31\x3f\x03\x9e\xe7\x75\xf5\x0f\x0e\x3e\x36\xb8\xf5\x22\
-\x2d\x60\x18\x55\x7c\x3a\x3b\x39\x19\xe3\x48\x24\x12\x48\x24\x12\
-\x88\x44\x22\x88\xc5\xe3\x20\x84\x60\x6a\x6a\x12\xc7\x8e\x1d\x45\
-\x5b\x5b\x1b\x06\x06\xb6\xa2\xa9\xa9\x09\x86\x61\x40\x4a\x09\xd7\
-\x75\xe1\xba\x2e\x56\x72\x1b\x21\x25\x6c\xdb\x42\xa1\x58\x84\xc6\
-\x39\x2e\x68\x6b\x03\x21\x04\xc5\x62\x11\xc5\x42\x11\xe5\x4a\x19\
-\x99\x4c\x06\x53\x53\x53\xb0\x6d\x1b\xb4\xda\xe4\xb5\x12\x37\xcd\
-\x0a\x7a\x7a\x7b\xa0\x6b\xfa\xeb\x8e\x1e\x39\xf2\x6d\xce\xf9\x32\
-\x1b\x1c\xe8\x07\x21\x24\xe4\x38\x4e\x0b\x80\xcd\xd1\x68\x94\x05\
-\x82\x41\x50\x4a\x40\x08\xad\xa2\x8d\x1f\x80\xa6\xeb\x08\x04\x03\
-\xa0\x94\x61\x72\x72\x12\xe3\xe3\xe3\xd8\xb2\xa5\x1f\x3d\x3d\xbd\
-\x60\x8c\xc1\xf3\x3c\x2c\x2e\x2e\x62\x6e\xee\x14\xe6\xe7\xe6\x91\
-\xcd\x64\x50\xa9\x54\x20\xa5\x44\x38\x12\x86\xa6\xe9\x90\xd2\x87\
-\xc4\xc2\xb2\x0f\xa7\x1d\x9d\x9d\xa0\x94\xa0\xb0\x5c\x40\xb1\x54\
-\x44\xb9\x5c\x06\xa5\xf4\x9c\xc0\xa5\x10\x48\x26\x93\x58\xb7\x6e\
-\x1d\x26\x27\x26\xdc\x67\x7e\xf5\xab\xef\x94\x4a\xa5\x3d\x94\xd2\
-\x52\x6d\x01\xa5\x72\xb9\xbc\x7b\x72\x62\xe2\x17\x0b\xf3\x0b\x5d\
-\x7a\xc0\xe8\x49\x24\x12\xd0\xb8\x56\xbf\x19\xa5\xb5\x6c\x50\x2c\
-\x2d\x9d\xc1\xc8\xc8\x30\x36\x6f\xde\x82\xf6\xf6\x76\x00\x40\x2e\
-\x9f\xc3\xf1\xe3\xc7\x30\x3a\x32\x82\xc9\x89\x09\xf3\xcc\xe9\xc5\
-\xd1\xa5\xd3\xa7\x9f\x59\x58\x98\x1f\x3e\xbd\xb0\x28\xf3\xf9\xe5\
-\x68\x32\x99\xd0\x56\xad\x5a\x0d\x21\x04\x5c\xc7\x41\xb9\x5c\x86\
-\xeb\x38\x68\x6f\xef\x40\xa5\x52\x46\xa5\x5c\xa9\x23\x98\x52\x0a\
-\xc2\xf3\x10\x08\x06\xd1\xdb\xdb\x0b\x42\x19\x7e\xf4\xc4\xee\xa7\
-\x7f\xf4\xc4\x13\xb7\x2c\x2f\x2f\x7f\x8d\x73\x5e\x3a\xa7\x89\xab\
-\xec\x71\x36\x9b\xcd\x3c\x38\x36\x3a\x7a\x32\x9f\xcf\x6f\x8e\xc7\
-\x13\x8d\xc1\x40\x00\x8c\x71\x50\x4a\xc0\x18\x85\xe3\xb8\x38\x76\
-\xec\x18\x5a\x5b\x2f\x40\x4f\x4f\x0f\x08\x21\x58\x5c\x5c\xc0\xe1\
-\x43\x87\x30\x3e\x3a\xea\x66\xb3\xb9\xaf\x72\xce\x3e\x44\x29\xfd\
-\x7c\x2c\x1e\xff\x7e\x20\x10\xf8\x7e\xa9\x54\xfe\x46\x3e\x97\xfd\
-\xc1\xc8\xf0\xb0\xeb\x38\xee\x25\xeb\xd7\xaf\x67\x84\x52\x58\xa6\
-\x89\x52\xa9\x04\x10\x82\xc6\xa6\x26\xe4\xb2\xb9\xfa\x8c\x01\x80\
-\xd5\xab\x57\xa3\xa9\xa9\x19\xcf\x3d\xfb\xec\xd8\xf7\x1e\xfe\xee\
-\xed\xd3\xd3\xd3\x9f\xd4\x75\x7d\x76\xe5\x90\x7d\x11\x95\x60\x8c\
-\x41\x29\x75\x74\x36\x95\xfa\xf6\xe8\xc8\x48\xb8\xbd\xa3\x63\x28\
-\x1a\x8b\x82\x73\x0d\x9a\xc6\x31\x37\x77\x0a\x95\x4a\x05\x9b\x36\
-\x6d\x04\xe7\x1a\x32\x99\x0c\x8e\x1c\x39\x82\x93\x63\xe3\x39\x42\
-\xe9\x0d\xb1\x68\xec\x3e\xc6\x59\x5a\x4a\xe9\xeb\x81\x2a\x15\x37\
-\x0c\x3d\xad\x69\xfa\x53\xa7\x66\x53\xbf\x4e\xa7\x97\x76\xf4\xf7\
-\x0f\x04\x1d\xc7\x81\xeb\x38\xa8\x94\xcb\x88\xc5\xe2\x00\x14\x0a\
-\x85\x02\x08\x21\x58\x77\xe1\x85\x20\x84\xe2\xde\xaf\x7c\xf9\xde\
-\x83\x2f\xbc\xf0\x2e\x42\xc8\xf3\x9a\xa6\xbd\x78\x12\xd7\x3a\x7c\
-\xe5\x5b\x4a\x09\x42\x48\xde\x75\xdd\x63\x91\x68\xa4\x8e\x02\x96\
-\x65\x23\x93\xc9\xa0\xad\xad\x1d\x9a\xa6\xc3\xb6\x6d\x4c\x4d\x4d\
-\xe1\x54\x6a\xd6\x32\x0c\x63\x57\x3c\x16\x7b\x5a\x4a\x51\x1f\x6e\
-\xe7\x8b\x21\x7f\xe2\x26\x9f\x9e\x4d\xa5\x76\x3d\xf7\xec\x33\x56\
-\x63\x63\x23\xb4\x2a\x62\xe5\xb2\x59\xc4\x62\xb1\x3a\x08\x40\x2a\
-\xe8\xba\x06\xd3\x34\x8f\x31\x4a\xf3\x2f\x15\xa7\x52\x0a\x5c\xd3\
-\xf5\x5b\x82\x81\x40\x3b\x00\xb5\x72\x75\x9e\xe7\xc9\x35\xbd\xbd\
-\x6f\x6b\x6c\x6c\x02\xe0\x43\x62\xa5\xe2\xd7\x68\x43\x43\x03\x94\
-\x52\xc8\x64\x32\x38\x73\x7a\x11\xae\xeb\xdc\xdf\xdc\xdc\xb2\x57\
-\x4a\x09\xc6\x3c\x78\x2b\x55\xda\x39\xd9\xa5\x60\x9c\xa3\xb1\xb1\
-\x71\xef\xf0\x89\x13\xf7\x77\x76\x76\xdd\x1e\x8d\x44\x20\x3c\x0f\
-\xa6\x69\x42\x37\x0c\x04\x83\x41\x38\xb6\x83\x6c\x36\x8b\x35\x3d\
-\x6b\xb0\x79\xcb\x96\x5b\xc7\x46\x47\x1b\xb9\xa6\x51\x28\x5f\xa0\
-\x2a\x7f\x82\x13\xd3\xac\x9c\xe2\x7d\x7d\x7d\x7f\x77\xf5\x35\xaf\
-\xdf\xe2\xba\xae\xcf\xdb\x39\x83\xc6\xb5\xb3\x93\x53\x29\x48\xa1\
-\xe0\xb9\x2e\x4c\xd3\x44\x38\x14\x86\xae\x6b\xf0\x3c\x81\x52\xa9\
-\x84\xe5\xfc\x72\x39\x14\x0a\x7d\x4d\x08\x0f\x4a\xa1\x5a\x36\x04\
-\x2b\xb9\xbc\x52\xaa\xae\x09\x64\x95\x7d\x72\x8d\x7f\x6d\x64\xf8\
-\xc4\x07\xb6\x5e\x74\x71\x98\x6b\x1a\x5c\xc7\x81\x59\x31\xc1\x19\
-\x83\xa6\x6b\xa8\x98\x15\x64\xd2\x19\x5c\x7b\xdd\x75\x03\x57\x5d\
-\x7d\xcd\x80\x94\x02\x42\x48\x78\x9e\x07\x21\x3c\xe8\xba\x81\xa7\
-\x7f\xbe\xe7\x28\xd7\x34\xad\x4c\x29\x85\x69\x9b\xd0\x98\x06\x1d\
-\x7e\x4a\x35\xe5\xd7\xaf\xf0\x3c\x18\x86\x01\x4a\x29\xa4\x10\x08\
-\x04\x83\x90\x52\x01\x70\xe1\xb9\x2e\x84\xf0\x26\x19\x63\x27\x5d\
-\x77\x85\xdc\x25\xd5\x5e\xaa\x4a\x98\xda\x30\x5a\xb9\x28\x4e\xd9\
-\xc9\xa5\xa5\xa5\xc9\xe5\x42\x61\x0b\xe7\x0c\x52\x30\xd8\xb6\x05\
-\x29\x25\x74\x4d\x83\xed\x38\x98\x9a\x9a\x02\xc8\xd9\xf2\xab\xa9\
-\x3b\x29\x04\x12\x89\x04\x28\xa5\x65\x4e\x08\xf1\xd3\x22\x15\x3c\
-\x78\x20\x0e\x01\xf4\xda\x2c\xf3\x87\x99\x57\x9d\xa0\x94\x52\xe8\
-\x9a\x56\x15\xe8\xaa\xfa\x3f\x71\x32\x18\x0c\x9d\x5b\xf7\x04\xd0\
-\xb8\x56\x2f\x3d\xc3\x30\x6a\x8a\x79\x45\x39\x71\x14\x8b\xc5\x93\
-\xf9\x7c\x6e\x4b\x4b\x73\x0b\xb8\xa6\xc1\x71\x1d\x38\xae\x07\x4a\
-\x29\x38\xf7\xe5\x68\x2d\xe8\x97\xa2\xfc\x42\x08\x50\x1f\x3e\x49\
-\xfd\x42\x4f\x78\x10\x9e\xdf\xc8\x5e\xd5\x4d\xf0\x3c\x0f\x9e\xe7\
-\x41\xd7\x75\xd0\xea\xc0\x52\x0a\x88\xc6\xa2\x60\x8c\xbe\xf8\x01\
-\xaa\xfa\xc3\x57\xf9\x2f\x25\xe6\x41\x08\xa0\x94\x84\x65\x5a\x20\
-\x94\x42\xd7\xfd\x8d\xf1\x84\x07\xc6\x38\x38\xf3\x75\x35\x01\x59\
-\x31\x8b\xfc\xdf\x84\x52\x7f\x71\x4a\x81\x33\xc6\x6b\xd0\x79\x76\
-\x80\x08\x01\xca\x28\x04\x11\x20\x1e\xa9\x13\x36\x23\x10\x80\xa6\
-\x71\x78\xae\x0b\x4a\x29\xe2\xf1\x38\xc2\xe1\x70\x9f\x65\x5a\x60\
-\x9c\xad\x74\x57\xe0\xba\x0e\x02\x81\x20\x08\x01\x6c\xc7\x01\x65\
-\xf4\x45\xa8\x44\x08\xed\x03\xf1\xfb\xc6\x1f\x5c\x02\x4a\x4a\x70\
-\xce\x21\xa5\xcf\x58\x15\xf7\xe3\x01\x45\xdd\x1c\x58\xb1\x03\xe0\
-\x42\x78\xe1\x70\x28\x8c\xb6\xb6\x36\xbf\xd1\x28\x03\xe3\x1c\x8c\
-\x51\x18\x86\xe1\x63\x7d\x3a\x0d\xd7\x75\x41\x08\x81\xa1\x1b\x28\
-\x95\x4a\xa0\x8c\x21\x12\x49\xa0\xa9\xa9\xb9\x67\x66\x7a\xba\x2f\
-\x10\x0c\x9e\x04\x54\x3d\x78\x5f\x2a\x56\x8d\x30\x25\xc1\x29\x07\
-\xab\x71\x12\x42\x60\x5b\x56\x1f\xe3\xac\x27\x12\x89\xc0\x30\x74\
-\xd8\xb6\x03\xdb\xb1\xa1\x6b\x7a\x75\x01\x12\x8c\x31\x38\x84\xc2\
-\xb1\x6d\x40\x2a\x08\xa5\xa0\xa4\x9f\x59\xcf\xf5\x20\x84\x17\xe6\
-\x27\x8e\x1f\xff\xf7\xe9\xe9\xe9\x76\x42\x88\x22\x35\x12\x07\xc0\
-\x71\x5d\xb9\x61\xc3\x86\xb7\x7d\xe4\xd6\x5b\x07\xca\xa5\x22\x4c\
-\xd3\x84\x63\xdb\xd0\xaa\x42\xc6\x75\x1d\x38\x8e\x83\x8d\x9b\x36\
-\x85\x27\x26\x26\x3e\x46\x29\xb9\x43\x29\xc0\xb2\x2c\x48\x29\xce\
-\x19\x38\xa4\xea\x44\x40\x29\x68\x9a\x0e\x42\x08\x2a\xe5\xca\xc7\
-\x5a\x5a\x57\x87\xe3\xb1\x38\x18\x67\x90\x15\x01\xdb\xb6\x91\x88\
-\x27\x40\xab\xfd\x26\x25\xb0\xe7\x67\x3f\x3b\x9c\x9a\x99\xfe\xde\
-\x4a\x18\x85\x9f\x09\x62\x59\xd6\x29\x6e\xdb\xf6\x03\x66\xa5\x52\
-\x4f\x89\x5a\xd1\x24\x87\x6d\x3b\x93\xc9\x64\xee\x8f\xc5\x13\xc8\
-\x66\x73\x30\x4d\x13\xa8\x0a\xf4\x52\xa9\x84\x72\xb9\x8c\x96\x96\
-\x16\x6c\xd8\xb8\xf1\xc3\xc7\x8e\x1e\x79\x3c\x16\x8b\xed\x15\x42\
-\xf8\x4a\xec\xfc\x9a\x3f\x6b\x49\xa2\x54\x2a\x5e\xc9\x75\xed\xc3\
-\xad\xad\xad\x68\x68\x68\x00\x05\xc5\x72\x61\x19\x84\x10\x44\xa2\
-\x51\x78\x55\x44\xcb\x66\xb3\x98\x9c\x9c\xf8\x4f\xb3\x52\xf9\x66\
-\x4d\xc0\xd4\x36\x59\x29\xe5\xd3\x1f\x42\x48\xb5\x64\xfc\xd2\xe1\
-\x9c\x83\x52\x0a\x21\x44\x22\x18\x0c\x6e\xb6\x6d\x1b\x84\x00\xb2\
-\x2a\xbc\x97\x0b\x05\x44\xab\x9a\xd7\x71\x1c\x14\x0a\x05\x5c\x36\
-\x34\x14\x68\x59\xb5\xea\xd1\x6c\x2e\x77\xcd\xf9\x06\xd6\xd9\xa6\
-\x25\xa0\x94\xa1\x58\x28\x5c\xe3\x38\xce\xa3\x9d\x9d\x9d\x81\xee\
-\xee\x35\xd0\x0d\x03\xae\xeb\x62\x71\x71\x11\x0d\x0d\x0d\x30\x0c\
-\x03\x8c\x73\x28\x28\x94\x2b\x65\x70\xc6\x36\x2b\xa5\x12\x35\x51\
-\x54\xcb\x4e\x4d\x4f\x9f\xc3\x85\x94\x52\xb0\x6d\x1b\x81\x40\xe0\
-\x5d\xdb\xaf\xbf\xfe\xdb\xef\x7c\xd7\xbb\xaf\x2b\x15\x4b\x48\xcd\
-\xa6\x00\x02\x50\x42\xe1\xb9\x2e\x6a\x14\xa0\x5c\x2a\x41\x29\x09\
-\x42\x08\x7a\x7a\x7a\x82\x66\xa5\xf2\x8e\xc5\xc5\xc5\xa4\x92\x72\
-\x06\x40\x3a\x18\x08\x80\x50\x0a\xdb\xb6\xe1\x79\x62\x7d\xb1\x58\
-\xfc\x34\xd3\xf8\x57\xd7\xf4\xf4\x84\x37\x6d\xda\x84\x96\x96\x16\
-\x28\xa5\x30\x39\x39\x09\xb3\x52\x41\x6f\x6f\x1f\x08\x21\x90\x4a\
-\x81\x51\x86\x50\x28\x8c\xae\xae\xae\x21\x29\xe5\xae\x4c\x26\x93\
-\x73\x5d\xf7\xe8\xf9\x6e\x49\xdd\x56\x71\x1c\x07\x84\x90\xd7\x5e\
-\x7a\xd9\x65\x77\x5f\xb7\xfd\xfa\x6b\x82\xa1\x20\xa6\xa7\xa7\x61\
-\x9a\x26\x02\x46\x00\x9a\x5e\x35\xa9\x18\x47\x63\x63\x23\x3a\x3a\
-\x3b\x91\x5e\x5a\x82\x65\x59\x30\x02\x81\xea\xd4\x96\xbe\xb4\x1c\
-\x1d\x33\x8b\xa5\xe2\x84\xe7\xba\xa3\xd5\xed\xbf\x90\x10\xd2\x9b\
-\x6c\x48\x06\xdb\xdb\x3b\xb0\xb6\x6f\x2d\x1a\x1a\x1b\x21\xa5\x44\
-\x2a\x35\x83\xd1\x91\x11\x6c\xd8\xb8\x11\x0d\xc9\x06\x38\x8e\x03\
-\xcf\xf3\xe0\x7a\x2e\x1c\xdb\x85\xeb\xb9\x30\xcd\x0a\xa6\x26\x27\
-\xf1\xfb\x03\x07\x9e\x4e\xa5\x52\x9f\x23\xc0\x73\xb5\x85\xf0\x6a\
-\xbd\xb7\xf6\xf4\xf4\x7c\x61\xfb\x8e\x1d\x37\x77\x77\xaf\xd1\x66\
-\x66\x66\x90\x39\x99\xa9\x3a\x6a\xcc\x87\x55\x71\x16\xef\x73\xb9\
-\x1c\xb8\xa6\xa1\xa1\xa1\x01\x52\x49\x78\x9e\x4f\x43\xb8\xc6\xd1\
-\xd9\xd1\x89\x78\x3c\x1e\xcc\xa4\xd3\x9b\x8b\xa5\xd2\xe6\x1a\x2b\
-\x4d\x24\x12\x68\x6e\x6e\xc6\xaa\x55\xab\xeb\xb0\x3d\x37\x77\x0a\
-\x23\xc3\xc3\x58\xd3\xd3\x8b\xe6\xe6\x16\x5f\x72\x72\xff\x7f\x42\
-\x4a\x30\x26\x20\x84\x8f\x7c\xbd\xbd\x7d\x58\xb5\x7a\xf5\x35\x63\
-\x63\x63\x57\x1c\x39\x78\xf0\xa1\x6c\x36\xfb\x19\x4a\xe9\x02\xaf\
-\x0a\x7a\x7d\x68\xdb\xb6\x77\x5c\xb8\x7e\x83\xf6\xec\x33\xfb\xc0\
-\xb9\xbf\xd3\x84\x90\xfa\x84\x86\x03\xac\x6e\x6d\x05\x65\x14\xc5\
-\xe5\x02\x32\xe9\x34\x94\x94\x88\x44\xa3\xb0\x6d\x0b\x42\xf8\xd3\
-\x3a\x18\x0a\x21\x14\x0e\x63\x55\xcb\x2a\x48\x25\x41\x08\x05\xe7\
-\x1c\x86\x61\xd4\x7b\xc3\x75\x5d\x8c\x8e\x8c\x20\x95\x9a\x41\x5f\
-\xdf\x5a\x74\x76\x75\x41\x4a\x89\xe5\x42\x01\x94\x12\x84\xc3\xe1\
-\x3a\x75\x60\xd5\x69\xcc\x18\x43\x38\x14\xc6\xe6\x4d\x9b\xb4\x7c\
-\x36\xfb\x8e\xa5\xa5\xa5\x7f\xa2\x94\xa2\xa6\xc8\x96\x67\x53\xa9\
-\xd2\xa5\x97\x0d\x5d\x6b\x5b\xb6\xaf\x47\x57\x88\xe9\x64\x32\x89\
-\xde\xbe\x5e\x34\x37\x35\xa3\xb5\xb5\x15\xa5\x62\x11\xb6\x6d\xc3\
-\x71\x1c\x28\x29\xa1\x71\xad\xee\xc2\x29\x00\x1a\x67\xe0\x5c\x83\
-\xae\xeb\xd0\x75\x03\x8c\x71\x28\x25\x51\x31\x2b\x98\x4d\xa5\x70\
-\xe4\xc8\x61\x94\xcb\x65\xf4\xf7\x0f\xa0\xbd\xa3\xa3\x8e\x38\x33\
-\xd3\xd3\x98\x9f\x9f\x87\x54\x12\xd1\x48\xa4\x9e\x29\xdf\x3f\xf2\
-\xf9\xd0\xec\x6c\x0a\xbf\x7e\xee\xb9\x4f\x4a\x29\x9f\xaa\x37\x31\
-\xa5\x14\x85\x42\xe1\x30\x65\x74\xe7\xb6\x6d\xaf\x59\x75\xea\xd4\
-\x29\x08\x21\x10\x0c\x04\xb0\x7e\xc3\x46\x84\xc3\x61\xec\x7e\xfc\
-\xb1\x5f\xee\x7e\xfc\xf1\x67\x37\x6c\xdc\x34\xd0\xd1\xd9\x09\xb3\
-\x52\x81\x6d\xdb\xd5\x91\x2e\x01\x28\x54\xca\x15\xe4\xb2\x59\x64\
-\x32\x19\x2c\x2f\xe7\x91\xcb\xe5\x91\xc9\xa4\xb1\xb0\x30\x8f\xa9\
-\xa9\x29\x4c\x4d\x4e\xa2\x54\x2a\xa2\xa3\xb3\x13\xfd\x03\x83\x88\
-\xc7\xe3\x75\x5a\x3e\x39\x31\x81\x74\x7a\x09\x3f\xfd\xf1\x8f\x1f\
-\x3a\x39\x3e\x9e\x25\x84\xac\x89\x44\x7c\xeb\x86\x56\x61\xb3\x58\
-\x2c\xe0\x99\x7d\xfb\x8e\x2c\x2e\x2e\x7e\x84\x73\xee\x9d\xa3\xc8\
-\x18\x63\xde\xcc\xf4\xf4\xec\x25\x97\x5d\xf6\xce\x70\x28\x84\xc6\
-\xc6\x26\x74\x77\xaf\xc1\xf3\xbf\xdb\x3f\xfb\x3f\xff\xfd\x5f\x77\
-\x8e\x8d\x8e\xde\xbe\xbc\xbc\xfc\xd8\xc8\xf0\x89\xde\x4b\x87\x86\
-\x06\x4a\xe5\x52\xad\xf1\x51\x83\xe2\x40\x20\x80\x40\x30\x00\xc0\
-\x87\x58\xcb\xb2\xea\xd7\xc4\xe3\x09\x74\x77\x77\x63\xdd\x85\xeb\
-\xd1\xdc\xdc\x5c\x67\xab\x3e\x80\xd8\x98\x4e\x4d\xe3\xa7\x4f\xfe\
-\xf8\xa1\x5c\x36\xfb\x9e\x7c\x3e\xff\xe0\xf8\xd8\xd8\xc2\x52\x3a\
-\x3d\xa0\xe9\x5a\x3c\x1a\x8d\x41\x4a\x81\x23\x47\x8e\xe0\xe0\x81\
-\x03\xef\x67\x8c\x0d\xd7\xca\xb1\xee\xcc\x51\x4a\x61\x59\xd6\x93\
-\x8f\xff\xe0\xd1\xdd\x7f\xfb\xf1\x8f\xef\x3c\x7c\xf0\xa0\xf9\xe0\
-\x03\x0f\x7c\x7d\x7a\x7a\xea\x5f\x0c\xc3\x38\x13\x0a\x85\x60\x56\
-\x2a\xac\xbd\xa3\xa3\xc3\x34\x2b\xc8\xe5\x72\xd0\xb8\x56\x27\x7d\
-\x8c\x31\x50\x42\xa0\x1b\x3a\xa2\xd1\x18\xb4\xaa\xb5\x5e\x23\x61\
-\xb5\xf9\x52\xab\x6d\xdf\xf1\xf0\xf9\x5e\x32\xd9\x80\xce\x8e\x2e\
-\x44\x63\xb1\x8e\x5c\x2e\xc7\x34\x4d\x13\x9e\xe7\xdd\x7f\xe4\xe0\
-\xc1\xc7\xa6\xa7\xa6\x3e\x39\x38\x38\xf8\xd1\xee\x35\x6b\x82\x47\
-\x0f\x1f\xfe\xa1\x90\xf2\xc9\x95\xd6\xe2\x39\x73\x80\x73\x8e\x74\
-\x3a\x3d\x7a\xf0\x85\xdf\x47\x7f\xbe\x67\xcf\xfb\x0b\x85\xc2\x43\
-\x81\x40\xa0\x5c\x1d\x6c\x88\x44\x22\x1f\x78\xcf\x2d\xef\xbb\x2d\
-\x93\xa9\x72\x23\x4a\xea\x01\x26\x12\x89\xaa\xb5\xe2\xfa\x7c\xa5\
-\x1a\x60\xcd\x3f\x35\x4d\x13\xf9\x7c\x1e\x0b\x0b\x0b\xc8\x65\xb3\
-\x48\x36\x24\xeb\xd9\x53\x4a\x21\x1a\x8d\xc2\xf5\xdc\xee\xf1\xb1\
-\xb1\x53\x52\x88\x17\xaa\xc3\xaa\x6c\x5b\xd6\x9e\x54\x2a\xf5\x64\
-\x6a\x66\x26\x96\xcb\xe5\xbe\x48\x29\x5d\xfc\xa3\xe7\x03\x4a\xa9\
-\xc5\x5c\x2e\xf7\x28\xe7\x7c\x71\xe5\xd0\xb0\x6d\xbb\x69\xfb\x8e\
-\x1d\x8f\xb4\x77\x74\xc4\xe6\xe7\xe7\x7d\x94\xa2\x67\x1f\xbe\xbc\
-\xbc\x0c\xcb\x32\xc1\x35\x1d\x8e\xe3\x40\xd3\xb5\xb3\xe5\xc5\x18\
-\x66\x66\x66\x30\x3f\x3f\x87\xc5\x85\x05\xcc\xcf\xcf\xa3\xa9\xa9\
-\x19\xe1\x70\xf8\x9c\xcd\x0b\x85\xc2\x58\x98\x9f\xbb\x68\x61\x61\
-\xe1\x41\xc6\x58\x65\x85\x5b\xb2\x68\x9a\xe6\xa3\xe7\x07\xff\x92\
-\x67\x64\x84\x90\xba\x9b\xb0\xd2\xe2\xee\xe8\xe8\xb8\x6b\xe8\xf2\
-\xcb\xdb\xa6\xa6\xa6\x6a\x54\x03\x84\x12\x18\x55\x2a\xf0\xcd\xfb\
-\xef\xbb\xd7\xb6\xac\xf1\xbe\xb5\x6b\xdf\x77\xfd\x8e\x9d\x5b\xc3\
-\x91\x30\x84\xa0\x55\x0e\xef\x33\xd4\xa7\xf7\xec\x39\x78\x6a\x76\
-\xf6\x5b\x94\xb1\xb5\xc9\x86\x86\xdb\x9a\x9a\x9a\xea\x1c\x5f\x29\
-\x85\xd5\xab\x57\x63\xe8\xf2\xcb\xdb\xa6\xa7\xa6\xee\x2a\x97\xcb\
-\x9f\x38\xc7\x3e\xf9\x03\x87\x1d\xf4\x15\x9c\xce\x80\x10\x32\x70\
-\xed\x75\xdb\x6f\xcd\xe7\xf3\x3e\xa1\x03\x20\xa4\x80\xe7\x7a\x88\
-\x46\xa3\x78\x66\xdf\xbe\x89\xf4\xd2\xd2\xa7\xca\xe5\xf2\xbd\x53\
-\x93\x93\x0f\x3b\x8e\x53\x17\x27\x35\xf7\xc0\x75\x5c\xcc\x9d\x3a\
-\xf5\xb0\x65\x9a\xf7\x96\x8a\xc5\x4f\xfd\xfa\xd9\x67\x27\xe6\xe6\
-\x4e\xbd\x68\xe3\xfa\xfb\x07\x30\xb8\x75\xeb\xad\x00\x06\x5e\x95\
-\x63\x56\xe1\x79\x68\x6b\x6b\xff\x4c\x7f\x7f\x7f\x50\x54\x4d\xa7\
-\x9a\xf5\x12\x08\x06\xb1\x74\x66\x09\xfb\x7f\xfb\x9b\xcf\xea\xba\
-\x5e\xae\x5a\xe9\x9a\xcf\x3a\x25\x84\xeb\x55\xa5\x9f\x0f\xb5\x8c\
-\x73\x8d\x52\x0a\x4d\xd3\xca\xe3\x63\x63\x9f\x7d\xfe\x77\xbf\x83\
-\x65\x59\xe7\x56\x00\xa5\x58\xd3\xd3\x13\x8c\x27\x12\x9f\x11\x7f\
-\xc0\xdd\xf8\xd3\x0e\xf9\x38\xc7\xe9\xd3\x8b\xf7\xdc\x7f\xdf\x37\
-\x1e\x13\x42\x62\xdd\xba\x75\x08\x06\x83\x90\x52\x22\x1a\x8d\xe2\
-\x97\xbf\xf8\xc5\x5e\xd3\x34\x1f\xa9\xd1\x5d\x05\x05\xd7\x73\x21\
-\xab\x9a\xd5\xf3\x7c\x9b\xa5\x2e\x3b\x49\x5d\xe1\x3d\xb2\xff\xb7\
-\xbf\xdd\x7b\xf2\xe4\xb8\xdf\x63\x8e\x8d\x91\xe1\x61\x3c\xf1\xc3\
-\x1f\x62\xcf\x53\x4f\x3d\x56\x2a\x16\xef\x79\x25\x67\x64\x2f\x7b\
-\x0c\x58\xc5\xdb\x03\x27\x8e\x1f\xdf\x75\x72\x7c\x7c\xe7\x65\x43\
-\x43\x77\x6f\x7b\xcd\x6b\x07\x2f\xb8\xa0\x0d\x27\x8e\x1f\x77\x87\
-\x8f\x1f\xbb\xcb\x30\x0c\xb5\x52\x0f\x0b\x4f\xd4\xcf\xb4\x84\xe7\
-\x41\x78\x1e\xce\x3f\x81\xd7\x34\x4d\xcd\xcf\xcd\xdd\xf5\xfc\xfe\
-\xfd\xcf\x84\x82\x41\xed\xf8\xf1\xe3\xf8\xdd\xfe\xfd\x87\x52\x33\
-\x33\x9f\x93\x52\xee\x7e\x25\x27\x94\x7f\xd2\x77\x25\x0c\xc3\x80\
-\x52\x6a\xf7\x33\xfb\xf6\xfd\xec\xd8\xd1\xa3\x1f\x19\xba\x7c\xdb\
-\xa7\x8f\x1f\x3b\xf6\x04\x08\xd9\xff\x47\x1c\x03\x28\x05\xa8\x3f\
-\xe0\x2c\x10\x42\xf6\x1f\x3e\x74\xe8\xa1\xa5\xa5\xa5\x1d\xe3\x63\
-\x63\x5f\x34\x4d\xf3\x3e\xce\xb9\xf5\x4a\x83\xff\x93\xbf\xec\x41\
-\x08\x41\x20\x10\xb0\x4a\xa5\xd2\x57\x9e\xfa\xe9\x4f\x1e\xd5\x34\
-\xad\xb2\xf2\x61\x55\xb5\xa4\x81\x00\xa6\x65\xc2\xb4\x2c\x30\x46\
-\x51\x2a\x73\x7f\x41\x84\x68\xe7\xfb\xb0\xb9\x5c\xee\xce\x74\x3a\
-\x7d\x37\x63\x2c\x75\x3e\xfa\xfd\xc5\xbe\xad\x52\x75\xd9\x52\x2f\
-\x5a\x20\xa5\xb0\x2d\x6b\x7a\xdf\xde\xbd\xcf\x13\x42\xc4\x4a\x4f\
-\x47\x29\xc5\x1c\xc7\x99\x7e\x91\x39\x4b\x69\xba\xd6\x3f\x7f\xce\
-\xeb\xff\x06\x00\x7e\xe5\xec\x94\xcb\x86\x8c\xb1\x00\x00\x00\x00\
-\x49\x45\x4e\x44\xae\x42\x60\x82\
-\x00\x00\x0f\xf6\
-\x89\
-\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
-\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x33\x0c\x5e\
-\xf4\xbb\x7b\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
-\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
-\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x0f\x85\x49\x44\x41\x54\
-\x78\xda\xad\x5a\x5b\x6c\x5c\x57\xb9\xfe\x67\xcf\x9e\xab\xc7\xf6\
-\x78\xec\xc4\xb7\x1a\xda\x26\x34\x21\x27\x81\x34\x04\xd2\x3c\xa4\
-\x84\x54\xa4\x2f\x48\x45\x08\x04\xe2\x85\x67\x2a\xf1\x82\x10\xe2\
-\x81\x47\x1e\x10\x42\xbc\x20\xc1\x33\x2f\x5c\x44\x85\x44\x41\xc0\
-\x39\xa4\x6d\xd2\x1e\x29\x4d\x7a\xda\x02\x0d\xc5\x86\x24\x4d\x1a\
-\x3b\xbe\xc4\x97\x19\x7b\x3c\x9e\xdb\xde\xfb\x7c\xdf\xbf\xd6\xda\
-\xb3\xc7\xf1\x39\xa4\x85\x6d\x2f\xed\xfb\x5a\xdf\x7f\xbf\xec\x49\
-\xbd\x76\xf5\xaa\xec\xde\x52\xa9\x94\xa4\xd3\x69\x89\xa2\x48\xc2\
-\x30\x14\xcf\xf3\x74\xcf\x73\x1e\xf3\x7e\x10\x04\x7a\xce\x0d\xf7\
-\xb2\x78\xfe\xc3\x38\xfc\x68\x36\x9b\x3d\x82\xfb\x8f\xe2\x7c\x08\
-\xfb\x01\x7b\x7f\xbb\xdb\xed\x6e\xe2\xf9\x9b\xad\x56\xeb\x6d\x5c\
-\xfa\x33\xce\xff\x86\xb9\xda\x6e\x0e\xce\xeb\xd6\x73\x18\xb8\x06\
-\xf7\x76\x0e\xd9\x6b\xf3\xe5\x7d\x6c\x5c\x08\x83\xef\x9e\xc9\xe7\
-\xf3\x9f\x2f\x14\x8b\x4f\x15\x8b\xc5\x83\x85\x7c\x3e\x9d\xcd\xe5\
-\x24\x93\xc9\x48\x9a\x84\x7a\x6e\xf1\x48\xc1\x74\xda\x6d\x69\xb6\
-\x5a\xb2\xd3\x68\x04\xdb\xdb\xdb\xd7\x1b\x8d\xc6\x0b\xcd\x66\xf3\
-\x39\xcc\xf5\x0a\x46\xf7\xfd\x60\x49\xbd\x17\x09\x70\xe0\x9e\x8f\
-\xed\x8b\x00\xfc\xb5\xe1\x72\xf9\xd4\xe0\xe0\xa0\xe0\xb8\xef\xfd\
-\x56\xbb\x23\x9d\x4e\x07\x60\xdb\x7a\x9e\xcf\x65\x25\x0b\xa2\xb2\
-\xd9\x4c\x8f\x09\x98\x6b\xab\x5e\x97\xcd\x5a\x4d\xaa\xd5\xea\x15\
-\x10\xf4\x43\x48\xe5\x17\x24\x84\xeb\x3f\xa8\x04\x1e\x98\x00\x4e\
-\x86\xfd\x79\x00\xfe\xce\xe8\xe8\xe8\xc7\x87\xcb\xc3\x78\xc6\x08\
-\x70\x75\xa3\x2a\xef\xdc\xb9\x2b\xb7\x31\x56\xd6\xd6\xa5\xb6\x55\
-\x97\x36\x08\x08\x02\xb3\x28\xa5\x41\xf0\xc3\x83\x25\xd9\x3f\x56\
-\x91\x87\x1f\x9a\x92\x87\x67\xa6\x64\x6c\xa4\x6c\x08\x86\x54\xd6\
-\xd7\xd7\x65\xf5\xde\xbd\xd7\xb6\xb6\xb6\xbe\x8d\x35\xff\xcb\xad\
-\xf9\x2f\x13\x60\x81\x97\xa0\xdb\xdf\xdd\xbf\x7f\xff\xb3\x95\xd1\
-\x51\x8a\x40\xd5\xe2\xda\xdc\x75\x79\xfd\xda\xdf\x00\x7e\x41\xb6\
-\x1b\x3b\x7c\x53\x7c\x3f\x2d\x19\xbc\xcb\xf7\x93\x8b\x73\x74\x30\
-\x57\xb7\x1b\x90\xff\x52\x2a\x16\x40\xc8\xb4\x9c\xfc\xc8\x11\xf9\
-\x8f\x43\x07\xc4\xc3\xb3\x3b\x3b\x3b\xb2\xb2\xb2\x12\xad\x2c\x2f\
-\xff\xa8\xdd\x6e\x7f\x0b\xef\xd4\xff\x25\x02\xec\xc2\x87\xcb\x23\
-\x23\x3f\x99\x9c\x98\xf8\xc4\x40\xa9\xa4\xf7\xdf\x9a\xbb\x21\x97\
-\xae\xbc\x2e\x77\x16\x96\x88\x59\x0a\xd0\x7b\xe8\xbf\xe4\xac\xaa\
-\xa4\xd3\x9e\xb1\x01\xb7\xb8\x95\x64\x97\x76\x00\xc9\xb4\x5a\x1d\
-\x69\x34\x9b\xb2\xd3\x6c\xe9\xfd\x99\xa9\x71\x39\xfb\xc4\x49\x39\
-\x06\x42\xb8\xad\xad\xae\xca\xc2\xdd\xbb\x57\x6b\xd5\xea\x57\x70\
-\x3a\xcb\x79\xde\x33\x01\x14\x21\xb6\xd3\x50\x97\xe7\x26\x26\x27\
-\xa7\x20\x01\xa9\x42\x35\x7e\xf7\xe2\x7f\x2b\x01\xe4\x58\x69\xa0\
-\xa8\xa3\x08\xf0\x7e\xc6\xd7\x6b\xff\xdf\xe6\x3c\x0e\xf7\x9d\x6e\
-\x17\x04\x34\x65\x6b\xbb\x21\xf5\x7a\x03\x32\x89\xe4\xe8\x63\x07\
-\xe4\x33\x4f\x9d\x51\x55\x83\x2a\xc9\xc2\xfc\xfc\xdd\xb5\xb5\xb5\
-\xcf\x03\xfc\x65\xf7\xee\x03\x11\xc0\x0d\x6a\x72\x7a\xdf\xbe\x7d\
-\xbf\x05\xf8\x0a\xa5\x71\xfd\xd6\xbc\xfc\xfa\xc2\x25\x59\xdb\xa8\
-\xc9\x60\x69\x40\x86\xb0\x08\xd5\xc0\x4f\x7b\xaa\x3a\x8e\x70\xc7\
-\xf5\xd4\x2e\x62\xac\xe7\xba\x8f\x10\xaa\x68\x1d\xea\x47\xbb\xd9\
-\xaa\x6f\xcb\xe8\xc8\xb0\x7c\xf6\xd3\x9f\x94\x83\x0f\xcf\xd0\x5b\
-\xc9\xbb\x77\xee\xac\xdf\x5b\x59\xf9\x0c\x9e\xbb\x9c\xda\x83\x41\
-\xff\x97\x04\x0e\x8d\x8f\x8f\xbf\x30\x39\x35\x35\x4d\xf0\x7f\x99\
-\xbd\x2e\xbf\xb9\xf0\xb2\x04\x58\x90\xdc\x29\xc3\xf3\x64\xb3\x34\
-\x60\x0f\x92\x32\xa0\xc9\x7d\xba\xcd\x94\xdc\x4f\x40\x64\xd0\xc6\
-\xfb\x50\x89\x09\xd5\x8e\x1c\x61\x6d\x78\xae\x2a\xb8\x5e\xdb\xac\
-\xeb\x9c\xcf\x80\x88\x8f\x1c\x3e\x28\x70\xb5\x72\xfb\xd6\xad\x85\
-\x7b\xf7\xee\x3d\x85\xb7\xe7\xee\x63\xf4\x1e\x54\x95\x60\xa8\x3f\
-\x1d\x9f\x98\x50\xf0\xd7\xfe\x7e\x43\x7e\xf3\xc2\x2b\xe2\xc1\x70\
-\xc7\x86\x06\x95\xf3\xbc\xce\x45\xbc\x14\x7d\xbd\x67\xc0\xa7\x1c\
-\x21\x31\x17\x92\xec\x8f\x77\x24\x83\xfb\x10\x04\x44\xa1\x21\x86\
-\x52\xe0\x9c\x39\x78\xaa\x3c\x54\x75\x63\x73\x0b\xd2\x7e\x59\x59\
-\x71\x0c\x44\xcc\xcc\xcc\x4c\xc3\xc5\xfe\x74\x63\x63\xe3\x93\xb8\
-\x54\xef\x23\x60\xb7\x98\x4b\xa5\xd2\xf7\x26\x26\x26\x4e\x30\x18\
-\xbd\x33\x7f\x57\x7e\x7f\xf1\xb2\x1a\x66\x05\xa2\x1d\x2a\x95\xd4\
-\x40\x09\x5c\x09\xb0\x86\x9a\x24\xa0\xc7\x90\x24\x63\x7a\x2a\x63\
-\x68\x31\x46\xcd\x43\x12\x11\xd0\x61\x50\x22\x41\x1a\xeb\x94\x25\
-\x0d\x66\x6d\x54\x6b\xf2\xbb\x4b\x97\xd5\xc6\x1e\x81\xcb\x9d\x9a\
-\x9a\x3a\x81\xa0\xf7\x3d\x48\xe4\xd9\x24\xd3\x3d\x82\x70\x40\x00\
-\xfa\xdc\xfe\xf1\xf1\xaf\x16\x0a\x05\xd9\x84\x3e\xfe\xe7\xcb\x57\
-\x70\xdd\x93\xb1\xca\x88\x54\xe0\xf7\x49\x48\xc6\xc7\x80\xc1\x66\
-\xfc\xe4\xc8\xc4\xc7\xea\x46\xf5\x38\x6d\x9e\xdb\xf5\x6c\x36\xc3\
-\x67\x7c\x8d\xd6\xf1\xf3\xb8\x96\xb5\xd7\x18\x2f\xb8\xd6\x28\xd6\
-\x24\xa6\x3f\xbc\xfc\x2a\xb0\x34\x04\x5a\x21\x70\xe3\x5f\x85\xa4\
-\xce\x25\x0d\xda\xb7\xde\x86\xdc\xc9\x95\xcb\xe5\xef\x63\xe8\xf9\
-\xc5\x2b\x6f\xaa\x71\x8d\x82\x23\x95\xf2\x90\xe5\xbc\xe1\xba\x49\
-\x13\x12\xfa\x4f\xcd\xb7\x4c\x20\x73\x7a\x76\xd0\xa7\x41\x3d\xf5\
-\xb1\x79\x55\x64\xed\xc1\x9c\x23\x60\x86\xe6\x38\xed\x45\x4a\x04\
-\x37\x3a\x8d\x97\x2e\xbf\x0e\x9b\x38\x23\x60\xae\x6c\x6e\x6e\x7e\
-\x1f\xaa\x74\x1a\x38\x5a\xb1\x0d\x70\x22\x50\xff\x39\xb8\xcc\xc7\
-\x09\xf0\xfa\xed\x79\x04\xa7\x45\x29\x0f\x0d\x61\xa2\x41\xe5\x9a\
-\xc6\x06\x82\x8e\x55\xa8\xa7\x46\x86\x30\x6b\xc0\xf8\x27\x67\x07\
-\xf3\xbe\xe4\x32\x46\x43\xa9\xe3\x8c\x01\xb5\x9d\x0e\xf6\xfd\xc0\
-\x43\x0b\x3a\x74\x04\xe0\xb9\x00\x04\xa4\x31\x1f\x19\xc7\x7b\x37\
-\xa1\xca\xff\x00\xa6\x0f\x7d\xf0\x21\x81\x67\x7c\xbc\x5e\xaf\x7f\
-\x0e\x36\xf1\x33\x62\xf2\xed\x64\x3e\x52\x84\xaf\x33\x50\x31\xfc\
-\xbf\xf1\xd7\x7f\x68\x50\x2a\x0f\x95\x90\xc7\xe4\xe2\xc0\x96\x26\
-\xd0\xb4\x17\x4b\xc1\x81\x4f\x59\x83\xf6\xe1\x99\x46\x06\x10\x13\
-\x52\x26\x3d\x40\x7e\xa3\x60\x91\xf0\xc9\x00\xf2\x25\xa8\xb3\x54\
-\x1b\x2d\x04\xb1\x76\x6c\xc0\x2e\x4a\x07\x96\x90\x80\x73\x71\x1f\
-\xa4\xa4\x80\xf9\xcb\x70\x1c\x1d\x44\xef\xd7\xaf\xcd\xc9\xa3\xb0\
-\x85\x91\x4a\x45\x56\x57\x57\xbf\x8e\xf8\xf0\x4b\xac\xdf\x55\x16\
-\x01\xdc\x69\x24\x66\x27\x09\xf4\xd6\xc2\x5d\x59\xaf\x6d\xc2\x60\
-\x4d\x90\x52\xe0\x16\xb0\x46\x58\xf5\x40\x3d\xee\x67\xb0\x2f\xe6\
-\x7c\xd5\xdd\x10\xc1\xe9\xfa\xdc\xac\x2c\x2f\x2f\xc3\x2d\xb6\x75\
-\x74\x34\x27\x0a\x84\x81\x70\x72\x72\x52\x8e\x1e\x3d\x2a\xa5\x72\
-\x49\xaa\xdb\x4d\xa4\x15\x61\xec\x8d\x02\x47\x08\x80\x77\xc1\x44\
-\x2f\x15\xea\xb5\xd2\x40\x41\x93\x43\x62\xa2\x56\x1c\xfc\xe0\xb4\
-\x40\xcd\x4f\x22\x01\x3c\xcd\x2c\x56\x55\xa8\x58\x28\x7c\x69\x60\
-\x40\x53\x77\xa8\xcf\x82\xba\xb2\x12\x38\x96\xd5\xe8\x6a\x80\xd3\
-\xd8\x0c\x21\x86\x00\x72\x3d\x03\xf3\x19\xc8\xa6\xd5\xf8\xe6\xe7\
-\xe7\x65\x6e\x6e\xae\x2f\x9f\xe7\x75\x17\xac\x18\x59\xe1\xcb\xe5\
-\xda\xb5\x6b\xf2\xc4\x13\x4f\xc8\xa1\x43\x87\x34\x5b\xdd\x6e\x43\
-\x65\xf0\x8a\xa7\x5e\x28\xb4\xcc\x09\x94\x88\x94\xe6\x61\x29\x19\
-\x04\x23\x99\x1c\x52\xb5\x49\xc0\xf0\xf0\xb0\xc0\xd1\x7c\x09\x1e\
-\xe9\x15\x4a\x20\x0b\xf0\x9f\xa6\x67\xa8\xc1\xf3\xac\x55\x37\xe5\
-\x21\xe4\x26\x69\xab\x1e\x3e\x38\xee\xc0\x3b\xf5\x31\xc6\x03\xd5\
-\x48\xa7\xd4\xa3\xcc\xce\xce\xca\xed\xdb\xb7\x8d\x6a\xe1\x79\xeb\
-\xde\xfa\xdc\xb3\x53\x15\xaa\xd5\x85\x0b\x17\x34\xfb\x24\x21\x24\
-\x6e\x1b\x86\xed\xec\x4a\x41\xab\x4d\x21\xf9\xd3\x94\xba\x8b\xc8\
-\x8f\x74\x05\x51\x7f\xf9\xde\x9a\x7a\xc7\x12\x98\x4d\xcc\x48\xfe\
-\xb2\x9e\x97\x4e\x1f\x41\x41\xf2\x08\x17\x5a\x5e\x5d\xd7\x97\xc7\
-\x2a\x65\x19\x46\xba\x40\xf0\x74\x71\x86\x08\xbf\x0f\x54\xce\x53\
-\xc3\x97\x77\xdf\x7d\x57\x6e\xdd\xba\xa5\x1c\xe7\x33\xe9\xc4\xb3\
-\x8e\x20\xdf\xf7\xe3\x7b\x7c\x87\xcf\x5e\x45\x06\xf0\xd6\x5b\x6f\
-\x21\x09\xcc\xc1\x8b\xf5\xdc\xa2\xaa\xac\x6f\xd6\xa4\x06\xf0\x9c\
-\xf1\x67\x1f\x30\x11\xdb\x12\x31\xe2\x1a\x08\x78\x04\xf3\x1f\x41\
-\x80\xf5\x8f\xe7\x72\x39\xdf\xb9\xac\x42\x21\xaf\x5e\x84\xba\x97\
-\xcb\x65\x34\xd7\x49\x43\x02\xb2\x2b\x60\x13\x1c\xf5\x9b\x6a\xe3\
-\x8c\xdc\x81\x76\xc0\xdd\x88\xaf\xd9\xf7\x5c\xaa\x7e\xe9\xd2\x25\
-\x95\x48\x31\x93\xda\x35\xb7\x49\xcb\xa9\xba\x54\x67\xaa\x10\x19\
-\x00\x55\x97\xd5\xf5\x9a\x3e\x03\xa6\x83\xc6\xf4\x71\x2f\x97\xcd\
-\x1e\x66\x80\xa2\x3b\xab\x37\x9a\xf0\x14\x05\x75\x61\x5c\x88\xc7\
-\x29\x2f\xbd\x57\xae\x04\xe2\x72\x72\xe3\xc6\x0d\x35\xd4\xdd\x49\
-\x9c\xcb\x66\xbd\x44\x4a\xad\x7b\xd6\x08\x00\xee\xee\x33\xcf\x79\
-\xf5\xd5\x57\x15\x58\x5a\xa2\xdd\x8b\x28\xa1\x74\x24\x24\x88\xc4\
-\x0c\x14\xf3\x52\xdf\xde\x51\xe2\xb9\x3e\x1c\xc3\x61\xa8\x9e\xf7\
-\x28\x39\xdc\xb5\xbe\x9a\xe5\x9f\x5b\x14\xc4\x61\xf8\x71\xf6\xc8\
-\xab\x14\x23\xdd\x29\x7c\xb1\xaa\x4f\x52\xad\xdc\x7b\x89\xe0\xd8\
-\x17\xb4\x74\x9e\xc4\xf3\xdc\x68\x3f\x35\x94\x95\x59\xa8\x64\x32\
-\x1d\x57\x90\x89\x32\x94\xc1\x92\xa0\x59\x14\xd1\xd5\x67\x8d\x2a\
-\x3e\x4a\x15\x1a\x22\x34\x13\x01\x3d\x4d\x01\x52\x89\xfc\xa5\xc8\
-\x22\xdd\xb7\x93\xb0\xb0\x60\xe6\xd8\xa8\xd1\x17\x2b\x07\x23\xcb\
-\xd1\xbd\x36\xe3\x16\x83\x3e\x22\x92\x00\xb9\x51\x85\x16\x17\x17\
-\x65\x6b\xb3\x86\xe7\x02\x65\x0e\x37\xaa\x31\xed\xa3\xc7\x1d\xd1\
-\x74\x83\x18\xba\x61\x60\x5c\x78\x26\x33\xc4\x5c\x48\x2b\x72\x06\
-\x0b\x23\x72\xaf\xf7\x86\x15\xa5\xda\x03\x0c\xaa\xcd\x22\x04\xa0\
-\xbb\x50\x1b\x06\x2a\xda\x80\xf3\xf5\x49\x2e\x07\xb6\xf2\xe2\x40\
-\xc4\xbc\x8f\x90\xe4\xb1\x23\x82\xcf\x6e\xae\x2e\xeb\x1a\x34\x5e\
-\xd6\x1a\xbb\xd9\xe2\x54\xa9\xd3\x09\x9c\x8a\x16\xfb\xb2\x51\x3f\
-\x51\xc7\xf6\x36\xd3\x0b\x2a\xc2\xb8\x77\xe0\xb7\xdb\xf5\x6a\xcc\
-\x99\xa4\xfe\xdb\x74\xa4\x4f\xa5\x1c\xf8\x64\x40\x4b\x12\x14\xab\
-\xa6\x7d\x9f\x4c\x29\x31\x2e\x21\x72\x33\xce\x84\xc1\xfd\x76\x61\
-\x30\x4a\x6c\x31\x4c\x25\x1a\x3c\xc8\x66\x8c\xcb\x4c\x02\x77\x5b\
-\xd0\x35\x1c\x1b\x41\x58\x1f\xc0\x23\xb5\xea\x86\x82\x25\x08\x82\
-\x4b\xea\xbb\x6b\x50\xb9\x3a\xd6\x3d\xe3\xc0\x07\xaa\xc3\x3d\x09\
-\xf0\x39\xa6\x1a\xdc\x90\x32\x4b\x01\x2e\x93\x7a\x9e\x8e\x4c\x9a\
-\xee\x60\x38\xed\x73\x59\xac\x95\x76\xc3\x67\xc7\x8c\x37\x18\x61\
-\x39\x5c\x95\x44\xb3\x51\x3d\xd6\xfa\xd5\x04\x17\x7a\x90\xe2\x20\
-\xea\x02\x54\x64\xb5\x5a\x55\x5d\x1b\x72\xf4\x58\x75\x08\xd0\x79\
-\x1e\x47\x90\x03\xcb\x7b\x4e\xad\x38\x1c\x01\xec\x29\x31\xc5\x18\
-\x19\x19\x91\x20\x95\x46\xc4\x0d\xb5\x67\x04\x2d\x51\x57\x9a\x64\
-\x28\x9f\x37\xee\x35\x0d\xa6\xaa\x7a\x6e\x92\x5d\x37\xb5\x75\x92\
-\xf6\x54\x02\xba\x60\x64\x8b\x0f\x1c\xb4\x3b\xdd\x44\x0d\x2b\x9a\
-\x74\x75\xe1\xf4\x26\xa7\x1f\x92\x63\xc7\x8e\x69\x2b\x84\xa2\xe7\
-\x9e\xc4\x24\x87\xb3\x13\xee\x55\x8d\xec\x35\xa7\x46\x1c\x27\x4e\
-\x9c\x60\xc5\x25\x91\xe7\x6b\xa6\x6a\x58\x6e\x9c\x85\xb3\x2d\xe3\
-\x10\x8c\xed\xb8\xae\x07\xe7\x60\xab\xd2\xc3\xc4\xb3\xca\x39\x70\
-\x6d\xa0\x90\x53\x57\xca\x24\x8a\x96\xd0\xb1\xe2\x96\x48\xe2\x4a\
-\xca\x64\xaf\x48\x09\x9a\x1d\x39\x75\xea\x14\x23\xa2\x82\x27\xc0\
-\x24\x70\x37\xe2\x73\x18\x7f\xd2\x16\x38\xc8\xfd\x73\xe7\xce\x49\
-\xbd\x05\xe9\x04\xa6\x56\x50\xc6\x99\x5a\x4d\xf3\xa1\x8e\xf6\x91\
-\x0c\xe3\xe8\x3e\x89\x91\xd8\xda\x86\x39\xb3\x1e\x00\xff\x09\x13\
-\x6b\x5f\x72\x08\xe9\x43\xec\x25\xac\x4a\x58\xd8\xb6\x18\x71\x8b\
-\xd0\x40\x01\x24\x5b\x94\xf3\xe7\xcf\x2b\x27\x9d\x24\x08\x78\xb7\
-\x34\x08\x7e\x87\x04\x26\xc0\x73\x7b\xe6\x99\x67\x24\x95\x2b\x5a\
-\x4e\x3b\xf0\x56\x08\x51\xc2\x11\xc4\x9e\x2b\x52\x8c\xdc\x30\x2f\
-\xa0\x07\x7f\xf2\x50\x40\xbc\x8d\x05\xdf\xe1\xc5\x32\x0a\x76\x5a\
-\x79\x07\x6a\x43\xd5\x09\x54\x4f\x7b\x2d\x91\x50\xd5\xa8\xd7\x4d\
-\x68\xc3\xfd\x1d\x38\x7a\x5c\xce\x9e\x3d\xab\x20\xe8\x0e\x1d\x01\
-\xc9\xd1\xb4\xee\xd4\x19\x32\xb7\xa7\x9f\x7e\x5a\x8e\x3c\x7e\x52\
-\xdd\xb2\x59\x23\x54\x26\x85\x56\xc2\x8e\x98\x40\xbd\x58\x57\x31\
-\x31\xad\x61\x8d\xc2\x67\xb1\xd6\x3b\x60\xdc\xdb\x4c\xa7\xdb\x58\
-\xe4\x8f\x38\xf9\x50\x11\xe2\x61\x1d\xc0\x66\x93\xa7\xdd\xb5\x94\
-\x61\x46\x92\x08\xa6\x01\x6a\xa0\x10\x24\x32\xc6\x26\x22\xf2\x93\
-\x67\x3f\x25\x43\xa8\xde\x9e\x7f\xfe\xf9\xd8\xa8\xfb\x1c\x71\xa2\
-\x6d\x4e\xb5\x21\xe7\x4f\x7c\xec\x63\x52\xad\x6f\xc7\xb6\xd6\x8b\
-\x23\x12\xb7\x60\xdc\x3c\x8c\x0d\x6d\x48\x9c\xdc\x2f\xc2\x85\x33\
-\x80\x62\xfc\x91\xb7\x7c\x3e\x02\x2e\xfc\xbc\xb9\xb3\xf3\x6c\x11\
-\xfa\x3c\x3e\x3a\xa2\x1d\x38\x72\x99\x76\x41\x8f\x10\xd1\xb7\x47\
-\xa6\x83\x10\xa5\x38\x1c\x41\x29\x8d\x9e\xec\xe5\x1c\xfb\xe8\x71\
-\x79\xec\xb1\xc7\xe4\xe2\xc5\x8b\x9a\x65\xa2\x76\x8d\x01\xd0\x33\
-\x55\x50\x49\xd1\xe8\x9f\x3c\xf3\xa4\xe4\xb1\x4e\x75\xab\x61\x2b\
-\x32\xc7\xf5\x5e\xbf\xc8\xb4\x5d\x7a\x04\x39\x83\x1f\xdf\x37\xa2\
-\xf3\xd5\x51\x5b\x80\xe9\x3f\xd7\x18\xf4\x57\x14\x18\xb8\xe9\x8f\
-\x8d\x8d\x5d\x9e\x98\x9c\x3c\x49\x91\xfd\x79\xf6\x86\x56\x41\xcc\
-\x3d\xd8\xe7\x49\x96\x8f\xae\x98\xd1\xe3\x94\x6d\xab\x78\xb6\x9f\
-\xca\x2c\x36\x9f\x55\x17\xd7\x80\x64\xb6\x2c\x11\xa8\xf6\xa4\x00\
-\xd0\xdd\x4c\x5e\xda\x3b\x4d\xbd\xef\xb8\x6e\x08\x08\x8d\xe7\xb3\
-\xb5\x71\xaf\x4e\x8e\xd4\x78\x5b\xb0\x9f\x3c\x72\xa2\xe3\x87\x0f\
-\x2a\xc3\x6e\xde\xb8\xf1\x3f\x48\x65\x58\xd8\x77\x3d\x9b\xde\x76\
-\xb7\xea\xf5\x1f\xb4\x20\x7e\x82\x9d\x99\xd8\xaf\x5d\x64\x8d\x96\
-\x36\x16\x38\x8e\xf4\x16\xb0\x89\x9a\xfa\x7a\x73\xde\x85\xae\xd7\
-\xea\x0d\xd9\x6e\xc1\x48\xf3\x25\x19\x9e\x9c\xd1\x11\xe5\x06\xf4\
-\xda\xce\xe6\x96\x04\x9d\x76\xdf\xfb\xfa\x67\xc1\xf7\x71\xdf\xae\
-\xab\x46\x0f\x2c\x33\x13\xfb\x34\x95\xa0\x64\x91\x48\xfe\x00\x31\
-\xa8\xab\xa9\xba\x0b\xe5\x9d\x76\xfb\x57\xa8\x33\xdf\xe4\xf9\xbe\
-\xca\xb0\x8c\x96\x07\xd5\xed\x85\x31\xa7\x0c\x47\x0c\xe7\x42\x6b\
-\xd4\x91\x02\x88\xfa\x88\x32\x8b\x76\xd5\xef\xef\xa8\xef\xef\xda\
-\x48\x1c\x5a\x62\x63\x4e\x5b\x09\xf4\xce\x25\x6e\xb1\xb8\x7b\x74\
-\x0e\xc4\xc2\x82\x86\x0e\x60\x6d\x75\xf5\x4d\x5c\xfb\x55\x9c\xf9\
-\x3a\xfd\x02\xa0\xd6\x46\xb5\xfa\x8d\x06\x3c\x09\x37\x76\x00\xd8\
-\xea\x33\x51\xd3\x26\x69\x04\x60\x89\xd0\xf7\x02\x03\x28\xd0\xc5\
-\x1c\xc0\x30\x6e\x97\x38\xc9\x84\xd6\x87\x3b\x75\x71\x05\xbc\x01\
-\x6f\xbd\x0e\x6c\x3c\x88\x6d\xc1\x04\xae\xae\x4d\xec\x88\x85\xdb\
-\xfa\xda\x1a\xbf\xe6\x7c\x83\x58\x1d\x6e\x2f\x99\xe6\x42\x05\x5e\
-\x44\xe1\xfd\x63\xbe\xc8\xba\xe0\x00\x5e\x24\x37\x3a\xea\x8b\x25\
-\xee\xe7\x04\x7d\x06\x67\xbb\x09\x96\xc0\x30\x72\xe0\x02\xcb\xd5\
-\xdd\xc7\x49\xc3\xb5\xcc\x23\x81\x51\x68\x25\x11\xc5\xe0\x39\xf7\
-\x81\x0f\x4c\x2b\x16\xba\xe8\x95\x95\x15\x62\x7b\xd1\x79\x36\xfd\
-\xe8\x98\xec\x69\xd2\x1e\xf0\xe0\x37\xd7\xd6\xd6\xde\xe0\xcd\x91\
-\xe1\x41\xa5\x9e\x85\xb5\x4a\x22\xb2\xe2\x4e\xf4\x72\x82\x84\x2d\
-\x38\xa3\xeb\x8d\xd0\x8e\xde\x79\xd8\x47\x64\x82\x21\xbb\xc0\x73\
-\x4d\xae\x5d\x19\x36\xaa\xbc\xb4\xb8\xf8\x06\x5c\xe7\x37\xbd\xb8\
-\x03\x68\x86\xbf\xc7\x87\x83\x3a\xf4\xec\xcb\xc8\xf8\x5e\xa8\x8c\
-\x8e\x4e\xd3\xad\x52\xdb\x6e\xce\x2f\x6a\x1e\xce\x64\xca\xf3\x8c\
-\x0b\xd5\x6c\xd1\x36\x77\x23\x3d\x74\x95\x9b\xab\x25\x7a\x89\x6d\
-\x14\xa7\x95\xbd\x88\xdb\x73\x95\x4e\xa2\xd4\x82\xae\x12\x48\xce\
-\x73\x6d\xda\xce\xd2\xd2\xd2\xc2\xfa\xfa\xfa\x97\x89\x6d\x77\xba\
-\xbf\xe7\x67\x56\x4c\x38\x07\x37\xf5\x05\x94\x9b\xbf\x45\x96\x58\
-\xd9\x8f\x89\x98\xc2\xde\xbc\xb3\xa4\x9f\x4a\x7d\xcd\xfb\x1d\x46\
-\x72\x30\xd1\x9d\x96\x44\x67\x3d\xea\x9b\x33\xde\xbb\x20\x15\xa7\
-\x26\x74\x06\x81\x51\xd5\x2c\xaa\xbf\x47\x66\xa6\x95\xf3\x0a\x7e\
-\x71\x71\x1d\x0c\xfd\x02\x31\xed\x85\x35\x75\x0d\x41\xe7\xbe\x8b\
-\xda\x42\xd4\x6e\xc2\xe9\xb1\xd1\xd1\xe7\x46\x2a\x95\x29\x5e\x63\
-\x23\xea\xf6\xc2\x92\x54\x11\xb8\x52\xb6\xbb\x90\xf2\x92\xc0\x53\
-\x7d\x8c\xdf\xdd\xdd\x8d\x12\xc4\x44\x09\x9b\x0a\xbb\xc6\x46\x98\
-\x26\x3c\x3c\x3d\xa1\x3a\x4f\x95\x5d\x5e\x5a\xba\x0b\x46\xea\x27\
-\xa6\x64\x01\xf4\x40\x04\x30\x28\x69\x1a\x1b\x04\x87\x11\x45\x7f\
-\x32\x3a\x36\xf6\x09\xdf\x77\x9f\x55\x6b\xb2\xb8\xb2\x26\x8d\x66\
-\xcb\x12\x6b\x4a\x51\xd3\x99\xb6\xc0\xa3\x7e\x15\x92\xd4\x2e\x29\
-\x84\xbd\x82\x9f\xe9\xc1\xe4\xfe\x51\x19\x1b\x31\x1d\x69\xe6\x47\
-\x4b\xcb\xcb\x57\x37\xd6\xd7\xbf\x82\xf9\x67\x5d\x52\xf7\x9e\x09\
-\xd0\x8f\xcd\x4c\xb5\xd3\xe9\x52\x21\x9f\xff\x2e\x88\x78\x16\xe9\
-\xb3\xc2\xe2\xe2\xeb\xb5\x2d\x25\x86\x6d\x78\xfd\x26\x9c\xea\x7d\
-\x6e\xb2\x94\x24\x8d\x20\xd6\x79\xb2\x9f\x39\x3d\xeb\xde\xb1\xf2\
-\xb0\x76\xc0\xdd\xc7\x74\xb8\x49\xfd\xcc\x8a\xcc\xf6\x5b\xe0\x7a\
-\xdd\x55\x78\xef\x9b\x80\xd0\x7e\xfe\xa1\x07\x01\xb0\xf3\x43\xc3\
-\xc3\xdf\x29\x97\xcb\x1f\x77\x65\xa0\x72\x0c\x92\xd8\xe4\xd7\x46\
-\x10\xc2\x63\xcd\xe1\xad\x7a\x70\x73\xc6\xce\x8e\x07\xeb\x69\x02\
-\x1f\xd4\xaf\x9b\xb9\x58\x22\x6c\xd3\xc0\xcf\xbf\x86\x48\xfb\x6d\
-\x00\xd7\x0f\xdd\xae\xc2\xfb\xb7\x10\xe0\x5c\x28\x7f\x6a\x80\xba\
-\xf4\x8b\x48\xfc\xbe\x36\x38\x38\x78\x8a\x84\xb8\x7e\xa8\x4a\x26\
-\xd1\xf7\x77\xa9\xb3\x69\x35\xf6\xbe\x25\xb8\x8d\x7a\xce\xcc\x72\
-\xb3\x56\xbb\x82\x54\xe6\x87\x88\x43\xbf\x70\xbf\x99\xe0\xda\xff\
-\x76\x02\xc4\xfe\x5a\x85\x1b\x24\xc2\x54\xfc\x4c\x81\x3f\xf6\x28\
-\x14\x9e\xca\x17\x0a\x07\xb3\xd9\x6c\x5a\x7b\xa0\x26\xbb\xeb\x9b\
-\x33\xd9\x6e\x81\x7a\x04\xd0\x73\xfd\xb1\x07\xb2\x4a\xfd\xb1\x07\
-\xe6\xea\x3a\xd5\x74\xc5\xfe\x83\x10\xf0\xbe\x7e\xad\x62\x5d\x26\
-\x67\x7c\x69\xa7\xd9\x7c\x09\xc1\x8f\xc8\x3f\x0c\xee\xea\xcf\x6d\
-\x84\x3f\xb7\xf1\xbc\x21\x78\x2a\xf3\x73\x9b\x20\xd8\x06\xf8\xcd\
-\xd0\xfe\xdc\x06\x92\x8c\x7f\x6e\x93\x98\x6f\x4f\x80\xff\x6c\xfb\
-\x5f\xb5\xb8\x45\x3e\xe2\x04\x60\xdc\x00\x00\x00\x00\x49\x45\x4e\
-\x44\xae\x42\x60\x82\
-"
-
-qt_resource_name = "\
-\x00\x05\
-\x00\x6f\xa6\x53\
-\x00\x69\
-\x00\x63\x00\x6f\x00\x6e\x00\x73\
-\x00\x0e\
-\x0d\x8d\xf4\xe7\
-\x00\x73\
-\x00\x74\x00\x6f\x00\x70\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x08\
-\x0b\x63\x58\x07\
-\x00\x73\
-\x00\x74\x00\x6f\x00\x70\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x0c\
-\x00\x3e\x02\x9f\
-\x00\x50\
-\x00\x79\x00\x43\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x69\x00\x63\x00\x6f\
-\x00\x0e\
-\x0e\xdf\xf7\x87\
-\x00\x70\
-\x00\x6c\x00\x61\x00\x79\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x08\
-\x02\x8c\x59\xa7\
-\x00\x70\
-\x00\x6c\x00\x61\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x0a\
-\x06\x88\x40\x07\
-\x00\x72\
-\x00\x65\x00\x63\x00\x6f\x00\x72\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x0b\
-\x0c\x4d\x7c\x67\
-\x00\x70\
-\x00\x72\x00\x6f\x00\x63\x00\x65\x00\x73\x00\x73\x00\x2e\x00\x70\x00\x6e\x00\x67\
-\x00\x0f\
-\x0c\x1d\x7e\x67\
-\x00\x72\
-\x00\x65\x00\x63\x00\x6f\x00\x72\x00\x64\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\
-"
-
-qt_resource_struct = "\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x02\
-\x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x20\x64\
-\x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x99\x71\
-\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xa6\
-\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x10\x2e\
-\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x00\xd0\xe6\
-\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x01\x00\x00\xbc\x19\
-\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x00\x66\x00\x00\x00\x00\x00\x01\x00\x00\x88\xae\
-"
-
-def qInitResources():
- QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-def qCleanupResources():
- QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-qInitResources()
+# -*- coding: utf-8 -*-
+
+# Resource object code
+#
+# Created: Fr 13. Jan 16:46:39 2012
+# by: The Resource Compiler for PyQt (Qt v4.5.2)
+#
+# WARNING! All changes made in this file will be lost!
+
+try:
+ from PyQt4 import QtCore
+except ImportError:
+ from PySide6 import QtCore
+
+qt_resource_data = "\
+\x00\x00\x10\x2a\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x30\x04\x7b\
+\x02\x60\x8a\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
+\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
+\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x0f\xb9\x49\x44\x41\x54\
+\x78\xda\xad\x5a\x5b\x8c\x5d\x57\x79\xfe\xf7\xe5\x5c\xe7\xcc\xcc\
+\xb9\x8c\x67\xc6\xe3\x4c\x73\xb3\x8b\x49\x4b\x0a\xc8\x40\xfd\x10\
+\x51\x11\xc1\x13\x52\x11\x02\x81\x78\xe1\x99\x48\xbc\x44\x28\xe5\
+\x01\xa9\x2f\x3c\x20\x84\x78\x41\x6a\x1f\xfa\x84\x84\x28\xa8\x11\
+\x12\x02\x41\x5b\x48\x42\x92\x4a\xae\x13\x92\xd0\xc6\x0d\x36\x18\
+\x27\xae\x3d\xf6\x8c\xe7\x3e\x73\xae\xfb\xda\xef\xfb\xd7\x5a\xe7\
+\xec\x33\x63\xa8\x13\x58\xa3\x3d\xfb\xbe\xd6\xf7\xdf\x2f\xfb\x78\
+\x2f\xbf\xf4\x92\x1c\x1d\x9e\xe7\x49\x10\x04\x92\xe7\xb9\x64\x59\
+\x26\xbe\xef\xeb\x9e\xe7\x3c\xe6\xfd\x34\x4d\xf5\x9c\x03\xf7\xca\
+\x78\xfe\xdd\x38\xfc\xab\x72\xb9\xfc\x08\xee\x3f\x84\xf3\x39\xec\
+\x67\xec\xfd\x5e\x92\x24\x07\x78\xfe\xda\x68\x34\x7a\x03\x97\xfe\
+\x0b\xe7\xbf\xc6\x5c\x91\x9b\x83\xf3\xba\xf5\x1c\x06\xae\xc1\xbd\
+\x9d\x43\xee\x36\x42\x79\x07\x83\x0b\x61\xe3\xbb\x8f\x55\xab\xd5\
+\x4f\xd5\xea\xf5\xc7\xeb\xf5\xfa\xe9\x5a\xb5\x1a\x94\x2b\x15\x29\
+\x95\x4a\x12\x90\x50\xdf\x2d\x9e\x2b\x98\x38\x8a\x64\x38\x1a\xc9\
+\xa0\xdf\x4f\x7b\xbd\xde\xd5\x7e\xbf\xff\xcc\x70\x38\x7c\x1a\x73\
+\xbd\x88\x2d\x79\x27\x58\xbc\xb7\x23\x01\x6e\xb8\x17\x62\x7c\x06\
+\x80\xbf\x38\xdf\x6c\x7e\x68\x76\x76\x56\x70\x3c\xf5\xfe\x28\x8a\
+\x25\x8e\x63\x80\x8d\xf4\xbc\x5a\x29\x4b\x19\x44\x95\xcb\xa5\x09\
+\x13\x30\xd7\x61\xb7\x2b\x07\xfb\xfb\xb2\xb7\xb7\x77\x11\x04\x7d\
+\x0b\x52\xf9\x3e\x09\xe1\xfa\xf7\x2a\x81\x7b\x26\x80\x93\x61\xff\
+\x31\x00\xfe\x6a\xa7\xd3\xf9\xc0\x7c\x73\x1e\xcf\x18\x01\x6e\xed\
+\xee\xc9\x9b\x37\x6e\xc9\x75\x6c\x77\xb6\x77\x64\xff\xb0\x2b\x11\
+\x08\x48\x53\xb3\x28\xa5\x41\xf0\xf3\xb3\x0d\x59\x5c\x68\xcb\x03\
+\xf7\xad\xc8\x03\xab\x2b\xb2\xd0\x6a\x1a\x82\x21\x95\x9d\x9d\x1d\
+\xd9\xda\xdc\x7c\xf9\xf0\xf0\xf0\x2b\x58\xf3\xdf\xdd\x9a\x7f\x34\
+\x01\x16\x78\x03\xba\xfd\xb5\xc5\xc5\xc5\x27\xda\x9d\x0e\x45\xa0\
+\x6a\x71\xe9\xca\x55\x79\xe5\xd2\xaf\x01\x7e\x4d\x7a\xfd\x01\xdf\
+\x94\x30\x0c\xa4\x84\x77\xf9\x7e\x71\x71\x6e\x31\xe6\x4a\x92\x94\
+\xfc\x97\x46\xbd\x06\x42\x4e\xc9\xb9\x47\x1f\x91\xbf\x78\xd7\xc3\
+\xe2\xe3\xd9\xc1\x60\x20\x77\xee\xdc\xc9\xef\x6c\x6c\xfc\x43\x14\
+\x45\x5f\xc6\x3b\xdd\x3f\x8a\x00\xbb\xf0\xd9\x66\xab\xf5\xed\x93\
+\xcb\xcb\x1f\x9c\x69\x34\xf4\xfe\xeb\x57\x7e\x27\xcf\x5f\x7c\x45\
+\x6e\xac\xad\x13\xb3\xd4\xa0\xf7\xd0\x7f\xa9\x58\x55\x09\x02\xdf\
+\xd8\x80\x5b\xdc\x4a\x32\xa1\x1d\x40\x32\xa3\x51\x2c\xfd\xe1\x50\
+\x06\xc3\x91\xde\x5f\x5d\x59\x92\xbf\xf9\xeb\x73\xf2\x1e\x10\xc2\
+\xb1\xbd\xb5\x25\x6b\xb7\x6e\xbd\xb4\xbf\xb7\xf7\x79\x9c\x5e\xe6\
+\x3c\x6f\x9b\x00\x8a\x10\xe3\x3c\xd4\xe5\xe9\xe5\x93\x27\x57\x20\
+\x01\xd9\x83\x6a\xfc\xe4\xd9\xff\x50\x02\xc8\xb1\xc6\x4c\x5d\xb7\
+\x3a\xc0\x87\xa5\x50\xaf\xfd\xa1\xe1\x3c\x0e\xf7\x71\x92\x80\x80\
+\xa1\x1c\xf6\xfa\xd2\xed\xf6\x21\x93\x5c\xfe\xf2\xcf\x1f\x96\x8f\
+\x3f\xfe\x98\xaa\x1a\x54\x49\xd6\x6e\xde\xbc\xb5\xbd\xbd\xfd\x29\
+\x80\xbf\xe0\xde\xbd\x27\x02\x38\xa0\x26\xe7\x4f\x9c\x38\xf1\x63\
+\x80\x6f\x53\x1a\x57\xdf\xba\x29\x3f\xfc\xf9\xf3\xb2\xbd\xbb\x2f\
+\xb3\x8d\x19\x99\xc3\x22\x54\x83\x30\xf0\x55\x75\x1c\xe1\x8e\xeb\
+\xde\x11\x62\xac\xe7\x3a\x46\x08\x55\xb4\x0b\xf5\xa3\xdd\x1c\x76\
+\x7b\xd2\x69\xcd\xcb\x27\x3e\xfa\x61\x39\xfd\xc0\x2a\xbd\x95\xfc\
+\xef\x8d\x1b\x3b\x9b\x77\xee\x7c\x1c\xcf\x5d\xf0\xee\xc2\xa0\xdf\
+\x27\x81\x77\x2d\x2d\x2d\x3d\x73\x72\x65\xe5\x14\xc1\xff\xf7\xe5\
+\xab\xf2\xa3\x9f\xbf\x20\x29\x16\x24\x77\x9a\xf0\x3c\xe5\x32\x0d\
+\xd8\x87\xa4\x0c\x68\x72\x9f\x6e\xd3\x93\xe3\x04\xe4\x06\xed\x78\
+\x9f\x29\x31\x99\xda\x91\x23\x2c\x82\xe7\xda\x03\xd7\xf7\x0f\xba\
+\x3a\xe7\xdf\x82\x88\x47\xcf\x9e\x16\xb8\x5a\xb9\xfe\xd6\x5b\x6b\
+\x9b\x9b\x9b\x8f\xe3\xed\x2b\xc7\x18\x7d\x17\xaa\x1a\x30\xd4\xef\
+\x2e\x2d\x2f\x2b\xf8\x4b\xbf\xf9\x9d\xfc\xe8\x99\x17\xc5\x87\xe1\
+\x2e\xcc\xcd\x2a\xe7\x79\x9d\x8b\xf8\x1e\x7d\xbd\x6f\xc0\x7b\x8e\
+\x90\x31\x17\x8a\xec\x1f\xef\x48\x06\xf7\x19\x08\xc8\x33\x43\x0c\
+\xa5\xc0\x39\x2b\xf0\x54\x55\xa8\xea\xee\xc1\x21\xa4\xfd\x82\xb2\
+\xe2\x3d\x20\x62\x75\x75\xf5\x14\x5c\xec\x77\x77\x77\x77\x3f\x8c\
+\x4b\xdd\x29\x02\x8e\x8a\xb9\xd1\x68\x7c\x7d\x79\x79\xf9\xfd\x0c\
+\x46\x6f\xde\xbc\x25\x3f\xfd\xc5\x05\x35\xcc\x36\x44\x3b\xd7\x68\
+\xa8\x81\x12\xb8\x12\x60\x0d\xb5\x48\xc0\x84\x21\x45\xc6\x4c\x54\
+\xc6\xd0\x62\x8c\x9a\x87\x24\x22\xa5\xc3\xa0\x44\xd2\x00\xeb\x34\
+\x25\x00\xb3\x76\xf7\xf6\xe5\x27\xcf\x5f\x50\x1b\x7b\x10\x2e\x77\
+\x65\x65\xe5\xfd\x08\x7a\x5f\x87\x44\x9e\x28\x32\xdd\x27\x08\x07\
+\x04\xa0\x3f\xb2\xb8\xb4\xf4\x85\x5a\xad\x26\x07\xd0\xc7\x7f\x7b\
+\xe1\x22\xae\xfb\xb2\xd0\x6e\x49\x1b\x7e\x9f\x84\x94\x42\x6c\x30\
+\xd8\x52\x58\xdc\x4a\xe3\x63\x75\xa3\x7a\x1c\x98\xe7\x8e\x3c\x5b\
+\x2e\xf1\x99\x50\xa3\xf5\xf8\x79\x5c\x2b\xdb\x6b\x8c\x17\x5c\xab\
+\x83\x35\x89\xe9\x5f\x5f\xf8\x4f\x60\xe9\x0b\xb4\x42\xe0\xc6\xbf\
+\x00\x49\x7d\xa4\x68\xd0\xa1\xf5\x36\xe4\x4e\xa5\xd9\x6c\x7e\x03\
+\x9b\x9e\xff\xe2\xe2\x6b\x6a\x5c\x1d\x70\xa4\xdd\x9c\xb3\x9c\x37\
+\x5c\x37\x69\x42\x41\xff\xa9\xf9\x96\x09\x64\xce\xc4\x0e\xa6\x34\
+\x68\xa2\x3e\x36\xaf\xca\xad\x3d\x98\x73\x04\xcc\xcc\x1c\x07\x7e\
+\xae\x44\x70\xd0\x69\x3c\x77\xe1\x15\xd8\xc4\x63\x02\xe6\xca\xc1\
+\xc1\xc1\x37\xa0\x4a\xe7\x81\x63\x34\xb6\x01\x4e\x04\xea\x3f\x09\
+\x97\xf9\x3e\x02\xbc\x7a\xfd\x26\x82\xd3\x6d\x69\xce\xcd\x61\xa2\
+\x59\xe5\x9a\xc6\x06\x82\x1e\xab\xd0\x44\x8d\x0c\x61\xd6\x80\xbd\
+\x89\x01\x3b\x42\xd4\x7c\x0b\xfa\x5f\x04\x9e\x59\xd0\x99\x23\x00\
+\xf6\x90\x82\x80\x00\xf3\x91\x71\xbc\x77\x0d\xaa\xfc\x5b\x60\x3a\
+\x73\xff\x7d\x02\xcf\xf8\xbe\x6e\xb7\xfb\x49\xd8\xc4\x3f\x73\x9d\
+\xd0\x4e\x16\x22\x45\x78\x92\x81\x8a\xe1\xff\xd5\xff\xf9\xad\x06\
+\xa5\xe6\x5c\x03\x79\x4c\x65\x1c\xd8\x02\x02\x0d\xfc\xb1\x14\x1c\
+\x78\xcf\x19\xb4\x67\xd8\xee\x80\x8f\xcd\xc0\x82\x2f\xba\x53\x67\
+\xc0\x2e\x4a\xa7\x96\x90\x94\x73\x71\x9f\x7a\x52\xc3\xfc\x4d\x38\
+\x8e\x18\xd1\xfb\x95\x4b\x57\xe4\x21\xd8\x42\xab\xdd\x96\xad\xad\
+\xad\x27\x11\x1f\xfe\x05\xeb\x27\x6a\xc4\x00\x77\x1e\x89\xd9\x39\
+\x02\x7d\x6b\xed\x96\xec\xec\x1f\xc0\x60\x4d\x90\x52\xe0\x16\xb0\
+\x46\x58\xf5\x40\x13\xee\x87\xf6\x7c\x62\xaa\x77\x1f\xde\xf4\x3f\
+\xe3\x4e\x99\x5e\x20\xa0\xa9\x21\x3b\x42\x00\x3c\x01\x13\x7d\x2f\
+\xd3\x6b\x8d\x99\x9a\x26\x87\xc4\x44\xad\x38\x7d\xff\x29\x81\x9a\
+\x9f\x43\x02\x78\x9e\x59\xac\xaa\x50\xbd\x56\xfb\xec\xcc\x8c\xa6\
+\xee\x50\x9f\x35\x75\x65\x0d\x64\x98\x65\x8d\xae\x06\x38\x8d\xcd\
+\x10\x62\x00\x7b\x56\xff\x0f\x90\x51\xf6\xc3\x81\x24\xcb\x23\x49\
+\x67\xb2\xa3\x8c\x1f\x83\x2d\xaa\x91\x5e\x4a\x72\x69\xad\xb5\xa5\
+\x59\x6e\x2a\x01\xbe\x7a\xa1\xcc\x32\x27\x55\x22\x3c\xcd\xc3\x3c\
+\x99\x05\x23\x99\x1c\x52\xb5\x49\xc0\xfc\xfc\xbc\xc0\xd1\x7c\x16\
+\x1e\xe9\x45\x4a\xa0\x0c\xf0\x1f\xa5\x67\xd8\x87\xe7\xd9\xde\x3b\
+\x90\xfb\x90\x9b\x04\x56\x3d\xc8\x61\x07\xde\xa9\x4f\xc1\x85\xc9\
+\x77\xee\xff\x27\xb9\x78\xf8\x9c\xa4\x8d\x81\x04\xe5\x40\xca\xd9\
+\x8c\x94\x3c\x93\x36\xc7\x19\xf2\x1e\xaf\x27\x19\x54\x20\xec\x55\
+\x25\x8f\x01\x74\x50\x92\x0c\xfb\xea\x8d\x79\x39\xf3\xe8\x19\x79\
+\xaa\xfb\xf7\x92\xa0\x14\x70\x76\xa5\xa0\xd5\xa6\x20\x1d\x4d\xa9\
+\x13\x44\x7e\xa4\x2b\x88\xfa\x1b\x9b\xdb\xea\x1d\x1b\x60\x36\x31\
+\x23\xf9\x2b\xfb\x7e\x10\x3c\x82\x82\xe4\x41\x2e\xb8\xb1\xb5\xa3\
+\x2f\x2f\xb4\x9b\x32\x8f\x74\x81\xe0\xe9\xe2\x0c\x11\xe1\x14\x78\
+\x37\x86\xcd\x81\xe4\x7e\x36\x36\x5c\x90\x2a\x95\x1c\x29\x06\xfe\
+\x82\x1c\xa0\x32\x80\xc2\x96\x79\x09\xc0\xe1\x99\x04\x92\xc3\x96\
+\x06\xa9\x44\x95\xe8\xd8\x7c\xaa\xb2\xa1\x59\x93\x1a\xc0\x73\xc6\
+\x9f\x13\xc0\x44\x6c\xeb\xc4\x88\x6b\x20\xe0\x41\xe0\x79\x04\x01\
+\x36\x7c\x6f\xa5\x52\x09\x9d\xcb\xaa\xd5\xaa\xea\x9f\xa9\x7b\x95\
+\x4a\x49\x73\x9d\x00\x12\x90\x23\x01\x9b\xa7\x9c\x30\xc8\xe0\xa1\
+\x7c\x63\xbc\x3c\x26\x27\x79\x13\xa4\xe3\x1f\xae\x79\x65\xec\xa1\
+\xe3\x95\x58\x12\x02\x0e\x73\x66\x20\x6a\xf0\xb5\xb8\x6a\x5c\xef\
+\x11\x22\xa8\x36\x2a\x75\xac\x4d\x75\xa6\x0a\x91\x81\x50\x75\xd9\
+\xda\xd9\xd7\x67\xc0\x74\xd0\x18\xbc\xd7\xaf\x94\xcb\x67\x19\xa0\
+\xe8\xce\xba\xfd\xa1\xcc\x40\x54\x81\x35\x50\x1e\x7b\xd0\xf9\x63\
+\x06\x69\xe3\x81\x26\x63\x1e\x8d\x50\xa4\xb2\x5b\x57\x5f\x1e\xe7\
+\xb1\x0c\xa4\x07\x1d\x4e\x60\x84\x48\x9f\xbd\xa1\x71\xd3\xc3\xda\
+\xb1\x79\x22\x3f\x51\xfb\x50\xd5\x39\xbe\x88\x72\x9f\x8e\x84\x04\
+\x91\x98\x99\x7a\x55\xba\xbd\x81\xce\x07\xa6\x23\xe8\x95\xcf\x86\
+\x08\x40\x0f\x91\xc3\x51\x9c\x6a\xbe\x3e\x5b\xab\x8e\xd5\x01\xc4\
+\xe9\xc3\xf4\x02\x8e\x53\xee\x1e\xbd\xc7\xa8\x34\x02\xc0\x91\x04\
+\x71\x09\xaa\xd4\x13\x0f\xcc\x4d\xa1\xf3\xa5\xac\x8a\xeb\x91\x01\
+\x0f\x63\xcc\xd3\x5c\x12\x01\xf7\x63\xcc\xb9\x33\x63\xbc\x0e\x9e\
+\x8d\x40\x6a\x1f\x35\x01\x19\x46\x95\xe1\xcc\x59\x21\x53\xad\xd8\
+\xc8\xac\x55\x21\xee\x12\x34\x33\x56\xba\x7a\x32\x9d\xcd\x03\xd6\
+\xb7\x73\x94\xb9\x89\x80\xbe\xa6\x00\x2e\xfc\x70\xd4\xf1\x12\x6b\
+\x09\x72\x94\x2a\x93\xda\xcc\x91\xc4\x46\xb5\x91\xf8\xc8\x5f\x72\
+\x14\x59\xe1\xa8\x22\x49\x40\xc0\xb8\xee\xf7\xcd\x0c\x99\xf1\xf7\
+\xe4\x3e\x0d\x98\x9e\x27\x2d\x81\x70\x78\xad\xd2\x61\x43\xf2\xc0\
+\x44\xe5\x18\xcc\x53\x9d\x07\xd8\xc0\xae\x41\x35\xae\x55\x2b\x63\
+\x82\x08\x8a\xe9\x06\x31\x24\x90\xac\x62\x2d\x95\xe6\x98\x4a\xd4\
+\x0d\x47\x53\x9b\x13\x39\x43\xf5\xc6\xa2\xa4\x3d\xb0\xdc\x23\xb7\
+\xa2\x38\x51\xee\xf0\x6e\x25\x2e\x03\x34\x08\x1b\x85\x92\xc2\xcd\
+\x05\xdd\xaa\xda\x43\x5c\x1d\xe8\xab\x04\x4e\xce\x13\x7c\x30\x2a\
+\xab\xf7\x89\xca\x43\x09\x86\x25\xb2\x5a\x68\x26\x04\x44\xe7\x4b\
+\x86\xa4\xc3\x4c\x0d\x97\xc0\x59\x24\xc9\x91\x22\xc6\xa9\x12\x09\
+\x2e\xc1\x3e\x89\x7d\xca\xad\x84\x85\x3a\x76\x32\x72\x7d\xb1\x6e\
+\x55\x8b\xe2\x53\x55\xf2\x35\x67\x30\x59\x64\x9c\x19\x50\xc8\x4e\
+\x92\x3c\x12\x6f\x10\xe8\xc6\x63\xe5\x3c\xb6\xcc\x4a\xa0\xdc\xaf\
+\x4b\xa9\x5f\x13\x3f\x81\x67\x1b\x95\x4c\x3c\xb1\x39\xb8\xce\xcd\
+\xb8\x54\xad\xea\xb5\x63\x81\xd1\xba\x75\x42\x74\xf7\x98\x4a\xa8\
+\xbc\xcb\x25\xe3\x32\x8b\xc0\xdd\x48\x21\x1d\x8a\xba\x85\xb0\x4e\
+\xbd\x3c\x44\x91\x91\xe7\x36\x30\x51\xf5\x00\x24\x4d\x46\x70\x93\
+\xc8\x28\xe3\xf0\x28\xfd\x50\x7d\x10\x05\xf0\x7e\x14\x22\x26\xe0\
+\xf9\xcc\x38\x06\x2f\xf5\x4d\x90\xcb\x0d\x93\x18\xfd\xeb\x95\xaa\
+\x16\xff\x41\x6e\xd2\x74\x07\xc3\x09\xc3\x65\xb1\x36\x25\xe9\x87\
+\xec\x98\xa9\xff\xf5\x03\xdd\x5c\x95\xa4\xbe\xce\x1a\x95\xaa\x17\
+\x8d\x98\xf5\x02\xbc\x42\xad\x5a\x46\x2d\x3b\x80\x09\x0e\x25\xf6\
+\x23\xf1\x87\xa1\xc4\xd1\xc0\x70\xed\x48\xa8\xa0\x81\xd7\xe2\x1a\
+\xf4\x3a\x96\x70\x00\xb5\x28\xd4\xe6\x89\x1f\x2b\x3e\x72\x9c\xf3\
+\xd2\x65\x27\x49\xa6\x4c\x89\x69\x57\x61\x30\xc5\x09\xe2\x32\xee\
+\x35\x00\xc3\x60\x87\xc0\x4e\x52\xae\x69\xeb\x24\xf0\x55\x02\xf4\
+\x38\x54\x4f\xdf\x26\x5c\x71\x9a\x8c\xc5\xe7\x32\x49\x72\xe1\x44\
+\xdb\x18\xd8\xec\x68\x5e\xf2\xfe\x86\x94\xba\x35\xa3\x62\xa5\xd8\
+\xd1\xae\x60\x7d\x48\x84\x6a\x26\xe4\x7a\x01\xbc\x17\x07\x52\x2f\
+\xd7\x64\x79\xa1\x63\x9c\x03\x0c\xd3\xf4\x91\x72\xb5\x3f\xbe\xc3\
+\x0e\x06\xfd\xbf\x4e\x65\xbb\x7b\xf4\x8c\xb4\x83\xd1\x50\x6d\xf1\
+\x5a\x18\x45\xd1\x65\x50\xa2\x2e\x6a\xa6\x56\x91\xde\x60\xa8\x49\
+\x54\x89\xc6\x42\xc3\xa2\x38\xfd\xc0\x88\xd0\xcb\xc7\xcd\x2e\x3a\
+\xd5\x2a\x8c\xad\xe1\xcf\x29\xa1\x41\x64\xd2\x87\x2c\xcd\x27\x3e\
+\x1d\x6a\x40\x09\x04\x30\x72\x46\xe3\xa4\x06\x9b\xf0\x8d\x2e\xa4\
+\x88\x01\xb5\x52\x5d\xbb\x76\x6c\x37\x1a\xc9\xdb\x6c\xd5\xd4\x6a\
+\x9a\x0f\x01\xb6\x4a\x26\x55\x02\x32\xc5\xc8\xf9\x23\xbc\x03\x02\
+\x2f\x87\xb0\xfe\x5f\x81\x88\x84\xd1\x78\x0e\xe9\xc3\xc6\xf6\x9e\
+\x82\xce\x20\x2a\x12\x66\xd4\x30\x2f\xe4\xf2\x32\x5e\x88\x13\x32\
+\x8f\xe1\x7a\x7e\x6c\xc4\x5d\xb6\xfb\x50\x3d\xcd\xb4\x43\x08\x7b\
+\x15\x49\xab\xb1\xa1\x0d\x36\xc1\x5c\x89\x73\xb8\x52\x33\x1f\x17\
+\xfe\x13\x13\x24\x06\xdf\x2f\x19\x4c\x20\x82\x18\x39\x50\x5e\x02\
+\x7a\xfa\xab\x10\x05\xc4\x1b\x70\x91\x6f\xa2\x1e\x38\xd3\x44\xc1\
+\x4e\x2b\x8f\xe1\x2a\xe9\x67\x53\x1b\x1b\xc6\xd5\x93\xda\x01\xd3\
+\x5e\xba\x5b\xd3\x59\x48\x60\xa2\xd4\xfd\xac\x94\x4e\x81\x8d\x8e\
+\x9c\x1f\x1d\x39\xe9\xf3\xb3\x42\x77\x22\x53\x26\x69\x8d\x90\x67\
+\x63\x62\x88\x01\xfc\x55\x4c\x94\x04\x6b\x14\x3e\xdb\xeb\xf5\xde\
+\x84\x26\xbc\xc1\x74\x3a\x02\x01\x3f\xc3\xc9\x99\x3a\xc4\x43\x4f\
+\xc0\x66\x93\xaf\xdd\x35\xe3\xca\xc6\xdc\xb1\x44\xf8\xb6\xa2\x62\
+\x8a\xfb\xe4\xd6\xdf\xc9\xe1\x99\x03\xa9\xa5\x35\x6b\x2a\xb6\x2a\
+\x93\xe3\x7d\x21\xb7\xe7\x36\x5c\x1c\x49\xe3\x7a\x43\xa2\xb9\x64\
+\x52\xe4\xb8\xa2\xdf\x82\x77\xef\x44\x90\x42\x04\xa3\x25\xf7\xeb\
+\x50\x5b\xb6\x5a\xb0\xfd\x8c\xb7\x98\x5a\xb1\x81\xf4\xbd\xe1\x60\
+\xf0\x44\x1d\x69\xea\x52\xa7\xa5\x1d\x38\x72\x85\xc6\x45\x8f\x90\
+\x33\x0b\xcd\x6d\x64\x25\xe7\x3d\xdb\x4d\xc0\xc5\x66\x6b\x5e\x16\
+\x82\xb6\x6d\xb1\x78\xd3\x75\xb1\x37\x51\x85\xb1\x0a\xb2\x68\xb1\
+\x60\xb5\xe5\x0e\xce\x66\xae\x3a\xb3\xfd\x22\xd3\x76\x99\x10\xe4\
+\xaa\xb6\xa5\x13\x2d\x9d\xab\x7b\x78\xc8\xc0\xfa\x3d\x2d\x29\x3d\
+\xa3\x67\x17\x50\x2c\xff\x12\x04\x9c\xeb\xb4\xe6\x64\x66\xa3\xaa\
+\xde\xc8\xf7\x2b\xba\xb4\xa7\x93\xb3\x78\xb7\xa5\xa0\x6f\x5c\x0c\
+\x53\x65\x46\x45\xd7\x45\xf6\x6c\x39\xa9\xfe\xdb\x12\x32\x09\xa6\
+\xf9\xd8\x45\x67\x76\x1e\xe3\x10\x8c\xca\x64\x8e\x41\x8e\x10\x5b\
+\xf8\x67\xb6\x0d\x39\x83\x40\xba\x80\x42\x9f\x36\x81\x6a\xec\x97\
+\xec\xd4\xd9\x8e\x8a\x96\x89\xc9\x61\xb7\xfb\xcd\xd1\x70\xa8\x3a\
+\xbf\xba\xbc\xa8\x5d\x64\xfd\x0a\x63\x63\x81\xe3\x48\x96\xbb\x42\
+\xdc\x76\x13\xb4\x0c\xcc\xa7\x8a\xf3\x24\x33\xef\x26\xd6\x8b\x8d\
+\x8f\xed\xfd\xe2\xfb\xfa\x67\x25\x33\xc5\x7d\xbb\xae\xbe\x0f\x2c\
+\xab\xcb\x27\x34\xd8\x81\xd1\x82\xa2\xfe\x9b\x70\xaf\x89\x96\xb7\
+\x4e\x6f\xe3\x28\xfa\x01\x28\x7b\x8d\xe7\x27\xda\xf3\xd2\x69\xce\
+\xc2\x78\xa2\x09\xd7\x74\x71\xc7\xb9\x6c\xcc\x49\x02\xc8\xa7\x40\
+\x99\x6d\x5c\xe3\xda\xcd\x01\x26\xb1\xb9\x23\xc4\x4a\x60\x72\x2e\
+\xe3\x16\x8b\xbb\xc7\x58\x40\x2c\x2c\x68\xc8\xfd\xed\xad\xad\xd7\
+\x70\xed\x07\x2e\xe5\xf1\x9d\x7e\x01\xd0\x68\x77\x6f\xef\x4b\xfd\
+\x5e\x4f\x6f\xb0\x03\xc0\x56\x1f\x27\x70\x44\xa4\x4e\x7f\x1d\xf7\
+\x52\x03\x28\xd5\xc5\x52\x0b\x30\x1b\xb7\x4b\x26\x92\xc9\xed\x75\
+\x23\x41\x47\x5c\x96\xb9\xee\x44\xa6\x35\x45\xea\xda\x2d\xb9\x09\
+\x5c\x04\xcc\xe4\x8e\x58\x38\x76\xb6\xb7\xa9\x3e\x5f\x22\x56\x87\
+\xdb\x2f\x76\x8d\x93\x38\x7e\x76\x73\x73\xf3\x1f\xf9\x22\x03\xcc\
+\xc3\x78\x51\x8b\x14\x9c\x67\x85\x7e\x4e\x3a\x65\x70\x96\xd3\x96\
+\xc0\x2c\x77\xe0\x52\xcb\xd5\xa3\xc7\x79\xc1\x68\x2d\xf3\xb4\x3e\
+\xc8\xac\x24\xf2\x31\x78\xce\xfd\xf0\x9f\x9d\x52\x2c\x70\x9b\xfc\
+\xf8\x41\x6c\xcf\x16\xbd\x99\x5f\xec\x69\xd2\x1e\xf0\xe0\x53\xdb\
+\xdb\xdb\xaf\xf2\x66\x6b\x7e\x56\xa9\x67\x61\xad\x92\xb0\xc6\x55\
+\xec\xe5\xa4\x05\x5b\x50\xe2\xb2\xe2\xe6\x54\x68\x72\x9e\x4d\x11\
+\x59\x60\xc8\x11\xf0\x5c\x93\x6b\xb7\xe7\x8d\x2a\xaf\xdf\xbe\xfd\
+\x2a\x5c\xe7\x53\xfe\xb8\x03\x68\xb6\xf0\x2e\x1f\x0e\xba\xd0\xb3\
+\xcf\x21\xe3\x7b\xa6\xdd\xe9\x9c\xa2\x5b\xa5\xb6\x5d\xbb\x79\xdb\
+\x14\x1e\x21\xdb\x2a\xa4\x7e\xe2\x6d\xd4\xdd\xea\xa1\x99\xeb\x0f\
+\x35\xb6\xa4\x10\x71\xb3\x82\xff\xcf\xac\xcb\x4c\xe8\x56\x2d\xe7\
+\xb9\x36\x8d\x78\x7d\x7d\x7d\x6d\x67\x67\xe7\x73\xc4\x76\x34\xdd\
+\xbf\xeb\x67\x56\x4c\x78\x65\x6b\x6b\xeb\xd3\x28\x37\x7f\xdc\x6a\
+\xb5\xda\x8b\x98\x88\x29\xec\xb5\x1b\xeb\xfa\xa9\x34\x2c\xb1\x98\
+\x70\x18\xc9\xc1\x42\x77\x5a\x0a\x9d\xf5\x7c\x6a\xce\x49\x20\x2b\
+\xa8\x80\x6b\xb9\xd3\x9e\xa8\xaa\xe5\xb0\x24\x0f\xae\x9e\x52\xce\
+\x2b\xf8\xdb\xb7\x77\xc0\xd0\x4f\x13\xd3\xdd\xb0\x7a\x97\x5e\x7f\
+\xfd\xf8\x45\x6d\x21\x6a\x03\xeb\xfc\x42\xa7\xf3\x74\xab\xdd\x5e\
+\xe1\x35\x7e\x36\xbd\xbe\xb6\x2e\x7b\x07\x5d\x6d\xe6\xea\x87\x3c\
+\xdf\x3b\x56\x2f\x7b\x93\x89\x1c\xfa\x29\x7a\x8a\xb9\x8f\xaa\x4d\
+\x62\x6c\x84\x69\xc2\x03\xa7\x96\x55\xe7\xa9\xb2\x1b\xeb\xeb\xb7\
+\xc0\x48\xfd\xc4\x54\xfc\xb0\x7e\x4f\x04\x04\x2e\x8d\x4d\xd3\xb3\
+\xed\x76\xfb\xdb\x9d\x85\x85\x0f\xba\xd4\x76\x6b\x77\x5f\x6e\xdf\
+\xd9\xd6\x12\xd3\xb3\xfd\x52\xcf\xf6\x46\x3d\x07\x3c\x9f\x56\x21\
+\xf1\x8e\x48\xc1\xd9\x0e\x8e\x99\x1e\x9c\x5c\xec\xc8\x42\xcb\x74\
+\xa4\xf9\x69\x69\x7d\x63\xe3\xa5\xdd\x9d\x9d\xcf\x63\xfe\xcb\x62\
+\x82\xed\xdb\x27\x40\xf3\x7b\x66\x83\x41\xd0\xa8\x55\xab\x5f\x03\
+\x11\x4f\xcc\xcc\xcc\x28\x2c\x2e\xbe\xb3\x7f\xa8\xc4\xb0\x0d\xaf\
+\xb9\xbc\x37\xf9\xdc\x34\xd5\x07\xb5\x6d\x45\xa7\xf3\x64\x3f\x73\
+\x7a\x7e\x63\x63\x74\x65\x07\xdc\x7d\x4c\x87\x9b\xd4\xcf\xac\xa3\
+\xd1\xe8\xcb\xe0\x7a\xd7\xb5\x6f\xde\x31\x01\x99\xfd\xfc\x43\x0f\
+\x02\x60\x1f\x9b\x9b\x9f\xff\x6a\xb3\xd9\xfc\x40\x95\x45\xb7\x1d\
+\xfc\x5c\x7a\xc0\xaf\x8d\x20\x84\xc7\xac\xe0\xdc\xb7\x30\x0e\x67\
+\xec\xec\x78\xb0\x60\x27\xf0\x59\xfd\xba\x59\x19\x4b\x04\xd1\x95\
+\x7e\xfe\x65\x44\xda\xaf\x00\xb8\x7e\xe8\x36\xa9\xf4\x9f\x88\x00\
+\xe7\x42\xf9\x53\x03\x54\x64\x9f\x41\xde\xf4\x45\xa4\xe0\x1f\x22\
+\x21\xbc\xef\x46\xb1\xef\xcf\x45\x39\x4c\x5b\x72\xf2\x2d\xc1\x0d\
+\xea\x39\x33\xcb\x83\xfd\xfd\x8b\x48\x65\xbe\x85\x38\xf4\x7d\xf7\
+\x9b\x09\xae\xfd\x27\x27\xc0\x75\xd1\x38\x20\x11\xa6\xe2\x8f\xd5\
+\xf8\x63\x8f\x5a\xed\xf1\x6a\xad\x76\xba\x5c\x2e\x07\x04\xcb\x7c\
+\x4a\xbc\xe3\xe9\x74\x3a\xfe\xd0\x3d\x4a\xa1\xe7\xfa\x63\x0f\x64\
+\x95\xfa\x63\x0f\xcc\x95\x38\xd5\xd4\xb6\xcd\x3d\x12\xf0\x8e\x7e\
+\xad\x62\x5d\x26\x67\x7c\x6e\x30\x1c\x3e\x87\xe0\x47\xe4\xef\x06\
+\x77\xf5\xe7\x36\xc2\x9f\xdb\xf8\xfe\x1c\x3c\x95\xf9\xb9\x4d\x9a\
+\xf6\x00\xfe\x20\xb3\x3f\xb7\x81\x24\xc7\x3f\xb7\x29\xcc\x77\x57\
+\x80\xff\xdf\xf8\x3f\xb6\x68\x91\x32\x93\x47\xe0\x2e\x00\x00\x00\
+\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x10\x32\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
+\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
+\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
+\x46\x00\x00\x0f\xb8\x49\x44\x41\x54\x78\xda\xac\x5a\x4b\xac\x1c\
+\x57\x99\xfe\xfe\x73\x4e\x3d\xba\xaa\xdf\x7d\x5f\xbe\xce\x1d\x27\
+\x8e\x99\x38\x61\x78\x2a\xc0\x44\xa3\x28\x08\x13\x34\x8b\xcc\x30\
+\x42\x41\x20\x36\xac\x89\xc4\x06\x21\xc4\x82\x25\x0b\x84\x10\x1b\
+\x24\x58\xb3\x61\x40\x13\x21\x21\x22\x66\x06\xf2\x20\xc9\x48\x1e\
+\x27\x38\x61\x26\x9e\x60\x07\xc7\x8e\xe5\xf7\xbd\x7d\xef\xed\xdb\
+\xb7\xbb\xfa\x51\xe7\x9c\x7f\x16\xe7\x54\x75\xf7\xb5\x9d\x71\xc2\
+\x94\x74\xba\xab\xbb\xab\xfb\xfc\xcf\xef\xff\xfe\xbf\x9a\x5e\x7d\
+\xe5\x15\x1c\x3c\x88\x08\x52\x4a\x30\x33\xac\xb5\x10\x42\xc0\x5a\
+\x0b\x66\x86\x10\x02\x44\x04\x63\x0c\x98\x19\x00\x60\xad\x0d\xa5\
+\x94\x0f\x02\xf8\x48\x18\x86\x0f\x11\xd1\x51\x29\x65\x9d\x88\x52\
+\xff\xf9\x50\x6b\xdd\x67\xe6\x0b\x93\xc9\xe4\x4d\x00\xff\xa5\xb5\
+\xfe\x93\x10\x62\x5a\xfc\x86\x10\xa2\xdc\xaf\x90\xc1\x18\x03\x22\
+\x2a\xf6\xc0\xed\x0e\x85\xf7\x71\x30\x33\x98\x59\x01\x78\x34\x8e\
+\xe3\x27\x2b\x49\x72\x22\x49\x92\x63\x95\x38\x96\x61\x14\x21\x08\
+\x02\x48\x21\x40\xa2\xd8\x9c\x61\x8c\x41\x3e\x9d\x62\x3c\x99\x60\
+\x94\x65\x66\x38\x1c\x9e\xcf\xb2\xec\xb9\xf1\x78\xfc\x34\x33\xbf\
+\xcc\xcc\xfa\xfd\xc8\x42\xef\xc5\x03\xd6\x5a\x10\x91\x52\x4a\x7d\
+\x29\x49\x92\xaf\x37\x9a\xcd\x4f\xd5\x6a\x35\x24\x49\xb2\xf0\xfd\
+\xc9\x34\x47\x9e\xe7\x18\x4f\xa6\x00\x80\x38\x0a\x11\x06\x01\xc2\
+\x30\x98\x19\xc1\x5a\xec\x0f\x06\xe8\xef\xed\xa1\xd7\xeb\x9d\x1a\
+\x0e\x87\x3f\xd2\x5a\xff\x82\x99\x35\x11\xdd\xb5\x07\xee\x5a\x01\
+\x63\x0c\x84\x10\x9f\xab\xd5\x6a\xdf\xed\x74\x3a\x9f\x68\x34\x1b\
+\x90\xd2\x39\xb0\xbb\xdb\xc3\xc5\xcb\xd7\x70\xe9\xf2\x35\x6c\x6e\
+\xef\x60\x6f\x7f\x80\x69\x9e\xc3\x18\xb7\xa9\x14\x02\x61\x18\xa0\
+\x51\xab\x62\x65\xa9\x8d\x7b\xef\x59\xc7\xbd\x1b\xeb\x58\x6a\x35\
+\x9d\xc2\x93\x09\x76\x76\x76\xd0\xdd\xda\x7a\x75\x7f\x7f\xff\x3b\
+\xd6\xda\xdf\x16\x7b\xfe\xc5\x0a\x78\xc1\xab\x61\x18\x7e\x6f\x65\
+\x65\xe5\xa9\x76\xa7\x43\x4a\x29\x58\xcb\x38\x73\xee\x3c\x4e\x9f\
+\xf9\x13\x2e\x5e\xbe\x8a\x61\x36\x02\x40\x50\x4a\x22\x90\x12\x52\
+\xca\x85\xcd\xad\xb5\xc8\x8d\x81\xd6\x06\x00\xa3\x9a\x54\x70\xef\
+\x3d\x87\xf1\xf0\x87\x1f\xc2\x07\x1f\xb8\x1f\x82\x08\xa3\xd1\x08\
+\x9b\x9b\x9b\xbc\x79\xf3\xe6\x8f\xa7\xd3\xe9\xb7\xad\xb5\x83\xbf\
+\x48\x01\xbf\xf1\xf1\x66\xab\xf5\xd3\x43\x6b\x6b\x9f\x4c\xab\x55\
+\x00\xc0\x1b\xe7\xde\xc6\x8b\xa7\x4e\xe3\xf2\xd5\x1b\x00\x01\x95\
+\x28\x42\x25\x8e\x11\xf9\x50\x91\x52\xb8\x1c\x28\x36\xf7\x9e\xd4\
+\xc6\x20\xcf\x73\x4c\x26\x39\xb2\xf1\x18\xa3\xf1\x04\x00\xb0\xb1\
+\xbe\x8a\x4f\xff\xed\xc3\xf8\xd0\x03\xf7\x03\x00\xb6\xbb\x5d\x5c\
+\xbd\x76\xed\x95\xbd\x5e\xef\xab\x00\xce\x12\xd1\x7b\x57\x40\x08\
+\x01\x00\x8f\x74\x3a\x9d\xa7\xd7\x0e\x1d\x5a\x0f\xc3\x10\xbd\xfd\
+\x01\x7e\xf3\xfc\x7f\xe0\x8d\x73\x6f\x43\x10\xa1\x9a\x26\xa8\xa6\
+\x09\x92\x38\x86\x0a\x14\x84\x17\xf8\xdd\x92\xbf\x78\xce\xb5\xc6\
+\x68\x3c\xc6\xfe\x30\xc3\x60\x90\x81\xc1\xf8\x9b\xbf\xbe\x1f\x4f\
+\x9c\x78\x14\x8d\x5a\x15\xfb\xfb\xfb\xb8\x7a\xe5\xca\xb5\xed\xed\
+\xed\x27\xad\xb5\x27\x8b\xef\xde\x15\x0a\x31\x33\x88\xe8\x91\xe5\
+\xe5\xe5\x67\xd6\x0e\x1d\x6a\x4b\x29\x71\xfe\x9d\x2b\xf8\xd5\xb3\
+\x2f\x62\x7b\x77\x0f\xf5\x5a\x15\xf5\x5a\x15\xd5\xa4\x02\x25\x05\
+\x00\x2a\x15\x2f\xac\x4e\x07\x94\xf1\xc8\x55\x9e\x2b\xa5\x10\x47\
+\x11\xea\xd5\x2a\x06\xb5\x11\xf6\xf6\x07\x38\xf3\xd6\x05\x5c\xdf\
+\xda\xc6\x3f\x3d\xfe\x18\x8e\xdd\xbb\x81\x23\x47\x8e\xac\x93\x10\
+\xcf\x6c\x6d\x6e\x3e\x61\xad\x3d\x49\xb7\x31\xd0\x9d\x3c\xf0\xc0\
+\xea\xea\xea\x73\x87\xd6\xd7\x0f\x4b\x29\xf1\xdf\x67\xcf\xe3\xd7\
+\xcf\xbe\x04\xc3\x8c\x46\xad\x8a\x66\xad\x86\x30\x54\x00\x04\x84\
+\x70\x42\x0b\x22\x90\x20\x10\x6e\x55\x80\x9d\xd4\xe5\xb3\x65\x06\
+\xb3\x85\xb5\x5c\x2a\x36\x9d\xe6\xe8\xed\xef\x63\xaf\x3f\x80\x10\
+\x84\xcf\x3f\xfe\x18\x3e\x7c\xfc\x18\xb2\x2c\xc3\xa5\x77\xde\xb9\
+\xba\xb5\xb5\x75\x02\xc0\xb9\x5b\x3c\x70\x1b\xad\xaa\xed\x4e\xe7\
+\x67\xab\x6b\x6b\x87\xa5\x94\x38\xf3\xd6\xdb\xf8\xf5\x73\x2f\x43\
+\x28\x85\xa5\x7a\x0d\xf5\x5a\x15\x52\x4a\x08\x41\x10\x24\x40\x42\
+\x38\xe1\xa9\x50\xa4\xb4\xc2\xbc\xf9\xcb\x27\x06\x83\x19\xb0\x6c\
+\xc1\xd6\x29\x63\x8c\x81\x94\x12\x51\x18\x20\x0e\x43\xec\xf6\xf7\
+\xf1\xab\x67\x5f\x02\x01\xf8\xd0\xf1\x63\xd8\xd8\xd8\x38\xac\xb5\
+\xfe\xd9\xee\xee\xee\x63\x00\x06\x77\x0c\x21\x66\x46\xb5\x5a\xfd\
+\xfe\xda\xda\xda\xc7\x83\x20\xc0\xc5\x2b\xd7\xf0\xaf\xbf\x3f\x89\
+\x30\x08\xd0\x6e\x35\x50\xaf\x56\x21\xa5\x80\x20\x67\xf9\xa2\x2a\
+\xcf\x2b\x30\x33\xc8\xbc\x61\x78\x2e\x8c\xdc\x6b\x6b\x2d\x18\x00\
+\x5b\x86\x91\x12\x96\x2d\xac\x91\x68\xb7\x9a\x90\x4a\x61\xb7\xb7\
+\x87\xdf\xbc\x78\x12\xd5\x34\xc1\x7d\x1b\xeb\x58\x5f\x5f\xff\xf8\
+\x78\x3c\xfe\x7e\x96\x65\x4f\xcd\x1b\x5d\x08\x21\x4a\x41\x82\x20\
+\xf8\xcc\xca\xea\xea\xd7\x2a\x95\x0a\xfa\x83\x21\xfe\xfd\xa5\x53\
+\x20\x12\x58\x6a\xb7\xd0\x6e\x36\x10\x06\x01\x02\x15\x20\x08\x14\
+\x02\x35\xbf\x82\xf2\x5c\x29\xe9\xcf\xa5\xbb\xee\xc0\xb5\x61\xa0\
+\xa0\x94\x42\x10\x04\xb3\xeb\x03\x85\xd0\xbf\x17\x86\x01\xda\xcd\
+\x06\x3a\xed\x16\x88\x08\xff\xf6\xd2\x7f\xa2\x3f\xc8\xd0\xee\x74\
+\xb0\xb2\xb2\xf2\x35\x29\xe5\x67\xe6\x13\x5a\x79\xb4\x01\x33\x47\
+\xcd\x66\xf3\x07\xcd\xa6\x2b\x2e\xbf\x3f\xf5\x3a\x06\xd9\x08\x9d\
+\x56\x13\xed\x66\xdd\x5b\xde\x59\xdd\xd1\x84\xb9\xf8\x07\x81\xbc\
+\x11\x88\x30\x97\x07\x0b\x11\x34\x0b\x1f\xcf\xab\xd8\xe7\x83\x7b\
+\x6d\x21\xac\x3b\x97\x82\xd1\x6e\x36\x1c\xa4\xee\xee\xe1\x85\x93\
+\xa7\xf1\xf9\xc7\x1f\xc5\xca\xea\x2a\xfa\xfd\xfe\x0f\x76\x77\x77\
+\x1f\x11\x42\x4c\xca\x1c\x60\x66\x04\x41\xf0\x85\x4e\xa7\xf3\x31\
+\x21\x04\xce\x5f\xba\x82\x8b\x97\xaf\xa3\x59\xaf\xa3\xdd\xac\x21\
+\x0c\x94\xab\x0d\x42\x40\x94\x21\x34\x0b\x23\xa7\x98\x4f\x60\x9a\
+\x25\x70\xa1\x08\x83\x81\xb9\xf8\x9f\x17\xdc\x7a\xa1\x6d\xa1\x80\
+\x31\x30\x82\x21\x05\xa1\xdd\xac\xc3\x5a\x8b\x0b\x57\xae\xe1\xcf\
+\x97\xae\xe0\x03\x47\xee\xc1\xf2\xf2\xf2\xc7\x06\x83\xc1\x17\xb4\
+\xd6\xff\x4c\x44\x50\xfe\xc7\x54\xad\x56\xfb\x46\x5a\xad\xc2\x18\
+\x8b\xd7\xfe\xe7\xcf\x88\xa2\x10\xcd\x7a\x15\x71\x14\x95\x85\x4d\
+\x12\x39\x05\xbc\x17\x0a\xe1\xa9\x48\x68\x72\x66\x2f\x04\x2f\xd3\
+\xc0\x0b\x3f\x0f\xa7\x45\x02\x17\x55\xda\x78\x45\x8c\x20\x08\x6b\
+\x61\x0c\xa1\x22\x04\x9a\xf5\x1a\x72\x6d\x70\xfa\xcc\x39\x1c\xdd\
+\x58\x47\xab\xdd\x46\xb7\xdb\xfd\xc6\xf6\xf6\xf6\xbf\x08\x21\xb4\
+\x02\x00\x29\xe5\x23\x8d\x66\xf3\x61\x22\xc2\x3b\x57\xaf\x61\x67\
+\xaf\x8f\x7a\xd5\x15\x29\x29\x25\xa4\x17\x58\x4a\xe1\x11\x68\x66\
+\x7d\xe5\x5f\xcf\x52\xf5\x0e\xac\x71\xf1\xc1\xc1\xa9\xb5\xc8\xb5\
+\x76\x89\x5c\x28\x62\x08\xda\x58\x08\xb2\x30\xd6\xa2\x9a\x56\x30\
+\x99\xe6\xd8\xd9\xeb\xe3\xe2\xe5\xeb\x38\x76\xe4\x30\x9a\xcd\xe6\
+\xc3\xbd\x5e\xef\x11\x66\x7e\x59\x11\x11\x92\x4a\xe5\xcb\x69\x9a\
+\x02\x00\xce\x5f\xba\x8a\x38\x0c\x51\x4d\x12\x84\x81\x82\x20\x27\
+\xb8\x52\x85\x22\x4e\x60\xf2\xf1\xdf\x1f\x0c\x30\x99\x4c\x21\x04\
+\xcd\x0a\xda\x02\xf6\x14\x35\x80\x17\x3d\x61\x2d\xa2\x28\x46\xbd\
+\x5e\x03\x5b\x86\x60\x0b\x6b\xac\x37\x8e\x81\x36\x16\x64\x0c\x84\
+\x20\xd4\xd2\x04\xd3\x3c\xc7\xf9\x4b\x57\x70\xec\xc8\x61\x34\x1a\
+\x0d\x54\x2a\x95\x2f\x67\x59\xf6\xb2\x02\x10\xa6\x69\xfa\xb8\x52\
+\x0a\x7b\x83\x21\xb6\x7b\x7d\xdc\xb3\xbe\xea\x42\x84\x08\x4a\xca\
+\x52\xf8\x22\x7c\xe6\x20\x0c\xa7\x5e\x3d\x8d\xb7\xce\xbf\x8d\x38\
+\x8e\x21\xa5\x72\xca\x49\xe9\x38\x90\x31\xe5\x32\x56\xbb\x67\xff\
+\x7a\x3c\xca\xf0\xe0\xf1\xe3\xf8\xc7\x7f\x78\x02\x9a\x75\x99\x57\
+\x64\x8c\xcf\x29\x8b\x9c\x08\xc6\x68\xd4\xaa\x09\x92\xa4\x82\x9b\
+\x5b\xdb\xe8\x0f\x86\xa8\xa6\x29\xd2\x34\x7d\x7c\x34\x1a\x85\x42\
+\x48\xf9\x50\x25\x49\xee\x03\x80\x9b\xdd\x1d\x08\x22\x2c\xb5\x9b\
+\x68\x54\x53\x28\xe9\x20\xce\x29\xa1\x16\x84\xc7\x41\x2b\x17\x76\
+\x17\x02\xa2\xf0\x92\x83\xa4\xf2\xf3\x22\x81\x8b\x7a\x70\x3b\x7a\
+\x23\xa5\x84\x54\x6e\xcf\x30\x50\x90\x52\xa2\x5e\xad\x62\xb9\xdd\
+\x84\x20\xc2\x8d\xee\x0e\x84\x94\x48\xd3\xf4\x3e\x21\xc4\x43\x42\
+\x29\xf5\xd1\x28\x8a\x54\x01\x59\x95\x4a\x8c\x40\x29\x54\xd3\x0a\
+\xa2\x28\x80\x92\x02\x52\xc9\xc5\xba\xe4\xc5\x15\xbe\xf2\xd2\x81\
+\x42\x46\x1e\x81\x6e\x79\xff\xc0\x2a\x60\xf8\x20\x17\x10\xc2\xd1\
+\x72\x29\x05\xe2\x30\x44\x2d\x4d\xa0\x94\x42\x52\xa9\xa0\xbb\xb3\
+\x07\x00\xa8\x24\x89\x92\x52\x7e\x54\x45\x61\x78\x3c\x0c\x02\x58\
+\xcb\x18\x64\x63\xa4\x49\x05\xd2\x27\x68\x9a\x54\x30\x9e\xe4\xb7\
+\x67\xab\x1e\x7e\x67\x7c\xc6\x82\x59\x80\x8d\x81\x61\x06\xe0\x28\
+\x30\xcf\x7d\x7e\xf0\xb0\xd6\x02\xbe\xcf\x2e\x2a\xf3\xdc\x26\x90\
+\x52\x22\xae\x84\x10\x82\x20\x21\x90\x26\x31\x06\xc3\x11\x98\x19\
+\x51\x14\x21\x0c\xc3\xe3\x82\x84\x38\x2a\x95\x84\x36\x06\xda\x18\
+\xc4\x51\x58\xe2\x78\x14\x86\x88\x42\x55\xb2\xc8\xc2\xea\x04\x38\
+\x3a\x3c\x99\xc0\x68\x5d\x0a\x68\xad\x8b\x71\x63\x34\x8c\xc9\x5d\
+\xfc\x7b\x8c\x67\xcb\xe0\xb2\x80\x59\xdf\x2c\x59\x64\xe3\x09\x72\
+\xad\x4b\xa3\xcc\xd3\x9a\x68\xae\x0d\x15\x20\x44\x51\x84\xdc\x18\
+\x18\x63\x11\x06\x01\x88\xe8\xa8\x52\x4a\xd5\x0b\x6b\x49\x21\x10\
+\x28\x59\x96\x1f\x00\x48\xa2\x08\xd6\x02\xda\x68\x08\x22\x18\xcf\
+\x1c\xb5\x27\x60\x0c\x27\x98\x6b\x38\x08\x00\x43\xd8\x59\x23\xe3\
+\x84\x9d\x2d\xf0\xac\x98\x15\x9c\x28\xcf\x8d\x8b\xf9\x30\x80\xf4\
+\x7b\x04\x4a\xa1\x12\x47\xb0\x45\xa2\x10\x10\x2a\xd7\x73\x68\x6b\
+\x9c\xac\x41\x50\x57\x42\x88\x04\x00\x72\x6d\x7c\xe2\x89\xc5\xa4\
+\x24\x42\x35\xad\x60\x34\x1a\x21\x1b\x4f\x30\xcd\x9d\xc5\x9d\x37\
+\x9c\x9e\xd6\x3a\x64\xf1\x7d\x04\x98\x0a\x7a\x52\x84\x90\x2d\x0b\
+\x96\x0b\x2b\x5b\x5e\x2b\x88\x60\x01\x68\x63\x60\xc6\x16\x61\xe0\
+\x04\x4f\xe2\xf8\x96\x2c\x17\x82\x20\xa5\x40\x9e\x1b\x04\x51\x00\
+\x21\x44\xb2\xc0\x46\xd5\x5c\x1f\x3b\x8f\x33\x42\x08\x24\x95\x18\
+\xa3\xc9\x14\xc6\x58\x48\xe1\xaa\x2f\x88\x3c\x8b\x34\xb0\xd6\x80\
+\x98\xbd\x01\xec\xc2\xf4\x61\x5e\x01\xb6\xee\xda\xc2\xc3\xe4\x7f\
+\x8b\xad\x0b\x29\x0a\x09\x49\x1c\x83\x04\xc1\x1a\x3e\x98\x7c\x5e\
+\xc6\x19\xfa\x29\x66\xce\x00\x20\x0c\x1c\x64\xde\x0e\x20\x8d\x76\
+\xb1\xdc\xaa\xd7\x10\x05\x01\xf6\xb3\xac\xcc\x0b\x14\x96\x35\x16\
+\x20\x06\x91\x3d\xd8\xde\x79\xda\xe0\x2c\x6f\x8c\xc5\x42\x7b\xe8\
+\x61\x55\x08\x42\xbd\x9a\x20\x89\x62\xe4\xc6\x40\xb2\xcf\x09\x5e\
+\x24\x84\x05\x8b\xf5\x70\x9c\x29\xad\x75\xdf\x8d\x3e\x24\xa4\x90\
+\x25\xaa\x00\xa2\x8c\xe3\x5c\xbb\xe2\x42\xcc\xa8\xa6\x09\x2a\x71\
+\x88\xfd\xe1\x08\x53\xad\x1d\x05\xf0\x05\x6a\xb1\x1f\xc0\x2c\x8c\
+\x0a\x25\xac\x59\x10\x9e\x7d\x97\x96\xc4\x31\xaa\x69\x02\x25\x05\
+\xb4\x76\x8a\xe6\x06\x50\x4a\x2e\x18\xd4\xb5\xa2\x6e\xe2\x61\x74\
+\x0e\xad\x75\x5f\x81\xf9\x82\x31\x06\x42\x0a\x04\x81\xc2\x64\x9a\
+\xc3\x32\x20\xfc\xa6\xb9\xd1\xa5\xfb\x8a\xe4\x0b\x82\x00\xcb\x6d\
+\x97\x60\x81\x92\xd0\x5a\x97\x63\x94\x79\x25\x66\x45\x8b\xcb\xa4\
+\x9d\x17\x3e\x0c\x03\xac\x2d\x75\x1c\x38\x58\xe3\xe7\x48\x1e\x82\
+\xd9\x22\xcf\x73\x28\xa5\x16\xa6\x7b\x51\x18\x42\x4a\x81\xc9\x58\
+\x83\x99\x2f\x88\xe9\x74\x7a\x56\x6b\x87\x30\x69\x25\x72\xc9\x64\
+\xad\x83\x4a\x6f\x59\x94\x55\x93\xcb\x61\x97\xb5\x8c\x38\x8a\x10\
+\x85\xc1\xad\x30\xaa\xb5\x5b\x3e\x37\x5c\x0d\xe0\x83\x91\x85\x28\
+\x08\x10\x47\xa1\xcb\x23\x3b\x43\x27\xe7\x17\x86\x36\x16\xb9\x36\
+\x2e\x8c\x7d\x8e\xa4\x95\x08\x04\x60\x3a\x99\x20\xcf\xf3\xb3\x42\
+\x1b\xf3\xc7\xe9\x74\xaa\x01\xa0\x5e\x4d\xcb\x61\x96\x65\x86\xd6\
+\x7a\xd6\x88\x60\x91\x02\x14\x38\x3e\xdf\x9c\x94\x8a\xf8\x55\x86\
+\xcf\xed\x16\x1c\x03\x2d\x72\x62\x26\xbc\xd7\xd5\xeb\xab\xb5\x2e\
+\xfb\x66\x6b\x19\xf5\xaa\x23\x9d\xe3\xf1\x58\x1b\x63\xfe\x28\xac\
+\x31\x6f\x8e\x46\xa3\x8b\x00\xd0\xac\x55\xa1\xa4\x44\x9e\x6b\x4c\
+\x73\x17\xdf\xf3\xfc\xc5\xfa\x98\xb6\x96\xe7\x30\x9e\x4a\x66\xfa\
+\x5e\x97\xe3\x47\x85\xf5\x2d\x18\xbe\x47\x28\x14\x07\x60\xac\xc5\
+\x74\xaa\x91\xe7\x1a\x4a\x0a\x34\xeb\x55\x30\x5b\x0c\x87\xc3\x8b\
+\xd6\xda\x37\x15\x11\x4d\x47\xa3\xd1\xef\xac\xb5\x1f\x48\x2a\x11\
+\xea\xd5\x04\xfb\xc3\x0c\x42\x0a\x48\x41\xce\x18\xf3\x4a\x10\x41\
+\xf8\x8e\x6a\x9a\xe7\xf8\xfb\xcf\x9e\xc0\xa7\x1f\xfd\xbb\x32\xe1\
+\x0a\x02\x77\x90\xe1\xcc\xcf\x84\x98\x19\x5a\x5b\xc4\x71\x54\xd6\
+\x95\x79\x92\x57\x8c\x60\x8a\xef\x4c\xb5\xc6\x54\xe7\xa8\x57\x53\
+\x24\x71\x84\x2c\xcb\x90\x65\xd9\xef\x00\x4c\x15\x03\x18\x65\xd9\
+\xcf\xc7\xa3\xd1\x53\x49\x9a\x62\xb5\xd3\x42\x6f\x7f\x00\x6b\xd9\
+\xf1\x1d\x6b\xc1\x42\xb8\x3c\xb0\x0c\x26\x06\x93\x9f\x26\x80\xd1\
+\x6c\x35\xa0\x64\xdb\x8f\x58\x68\xb1\x2f\xa6\x59\x28\x94\xed\xa4\
+\xb5\x30\x5e\x58\x63\x0c\xf2\x5c\x97\x1e\x9d\x79\xb5\xa8\xe2\xb3\
+\x1e\xda\x5a\x8b\xd5\xe5\x16\x00\x60\xb0\xbf\x8f\xd1\x68\xf4\x73\
+\x22\x82\x22\x17\x67\x27\xfb\xfd\xfe\x1f\x92\x34\x7d\xb8\xd3\xaa\
+\x23\xbd\x19\x63\x32\xcd\x21\x44\x04\x80\x41\xcc\xb0\x4c\x10\xf0\
+\x70\x28\x00\xc0\x42\x58\x57\x15\xe7\x21\x94\xe0\xf1\xdb\x2b\x32\
+\x43\xcd\x19\xf1\xb3\xfe\x77\x0a\x32\xe8\x72\x06\xb3\xf7\x78\xc6\
+\x9b\xac\x1f\x43\xa6\x95\x18\x4b\xcd\x06\xb4\xd6\xe8\xf5\x7a\x7f\
+\x30\xc6\x9c\xf4\x13\x15\x01\x29\xa5\xde\x1f\x0c\x7e\x38\x19\x8f\
+\x21\x85\xc0\xc6\xda\x0a\xb4\x76\x82\x71\x31\x9c\x2d\xa7\x08\x45\
+\x23\xce\x25\x2d\x30\x86\x17\x9a\x73\xed\xd1\x48\x9b\x82\xdc\x99\
+\x12\xdd\x0e\x7e\xdf\xa2\x10\xde\x2e\x5a\xdf\xef\xeb\x50\xcd\x60\
+\x63\x6d\x19\x42\x10\xfa\xfd\x3e\x06\x83\xc1\x0f\x95\x52\x5a\x4a\
+\xe9\xaa\x15\x11\x21\x9f\x4e\x7f\xd9\xeb\xf5\x5e\x07\x80\xe5\x76\
+\x03\x9d\x66\x0d\xd3\xe9\x74\x66\x35\x6b\xbd\x65\x66\x6c\xd2\x82\
+\x61\x61\x67\x89\x3d\xb7\xca\x1e\xd7\xaf\x42\x60\x63\x3c\xf9\x63\
+\xeb\x9b\xfa\xf9\xd7\x28\x47\x2c\xc5\x67\x79\x9e\xa3\xd3\xac\x61\
+\xb9\xdd\x84\xd6\x1a\xdb\xdd\xee\xeb\x79\x9e\xff\xb2\xa8\x35\xa2\
+\xe4\x28\xcc\x93\xdd\x5e\xef\x9b\xd9\x70\x08\x00\x38\xba\xb1\x8e\
+\x28\x0c\x90\xe7\x79\xa9\x84\x29\xe2\xb7\xb0\x9e\x71\x02\x19\x76\
+\x55\xd6\x7a\x58\x2c\xea\xc4\xcc\x33\x05\xe4\x3a\x0f\x1a\x3b\xbb\
+\x66\x46\x33\xe0\x7e\x87\xd9\x2b\xe2\x60\x3c\x0c\x14\x8e\x6e\xac\
+\x03\x00\x76\xb6\xb7\xd1\xeb\xf5\xbe\xc9\xcc\x93\x42\x6e\x31\x3f\
+\x35\xd6\x79\xfe\xfc\xd6\xd6\xd6\x4f\xb4\xd6\x88\xa3\x10\xf7\x6f\
+\xac\x83\xd9\x4d\x0e\xec\xdc\x3c\xc7\x2c\x24\x9c\xb7\xb4\x57\xd0\
+\x96\xc4\xcd\x94\xf5\x60\xf1\x9c\xe7\x92\xd6\x96\x4d\x8f\xf1\x5c\
+\xa9\xb0\xbc\xd6\x1a\xcc\x16\xf7\xff\xd5\x61\xc4\x51\x88\xe1\x70\
+\x88\xcd\xcd\xcd\x9f\x68\xad\x9f\x9f\x47\x33\xb1\xd8\xe2\x09\x0c\
+\x87\xc3\x6f\x6d\x6f\x6f\xbf\xc6\xcc\x68\x35\x6a\x38\xba\xb1\x0e\
+\x63\xb4\xf3\x04\xcf\xb8\x7f\x31\xcb\x31\x73\xb9\x60\x99\xcb\xf7\
+\x8c\x0f\x23\x33\x77\x5d\x69\x79\x9e\x79\xa0\x34\xc8\x01\xe1\x8d\
+\xd1\x38\xba\xb1\x8e\x76\xc3\x85\xf2\x8d\xeb\xd7\x5f\xcb\xb2\xec\
+\x5b\xa2\x9c\x00\xba\xa5\x6e\x73\xe3\x60\xb0\xdd\xed\x7e\x25\x50\
+\xea\xb9\x76\xa7\x73\x78\xb5\xd3\x02\x01\xb8\x70\xe5\xba\x6b\x3c\
+\x94\x84\x10\x0c\xe6\x19\xda\x08\x22\x30\x01\x44\xbc\x30\x91\xbb\
+\xdd\x60\x0b\x73\x15\xd7\xce\xe1\xbf\xf5\x90\xa9\x73\x0d\xeb\x2d\
+\xbf\xda\x69\xc1\x18\x83\x1b\x37\x6e\x5c\xdd\xd9\xd9\xf9\x0a\x80\
+\xc1\x41\xb2\x78\xa7\x1b\x1c\xe7\xba\xdd\xee\x17\x49\x88\x67\x5a\
+\xad\x56\x7b\xa5\xd3\x42\xa0\x14\x2e\x5c\xbe\x81\x7c\x3a\x85\x0a\
+\x02\x14\x03\x0a\x02\xc3\xf0\x81\x86\xfe\x96\xc1\xd0\x81\x42\x86\
+\x03\xd3\x09\x30\xac\x71\xa1\x1a\xaa\x00\xf7\x6d\x1c\x46\xbb\x51\
+\x73\xc2\x5f\xbf\xbe\xb3\xdd\xed\x7e\x91\x99\xcf\xdd\xf5\x1d\x1a\
+\x22\x82\x65\x3e\xb9\xb5\xb5\xf5\x04\x5b\xfb\x74\xab\xdd\x5e\x6f\
+\x35\x6a\xf8\x60\x1c\xe1\xd2\xd5\x1b\xe8\xf5\x07\xb0\x0e\x7e\x7d\
+\xf1\x82\x6f\x66\x16\x07\x5b\x07\xa7\xbb\xbc\x70\xb7\x66\x6e\x46\
+\xaa\x5d\x8e\xb4\xea\x55\xdc\x7b\x78\x0d\x71\x14\x22\xcf\x73\xdc\
+\xbc\x71\xe3\x5a\xb7\xdb\x7d\x92\x99\x4f\x16\x33\xdc\x5b\x64\x3d\
+\xf3\xc6\x1b\xb7\xbf\xc9\x57\xd0\x58\x63\x8e\xb7\xdb\xed\x9f\x76\
+\x96\x96\x3e\x59\x50\xdb\xee\xee\x1e\xae\x6f\x6e\x23\x1b\x4f\x5c\
+\xee\x48\xd7\x8a\x96\x63\xa0\xa2\x11\x99\xf7\x04\x1d\xf0\x42\x91\
+\x3b\xcc\x48\xe2\x08\x87\x56\x3a\x58\x6a\xb9\x89\xf4\x28\xcb\x70\
+\xe3\xe6\xcd\x57\x76\x77\x76\xbe\x4a\x44\x67\x0b\x52\x77\x3b\x05\
+\xde\xf5\x4e\xbd\xb7\xe8\xd9\xde\xde\xde\x89\xc9\x64\xf2\xbd\xce\
+\xd2\xd2\x53\x69\x9a\xd2\x52\xab\x81\x76\xa3\x86\x9d\xbd\x7d\x74\
+\x77\xf7\x30\xc8\x46\xd0\x3a\xf7\xd5\xb7\xc8\x8d\x5b\x92\xa0\x8c\
+\x79\x30\x43\x4a\x81\x7a\x35\xc1\x52\xb3\x81\x76\xb3\x56\x8e\x56\
+\x7a\xbd\x1e\x6f\xde\xbc\xf9\xe3\xc9\x64\xf2\xed\x22\xe6\xef\x74\
+\x83\xef\xff\xf4\x00\x11\xc1\xfa\xe9\x83\xb1\x16\x82\xe8\x73\xf5\
+\x46\xe3\xbb\xcd\x66\xf3\x13\x71\x1c\x97\xd7\x8f\xc6\x13\xf4\x87\
+\x19\x06\xd9\x08\xa3\xf1\xc4\x71\x78\x1f\x1e\xf0\xa3\x18\x10\x21\
+\x50\x12\x95\x38\x42\x35\xa9\xa0\x96\x26\x48\xe2\xa8\xf4\xc8\x60\
+\x30\xc0\xce\xf6\xf6\xab\xfd\x7e\xff\x3b\xc6\x98\xdf\x0a\x21\xa0\
+\xb5\x2e\xff\x43\x71\x27\x0f\xdc\xb5\x02\x05\x84\x12\x91\x0a\x82\
+\xe0\x4b\x49\x9a\x7e\xbd\x56\xab\x7d\xca\xcd\x44\x67\xad\xdf\xfc\
+\xdc\xbf\xe8\x27\xdc\x58\x72\x76\x2f\xa1\x38\xf2\x3c\x47\x96\x65\
+\xe8\xef\xed\x9d\xda\x1f\x0c\x7e\xa4\xf3\xfc\x17\xc5\x7f\x26\x88\
+\xe8\xff\x5f\x81\x62\x8a\xe6\x3a\x24\xab\x88\xe8\xd1\x4a\x1c\x3f\
+\x59\xa9\x54\x4e\xc4\x95\xca\xb1\x30\x0c\xa5\x52\x0a\x52\x88\xc5\
+\x9b\x7c\xc0\x1c\xfb\xcc\x31\x99\x4c\xcc\x28\xcb\xce\x67\x59\xf6\
+\xdc\x68\x34\x7a\x9a\x99\x5f\x26\x22\x5d\x4c\xeb\x8a\x91\xcb\xdd\
+\x28\xf0\xbe\xfe\xad\xe2\x21\x53\x83\xf9\x85\xd1\x78\xfc\xc2\x70\
+\x38\x0c\xa5\x52\x0f\x0a\xa2\x8f\x84\x61\xf8\x10\x88\x8e\x4a\x21\
+\xea\x24\x44\xea\x81\x60\x68\x8c\xe9\x5b\xff\x77\x1b\xb6\xb6\xfc\
+\xbb\xcd\xfc\xfd\xe5\x77\x8b\xf5\x3b\x1d\xff\x3b\x00\x71\x48\xea\
+\xdb\x9a\xd7\x91\x4c\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\
+\x82\
+\x00\x00\x68\x46\
+\x00\
+\x00\x01\x00\x08\x00\x30\x30\x00\x00\x00\x00\x08\x00\xa8\x0e\x00\
+\x00\x86\x00\x00\x00\x20\x20\x00\x00\x00\x00\x08\x00\xa8\x08\x00\
+\x00\x2e\x0f\x00\x00\x18\x18\x00\x00\x00\x00\x08\x00\xc8\x06\x00\
+\x00\xd6\x17\x00\x00\x10\x10\x00\x00\x00\x00\x08\x00\x68\x05\x00\
+\x00\x9e\x1e\x00\x00\x30\x30\x00\x00\x00\x00\x20\x00\xa8\x25\x00\
+\x00\x06\x24\x00\x00\x20\x20\x00\x00\x00\x00\x20\x00\xa8\x10\x00\
+\x00\xae\x49\x00\x00\x18\x18\x00\x00\x00\x00\x20\x00\x88\x09\x00\
+\x00\x56\x5a\x00\x00\x10\x10\x00\x00\x00\x00\x20\x00\x68\x04\x00\
+\x00\xde\x63\x00\x00\x28\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\
+\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\
+\x00\x7a\x52\x1a\x00\x7c\x53\x1b\x00\x85\x5a\x1d\x00\x89\x5c\x1e\
+\x00\x8d\x5e\x1e\x00\x90\x61\x1f\x00\x87\x5e\x26\x00\x82\x5d\x29\
+\x00\x89\x60\x27\x00\x8d\x62\x26\x00\x88\x60\x29\x00\x8f\x67\x2d\
+\x00\x94\x63\x20\x00\x92\x65\x27\x00\x9b\x68\x21\x00\x9e\x6a\x22\
+\x00\x94\x67\x28\x00\x96\x6a\x2d\x00\x9c\x6c\x29\x00\x9c\x6d\x2d\
+\x00\x82\x61\x33\x00\x8a\x66\x34\x00\x81\x63\x3a\x00\x83\x64\x3b\
+\x00\x85\x66\x3b\x00\x86\x67\x3c\x00\x89\x69\x3d\x00\x8c\x6b\x3d\
+\x00\x95\x6d\x36\x00\x9c\x6f\x32\x00\x98\x6f\x37\x00\x9f\x71\x31\
+\x00\x99\x70\x37\x00\x99\x71\x38\x00\x99\x74\x3f\x00\xa0\x6b\x22\
+\x00\xa2\x6d\x23\x00\xa5\x6f\x24\x00\xa0\x6e\x2a\x00\xa7\x70\x24\
+\x00\xa8\x71\x24\x00\xac\x73\x25\x00\xae\x75\x25\x00\xa4\x71\x2c\
+\x00\xa8\x75\x2f\x00\xb1\x77\x26\x00\xb5\x79\x27\x00\xb9\x7c\x28\
+\x00\xbc\x7e\x28\x00\xa0\x72\x32\x00\xa1\x74\x34\x00\xa6\x76\x35\
+\x00\xab\x77\x30\x00\xaf\x7d\x37\x00\xa2\x76\x39\x00\xa0\x77\x3e\
+\x00\xa4\x78\x3b\x00\xa2\x79\x3f\x00\xa4\x79\x3c\x00\xb1\x7c\x31\
+\x00\x8b\x70\x4b\x00\x8d\x72\x4d\x00\x93\x72\x43\x00\x95\x72\x42\
+\x00\x9a\x75\x41\x00\x99\x75\x45\x00\x96\x77\x4b\x00\x95\x77\x4e\
+\x00\x9d\x79\x49\x00\x99\x7a\x4e\x00\x9c\x7c\x4f\x00\x90\x76\x53\
+\x00\x92\x78\x55\x00\x95\x7a\x54\x00\x9d\x7d\x50\x00\x94\x7c\x5b\
+\x00\x94\x7d\x5d\x00\x98\x7f\x5c\x00\xa2\x7b\x44\x00\xa3\x7c\x45\
+\x00\xa2\x7c\x49\x00\xa5\x7f\x4c\x00\xa0\x7f\x53\x00\xbf\x80\x29\
+\x00\xbf\x85\x34\x00\xc1\x81\x29\x00\xc4\x83\x2a\x00\xc6\x85\x2b\
+\x00\xc8\x86\x2b\x00\xcb\x88\x2c\x00\xcd\x89\x2c\x00\xd1\x8c\x2d\
+\x00\xc6\x8a\x36\x00\xd3\x8f\x30\x00\xd4\x90\x33\x00\xd4\x91\x35\
+\x00\xd5\x93\x38\x00\xd5\x94\x3a\x00\xd5\x95\x3d\x00\x9c\x82\x5f\
+\x00\xab\x83\x49\x00\xa8\x84\x53\x00\xae\x87\x50\x00\xa9\x85\x55\
+\x00\xaf\x89\x56\x00\xa1\x84\x5d\x00\xb7\x8e\x55\x00\xb8\x8f\x56\
+\x00\x9c\x83\x60\x00\x9f\x86\x62\x00\x93\x83\x6d\x00\x9d\x87\x68\
+\x00\x98\x87\x6f\x00\x9a\x88\x6f\x00\x97\x87\x70\x00\x9d\x8a\x70\
+\x00\x9e\x8e\x77\x00\x99\x8e\x7f\x00\x9d\x90\x7e\x00\xa0\x86\x62\
+\x00\xa0\x87\x64\x00\xac\x8d\x62\x00\xa3\x8b\x6a\x00\xa2\x8c\x6e\
+\x00\xa9\x8f\x6a\x00\xac\x93\x6f\x00\xb3\x92\x63\x00\xb6\x93\x64\
+\x00\xbc\x97\x62\x00\xb9\x96\x64\x00\xbe\x98\x64\x00\xbc\x9a\x6b\
+\x00\xa0\x8c\x70\x00\xa6\x90\x71\x00\xab\x93\x72\x00\xa0\x90\x7a\
+\x00\xa2\x92\x7d\x00\xaf\x9a\x7d\x00\xb1\x96\x72\x00\xb4\x98\x72\
+\x00\xb4\x9c\x79\x00\xb2\x9c\x7d\x00\xb6\x9e\x7c\x00\xbe\xa4\x7f\
+\x00\xc1\x91\x4d\x00\xd0\x93\x40\x00\xd6\x97\x40\x00\xd6\x98\x42\
+\x00\xd7\x9a\x45\x00\xd8\x9b\x47\x00\xd8\x9c\x49\x00\xd9\x9e\x4c\
+\x00\xd2\x9c\x51\x00\xda\xa0\x4f\x00\xda\xa1\x51\x00\xdb\xa2\x54\
+\x00\xdb\xa4\x57\x00\xdb\xa5\x59\x00\xdc\xa5\x59\x00\xdc\xa6\x5c\
+\x00\xdd\xa8\x5e\x00\xc4\xa0\x6f\x00\xca\xa1\x69\x00\xdd\xa9\x62\
+\x00\xde\xaa\x64\x00\xde\xac\x66\x00\xdf\xad\x69\x00\xd8\xab\x6e\
+\x00\xde\xaf\x6d\x00\xc3\xa2\x73\x00\xe0\xb0\x6d\x00\xe0\xb2\x71\
+\x00\xe2\xb4\x76\x00\xe2\xb6\x78\x00\xe3\xb8\x7c\x00\xe4\xba\x7f\
+\x00\x9e\x94\x86\x00\xa1\x94\x82\x00\xa5\x98\x86\x00\xa8\x99\x83\
+\x00\xaf\x9c\x81\x00\xb1\x9d\x81\x00\xbc\xa7\x8a\x00\xc4\xac\x8c\
+\x00\xca\xaf\x89\x00\xc8\xb0\x8e\x00\xce\xb3\x8e\x00\xd5\xb8\x8f\
+\x00\xe4\xba\x80\x00\xe5\xbc\x85\x00\xe6\xbf\x89\x00\xe6\xc1\x8d\
+\x00\xe8\xc3\x91\x00\xe8\xc4\x93\x00\xe8\xc5\x95\x00\xe9\xc6\x98\
+\x00\xea\xc9\x9b\x00\xea\xca\x9d\x00\xeb\xcc\xa1\x00\xec\xce\xa5\
+\x00\xec\xcf\xa8\x00\xed\xd1\xaa\x00\xed\xd2\xad\x00\xee\xd4\xaf\
+\x00\xee\xd4\xb0\x00\xef\xd6\xb4\x00\xf0\xd9\xb9\x00\xf1\xdb\xbc\
+\x00\xf1\xdc\xbf\x00\xf2\xdd\xc1\x00\xf2\xdf\xc4\x00\xf3\xe1\xc7\
+\x00\xf3\xe1\xc9\x00\xf5\xe6\xd1\x00\xe1\x00\xf0\x00\xf0\x11\xff\
+\x00\xf2\x31\xff\x00\xf4\x51\xff\x00\xf6\x71\xff\x00\xf7\x91\xff\
+\x00\xf9\xb1\xff\x00\xfb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x1b\x00\x2f\x00\x2d\x00\x50\x00\x3f\x00\x70\x00\x52\x00\x90\
+\x00\x63\x00\xb0\x00\x76\x00\xcf\x00\x88\x00\xf0\x00\x99\x11\xff\
+\x00\xa6\x31\xff\x00\xb4\x51\xff\x00\xc2\x71\xff\x00\xcf\x91\xff\
+\x00\xdc\xb1\xff\x00\xeb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x08\x00\x2f\x00\x0e\x00\x50\x00\x15\x00\x70\x00\x1b\x00\x90\
+\x00\x21\x00\xb0\x00\x26\x00\xcf\x00\x2c\x00\xf0\x00\x3e\x11\xff\
+\x00\x58\x31\xff\x00\x71\x51\xff\x00\x8c\x71\xff\x00\xa6\x91\xff\
+\x00\xbf\xb1\xff\x00\xda\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x87\
+\x52\x3b\x2a\x2a\x2a\x2a\x3b\x52\x7b\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x3b\x31\x9c\
+\xbe\xc6\xd3\xd3\xd3\xd3\xc6\xbe\x9c\x54\x37\x86\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x5c\xbf\xd0\xce\
+\xc2\xad\xa0\x9c\x9c\xa0\xad\xc0\xce\xd0\xbf\x5c\x32\x75\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x57\xbe\xce\xc2\xa0\x5c\
+\x5c\x5f\x5c\x5f\x5f\x5c\x5f\x5c\x5f\x9c\xc2\xce\xbe\x57\x47\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x3b\x63\xc9\xc2\x98\x5c\x5f\x5f\
+\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x5f\x5c\x98\xc2\xc9\x63\x23\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x37\xa1\xc6\xa5\x5b\x5b\x5c\x5c\x5c\
+\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x5f\x5c\x5f\x5c\x5b\xa9\xc6\xa1\
+\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x37\x9a\xbf\x96\x57\x5a\x5b\x5c\x98\xaf\
+\xbf\xc2\xc2\xc2\xc2\xc2\xc2\xbf\x9c\x5c\x5c\x5b\x5b\x57\x96\xbf\
+\x9a\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x7a\x57\xad\x63\x54\x57\x57\x5a\x60\xb0\x9c\
+\x57\x31\x31\x31\x31\x31\x31\x98\xaf\x60\x5b\x5b\x57\x57\x54\x63\
+\xaf\x57\x43\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x25\xa0\x63\x31\x31\x54\x57\x57\xa0\x98\x11\
+\x00\x00\x00\x00\x00\x00\x00\x12\xa0\x9c\x5a\x57\x57\x54\x31\x31\
+\x94\xa0\x25\x77\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x1d\x63\x98\x2e\x2f\x31\x31\x54\x63\xa0\x25\x00\
+\x00\x00\x00\x00\x00\x00\x00\xb8\x31\xa1\x5c\x57\x54\x31\x31\x2f\
+\x2e\x98\x63\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x2a\x9c\x54\x2e\x2e\x2f\x31\x54\xa0\x5c\x3f\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x23\x94\x98\x54\x31\x31\x2f\x2e\
+\x2e\x57\x9c\x2a\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x42\x60\x95\x2a\x2a\x2a\x2e\x2f\x63\x9c\x0d\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2a\xa1\x57\x31\x2f\x2e\x2e\
+\x2a\x2a\x95\x60\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x0d\xa0\x57\x28\x2a\x2a\x2a\x31\x9c\x57\x4a\x00\x00\
+\x00\x00\x00\xb5\x00\x00\x00\x00\x00\x1d\x96\x95\x2f\x2e\x2a\x2a\
+\x2a\x2a\x57\xa0\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x2e\xa0\x25\x25\x2a\x2a\x2a\x60\x9c\x05\x00\x00\x00\
+\x00\x00\x8e\x07\x00\x00\x00\x00\x00\x00\x2f\xa1\x31\x2e\x2a\x2a\
+\x28\x25\x2a\xa0\x2f\x73\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x7d\x5b\x95\x25\x25\x25\x2a\x2a\xa0\x57\x4a\x00\x00\x00\
+\x00\x00\x0c\x10\x6f\x00\x00\x00\x00\x00\x0c\x9a\x94\x2a\x2a\x28\
+\x28\x25\x25\x95\x5b\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x47\x63\x98\x5c\x5b\x31\x25\x5f\x9c\x03\x00\x00\x00\x00\
+\x00\x8a\x2e\x94\x08\x00\x00\x00\x00\x00\xb7\x2e\xa1\x2f\x28\x25\
+\x2f\x57\x5c\x98\x63\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x16\xa0\xad\xa5\xa1\xa1\xa0\xa9\x57\x48\x00\x00\x00\x00\
+\x00\x0a\x9c\xa5\x25\x6f\x00\x00\x00\x00\x00\x08\x9c\x95\x63\x9c\
+\xa0\xa1\xa5\xad\x9c\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x02\xb0\xab\xa5\xa5\xa5\xa9\xab\x03\x00\x00\x00\x00\x00\
+\x4e\x57\xa1\x98\x63\x18\x00\x00\x00\x00\x00\x8e\x60\xaf\xa0\xa1\
+\xa1\xa5\xa5\xab\xad\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x02\xbe\xad\xa9\xa5\xa5\xb0\x5c\x18\x77\xb1\x00\x00\x77\
+\x03\xab\xa5\xa1\xad\x0d\x77\x00\x00\x00\x00\x00\x0d\xaf\xab\xa1\
+\xa5\xa5\xa9\xad\xaf\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x3f\xad\xaf\xab\xa9\xad\xb0\x31\x28\x2a\x2a\x2a\x2a\x2a\
+\x60\xaf\x9c\x9c\xad\x63\x18\x00\x00\x00\x00\x00\x7e\x63\xb0\xa5\
+\xa9\xa9\xab\xb0\x9c\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x4b\xa0\xb0\xad\xab\xbe\xbe\xbe\xbe\xb0\xb0\xb0\xb0\xb0\
+\xb0\xab\xa0\xa0\xa5\xb0\x0e\x00\x00\x00\x00\x00\x00\x25\xbe\xad\
+\xa9\xab\xab\xb0\xa0\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x8e\x63\xbe\xad\xad\xab\xa9\xa9\xa5\xa5\xa5\xa5\xa5\xa1\
+\xa1\xa1\xa1\xa1\xa1\xb0\x5c\x3e\x00\x00\x00\x00\x00\x53\x9c\xb0\
+\xab\xad\xad\xbe\x60\x4c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x57\xc2\xaf\xad\xad\xab\xab\xa9\xa9\xa9\xa5\xa5\xa5\
+\xa5\xa5\xa5\xa5\xa5\xaf\xbe\x0d\x00\x00\x00\x00\x00\x00\x57\xc0\
+\xb0\xad\xaf\xc2\x31\xb3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x0e\xc6\xbf\xaf\xad\xad\xad\xab\xab\xab\xa9\xa9\xa9\
+\xa9\xa5\xa5\xa5\xa5\xa9\xc4\x63\x4a\x00\x00\x00\x00\x00\x1d\xbe\
+\xc0\xaf\xbf\xc2\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x7a\xa1\xc8\xb0\xaf\xaf\xad\xad\xad\xad\xab\xab\xab\
+\xab\xa9\xab\xa9\xa9\xab\xbf\xbf\x0a\x00\x00\x00\x00\x00\x00\x60\
+\xc8\xbe\xc8\xa1\x43\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x31\xcb\xc2\xb0\xb0\xaf\xaf\xad\xad\xad\xad\xad\
+\xad\xab\xab\xad\xab\xab\xaf\xcb\x5f\x4c\x00\x00\x00\x00\x00\x14\
+\xc6\xcb\xcb\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x68\xad\xce\xbe\xb0\xb0\xb0\xb0\xaf\xaf\xad\xad\
+\xad\xad\xad\xad\xad\xad\xad\xc4\xc2\x0e\x00\x00\x00\x00\x00\x90\
+\xa5\xce\xa5\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x2a\xcb\xc9\xbe\xbe\xbe\xb0\xb0\xb0\xb0\xaf\
+\xaf\xaf\xad\xad\xaf\xaf\xaf\xb0\xce\x5b\x74\x00\x00\x00\x00\x00\
+\x2a\xc9\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xbb\x5c\xd0\xc8\xbe\xbe\xbe\xb0\xb0\xb0\xb0\
+\xb0\xb0\xb0\xb0\xb0\xb0\xb0\xb0\xc8\xbe\x12\x00\x00\x00\x00\x00\
+\x80\x25\x8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x80\x63\xd0\xc8\xbe\xbe\xbe\xbe\xbe\xbe\
+\xbe\xb0\xb0\xb0\xb0\xb0\xb0\xbe\xc0\xd0\x57\x75\x00\x00\x00\x00\
+\x00\x69\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x65\x94\xd0\xcb\xbf\xbe\xbe\xbe\xbe\
+\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xcb\xb0\x42\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x81\x60\xcb\xd0\xc6\xbf\xbf\xbf\
+\xbf\xbf\xbf\xbe\xbe\xbe\xbf\xbf\xbf\xc4\xd0\x2f\xb2\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbb\x2a\xaf\xd4\xd0\xc8\xc2\
+\xbf\xbf\xbf\xbf\xbf\xbf\xbf\xbf\xc2\xc9\xd4\xaf\x50\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x6c\x54\xb0\xd3\xd6\
+\xd5\xd0\xce\xce\xce\xce\xd0\xd5\xd6\xd3\xb0\x54\x68\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x3c\x94\
+\xab\xc6\xc8\xd0\xd0\xc8\xc6\xab\x94\x2d\x84\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\xbc\x35\
+\x8b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\xbb\x83\x6c\x6c\x6b\x6b\x81\xb9\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x33\x36\x00\x00\x00\x00\x00\x91\
+\x2a\x2c\x78\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x50\x2a\x2f\x00\x00\x00\x00\x00\x00\x00\
+\x55\xa5\x5f\x33\x64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x50\x31\x9c\x5f\xa2\x00\x00\x00\x00\x00\x00\x00\
+\x00\x5d\xad\xbe\x60\x27\x47\x89\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x86\x33\x54\xa0\xc0\x63\xa3\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x99\xa0\xc8\xbf\x9c\x31\x14\x42\x70\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb5\x6a\x1e\
+\x2e\x94\xaf\xc2\xbf\x5f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\xa9\x95\xc6\xc8\xc0\xb0\x98\x5b\x2e\x10\x23\x23\x6d\
+\x6d\x6d\x89\xb3\x00\x00\x7b\x78\x6d\x47\x42\x14\x25\x57\x95\xa9\
+\xc0\xc2\xc6\xa9\x94\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x63\xa5\xce\xc8\xc2\xbe\xad\xa5\x98\x95\x5b\
+\x5c\x5c\x54\x31\x31\x31\x5b\x5c\x5c\x63\x96\xa1\xab\xb0\xbf\xc4\
+\xcb\xbe\x95\xa9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\xc4\x94\xad\xcb\xcb\xc6\xc0\xb0\xad\xa5\
+\xa0\x9c\xa0\xa0\xa0\xa0\x9c\xa0\xa5\xab\xb0\xbe\xc2\xc8\xce\xbf\
+\x96\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\xc4\x95\xa5\xc2\xd0\xce\xcb\xc8\
+\xc4\xc0\xbe\xb0\xb0\xbe\xbf\xc2\xc6\xc8\xcb\xd0\xc9\xad\x94\xa0\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\x63\x9c\xaf\xc2\
+\xc8\xd0\xd3\xd3\xd3\xd3\xd3\xc8\xc8\xbe\xa5\x95\x9c\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc2\xaf\
+\xa5\x95\x63\x63\x63\x63\x63\xa1\xa1\xc0\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\
+\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xc0\
+\x0f\xff\xff\x00\x00\xff\xfe\x00\x01\xff\xff\x00\x00\xff\xfc\x00\
+\x00\x7f\xff\x00\x00\xff\xf0\x00\x00\x3f\xff\x00\x00\xff\xe0\x00\
+\x00\x1f\xff\x00\x00\xff\xc0\x00\x00\x0f\xff\x00\x00\xff\x80\x00\
+\x00\x07\xff\x00\x00\xff\x00\x00\x00\x03\xff\x00\x00\xff\x00\x1f\
+\xc0\x01\xff\x00\x00\xfe\x00\x3f\xc0\x01\xff\x00\x00\xfe\x00\x3f\
+\xe0\x00\xff\x00\x00\xfc\x00\x7f\xf0\x00\xff\x00\x00\xfc\x00\x7d\
+\xf0\x00\xff\x00\x00\xfc\x00\xf9\xf8\x00\x7f\x00\x00\xf8\x00\xf8\
+\xf8\x00\x7f\x00\x00\xf8\x01\xf0\xf8\x00\x7f\x00\x00\xf8\x01\xf0\
+\x7c\x00\x7f\x00\x00\xf8\x03\xe0\x7c\x00\x7f\x00\x00\xf8\x00\xc0\
+\x3e\x00\x7f\x00\x00\xf8\x00\x00\x3e\x00\x7f\x00\x00\xf8\x00\x00\
+\x3f\x00\x7f\x00\x00\xf8\x00\x00\x1f\x00\x7f\x00\x00\xfc\x00\x00\
+\x1f\x80\x7f\x00\x00\xfc\x00\x00\x0f\x80\xff\x00\x00\xfc\x00\x00\
+\x0f\xc0\xff\x00\x00\xfe\x00\x00\x07\xc1\xff\x00\x00\xfe\x00\x00\
+\x07\xc1\xff\x00\x00\xff\x00\x00\x03\xe3\xff\x00\x00\xff\x00\x00\
+\x03\xe3\xff\x00\x00\xff\x80\x00\x01\xf7\xff\x00\x00\xff\xc0\x00\
+\x01\xff\xff\x00\x00\xff\xe0\x00\x00\xff\xff\x00\x00\xff\xf0\x00\
+\x00\xff\xff\x00\x00\xff\xfc\x00\x00\xff\xff\x00\x00\xff\xff\x00\
+\x03\xff\xf7\x00\x00\x8f\xff\xe0\x1f\xff\xe7\x00\x00\xc3\xff\xff\
+\xff\xff\x8f\x00\x00\xe0\xff\xff\xff\xfe\x0f\x00\x00\xf0\x1f\xff\
+\xff\xf0\x1f\x00\x00\xf8\x03\xff\xff\x00\x7f\x00\x00\xfc\x00\x01\
+\x80\x00\xff\x00\x00\xff\x00\x00\x00\x01\xff\x00\x00\xff\x80\x00\
+\x00\x07\xff\x00\x00\xff\xe0\x00\x00\x1f\xff\x00\x00\xff\xfc\x00\
+\x00\xff\xff\x00\x00\xff\xff\x80\x07\xff\xff\x00\x00\x28\x00\x00\
+\x00\x20\x00\x00\x00\x40\x00\x00\x00\x01\x00\x08\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95\x6f\x3b\x00\x9b\x74\x3f\
+\x00\xa3\x6d\x23\x00\xa4\x6e\x23\x00\xa6\x6f\x24\x00\xa7\x70\x24\
+\x00\xa9\x72\x24\x00\xac\x73\x25\x00\xaf\x75\x25\x00\xac\x76\x2b\
+\x00\xb1\x76\x25\x00\xb4\x78\x26\x00\xb8\x7b\x27\x00\xb9\x7c\x27\
+\x00\xb3\x79\x29\x00\xb5\x7a\x28\x00\xb7\x7c\x29\x00\xba\x7d\x29\
+\x00\xbd\x7f\x28\x00\xba\x7f\x2e\x00\xa8\x76\x34\x00\xa4\x78\x3a\
+\x00\xa7\x7b\x3f\x00\xae\x7d\x38\x00\xab\x7e\x3e\x00\xb1\x7c\x32\
+\x00\x85\x6b\x47\x00\x87\x6f\x4c\x00\x9c\x79\x47\x00\x93\x75\x4a\
+\x00\x90\x74\x4e\x00\x95\x77\x4d\x00\x9a\x79\x4c\x00\x9e\x7d\x4e\
+\x00\x89\x75\x58\x00\x92\x76\x51\x00\x94\x78\x51\x00\x9b\x7c\x52\
+\x00\x9f\x7e\x50\x00\x92\x7b\x5c\x00\xa2\x7a\x43\x00\xa6\x7c\x43\
+\x00\xac\x7f\x42\x00\x8d\x7e\x69\x00\x8b\x7e\x6c\x00\x93\x7e\x62\
+\x00\xbf\x80\x29\x00\xbf\x85\x35\x00\xb2\x80\x3a\x00\xb5\x81\x39\
+\x00\xb5\x83\x3e\x00\xbf\x87\x3a\x00\xc2\x82\x2a\x00\xc7\x85\x2a\
+\x00\xc1\x83\x2c\x00\xcd\x89\x2c\x00\xd2\x8d\x2d\x00\xc2\x87\x36\
+\x00\xc7\x89\x33\x00\xc4\x88\x35\x00\xc9\x89\x32\x00\xcd\x8c\x31\
+\x00\xc1\x8a\x3e\x00\xcb\x8d\x38\x00\xc8\x8e\x3e\x00\xcd\x90\x3b\
+\x00\xd0\x91\x3b\x00\xd4\x93\x39\x00\xd5\x94\x3b\x00\xd0\x92\x3d\
+\x00\xd3\x94\x3c\x00\xd5\x95\x3e\x00\xa7\x81\x4e\x00\xa8\x81\x4a\
+\x00\xaf\x85\x4b\x00\xa8\x83\x4f\x00\xae\x85\x4c\x00\xb9\x86\x42\
+\x00\xbb\x88\x42\x00\xbc\x89\x42\x00\xbb\x8b\x47\x00\xbd\x8a\x44\
+\x00\xb4\x88\x4b\x00\xb0\x88\x4f\x00\xb6\x8b\x4f\x00\xb8\x89\x48\
+\x00\xba\x8c\x4b\x00\xbc\x8c\x4a\x00\xba\x8b\x4d\x00\xbb\x8d\x4d\
+\x00\xa4\x83\x54\x00\xa8\x84\x52\x00\xab\x8b\x5f\x00\xb1\x8a\x54\
+\x00\xb9\x8e\x51\x00\xb2\x8e\x5c\x00\xbd\x90\x53\x00\xbe\x93\x57\
+\x00\xbd\x93\x59\x00\xbe\x94\x59\x00\xbd\x94\x5d\x00\x97\x80\x61\
+\x00\x96\x82\x67\x00\x9c\x84\x63\x00\x98\x83\x66\x00\x94\x82\x69\
+\x00\x91\x83\x6f\x00\x94\x84\x6d\x00\x9a\x85\x68\x00\x98\x86\x6d\
+\x00\x97\x87\x72\x00\x97\x89\x76\x00\x9e\x8d\x76\x00\x92\x88\x79\
+\x00\x95\x8a\x7b\x00\x91\x88\x7c\x00\x98\x8c\x7c\x00\xa1\x85\x60\
+\x00\xa0\x87\x65\x00\xa7\x8b\x66\x00\xac\x8c\x61\x00\xa6\x8d\x6a\
+\x00\xb0\x8f\x60\x00\xbd\x97\x65\x00\xbf\x98\x61\x00\xbb\x98\x65\
+\x00\xa8\x90\x70\x00\xad\x94\x72\x00\xa5\x92\x79\x00\xaa\x96\x79\
+\x00\xbb\xa0\x7c\x00\xc2\x8e\x43\x00\xc5\x8e\x41\x00\xc3\x8e\x44\
+\x00\xc8\x8f\x42\x00\xc5\x90\x46\x00\xcd\x92\x42\x00\xcf\x95\x43\
+\x00\xc1\x90\x4b\x00\xc6\x92\x4b\x00\xcf\x97\x48\x00\xd1\x95\x43\
+\x00\xd4\x96\x42\x00\xd1\x97\x46\x00\xd4\x97\x44\x00\xd7\x9b\x47\
+\x00\xd1\x97\x48\x00\xd2\x99\x49\x00\xd5\x9a\x4b\x00\xd7\x9c\x49\
+\x00\xd8\x9b\x48\x00\xd8\x9d\x4a\x00\xd8\x9f\x4f\x00\xc0\x91\x50\
+\x00\xc5\x96\x56\x00\xcb\x98\x51\x00\xce\x9b\x53\x00\xcc\x9b\x56\
+\x00\xce\x9c\x57\x00\xc3\x97\x5a\x00\xcf\x9d\x59\x00\xcd\x9e\x5c\
+\x00\xd7\xa0\x54\x00\xd9\xa0\x51\x00\xd9\xa2\x56\x00\xdb\xa4\x57\
+\x00\xd1\xa1\x5f\x00\xda\xa3\x58\x00\xdb\xa5\x5a\x00\xdc\xa5\x5b\
+\x00\xdc\xa7\x5d\x00\xdd\xa8\x5e\x00\xce\xa2\x64\x00\xcd\xa5\x6f\
+\x00\xd6\xa5\x61\x00\xdd\xa9\x61\x00\xdd\xaa\x64\x00\xde\xac\x66\
+\x00\xd3\xa6\x69\x00\xd6\xa8\x69\x00\xd5\xaa\x6f\x00\xda\xab\x6a\
+\x00\xde\xad\x69\x00\xda\xad\x6f\x00\xde\xae\x6c\x00\xc7\xa3\x71\
+\x00\xcc\xa7\x73\x00\xcd\xa9\x76\x00\xd3\xab\x74\x00\xd5\xad\x76\
+\x00\xd5\xaf\x7c\x00\xdf\xb5\x7a\x00\xdb\xb3\x7e\x00\xdc\xb3\x7c\
+\x00\xe0\xaf\x6c\x00\xe0\xb0\x6e\x00\xe0\xb1\x71\x00\xe0\xb3\x74\
+\x00\xe1\xb4\x75\x00\xe2\xb6\x79\x00\xe1\xb7\x7c\x00\xe3\xb8\x7b\
+\x00\xe3\xb8\x7d\x00\x91\x89\x80\x00\x92\x8c\x83\x00\xa8\x9a\x87\
+\x00\xa9\x9b\x88\x00\xb9\xa3\x84\x00\xb2\xa0\x88\x00\xce\xaf\x84\
+\x00\xc0\xa9\x8a\x00\xc8\xb0\x8e\x00\xd5\xb2\x82\x00\xda\xb6\x85\
+\x00\xd7\xb6\x88\x00\xd9\xbc\x94\x00\xe4\xba\x81\x00\xe4\xbc\x83\
+\x00\xe4\xbd\x86\x00\xe2\xbd\x8b\x00\xe5\xbe\x89\x00\xe6\xc0\x8b\
+\x00\xe6\xc0\x8d\x00\xe7\xc2\x90\x00\xe8\xc5\x95\x00\xe5\xc4\x98\
+\x00\xe9\xc8\x9a\x00\xea\xc9\x9d\x00\xea\xcc\xa1\x00\xeb\xce\xa6\
+\x00\xec\xce\xa5\x00\xed\xd2\xac\x00\xee\xd4\xb0\x00\xef\xd6\xb4\
+\x00\xf0\xd9\xba\x00\xcf\x91\xff\x00\xdc\xb1\xff\x00\xeb\xd1\xff\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x08\x00\x2f\x00\x0e\x00\x50\
+\x00\x15\x00\x70\x00\x1b\x00\x90\x00\x21\x00\xb0\x00\x26\x00\xcf\
+\x00\x2c\x00\xf0\x00\x3e\x11\xff\x00\x58\x31\xff\x00\x71\x51\xff\
+\x00\x8c\x71\xff\x00\xa6\x91\xff\x00\xbf\xb1\xff\x00\xda\xd1\xff\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\xcf\x6d\x24\x1f\x25\x69\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\
+\x9b\xca\xdf\xe5\xe4\xe3\xde\xb6\x61\x68\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7a\xa0\xdd\
+\xd9\xa6\x92\x45\x44\x48\x98\xb1\xdd\xc2\x53\x75\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x79\xc3\xc9\x45\
+\x39\x39\x39\x39\x39\x39\x39\x39\x39\xa4\xdd\x9c\x2e\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5d\xb6\xa5\x3d\x38\
+\x44\xb0\xc4\xc5\xc4\xc5\xc3\x48\x39\x38\x40\xc3\x9e\x6b\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x82\x90\x8e\x13\x35\x3e\
+\xac\x52\x5f\x64\x64\x62\x52\xac\x3e\x36\x35\x37\xa5\x32\x73\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x8a\x11\x0e\x2f\x8f\
+\x3f\x00\x00\x00\x00\x00\x00\x89\x43\x35\x2f\x0d\x12\x90\x02\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7b\x8a\x0c\x0b\x10\x37\x90\
+\x22\x00\x00\x00\x00\x00\x00\x54\x95\x2f\x0d\x0c\x09\x3c\x3a\x73\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x46\x07\x08\x0c\x89\x32\
+\x00\x00\x00\x00\x00\x00\x00\x00\x84\x3b\x0c\x09\x08\x0f\x91\x1c\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3b\x1a\x04\x06\x14\x91\x28\
+\x00\x00\x00\x21\x2d\x00\x00\x00\x5e\x95\x10\x08\x06\x04\x87\x16\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x47\x3d\x12\x07\x42\x1a\x00\
+\x00\x00\x00\x0a\x01\x00\x00\x00\x00\x41\x30\x06\x08\x12\x46\x32\
+\x00\x00\x00\x00\x00\x00\x00\x00\x80\xb7\xb2\xac\xb0\x9d\x25\x00\
+\x00\x00\x27\xa6\x34\x72\x00\x00\x00\x2a\xa4\x98\xaa\xb0\xc3\x5a\
+\xcd\x00\x00\x00\x00\x00\x00\x00\x78\xc8\xc3\xb1\xc4\x33\x2c\x00\
+\x00\x70\x51\xac\xac\x1b\x00\x00\x00\xd3\xa2\xb1\xb0\xb2\xc3\x64\
+\xcc\x00\x00\x00\x00\x00\x00\x00\xd0\xc5\xc5\xc4\xca\x86\x50\x50\
+\x50\x88\xb2\xa6\xb7\x52\x74\x00\x00\x00\x5f\xc7\xb2\xc3\xc7\x59\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\xc7\xc5\xc6\xc4\xc4\xc3\
+\xc3\xb7\xb2\xab\xac\xaf\x20\x00\x00\x00\x83\xb9\xc5\xc4\xca\x4e\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\xdb\xc7\xc5\xc4\xc3\xb7\
+\xb2\xb2\xb1\xb1\xb0\xc8\x51\x00\x00\x00\x00\x8b\xda\xc8\xde\x1d\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5e\xdf\xca\xc7\xc6\xc5\xc4\
+\xc3\xc3\xb7\xb7\xb7\xc7\xc8\x23\x00\x00\x00\x7e\xda\xdb\xcb\x6a\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xe0\xcb\xc8\xc8\xc7\
+\xc7\xc5\xc5\xc5\xc4\xc6\xe1\x58\x00\x00\x00\x00\xae\xe6\x55\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x65\xe6\xdd\xcb\xca\xca\
+\xc8\xc7\xc7\xc7\xc7\xc7\xda\xc1\x66\x00\x00\x00\x7d\xd6\x7a\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9f\xe5\xde\xd9\xd9\
+\xd9\xcb\xcb\xcb\xcb\xcb\xcb\xe3\x59\x00\x00\x00\x00\x5b\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa1\xe8\xe0\xda\
+\xda\xd9\xd9\xd9\xd9\xd9\xd9\xe1\xc0\x6c\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\xe2\xe6\
+\xe1\xde\xdd\xdd\xdb\xdb\xdd\xe0\xea\x4d\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xba\xb5\
+\xe9\xeb\xe8\xe5\xe5\xe7\xe9\xeb\xe5\x52\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\xd2\xb3\xbf\xd7\xd8\xd5\xbd\x7c\xd1\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x7f\x00\x00\x00\xbb\x29\x6e\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\xce\x17\x60\x00\x00\x00\x00\xa7\x95\x4b\x67\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x81\x56\
+\x94\x64\x00\x00\x00\x00\x00\x00\xb8\xc4\xb5\x56\x1e\x6f\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x71\x26\x9a\xb4\xb9\
+\xad\x00\x00\x00\x00\x00\x00\x00\x00\xde\xc5\xe0\xdf\xa8\x85\x18\
+\x19\x49\x4c\x76\x77\x76\x5c\x4a\x2b\x31\x85\xb1\xde\xdd\xb9\xd4\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc7\xc8\xe5\xe1\xc8\
+\xb2\x99\x97\x93\x8d\x93\x96\xa4\xb2\xcb\xe0\xe5\xc6\xb8\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcb\xca\xe0\
+\xe3\xe3\xe4\xe1\xe1\xe3\xe1\xe4\xe1\xdf\xc9\xbc\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\xde\xc7\xb9\xb2\xb2\xb2\xc3\xc6\xdc\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xf0\x3f\xff\xff\xc0\x07\
+\xff\xff\x00\x01\xff\xfe\x00\x00\xff\xfc\x00\x00\x7f\xf8\x00\x00\
+\x3f\xf8\x0f\xc0\x3f\xf0\x0f\xc0\x1f\xf0\x1f\xe0\x1f\xf0\x1c\xe0\
+\x1f\xf0\x3c\xf0\x1f\xe0\x38\x70\x0f\xe0\x30\x70\x0f\xe0\x00\x38\
+\x1f\xf0\x00\x38\x1f\xf0\x00\x3c\x1f\xf0\x00\x1c\x1f\xf8\x00\x1e\
+\x3f\xf8\x00\x0e\x3f\xfc\x00\x0f\x7f\xfe\x00\x07\xff\xff\x00\x07\
+\xff\xff\x80\x07\xff\xff\xe0\x0f\xfb\x8f\xff\xff\xe3\xc3\xff\xff\
+\x87\xe0\x7f\xfc\x0f\xf0\x00\x00\x1f\xfc\x00\x00\x7f\xff\x00\x01\
+\xff\xff\xe0\x0f\xff\x28\x00\x00\x00\x18\x00\x00\x00\x30\x00\x00\
+\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x9e\x74\x3a\x00\x9a\x72\x3c\x00\xa9\x71\x24\x00\xac\x73\x25\
+\x00\xae\x75\x25\x00\xaa\x75\x2c\x00\xb5\x79\x27\x00\xb9\x7d\x28\
+\x00\xbd\x7e\x28\x00\xa0\x78\x3f\x00\x96\x73\x42\x00\x99\x79\x4d\
+\x00\x9f\x7c\x4c\x00\x94\x77\x50\x00\x96\x79\x51\x00\x94\x7a\x55\
+\x00\x9c\x7e\x54\x00\x94\x7b\x59\x00\x86\x7a\x69\x00\x8b\x7f\x70\
+\x00\xbc\x80\x2c\x00\xb6\x84\x3f\x00\xbb\x85\x3a\x00\xbe\x88\x3d\
+\x00\xc2\x82\x2a\x00\xc7\x86\x2d\x00\xc9\x86\x2b\x00\xd1\x8d\x2d\
+\x00\xc6\x87\x30\x00\xc7\x88\x31\x00\xc6\x8b\x39\x00\xc1\x8a\x3d\
+\x00\xc9\x8d\x3c\x00\xca\x90\x3f\x00\xd3\x94\x3d\x00\xd6\x97\x3f\
+\x00\xaf\x82\x45\x00\xa7\x82\x4f\x00\xaa\x81\x4a\x00\xb5\x84\x40\
+\x00\xb0\x84\x47\x00\xb8\x87\x45\x00\xbe\x8b\x45\x00\xb1\x85\x49\
+\x00\xb7\x8a\x4b\x00\xb4\x88\x4d\x00\xb9\x8b\x4b\x00\xbd\x8f\x4f\
+\x00\xa1\x81\x54\x00\xa8\x85\x54\x00\xb0\x89\x52\x00\xb9\x8e\x52\
+\x00\xb9\x93\x5c\x00\xbc\x95\x5f\x00\x95\x80\x62\x00\x98\x81\x62\
+\x00\x9a\x87\x6c\x00\x95\x89\x78\x00\x93\x8a\x7c\x00\xad\x8f\x65\
+\x00\xa8\x8d\x68\x00\xaf\x91\x68\x00\xa2\x8d\x70\x00\xa8\x92\x73\
+\x00\xaa\x95\x77\x00\xc1\x8e\x47\x00\xca\x90\x41\x00\xcd\x93\x42\
+\x00\xcf\x95\x44\x00\xcc\x95\x49\x00\xd3\x96\x41\x00\xd2\x96\x44\
+\x00\xd7\x9c\x4a\x00\xd6\x9d\x4d\x00\xd8\x9d\x4a\x00\xcb\x98\x52\
+\x00\xc4\x98\x5a\x00\xca\x9c\x5e\x00\xda\xa2\x54\x00\xd1\xa0\x5c\
+\x00\xdc\xa6\x5b\x00\xd9\xa5\x5d\x00\xdd\xa8\x5e\x00\xc0\x9a\x66\
+\x00\xca\x9e\x62\x00\xcf\xa2\x64\x00\xc7\xa1\x6d\x00\xcf\xa5\x6b\
+\x00\xdb\xa8\x61\x00\xdd\xa9\x61\x00\xde\xab\x64\x00\xde\xac\x66\
+\x00\xd0\xa4\x68\x00\xda\xaa\x68\x00\xde\xad\x69\x00\xda\xad\x6f\
+\x00\xdf\xaf\x6c\x00\xc4\xa2\x73\x00\xc7\xa4\x74\x00\xcc\xa7\x73\
+\x00\xcd\xa8\x75\x00\xdb\xaf\x73\x00\xd2\xac\x78\x00\xd6\xaf\x79\
+\x00\xd9\xb2\x7c\x00\xe0\xaf\x6d\x00\xe0\xb0\x6e\x00\xe0\xb1\x71\
+\x00\xe1\xb4\x75\x00\xe2\xb6\x79\x00\xe2\xb7\x7c\x00\xe3\xb8\x7d\
+\x00\x95\x8d\x82\x00\x9b\x91\x83\x00\xc7\xab\x85\x00\xcf\xae\x81\
+\x00\xcf\xb1\x86\x00\xca\xb3\x92\x00\xe0\xb8\x80\x00\xe4\xba\x81\
+\x00\xe4\xbc\x83\x00\xe4\xbc\x85\x00\xe5\xbf\x89\x00\xe6\xc0\x8c\
+\x00\xe7\xc3\x91\x00\xe8\xc4\x93\x00\xe8\xc6\x96\x00\xe8\xc7\x99\
+\x00\xea\xca\x9e\x00\xec\xd0\xaa\x00\xed\xd2\xad\x00\x90\x74\x00\
+\x00\xb0\x8e\x00\x00\xcf\xa9\x00\x00\xf0\xc3\x00\x00\xff\xd2\x11\
+\x00\xff\xd8\x31\x00\xff\xdd\x51\x00\xff\xe4\x71\x00\xff\xea\x91\
+\x00\xff\xf0\xb1\x00\xff\xf6\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x2f\x14\x00\x00\x50\x22\x00\x00\x70\x30\x00\x00\x90\x3e\x00\
+\x00\xb0\x4d\x00\x00\xcf\x5b\x00\x00\xf0\x69\x00\x00\xff\x79\x11\
+\x00\xff\x8a\x31\x00\xff\x9d\x51\x00\xff\xaf\x71\x00\xff\xc1\x91\
+\x00\xff\xd2\xb1\x00\xff\xe5\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x2f\x03\x00\x00\x50\x04\x00\x00\x70\x06\x00\x00\x90\x09\x00\
+\x00\xb0\x0a\x00\x00\xcf\x0c\x00\x00\xf0\x0e\x00\x00\xff\x20\x12\
+\x00\xff\x3e\x31\x00\xff\x5c\x51\x00\xff\x7a\x71\x00\xff\x97\x91\
+\x00\xff\xb6\xb1\x00\xff\xd4\xd1\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x2f\x00\x0e\x00\x50\x00\x17\x00\x70\x00\x21\x00\x90\x00\x2b\
+\x00\xb0\x00\x36\x00\xcf\x00\x40\x00\xf0\x00\x49\x00\xff\x11\x5a\
+\x00\xff\x31\x70\x00\xff\x51\x86\x00\xff\x71\x9c\x00\xff\x91\xb2\
+\x00\xff\xb1\xc8\x00\xff\xd1\xdf\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x2f\x00\x20\x00\x50\x00\x36\x00\x70\x00\x4c\x00\x90\x00\x62\
+\x00\xb0\x00\x78\x00\xcf\x00\x8e\x00\xf0\x00\xa4\x00\xff\x11\xb3\
+\x00\xff\x31\xbe\x00\xff\x51\xc7\x00\xff\x71\xd1\x00\xff\x91\xdc\
+\x00\xff\xb1\xe5\x00\xff\xd1\xf0\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x2c\x00\x2f\x00\x4b\x00\x50\x00\x69\x00\x70\x00\x87\x00\x90\
+\x00\xa5\x00\xb0\x00\xc4\x00\xcf\x00\xe1\x00\xf0\x00\xf0\x11\xff\
+\x00\xf2\x31\xff\x00\xf4\x51\xff\x00\xf6\x71\xff\x00\xf7\x91\xff\
+\x00\xf9\xb1\xff\x00\xfb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x1b\x00\x2f\x00\x2d\x00\x50\x00\x3f\x00\x70\x00\x52\x00\x90\
+\x00\x63\x00\xb0\x00\x76\x00\xcf\x00\x88\x00\xf0\x00\x99\x11\xff\
+\x00\xa6\x31\xff\x00\xb4\x51\xff\x00\xc2\x71\xff\x00\xcf\x91\xff\
+\x00\xdc\xb1\xff\x00\xeb\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x08\x00\x2f\x00\x0e\x00\x50\x00\x15\x00\x70\x00\x1b\x00\x90\
+\x00\x21\x00\xb0\x00\x26\x00\xcf\x00\x2c\x00\xf0\x00\x3e\x11\xff\
+\x00\x58\x31\xff\x00\x71\x51\xff\x00\x8c\x71\xff\x00\xa6\x91\xff\
+\x00\xbf\xb1\xff\x00\xda\xd1\xff\x00\xff\xff\xff\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x57\
+\x64\x57\x35\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x41\x56\x7c\x5c\x4b\x24\x4b\x5c\x7c\x56\x38\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x6e\x4a\x1c\x1c\x1c\
+\x1c\x1c\x1c\x1c\x4a\x6e\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x3e\x5b\x1e\x1b\x49\x59\x50\x50\x50\x5c\x1c\x1b\x1e\x5b\x0c\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x22\x15\x08\x1a\x46\x00\x00\
+\x00\x00\x2d\x47\x19\x09\x15\x22\x71\x00\x00\x00\x00\x00\x00\x00\
+\x2c\x1f\x05\x07\x48\x0d\x00\x00\x00\x00\x00\x45\x09\x07\x05\x1f\
+\x0a\x00\x00\x00\x00\x00\x00\x00\x18\x04\x03\x08\x20\x00\x00\x00\
+\x72\x00\x00\x2e\x21\x05\x04\x05\x18\x00\x00\x00\x00\x00\x00\x00\
+\x23\x08\x03\x44\x0c\x00\x00\x3d\x02\x00\x00\x00\x45\x04\x03\x07\
+\x23\x3b\x00\x00\x00\x00\x00\x00\x6a\x5b\x5a\x42\x00\x00\x00\x20\
+\x44\x71\x00\x00\x2c\x4f\x51\x5a\x61\x13\x00\x00\x00\x00\x00\x00\
+\x6e\x5f\x6d\x06\x0f\x0f\x01\x5f\x5c\x0b\x00\x00\x00\x5c\x5b\x5f\
+\x6b\x14\x00\x00\x00\x00\x00\x00\x5c\x6c\x6d\x6d\x6c\x6c\x6b\x5a\
+\x53\x4c\x00\x00\x00\x30\x6c\x6c\x5a\x00\x00\x00\x00\x00\x00\x00\
+\x4e\x6e\x6d\x6b\x61\x5c\x5c\x5b\x5b\x70\x0c\x00\x00\x73\x7a\x6e\
+\x4d\x00\x00\x00\x00\x00\x00\x00\x54\x7f\x6e\x6d\x6c\x6c\x6b\x61\
+\x61\x6d\x55\x00\x00\x00\x5d\x81\x32\x00\x00\x00\x00\x00\x00\x00\
+\x00\x69\x7b\x70\x6e\x6e\x6d\x6d\x6d\x6d\x80\x10\x00\x00\x62\x68\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x75\x7f\x7c\x78\x78\x70\x78\
+\x70\x70\x7c\x55\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x63\x7d\x80\x7a\x7a\x7a\x79\x7a\x7a\x81\x37\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x58\x82\x83\x81\
+\x81\x81\x83\x83\x2f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x67\x65\x74\x65\x65\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x4e\x2a\x3a\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x33\x17\x00\x00\x00\x00\x60\
+\x6b\x34\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x39\x34\
+\x5e\x50\x00\x00\x00\x00\x00\x00\x80\x6d\x7f\x59\x42\x16\x25\x27\
+\x31\x26\x29\x28\x2b\x52\x7c\x70\x65\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x7a\x70\x81\x7e\x70\x6b\x61\x6a\x6e\x7c\x81\x79\x66\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x79\x6f\x70\
+\x70\x70\x6e\x77\x76\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\
+\x00\xff\x01\xff\x00\xfc\x00\x7f\x00\xf8\x00\x3f\x00\xf0\x00\x1f\
+\x00\xf0\x78\x0f\x00\xe0\x7c\x0f\x00\xe0\xec\x0f\x00\xe0\xce\x07\
+\x00\xe1\xc6\x07\x00\xe0\x07\x07\x00\xe0\x07\x0f\x00\xe0\x03\x0f\
+\x00\xe0\x03\x8f\x00\xf0\x01\x9f\x00\xf0\x01\xdf\x00\xf8\x00\xff\
+\x00\xfe\x00\xff\x00\xff\x83\xff\x00\x8f\xff\xf3\x00\xc3\xff\x87\
+\x00\xe0\x00\x0f\x00\xf8\x00\x3f\x00\xff\x00\xff\x00\x28\x00\x00\
+\x00\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x08\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x71\x24\x00\xae\x74\x25\
+\x00\xbd\x7f\x29\x00\x94\x73\x45\x00\xa4\x7e\x4a\x00\xa1\x7e\x4e\
+\x00\x95\x7f\x60\x00\xbd\x81\x2e\x00\xbe\x83\x32\x00\xc2\x82\x29\
+\x00\xc3\x87\x35\x00\xc3\x88\x36\x00\xc5\x89\x35\x00\xc3\x89\x38\
+\x00\xc4\x8a\x3a\x00\xca\x8f\x3d\x00\xd2\x8f\x32\x00\xbb\x88\x43\
+\x00\xb7\x8a\x4d\x00\xbb\x8c\x4a\x00\xa2\x82\x56\x00\xa6\x85\x56\
+\x00\xb0\x88\x51\x00\xb7\x92\x5f\x00\xbb\x93\x5a\x00\xbd\x94\x5a\
+\x00\x98\x83\x66\x00\x9d\x88\x6b\x00\x9b\x8b\x75\x00\xa3\x89\x65\
+\x00\xa4\x8a\x67\x00\xa7\x90\x6f\x00\xaa\x91\x6e\x00\xa1\x8e\x72\
+\x00\xaf\x97\x75\x00\xae\x99\x7a\x00\xcf\x95\x44\x00\xc1\x91\x4e\
+\x00\xd2\x96\x43\x00\xd5\x9d\x4f\x00\xd8\x9d\x4a\x00\xd9\x9e\x4d\
+\x00\xc5\x96\x54\x00\xcd\x9b\x57\x00\xcd\x9c\x57\x00\xc4\x99\x5e\
+\x00\xcf\x9e\x59\x00\xd5\x9e\x53\x00\xd8\xa4\x5b\x00\xdc\xa5\x5a\
+\x00\xd8\xa5\x5f\x00\xdc\xa8\x5e\x00\xce\xa4\x6a\x00\xd0\xa2\x61\
+\x00\xde\xab\x65\x00\xde\xac\x67\x00\xd6\xa9\x69\x00\xde\xae\x69\
+\x00\xd8\xab\x6c\x00\xdf\xaf\x6c\x00\xc2\xa1\x72\x00\xcd\xab\x7b\
+\x00\xd1\xa9\x73\x00\xdc\xb1\x74\x00\xdc\xb3\x7a\x00\xda\xb2\x7d\
+\x00\xe0\xaf\x6d\x00\xe0\xb0\x6e\x00\xe1\xb2\x72\x00\xe1\xb4\x76\
+\x00\xe2\xb7\x7a\x00\xe3\xb8\x7e\x00\xe4\xba\x7f\x00\xac\x9f\x8d\
+\x00\xcb\xae\x85\x00\xd4\xb3\x84\x00\xc9\xb1\x90\x00\xd2\xb6\x91\
+\x00\xd3\xba\x98\x00\xe3\xba\x80\x00\xe4\xba\x80\x00\xe4\xbc\x83\
+\x00\xe3\xbc\x86\x00\xe7\xc2\x8f\x00\xe7\xc3\x92\x00\xe7\xc4\x93\
+\x00\xe6\xc4\x95\x00\xe8\xc5\x94\x00\xe9\xc7\x99\x00\xea\xca\x9e\
+\x00\xdd\xc5\xa2\x00\xde\xc8\xa8\x00\xec\xd0\xa8\x00\xd4\xff\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x14\x2f\x00\x00\x22\x50\x00\
+\x00\x30\x70\x00\x00\x3d\x90\x00\x00\x4c\xb0\x00\x00\x59\xcf\x00\
+\x00\x67\xf0\x00\x00\x78\xff\x11\x00\x8a\xff\x31\x00\x9c\xff\x51\
+\x00\xae\xff\x71\x00\xc0\xff\x91\x00\xd2\xff\xb1\x00\xe4\xff\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x26\x2f\x00\x00\x40\x50\x00\
+\x00\x5a\x70\x00\x00\x74\x90\x00\x00\x8e\xb0\x00\x00\xa9\xcf\x00\
+\x00\xc2\xf0\x00\x00\xd1\xff\x11\x00\xd8\xff\x31\x00\xde\xff\x51\
+\x00\xe3\xff\x71\x00\xe9\xff\x91\x00\xef\xff\xb1\x00\xf6\xff\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x26\x00\x00\x50\x41\x00\
+\x00\x70\x5b\x00\x00\x90\x74\x00\x00\xb0\x8e\x00\x00\xcf\xa9\x00\
+\x00\xf0\xc3\x00\x00\xff\xd2\x11\x00\xff\xd8\x31\x00\xff\xdd\x51\
+\x00\xff\xe4\x71\x00\xff\xea\x91\x00\xff\xf0\xb1\x00\xff\xf6\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x14\x00\x00\x50\x22\x00\
+\x00\x70\x30\x00\x00\x90\x3e\x00\x00\xb0\x4d\x00\x00\xcf\x5b\x00\
+\x00\xf0\x69\x00\x00\xff\x79\x11\x00\xff\x8a\x31\x00\xff\x9d\x51\
+\x00\xff\xaf\x71\x00\xff\xc1\x91\x00\xff\xd2\xb1\x00\xff\xe5\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x03\x00\x00\x50\x04\x00\
+\x00\x70\x06\x00\x00\x90\x09\x00\x00\xb0\x0a\x00\x00\xcf\x0c\x00\
+\x00\xf0\x0e\x00\x00\xff\x20\x12\x00\xff\x3e\x31\x00\xff\x5c\x51\
+\x00\xff\x7a\x71\x00\xff\x97\x91\x00\xff\xb6\xb1\x00\xff\xd4\xd1\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x00\x0e\x00\x50\x00\x17\
+\x00\x70\x00\x21\x00\x90\x00\x2b\x00\xb0\x00\x36\x00\xcf\x00\x40\
+\x00\xf0\x00\x49\x00\xff\x11\x5a\x00\xff\x31\x70\x00\xff\x51\x86\
+\x00\xff\x71\x9c\x00\xff\x91\xb2\x00\xff\xb1\xc8\x00\xff\xd1\xdf\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2f\x00\x20\x00\x50\x00\x36\
+\x00\x70\x00\x4c\x00\x90\x00\x62\x00\xb0\x00\x78\x00\xcf\x00\x8e\
+\x00\xf0\x00\xa4\x00\xff\x11\xb3\x00\xff\x31\xbe\x00\xff\x51\xc7\
+\x00\xff\x71\xd1\x00\xff\x91\xdc\x00\xff\xb1\xe5\x00\xff\xd1\xf0\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x2c\x00\x2f\x00\x4b\x00\x50\
+\x00\x69\x00\x70\x00\x87\x00\x90\x00\xa5\x00\xb0\x00\xc4\x00\xcf\
+\x00\xe1\x00\xf0\x00\xf0\x11\xff\x00\xf2\x31\xff\x00\xf4\x51\xff\
+\x00\xf6\x71\xff\x00\xf7\x91\xff\x00\xf9\xb1\xff\x00\xfb\xd1\xff\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x1b\x00\x2f\x00\x2d\x00\x50\
+\x00\x3f\x00\x70\x00\x52\x00\x90\x00\x63\x00\xb0\x00\x76\x00\xcf\
+\x00\x88\x00\xf0\x00\x99\x11\xff\x00\xa6\x31\xff\x00\xb4\x51\xff\
+\x00\xc2\x71\xff\x00\xcf\x91\xff\x00\xdc\xb1\xff\x00\xeb\xd1\xff\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x08\x00\x2f\x00\x0e\x00\x50\
+\x00\x15\x00\x70\x00\x1b\x00\x90\x00\x21\x00\xb0\x00\x26\x00\xcf\
+\x00\x2c\x00\xf0\x00\x3e\x11\xff\x00\x58\x31\xff\x00\x71\x51\xff\
+\x00\x8c\x71\xff\x00\xa6\x91\xff\x00\xbf\xb1\xff\x00\xda\xd1\xff\
+\x00\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4a\x35\x3c\x3c\x3c\x39\x1f\
+\x00\x00\x00\x00\x00\x00\x00\x00\x23\x31\x11\x29\x2a\x2a\x11\x28\
+\x19\x00\x00\x00\x00\x00\x00\x00\x10\x03\x25\x24\x00\x4b\x27\x0a\
+\x0b\x05\x00\x00\x00\x00\x00\x12\x02\x08\x16\x00\x00\x00\x2e\x03\
+\x02\x0e\x00\x00\x00\x00\x00\x0d\x01\x0e\x00\x00\x04\x00\x00\x0f\
+\x01\x09\x1b\x00\x00\x00\x00\x43\x37\x06\x00\x22\x30\x00\x00\x1a\
+\x32\x38\x07\x00\x00\x00\x00\x3c\x45\x2f\x2d\x33\x34\x15\x00\x00\
+\x44\x45\x1c\x00\x00\x00\x00\x35\x46\x44\x3c\x38\x38\x3b\x00\x00\
+\x35\x51\x00\x00\x00\x00\x00\x4e\x54\x47\x46\x45\x45\x48\x1e\x00\
+\x4f\x3d\x00\x00\x00\x00\x00\x00\x3f\x55\x51\x51\x48\x49\x42\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4c\x57\x5a\x58\x59\x5d\x22\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5c\x5b\x00\x00\
+\x00\x00\x00\x20\x00\x00\x41\x18\x1d\x00\x00\x00\x00\x00\x00\x00\
+\x00\x21\x2c\x00\x00\x00\x00\x00\x50\x40\x2b\x14\x17\x13\x26\x36\
+\x51\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x56\x52\x48\x48\x53\x4d\
+\x00\x00\x00\x00\x00\xff\xff\x00\x00\xf0\x1f\x00\x00\xe0\x0f\x00\
+\x00\xe1\x07\x00\x00\xc3\x87\x00\x00\xc6\xc3\x00\x00\xc4\xc3\x00\
+\x00\xc0\x63\x00\x00\xc0\x67\x00\x00\xc0\x27\x00\x00\xe0\x3f\x00\
+\x00\xf0\x1f\x00\x00\xfe\x7d\x00\x00\x8f\xf3\x00\x00\xe0\x07\x00\
+\x00\xf8\x1f\x00\x00\x28\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\
+\x00\x01\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x0b\x00\x00\x00\
+\x11\x00\x00\x00\x15\x00\x00\x00\x18\x00\x00\x00\x1a\x00\x00\x00\
+\x1a\x00\x00\x00\x18\x00\x00\x00\x15\x00\x00\x00\x11\x00\x00\x00\
+\x0b\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
+\x0a\x00\x00\x00\x16\x00\x00\x00\x23\x00\x00\x00\x30\x00\x00\x00\
+\x3b\x00\x00\x00\x44\x00\x00\x00\x4a\x00\x00\x00\x4c\x00\x00\x00\
+\x4c\x00\x00\x00\x4a\x00\x00\x00\x44\x00\x00\x00\x3b\x00\x00\x00\
+\x30\x00\x00\x00\x23\x00\x00\x00\x16\x00\x00\x00\x0a\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x18\x00\x00\x00\
+\x2d\x4e\x34\x11\x65\x77\x50\x1a\x9c\x8e\x5f\x1f\xcb\x98\x66\x21\
+\xe1\xa7\x70\x24\xff\xa7\x70\x24\xff\xa7\x70\x24\xff\xa7\x70\x24\
+\xff\x98\x66\x21\xe2\x8c\x5e\x1e\xce\x70\x4b\x18\xa4\x42\x2d\x0f\
+\x77\x00\x00\x00\x4f\x00\x00\x00\x41\x00\x00\x00\x2d\x00\x00\x00\
+\x18\x00\x00\x00\x0a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x05\x00\x00\x00\x14\x00\x00\x00\x2b\x70\x4b\x18\x8a\x98\x66\x21\
+\xdf\xbc\x7e\x28\xff\xdb\xa2\x54\xff\xe5\xbd\x85\xff\xea\xca\x9e\
+\xff\xf2\xde\xc2\xff\xf2\xde\xc2\xff\xf2\xde\xc2\xff\xf2\xde\xc2\
+\xff\xea\xca\x9e\xff\xe5\xbd\x85\xff\xdb\xa2\x54\xff\xbc\x7e\x28\
+\xff\x96\x64\x20\xe2\x65\x44\x16\x9c\x00\x00\x00\x55\x00\x00\x00\
+\x43\x00\x00\x00\x2b\x00\x00\x00\x14\x00\x00\x00\x05\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\
+\x1c\x6b\x48\x17\x77\x99\x67\x21\xe9\xd0\x8c\x2d\xff\xe6\xbf\x8a\
+\xff\xf1\xdb\xbc\xff\xef\xd5\xb2\xff\xe7\xc2\x8f\xff\xe2\xb4\x75\
+\xff\xdc\xa6\x5b\xff\xda\xa1\x51\xff\xda\xa1\x51\xff\xdc\xa6\x5b\
+\xff\xe2\xb4\x75\xff\xe7\xc2\x8f\xff\xef\xd5\xb2\xff\xf1\xdb\xbc\
+\xff\xe6\xbf\x8a\xff\xd0\x8c\x2d\xff\x98\x66\x21\xec\x57\x3a\x13\
+\x92\x00\x00\x00\x52\x00\x00\x00\x39\x00\x00\x00\x1c\x00\x00\x00\
+\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x07\x3a\x27\x0d\x2c\x8e\x5f\x1f\
+\xc3\xc1\x81\x29\xff\xe5\xbc\x84\xff\xef\xd6\xb4\xff\xe8\xc4\x93\
+\xff\xdb\xa4\x57\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xdb\xa4\x57\
+\xff\xe8\xc4\x93\xff\xef\xd6\xb4\xff\xe5\xbc\x84\xff\xc1\x81\x29\
+\xff\x86\x5a\x1d\xce\x19\x11\x06\x64\x00\x00\x00\x40\x00\x00\x00\
+\x1e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x07\x38\x26\x0c\x2c\x93\x63\x20\xdb\xd5\x93\x38\
+\xff\xec\xcf\xa8\xff\xe8\xc4\x93\xff\xd9\x9e\x4c\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd9\x9e\x4d\xff\xe8\xc4\x93\xff\xec\xcf\xa8\
+\xff\xd5\x93\x38\xff\x8e\x5f\x1f\xe2\x17\x0f\x05\x65\x00\x00\x00\
+\x40\x00\x00\x00\x1e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x05\x3b\x28\x0d\x29\x91\x61\x1f\xdb\xdd\xa8\x5e\xff\xea\xca\x9e\
+\xff\xdd\xa9\x61\xff\xcb\x88\x2c\xff\xce\x8a\x2c\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd0\x8c\x2d\xff\xcb\x88\x2c\xff\xdf\xad\x69\
+\xff\xea\xca\x9e\xff\xdd\xa8\x5e\xff\x8c\x5e\x1e\xe2\x17\x0f\x05\
+\x65\x00\x00\x00\x3f\x00\x00\x00\x1b\x00\x00\x00\x05\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x47\x30\x10\
+\x22\x90\x61\x1f\xda\xda\xa0\x4f\xff\xe6\xbf\x8a\xff\xd8\x9b\x47\
+\xff\xc4\x83\x2a\xff\xc8\x86\x2b\xff\xcd\x89\x2c\xff\xd0\x8c\x2d\
+\xff\xd8\x9c\x4a\xff\xe2\xb5\x77\xff\xe6\xbf\x8a\xff\xe8\xc3\x91\
+\xff\xe8\xc3\x91\xff\xe8\xc3\x91\xff\xe8\xc3\x91\xff\xe8\xc3\x91\
+\xff\xe8\xc3\x91\xff\xe6\xbf\x8a\xff\xda\xa1\x51\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xcd\x89\x2c\xff\xc9\x87\x2b\xff\xc4\x83\x2a\
+\xff\xd8\x9b\x47\xff\xe6\xbf\x8a\xff\xda\xa0\x4f\xff\x8b\x5d\x1e\
+\xe2\x17\x0f\x05\x63\x00\x00\x00\x37\x00\x00\x00\x12\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x87\x5b\x1d\
+\xaf\xc4\x83\x2a\xff\xe2\xb4\x75\xff\xd5\x94\x3a\xff\xbe\x7f\x29\
+\xff\xc1\x81\x29\xff\xc6\x85\x2b\xff\xc9\x87\x2b\xff\xd4\x92\x36\
+\xff\xe3\xb8\x7c\xff\xda\xa1\x51\xff\xc6\x85\x2b\xff\xba\x7d\x28\
+\xff\xba\x7d\x28\xff\xba\x7d\x28\xff\xba\x7d\x28\xff\xba\x7d\x28\
+\xff\xba\x7d\x28\xff\xd8\x9c\x48\xff\xe2\xb5\x77\xff\xd4\x91\x35\
+\xff\xce\x8a\x2c\xff\xc9\x87\x2b\xff\xc6\x85\x2b\xff\xc3\x83\x2a\
+\xff\xbe\x7f\x29\xff\xd5\x94\x3b\xff\xe2\xb5\x77\xff\xc4\x83\x2a\
+\xff\x77\x50\x1a\xc5\x00\x00\x00\x50\x00\x00\x00\x29\x00\x00\x00\
+\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x02\x78\x51\x1a\x60\xa0\x6b\x22\
+\xff\xdc\xa5\x59\xff\xd6\x96\x3e\xff\xb7\x7b\x27\xff\xba\x7d\x28\
+\xff\xbe\x7f\x29\xff\xc3\x83\x2a\xff\xc6\x85\x2b\xff\xdc\xa6\x5b\
+\xff\xd8\x9c\x4a\xff\x90\x61\x1f\xf4\x68\x46\x17\x70\x7c\x53\x1b\
+\x4c\x87\x5b\x1d\x45\x8b\x5d\x1e\x44\x8b\x5d\x1e\x44\x8b\x5d\x1e\
+\x44\x8b\x5d\x1e\x55\x91\x61\x1f\xf0\xdc\xa5\x59\xff\xda\xa1\x51\
+\xff\xc9\x87\x2b\xff\xc6\x85\x2b\xff\xc3\x83\x2a\xff\xbf\x80\x29\
+\xff\xbc\x7e\x28\xff\xb7\x7b\x27\xff\xd6\x96\x3e\xff\xdc\xa6\x5b\
+\xff\xa0\x6b\x22\xff\x44\x2e\x0f\x88\x00\x00\x00\x42\x00\x00\x00\
+\x18\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x8b\x5d\x1e\xe5\xd5\x94\x3b\
+\xff\xd8\x9c\x48\xff\xb1\x77\x26\xff\xb4\x79\x27\xff\xb7\x7b\x27\
+\xff\xbc\x7e\x28\xff\xbf\x80\x29\xff\xd5\x93\x38\xff\xdc\xa6\x5c\
+\xff\xa2\x6d\x23\xff\x5e\x3f\x14\x7a\x00\x00\x00\x15\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x8c\x5e\x1e\x82\xba\x7d\x28\xff\xdd\xa8\x5e\
+\xff\xce\x8a\x2c\xff\xc3\x83\x2a\xff\xbf\x80\x29\xff\xbc\x7e\x28\
+\xff\xb9\x7c\x28\xff\xb5\x79\x27\xff\xb2\x77\x26\xff\xd8\x9c\x48\
+\xff\xd5\x94\x3b\xff\x87\x5b\x1d\xec\x00\x00\x00\x54\x00\x00\x00\
+\x2c\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x01\x7c\x53\x1b\x6d\xa8\x71\x24\xff\xdb\xa4\x57\
+\xff\xbf\x80\x29\xff\xaf\x76\x26\xff\xb2\x77\x26\xff\xb5\x79\x27\
+\xff\xb9\x7c\x28\xff\xbf\x80\x29\xff\xdc\xa5\x59\xff\xce\x8a\x2c\
+\xff\x7f\x55\x1b\xd3\x00\x00\x00\x24\x00\x00\x00\x06\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x06\x00\x00\x00\
+\x03\x00\x00\x00\x00\x8e\x5f\x1f\x10\x8c\x5e\x1e\xe0\xd6\x97\x40\
+\xff\xd9\x9e\x4d\xff\xbf\x80\x29\xff\xbc\x7e\x28\xff\xb9\x7c\x28\
+\xff\xb5\x79\x27\xff\xb2\x77\x26\xff\xaf\x76\x26\xff\xc1\x81\x29\
+\xff\xdb\xa4\x57\xff\xa8\x71\x24\xff\x56\x3a\x13\x9b\x00\x00\x00\
+\x3f\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x04\x86\x5a\x1d\xd5\xd4\x90\x33\xff\xd7\x98\x42\
+\xff\xa8\x71\x24\xff\xac\x73\x25\xff\xaf\x76\x26\xff\xb2\x77\x26\
+\xff\xb5\x79\x27\xff\xd5\x93\x38\xff\xdb\xa3\x56\xff\x91\x61\x1f\
+\xff\x36\x24\x0c\x51\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\
+\x00\x00\x00\x00\x04\x00\x00\x00\x14\x00\x00\x00\x1b\x00\x00\x00\
+\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x89\x5c\x1e\x61\xa8\x71\x24\
+\xff\xdd\xa8\x5e\xff\xc4\x83\x2a\xff\xb9\x7c\x28\xff\xb5\x79\x27\
+\xff\xb2\x77\x26\xff\xaf\x76\x26\xff\xad\x74\x25\xff\xaa\x72\x25\
+\xff\xd7\x98\x42\xff\xd4\x90\x33\xff\x78\x51\x1a\xd8\x00\x00\x00\
+\x4d\x00\x00\x00\x20\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x6a\x47\x17\x29\x8e\x5f\x1f\xff\xdc\xa5\x59\xff\xc6\x85\x2b\
+\xff\xa7\x70\x24\xff\xaa\x72\x25\xff\xac\x73\x25\xff\xaf\x76\x26\
+\xff\xb7\x7b\x27\xff\xdb\xa4\x57\xff\xc4\x83\x2a\xff\x73\x4d\x19\
+\xbc\x00\x00\x00\x22\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\
+\x00\x4a\x32\x10\x1d\x6d\x49\x18\x8a\x00\x00\x00\x3a\x00\x00\x00\
+\x1a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x87\x5b\x1d\
+\xe1\xd8\x9b\x47\xff\xd7\x98\x42\xff\xb5\x79\x27\xff\xb2\x77\x26\
+\xff\xaf\x76\x26\xff\xad\x74\x25\xff\xaa\x72\x25\xff\xa7\x70\x24\
+\xff\xc6\x85\x2b\xff\xdc\xa5\x59\xff\x8e\x5f\x1f\xff\x28\x1b\x09\
+\x6b\x00\x00\x00\x2d\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x78\x51\x1a\x69\xb2\x77\x26\xff\xdc\xa5\x59\xff\xa2\x6d\x23\
+\xff\xa3\x6d\x23\xff\xa7\x70\x24\xff\xaa\x72\x25\xff\xac\x73\x25\
+\xff\xd4\x90\x33\xff\xdb\xa3\x56\xff\x8c\x5e\x1e\xff\x35\x24\x0c\
+\x51\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x04\x77\x50\x1a\x8f\x82\x57\x1c\xf4\x00\x00\x00\x52\x00\x00\x00\
+\x2e\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x82\x57\x1c\
+\x72\xb4\x79\x27\xff\xdd\xa8\x5f\xff\xbc\x7e\x28\xff\xaf\x76\x26\
+\xff\xad\x74\x25\xff\xaa\x72\x25\xff\xa7\x70\x24\xff\xa5\x6f\x24\
+\xff\xa7\x70\x24\xff\xdc\xa5\x59\xff\xb2\x77\x26\xff\x52\x37\x12\
+\x99\x00\x00\x00\x38\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7c\x53\x1b\xa6\xc9\x87\x2b\xff\xd7\x98\x42\xff\x9e\x6a\x22\
+\xff\xa2\x6d\x23\xff\xa3\x6d\x23\xff\xa7\x70\x24\xff\xaf\x76\x26\
+\xff\xdc\xa5\x59\xff\xc3\x83\x2a\xff\x6f\x4a\x18\xbc\x00\x00\x00\
+\x22\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x01\x47\x30\x10\
+\x1d\x81\x57\x1c\xf2\x9b\x68\x21\xff\x4f\x35\x11\x9c\x00\x00\x00\
+\x44\x00\x00\x00\x19\x00\x00\x00\x02\x00\x00\x00\x00\x82\x57\x1c\
+\x10\x81\x57\x1c\xf0\xda\xa0\x4f\xff\xd6\x96\x3e\xff\xac\x73\x25\
+\xff\xaa\x72\x25\xff\xa7\x70\x24\xff\xa5\x6f\x24\xff\xa2\x6d\x23\
+\xff\xa0\x6b\x22\xff\xd7\x98\x42\xff\xc9\x87\x2b\xff\x6a\x47\x17\
+\xc3\x00\x00\x00\x3f\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7c\x53\x1b\xc5\xd6\x96\x3d\xff\xd8\x9c\x4a\xff\xd2\x8d\x2d\
+\xff\xcb\x88\x2c\xff\xb9\x7c\x28\xff\xa3\x6d\x23\xff\xd3\x8f\x30\
+\xff\xdb\xa4\x57\xff\x87\x5b\x1d\xff\x33\x22\x0b\x51\x00\x00\x00\
+\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x06\x72\x4d\x19\
+\x90\xb2\x77\x26\xff\xd6\x97\x40\xff\x78\x51\x1a\xec\x00\x00\x00\
+\x54\x00\x00\x00\x2c\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7d\x54\x1b\x82\xb2\x77\x26\xff\xdd\xa8\x5f\xff\xb4\x79\x27\
+\xff\xa7\x70\x24\xff\xa3\x6d\x23\xff\xb4\x79\x27\xff\xc4\x83\x2a\
+\xff\xd2\x8d\x2d\xff\xd8\x9c\x4a\xff\xd6\x96\x3d\xff\x70\x4b\x18\
+\xd8\x00\x00\x00\x44\x00\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7c\x53\x1b\xe2\xdc\xa5\x59\xff\xe1\xb2\x70\xff\xde\xaa\x63\
+\xff\xdd\xa8\x5f\xff\xdd\xa8\x5e\xff\xdc\xa6\x5c\xff\xdf\xad\x69\
+\xff\xc3\x83\x2a\xff\x6a\x47\x17\xbd\x00\x00\x00\x28\x00\x00\x00\
+\x0c\x00\x00\x00\x06\x00\x00\x00\x07\x4f\x35\x11\x32\x84\x59\x1d\
+\xf3\xda\xa1\x52\xff\xde\xaa\x63\xff\xa0\x6b\x22\xff\x4e\x34\x11\
+\x9b\x00\x00\x00\x40\x00\x00\x00\x16\x00\x00\x00\x02\x00\x00\x00\
+\x00\x7d\x54\x1b\x10\x7c\x53\x1b\xf0\xdb\xa2\x54\xff\xd7\x9a\x45\
+\xff\xd5\x94\x3b\xff\xdb\xa2\x54\xff\xdc\xa6\x5c\xff\xdd\xa8\x5f\
+\xff\xde\xaa\x63\xff\xe1\xb2\x70\xff\xdb\xa4\x57\xff\x72\x4d\x19\
+\xe2\x00\x00\x00\x4a\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7a\x52\x1a\xff\xe3\xb8\x7d\xff\xe0\xb0\x6e\xff\xde\xac\x66\
+\xff\xde\xaa\x63\xff\xdd\xa9\x61\xff\xdf\xae\x6b\xff\xe0\xb0\x6e\
+\xff\x84\x59\x1d\xff\x26\x1a\x09\x66\x00\x00\x00\x35\x00\x00\x00\
+\x27\x00\x00\x00\x26\x00\x00\x00\x27\x6d\x49\x18\xb3\xc1\x81\x29\
+\xff\xdd\xa8\x5e\xff\xd9\x9e\x4c\xff\xd5\x93\x38\xff\x6b\x48\x17\
+\xd8\x00\x00\x00\x51\x00\x00\x00\x28\x00\x00\x00\x08\x00\x00\x00\
+\x00\x00\x00\x00\x00\x78\x51\x1a\x91\xd4\x91\x35\xff\xe2\xb5\x77\
+\xff\xdc\xa6\x5c\xff\xdd\xa8\x5e\xff\xdd\xa8\x5f\xff\xde\xaa\x63\
+\xff\xde\xac\x66\xff\xe0\xb0\x6d\xff\xe2\xb4\x75\xff\x7a\x52\x1a\
+\xff\x00\x00\x00\x49\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7c\x53\x1b\xff\xe4\xba\x80\xff\xe1\xb2\x70\xff\xdf\xad\x69\
+\xff\xde\xac\x66\xff\xde\xab\x64\xff\xe3\xb7\x7a\xff\xce\x8a\x2c\
+\xff\x6d\x49\x18\xd8\x3b\x28\x0d\x86\x3f\x2a\x0e\x80\x3f\x2a\x0e\
+\x7e\x3f\x2a\x0e\x7e\x49\x31\x10\x89\x86\x5a\x1d\xff\xe0\xb0\x6e\
+\xff\xde\xaa\x63\xff\xdd\xa8\x5f\xff\xe2\xb5\x77\xff\x8e\x5f\x1f\
+\xff\x3b\x28\x0d\x86\x00\x00\x00\x3d\x00\x00\x00\x13\x00\x00\x00\
+\x01\x00\x00\x00\x00\x77\x50\x1a\x21\x8e\x5f\x1f\xff\xe2\xb6\x78\
+\xff\xdf\xae\x6b\xff\xdd\xa8\x5f\xff\xde\xaa\x63\xff\xde\xac\x66\
+\xff\xdf\xad\x68\xff\xe1\xb2\x72\xff\xe2\xb6\x78\xff\x7c\x53\x1b\
+\xff\x00\x00\x00\x43\x00\x00\x00\x15\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7d\x54\x1b\xd2\xe2\xb4\x75\xff\xe2\xb6\x78\xff\xe0\xb0\x6d\
+\xff\xdf\xad\x69\xff\xe1\xb2\x70\xff\xe4\xba\x7f\xff\xba\x7d\x28\
+\xff\xa7\x70\x24\xff\xa7\x70\x24\xff\xa8\x71\x24\xff\xa8\x71\x24\
+\xff\xa8\x71\x24\xff\xa7\x70\x24\xff\xd4\x91\x35\xff\xe2\xb6\x78\
+\xff\xdb\xa4\x57\xff\xdb\xa2\x54\xff\xe2\xb4\x75\xff\xd5\x94\x3b\
+\xff\x70\x4b\x18\xd8\x00\x00\x00\x4f\x00\x00\x00\x23\x00\x00\x00\
+\x06\x00\x00\x00\x00\x00\x00\x00\x00\x7d\x54\x1b\xa1\xd6\x96\x3d\
+\xff\xe3\xb7\x7a\xff\xde\xab\x64\xff\xde\xac\x66\xff\xdf\xad\x69\
+\xff\xe0\xb0\x6d\xff\xe2\xb6\x78\xff\xdb\xa4\x57\xff\x70\x4b\x18\
+\xd7\x00\x00\x00\x3d\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7f\x55\x1b\xc3\xdc\xa6\x5b\xff\xe3\xb8\x7c\xff\xe1\xb2\x70\
+\xff\xe0\xb0\x6d\xff\xe4\xba\x7f\xff\xe4\xba\x80\xff\xe4\xba\x80\
+\xff\xe4\xba\x80\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
+\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe0\xb0\x6d\
+\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xde\xac\x66\xff\xe3\xb8\x7c\
+\xff\x94\x63\x20\xff\x24\x18\x08\x70\x00\x00\x00\x38\x00\x00\x00\
+\x11\x00\x00\x00\x01\x00\x00\x00\x00\x7d\x54\x1b\x31\xa5\x6f\x24\
+\xff\xe4\xba\x80\xff\xe1\xb2\x72\xff\xdf\xad\x69\xff\xe0\xb0\x6d\
+\xff\xe0\xb0\x6e\xff\xe3\xb8\x7c\xff\xdc\xa5\x59\xff\x73\x4d\x19\
+\xd6\x00\x00\x00\x35\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x81\x57\x1c\x93\xd6\x96\x3d\xff\xe4\xbb\x82\xff\xe1\xb3\x73\
+\xff\xe1\xb2\x70\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xad\x69\
+\xff\xde\xac\x66\xff\xde\xac\x66\xff\xde\xaa\x63\xff\xde\xaa\x63\
+\xff\xdd\xa9\x61\xff\xdd\xa8\x5f\xff\xdd\xa8\x5f\xff\xdd\xa8\x5e\
+\xff\xdd\xa8\x5e\xff\xdd\xa8\x5e\xff\xdd\xa8\x5e\xff\xe4\xba\x7f\
+\xff\xd2\x8d\x2d\xff\x6a\x47\x17\xc5\x00\x00\x00\x4d\x00\x00\x00\
+\x22\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x82\x57\x1c\
+\xc1\xdb\xa3\x56\xff\xe3\xb8\x7d\xff\xe0\xb0\x6d\xff\xe1\xb2\x70\
+\xff\xe1\xb2\x72\xff\xe4\xbb\x82\xff\xd4\x90\x33\xff\x68\x46\x17\
+\xb4\x00\x00\x00\x2a\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x84\x59\x1d\x62\xc6\x85\x2b\xff\xe8\xc3\x91\xff\xe2\xb6\x78\
+\xff\xe1\xb3\x73\xff\xe1\xb2\x72\xff\xe0\xb0\x6e\xff\xe0\xb0\x6d\
+\xff\xdf\xae\x6b\xff\xdf\xad\x69\xff\xdf\xad\x68\xff\xde\xac\x66\
+\xff\xde\xab\x64\xff\xde\xab\x64\xff\xde\xaa\x63\xff\xde\xaa\x63\
+\xff\xde\xaa\x63\xff\xdd\xa9\x61\xff\xdd\xa9\x61\xff\xe2\xb5\x77\
+\xff\xe4\xba\x7f\xff\x90\x61\x1f\xff\x28\x1b\x09\x6f\x00\x00\x00\
+\x36\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x84\x59\x1d\
+\x51\xc1\x81\x29\xff\xe7\xc2\x8f\xff\xe3\xb7\x7a\xff\xe1\xb3\x73\
+\xff\xe2\xb6\x78\xff\xe8\xc3\x91\xff\xba\x7d\x28\xff\x52\x37\x12\
+\x83\x00\x00\x00\x1d\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x87\x5b\x1d\x10\x94\x63\x20\xff\xea\xca\x9d\xff\xe5\xbe\x87\
+\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb2\x72\
+\xff\xe0\xb0\x6e\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
+\xff\xdf\xad\x69\xff\xdf\xad\x68\xff\xdf\xad\x68\xff\xde\xac\x66\
+\xff\xde\xac\x66\xff\xde\xac\x66\xff\xde\xac\x66\xff\xdf\xad\x69\
+\xff\xe9\xc6\x96\xff\xd5\x94\x3b\xff\x6a\x47\x17\xba\x00\x00\x00\
+\x49\x00\x00\x00\x1e\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
+\x00\x87\x5b\x1d\xe1\xe4\xba\x7f\xff\xe7\xc2\x8e\xff\xe2\xb5\x77\
+\xff\xe5\xbe\x87\xff\xe8\xc4\x93\xff\x89\x5c\x1e\xff\x1e\x14\x07\
+\x47\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x89\x5c\x1e\xb2\xdd\xa8\x5f\xff\xeb\xcd\xa3\
+\xff\xe3\xb8\x7c\xff\xe2\xb6\x78\xff\xe2\xb5\x77\xff\xe2\xb4\x75\
+\xff\xe1\xb3\x73\xff\xe1\xb2\x72\xff\xe1\xb2\x70\xff\xe0\xb0\x6e\
+\xff\xe0\xb0\x6d\xff\xe0\xb0\x6d\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
+\xff\xdf\xae\x6b\xff\xdf\xae\x6b\xff\xdf\xae\x6b\xff\xdf\xae\x6b\
+\xff\xe6\xbf\x8a\xff\xe6\xbf\x8a\xff\x89\x5c\x1e\xf5\x17\x0f\x05\
+\x62\x00\x00\x00\x33\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\
+\x00\x89\x5c\x1e\x72\xd4\x92\x36\xff\xec\xcf\xa6\xff\xe4\xba\x80\
+\xff\xeb\xcd\xa3\xff\xdd\xa8\x5f\xff\x7a\x52\x1a\xc8\x00\x00\x00\
+\x27\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x8b\x5d\x1e\x41\xb9\x7c\x28\xff\xee\xd4\xaf\
+\xff\xe8\xc3\x91\xff\xe3\xb8\x7c\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\
+\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
+\xff\xe1\xb2\x72\xff\xe1\xb2\x70\xff\xe1\xb2\x70\xff\xe0\xb0\x6e\
+\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\xff\xe0\xb0\x6e\
+\xff\xe2\xb5\x77\xff\xed\xd1\xab\xff\xd3\x8e\x2e\xff\x66\x44\x16\
+\xb1\x00\x00\x00\x47\x00\x00\x00\x1a\x00\x00\x00\x02\x00\x00\x00\
+\x00\x8c\x5e\x1e\x10\x96\x64\x20\xf0\xea\xca\x9e\xff\xed\xd1\xaa\
+\xff\xee\xd4\xaf\xff\xa7\x70\x24\xff\x52\x37\x12\x6e\x00\x00\x00\
+\x14\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x5f\x1f\xc1\xe1\xb2\x72\
+\xff\xee\xd4\xb0\xff\xe4\xbb\x82\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\
+\xff\xe3\xb8\x7c\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\xff\xe2\xb5\x77\
+\xff\xe2\xb5\x77\xff\xe2\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
+\xff\xe1\xb3\x73\xff\xe1\xb3\x73\xff\xe1\xb3\x73\xff\xe1\xb3\x73\
+\xff\xe1\xb3\x73\xff\xe9\xc7\x99\xff\xe8\xc5\x94\xff\x8e\x5f\x1f\
+\xf5\x00\x00\x00\x56\x00\x00\x00\x2e\x00\x00\x00\x0a\x00\x00\x00\
+\x00\x00\x00\x00\x00\x8e\x5f\x1f\x91\xdd\xa9\x61\xff\xef\xd6\xb4\
+\xff\xde\xac\x66\xff\x82\x57\x1c\xd2\x00\x00\x00\x25\x00\x00\x00\
+\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x61\x1f\x31\xa8\x71\x24\
+\xff\xed\xd1\xab\xff\xec\xcf\xa6\xff\xe4\xba\x80\xff\xe4\xba\x80\
+\xff\xe4\xba\x7f\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
+\xff\xe3\xb7\x7a\xff\xe2\xb6\x78\xff\xe2\xb6\x78\xff\xe2\xb6\x78\
+\xff\xe2\xb5\x77\xff\xe2\xb5\x77\xff\xe2\xb5\x77\xff\xe2\xb5\x77\
+\xff\xe2\xb6\x78\xff\xe4\xba\x7f\xff\xef\xd6\xb4\xff\xcd\x89\x2c\
+\xff\x5b\x3d\x14\x9c\x00\x00\x00\x44\x00\x00\x00\x19\x00\x00\x00\
+\x02\x00\x00\x00\x00\x8e\x5f\x1f\x21\xa8\x71\x24\xff\xec\xcf\xa8\
+\xff\xa0\x6b\x22\xff\x4f\x35\x11\x59\x00\x00\x00\x0f\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\
+\x81\xd2\x8d\x2d\xff\xf0\xd9\xb9\xff\xec\xce\xa5\xff\xe5\xbc\x84\
+\xff\xe4\xbb\x82\xff\xe4\xba\x80\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
+\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
+\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\
+\xff\xe3\xb8\x7c\xff\xe3\xb8\x7c\xff\xec\xce\xa5\xff\xe4\xba\x80\
+\xff\x8e\x5f\x1f\xec\x00\x00\x00\x54\x00\x00\x00\x2c\x00\x00\x00\
+\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x93\x63\x20\xb2\xa2\x6d\x23\
+\xff\x81\x57\x1c\x96\x00\x00\x00\x17\x00\x00\x00\x03\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x96\x64\x20\xb1\xd6\x96\x3d\xff\xf0\xd9\xba\xff\xec\xcf\xa6\
+\xff\xe5\xbd\x85\xff\xe5\xbc\x84\xff\xe4\xbb\x82\xff\xe4\xbb\x82\
+\xff\xe4\xba\x80\xff\xe4\xba\x80\xff\xe4\xba\x80\xff\xe4\xba\x7f\
+\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\xff\xe4\xba\x7f\
+\xff\xe4\xba\x7f\xff\xe4\xba\x80\xff\xe7\xc2\x8f\xff\xf0\xd9\xb9\
+\xff\xc4\x83\x2a\xff\x5e\x3f\x14\x9b\x00\x00\x00\x40\x00\x00\x00\
+\x16\x00\x00\x00\x02\x00\x00\x00\x00\x96\x64\x20\x41\x96\x64\x20\
+\xc1\x00\x00\x00\x0d\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x99\x67\x21\x10\x99\x67\x21\xd0\xd6\x96\x3e\xff\xf0\xd9\xba\
+\xff\xed\xd1\xaa\xff\xe6\xbf\x8a\xff\xe5\xbd\x85\xff\xe5\xbd\x85\
+\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\
+\xff\xe5\xbc\x84\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe5\xbc\x84\
+\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xe5\xbc\x84\xff\xed\xd1\xab\
+\xff\xe3\xb7\x7a\xff\x89\x5c\x1e\xd8\x00\x00\x00\x51\x00\x00\x00\
+\x28\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x99\x67\x21\
+\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x9d\x69\x22\x10\x9b\x68\x21\xb1\xd4\x90\x33\
+\xff\xee\xd4\xaf\xff\xf0\xd9\xba\xff\xea\xca\x9e\xff\xe6\xbf\x89\
+\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\
+\xff\xe5\xbe\x87\xff\xe5\xbd\x85\xff\xe5\xbd\x85\xff\xe5\xbd\x85\
+\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe5\xbe\x87\xff\xe9\xc7\x98\
+\xff\xf1\xdb\xbc\xff\xb4\x79\x27\xff\x4c\x33\x11\x85\x00\x00\x00\
+\x39\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x03\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9e\x6a\x22\
+\x81\xad\x74\x25\xff\xe2\xb5\x77\xff\xf3\xe1\xc7\xff\xf1\xdc\xbf\
+\xff\xec\xcf\xa6\xff\xe8\xc3\x91\xff\xe6\xbf\x89\xff\xe6\xbf\x89\
+\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe6\xbf\x89\
+\xff\xe6\xbf\x89\xff\xe6\xbf\x89\xff\xe8\xc3\x91\xff\xec\xcf\xa6\
+\xff\xf3\xe1\xc7\xff\xe2\xb5\x77\xff\x91\x61\x1f\xd3\x00\x00\x00\
+\x31\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x03\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\
+\x00\x00\x00\x00\x09\x00\x00\x00\x18\x00\x00\x00\x1a\x00\x00\x00\
+\x0e\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x9d\x69\x22\x21\xa0\x6b\x22\xc1\xbe\x7f\x29\xff\xe3\xb8\x7d\
+\xff\xf2\xdf\xc4\xff\xf5\xe6\xd1\xff\xf3\xe1\xc9\xff\xf1\xdb\xbc\
+\xff\xee\xd4\xb0\xff\xee\xd4\xaf\xff\xee\xd4\xaf\xff\xee\xd4\xb0\
+\xff\xf1\xdb\xbc\xff\xf3\xe1\xc9\xff\xf5\xe6\xd1\xff\xf2\xdf\xc4\
+\xff\xe3\xb8\x7d\xff\xbe\x7f\x29\xff\x99\x67\x21\xc9\x00\x00\x00\
+\x13\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x09\x00\x00\x00\
+\x19\x00\x00\x00\x1f\x00\x00\x00\x0f\x00\x00\x00\x02\x00\x00\x00\
+\x00\x9e\x6a\x22\x76\x33\x22\x0b\x33\x00\x00\x00\x3a\x00\x00\x00\
+\x32\x00\x00\x00\x1e\x00\x00\x00\x0d\x00\x00\x00\x03\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\x41\xa2\x6d\x23\
+\xa2\xad\x74\x25\xf0\xd6\x96\x3e\xff\xe0\xb0\x6d\xff\xea\xca\x9e\
+\xff\xeb\xcd\xa3\xff\xf1\xdc\xbf\xff\xf1\xdc\xbf\xff\xeb\xcd\xa3\
+\xff\xea\xca\x9e\xff\xe0\xb0\x6d\xff\xd6\x96\x3e\xff\xa3\x6d\x23\
+\xf1\x9b\x68\x21\xa9\x8b\x5d\x1e\x4c\x00\x00\x00\x07\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x02\x00\x00\x00\x08\x00\x00\x00\x15\x00\x00\x00\x2a\x86\x5a\x1d\
+\x9e\x45\x2e\x0f\x4d\x00\x00\x00\x12\x00\x00\x00\x01\x00\x00\x00\
+\x00\xad\x74\x25\x82\xa7\x70\x24\xf1\x82\x57\x1c\x9e\x1a\x12\x06\
+\x5b\x00\x00\x00\x48\x00\x00\x00\x32\x00\x00\x00\x1b\x00\x00\x00\
+\x0c\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xa5\x6f\x24\x41\xa3\x6d\x23\x83\xa3\x6d\x23\
+\xb3\xa3\x6d\x23\xc3\xa2\x6d\x23\xc4\xa2\x6d\x23\xc4\xa2\x6d\x23\
+\xc4\xa2\x6d\x23\xb5\x9d\x69\x22\x88\x93\x63\x20\x49\x00\x00\x00\
+\x06\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\
+\x15\x00\x00\x00\x28\x5c\x3e\x14\x6e\x9e\x6a\x22\xe9\xa8\x71\x24\
+\xe8\x00\x00\x00\x29\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\
+\x00\xb9\x7c\x28\x10\xb4\x79\x27\xd1\xad\x74\x25\xff\xa0\x6b\x22\
+\xf3\x73\x4d\x19\xad\x00\x00\x00\x57\x00\x00\x00\x46\x00\x00\x00\
+\x32\x00\x00\x00\x1e\x00\x00\x00\x11\x00\x00\x00\x08\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
+\x04\x00\x00\x00\x0c\x00\x00\x00\x18\x00\x00\x00\x29\x5b\x3d\x14\
+\x6e\x91\x61\x1f\xd3\xaa\x72\x25\xff\xb5\x79\x27\xff\x81\x57\x1c\
+\x74\x00\x00\x00\x12\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xbc\x7e\x28\x31\xbc\x7e\x28\xf0\xde\xaa\x63\
+\xff\xd3\x8f\x30\xff\x98\x66\x21\xeb\x70\x4b\x18\xb0\x19\x11\x06\
+\x62\x00\x00\x00\x4a\x00\x00\x00\x3a\x00\x00\x00\x29\x00\x00\x00\
+\x1b\x00\x00\x00\x12\x00\x00\x00\x0b\x00\x00\x00\x05\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
+\x04\x00\x00\x00\x08\x00\x00\x00\x0f\x00\x00\x00\x17\x00\x00\x00\
+\x22\x00\x00\x00\x32\x56\x3a\x13\x73\x90\x61\x1f\xd4\xb7\x7b\x27\
+\xff\xdb\xa4\x57\xff\xd3\x8f\x30\xff\xa5\x6f\x24\xa7\x00\x00\x00\
+\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x83\x2a\x31\xc3\x83\x2a\
+\xf0\xe2\xb4\x75\xff\xe5\xbd\x85\xff\xd4\x92\x36\xff\x9d\x69\x22\
+\xf5\x7f\x55\x1b\xc4\x56\x3a\x13\x8f\x00\x00\x00\x54\x00\x00\x00\
+\x49\x00\x00\x00\x3c\x00\x00\x00\x30\x00\x00\x00\x24\x00\x00\x00\
+\x1b\x00\x00\x00\x16\x00\x00\x00\x12\x00\x00\x00\x0e\x00\x00\x00\
+\x0d\x00\x00\x00\x0c\x00\x00\x00\x09\x00\x00\x00\x07\x00\x00\x00\
+\x06\x00\x00\x00\x08\x00\x00\x00\x0a\x00\x00\x00\x0c\x00\x00\x00\
+\x0e\x00\x00\x00\x11\x00\x00\x00\x14\x00\x00\x00\x19\x00\x00\x00\
+\x20\x00\x00\x00\x2a\x00\x00\x00\x37\x36\x24\x0c\x5b\x6f\x4a\x18\
+\x9c\x98\x66\x21\xea\xbf\x80\x29\xff\xdc\xa6\x5c\xff\xe6\xc0\x8c\
+\xff\xd5\x93\x38\xff\xb4\x79\x27\xb1\x00\x00\x00\x13\x00\x00\x00\
+\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc9\x87\x2b\
+\x31\xc9\x87\x2b\xd1\xdc\xa5\x59\xff\xeb\xcc\xa1\xff\xe6\xbf\x8a\
+\xff\xda\xa1\x51\xff\xba\x7d\x28\xff\x99\x67\x21\xf5\x82\x57\x1c\
+\xce\x68\x46\x17\xa5\x3d\x29\x0d\x77\x00\x00\x00\x52\x00\x00\x00\
+\x4b\x00\x00\x00\x44\x00\x00\x00\x3d\x00\x00\x00\x36\x00\x00\x00\
+\x33\x00\x00\x00\x31\x00\x00\x00\x2b\x00\x00\x00\x27\x00\x00\x00\
+\x26\x00\x00\x00\x29\x00\x00\x00\x2e\x00\x00\x00\x32\x00\x00\x00\
+\x36\x00\x00\x00\x3b\x00\x00\x00\x41\x33\x22\x0b\x5f\x5b\x3d\x14\
+\x86\x7a\x52\x1a\xb4\x94\x63\x20\xea\xb1\x77\x26\xff\xd6\x96\x3d\
+\xff\xe2\xb5\x77\xff\xe9\xc6\x96\xff\xe5\xbe\x87\xff\xd3\x8e\x2e\
+\xff\xa3\x6d\x23\x79\x00\x00\x00\x0e\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\xd3\x8e\x2e\x10\xd0\x8c\x2d\xb1\xd7\x9a\x45\xff\xea\xc9\x9b\
+\xff\xeb\xcc\xa0\xff\xe7\xc2\x8f\xff\xe3\xb8\x7c\xff\xd8\x9c\x48\
+\xff\xcb\x88\x2c\xff\xaf\x76\x26\xff\x9b\x68\x21\xff\x8e\x5f\x1f\
+\xe2\x87\x5b\x1d\xd8\x70\x4b\x18\xaf\x72\x4d\x19\xad\x72\x4d\x19\
+\xac\x57\x3a\x13\x8c\x4c\x33\x11\x80\x4e\x34\x11\x7e\x4e\x34\x11\
+\x7e\x6b\x48\x17\x9f\x72\x4d\x19\xab\x72\x4d\x19\xac\x84\x59\x1d\
+\xcb\x89\x5c\x1e\xd6\x98\x66\x21\xf5\xa3\x6d\x23\xff\xc1\x81\x29\
+\xff\xd7\x98\x42\xff\xdf\xad\x69\xff\xe6\xc0\x8c\xff\xe8\xc5\x94\
+\xff\xea\xca\x9e\xff\xdf\xad\x68\xff\xcb\x88\x2c\xe6\x9e\x6a\x22\
+\x56\x00\x00\x00\x0a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8f\x30\x51\xd3\x8f\x30\
+\xf0\xde\xab\x64\xff\xee\xd4\xb0\xff\xeb\xcc\xa1\xff\xe8\xc3\x91\
+\xff\xe4\xbb\x82\xff\xe1\xb2\x72\xff\xde\xab\x64\xff\xd9\x9e\x4c\
+\xff\xd7\x99\x43\xff\xce\x8a\x2c\xff\xd0\x8c\x2d\xff\xd2\x8d\x2d\
+\xff\xbe\x7f\x29\xff\xb7\x7b\x27\xff\xb7\x7b\x27\xff\xb7\x7b\x27\
+\xff\xcd\x89\x2c\xff\xd2\x8d\x2d\xff\xd0\x8c\x2d\xff\xd5\x93\x38\
+\xff\xd8\x9b\x47\xff\xdd\xa8\x5e\xff\xe0\xb0\x6d\xff\xe3\xb8\x7d\
+\xff\xe6\xbf\x8a\xff\xe9\xc7\x98\xff\xed\xd1\xaa\xff\xe5\xbd\x85\
+\xff\xd7\x99\x43\xff\xc6\x85\x2b\xae\x6a\x47\x17\x21\x00\x00\x00\
+\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd5\x93\x38\
+\x10\xd4\x91\x35\x82\xd6\x96\x3e\xff\xe1\xb3\x73\xff\xed\xd2\xad\
+\xff\xee\xd4\xaf\xff\xea\xca\x9e\xff\xe6\xc0\x8c\xff\xe3\xb8\x7d\
+\xff\xe1\xb2\x72\xff\xde\xab\x64\xff\xdc\xa6\x5b\xff\xdb\xa2\x54\
+\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xdc\xa5\x59\xff\xdc\xa5\x59\
+\xff\xdb\xa4\x57\xff\xdc\xa5\x59\xff\xdd\xa9\x61\xff\xe0\xb0\x6d\
+\xff\xe3\xb7\x7a\xff\xe5\xbd\x85\xff\xe8\xc5\x94\xff\xeb\xcd\xa3\
+\xff\xef\xd7\xb5\xff\xe6\xbf\x8a\xff\xd8\x9b\x47\xff\xd2\x8d\x2d\
+\xc9\xb1\x77\x26\x50\x00\x00\x00\x08\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xd3\x8f\x31\x21\xd4\x91\x35\x82\xd4\x92\x36\
+\xf0\xde\xab\x64\xff\xe8\xc3\x91\xff\xf0\xd9\xb9\xff\xef\xd7\xb5\
+\xff\xed\xd1\xaa\xff\xeb\xcc\xa1\xff\xe9\xc6\x96\xff\xe7\xc2\x8e\
+\xff\xe5\xbc\x84\xff\xe4\xba\x7f\xff\xe3\xb8\x7d\xff\xe4\xba\x80\
+\xff\xe5\xbe\x87\xff\xe8\xc3\x91\xff\xea\xc9\x9b\xff\xec\xce\xa5\
+\xff\xee\xd4\xaf\xff\xf0\xd9\xba\xff\xec\xcf\xa8\xff\xe2\xb5\x77\
+\xff\xd6\x97\x40\xff\xd3\x8f\x30\xc7\xb5\x79\x27\x4e\x00\x00\x00\
+\x08\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\xd4\x92\x36\x61\xd4\x91\x35\xb2\xd5\x93\x38\xff\xdb\xa2\x54\
+\xff\xe2\xb5\x77\xff\xe8\xc3\x91\xff\xeb\xcc\xa1\xff\xf1\xdb\xbc\
+\xff\xf2\xde\xc2\xff\xf2\xdd\xc1\xff\xf2\xdd\xc1\xff\xf2\xdd\xc1\
+\xff\xf2\xde\xc2\xff\xeb\xcc\xa0\xff\xeb\xcc\xa1\xff\xe4\xba\x80\
+\xff\xde\xac\x66\xff\xd7\x98\x42\xff\xd3\x8f\x31\xd5\xc8\x86\x2b\
+\x7c\xa8\x71\x24\x2a\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x91\x35\
+\x41\xd4\x90\x33\x83\xd3\x8f\x31\xa4\xd4\x91\x35\xc3\xd5\x93\x38\
+\xf0\xd5\x93\x38\xff\xd5\x93\x38\xff\xd5\x93\x38\xff\xd5\x93\x38\
+\xff\xd5\x93\x38\xff\xd3\x8f\x31\xc5\xd3\x8f\x31\xc4\xce\x8a\x2c\
+\x89\xc6\x85\x2b\x59\xb5\x79\x27\x27\x00\x00\x00\x04\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x80\
+\x01\xff\xff\x08\x8e\xff\xfe\x00\x00\x7f\xff\x08\x8e\xff\xf8\x00\
+\x00\x1f\xff\x08\x8e\xff\xf0\x00\x00\x0f\xff\x08\x8e\xff\xe0\x00\
+\x00\x07\xff\x08\x8e\xff\xc0\x00\x00\x03\xff\x08\x8e\xff\x80\x00\
+\x00\x01\xff\x08\x8e\xff\x00\x00\x00\x00\xff\x08\x8e\xfe\x00\x00\
+\x00\x00\x7f\x08\x8e\xfe\x00\x00\x00\x00\x7f\x08\x8e\xfc\x00\x00\
+\x00\x00\x3f\x08\x8e\xfc\x00\x07\xc0\x00\x3f\x08\x8e\xf8\x00\x0c\
+\x40\x00\x1f\x08\x8e\xf8\x00\x08\x60\x00\x1f\x08\x8e\xf8\x00\x18\
+\x30\x00\x1f\x08\x8e\xf8\x00\x10\x30\x00\x1f\x08\x8e\xf8\x00\x20\
+\x10\x00\x1f\x08\x8e\xf8\x00\x20\x18\x00\x1f\x08\x8e\xf8\x00\x00\
+\x08\x00\x1f\x08\x8e\xf8\x00\x00\x0c\x00\x1f\x08\x8e\xf8\x00\x00\
+\x04\x00\x1f\x08\x8e\xf8\x00\x00\x06\x00\x1f\x08\x8e\xf8\x00\x00\
+\x02\x00\x1f\x08\x8e\xf8\x00\x00\x03\x00\x1f\x08\x8e\xf8\x00\x00\
+\x03\x00\x1f\x08\x8e\xf8\x00\x00\x01\x80\x3f\x08\x8e\xfc\x00\x00\
+\x01\x80\x3f\x08\x8e\xfc\x00\x00\x00\x80\x3f\x08\x8e\xfe\x00\x00\
+\x00\xc0\x7f\x08\x8e\xfe\x00\x00\x00\x40\x7f\x08\x8e\xff\x00\x00\
+\x00\x60\xff\x08\x8e\xff\x80\x00\x00\x21\xff\x08\x8e\xff\x80\x00\
+\x00\x33\xff\x08\x8e\xff\xc0\x00\x00\x3f\xff\x08\x8e\x8f\xf0\x00\
+\x00\x3f\xf0\x08\x8e\x83\xf8\x00\x00\x3f\xc0\x08\x8e\x80\xfe\x00\
+\x00\x7f\x00\x08\x8e\x80\x3f\xc0\x03\xfc\x01\x08\x8e\x80\x07\xff\
+\xff\xe0\x01\x08\x8e\xc0\x00\x7f\xfe\x00\x03\x08\x8e\xe0\x00\x00\
+\x00\x00\x07\x08\x8e\xf0\x00\x00\x00\x00\x0f\x08\x8e\xf8\x00\x00\
+\x00\x00\x1f\x08\x8e\xfe\x00\x00\x00\x00\x7f\x08\x8e\xff\x00\x00\
+\x00\x00\xff\x08\x8e\xff\xc0\x00\x00\x03\xff\x08\x8e\xff\xf8\x00\
+\x00\x0f\xff\x08\x8e\xff\xff\x00\x00\x7f\xff\x08\x8e\x28\x00\x00\
+\x00\x20\x00\x00\x00\x40\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x02\x00\x00\x00\x06\x00\x00\x00\x12\x00\x00\x00\x1c\x00\x00\x00\
+\x26\x00\x00\x00\x2a\x00\x00\x00\x2a\x00\x00\x00\x26\x00\x00\x00\
+\x1c\x00\x00\x00\x12\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\
+\x16\x22\x17\x07\x3d\x54\x38\x12\x80\x63\x42\x15\xa4\x6f\x4a\x18\
+\xc1\x6f\x4a\x18\xc3\x6c\x48\x17\xbd\x60\x40\x14\xa5\x40\x2b\x0e\
+\x76\x0e\x0a\x03\x4b\x00\x00\x00\x2d\x00\x00\x00\x16\x00\x00\x00\
+\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x02\x0b\x08\x02\x1a\x2d\x1e\x09\x59\x9b\x6c\x2b\
+\xc4\xc4\x94\x52\xf8\xe3\xb6\x7a\xff\xe6\xc1\x8d\xff\xea\xca\x9e\
+\xff\xea\xc9\x9c\xff\xe9\xc8\x9a\xff\xe6\xc0\x8b\xff\xda\xab\x6a\
+\xff\xba\x8b\x4a\xf2\x6c\x48\x17\xab\x24\x18\x07\x6b\x00\x00\x00\
+\x30\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x02\x1b\x12\x06\x17\x75\x4e\x19\xa3\xbe\x8f\x4d\xeb\xe5\xbf\x8a\
+\xff\xe4\xba\x80\xff\xda\xa2\x55\xff\xd7\x9b\x47\xff\xd5\x94\x3b\
+\xff\xd4\x93\x39\xff\xd6\x96\x3f\xff\xd8\x9d\x4a\xff\xdd\xaa\x63\
+\xff\xe5\xbe\x88\xff\xdc\xb3\x7b\xfd\xac\x7b\x37\xe5\x41\x2b\x0e\
+\x8a\x05\x03\x01\x3f\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x29\x1c\x09\
+\x2e\x8f\x64\x29\xbc\xdf\xaf\x6c\xff\xe1\xb6\x7b\xff\xd4\x93\x3a\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd8\xa0\x51\xff\xe4\xbd\x87\xff\xca\x96\x4d\
+\xf9\x61\x43\x1a\xae\x05\x03\x01\x41\x00\x00\x00\x17\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x22\x17\x07\x11\x8d\x62\x25\
+\xbb\xd7\xa8\x67\xfb\xd8\xa1\x56\xff\xca\x89\x31\xff\xce\x8a\x2c\
+\xff\xd4\x93\x39\xff\xdd\xa9\x62\xff\xe0\xb0\x6e\xff\xe0\xb1\x6f\
+\xff\xe0\xb1\x6f\xff\xe0\xb1\x6f\xff\xdf\xaf\x6c\xff\xd5\x95\x3d\
+\xff\xd2\x8d\x2d\xff\xcc\x89\x2c\xff\xcb\x8d\x38\xff\xdf\xae\x6b\
+\xff\xcb\x99\x52\xf9\x48\x2f\x0f\x98\x05\x03\x01\x3b\x00\x00\x00\
+\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x1f\x15\x06\x0d\x6c\x49\x17\x93\xd2\x97\x47\
+\xff\xd1\x95\x43\xff\xbd\x7f\x28\xff\xc3\x83\x2a\xff\xcc\x8c\x32\
+\xff\xdc\xa7\x5d\xff\xb8\x82\x37\xed\xa5\x6f\x23\xc7\xa9\x71\x24\
+\xc1\xaa\x72\x24\xc1\xaa\x72\x24\xc3\xb9\x81\x33\xe9\xdc\xa7\x5d\
+\xff\xcf\x8d\x31\xff\xc7\x85\x2a\xff\xc2\x82\x2a\xff\xc1\x83\x2c\
+\xff\xd7\xa0\x54\xff\xb4\x7c\x30\xf2\x3b\x27\x0c\x8a\x00\x00\x00\
+\x26\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x2c\x1d\x09\x42\xae\x77\x2b\xe8\xcf\x95\x43\
+\xff\xb7\x7c\x29\xff\xb9\x7c\x27\xff\xbf\x80\x29\xff\xd4\x96\x41\
+\xff\xc1\x8a\x3e\xff\x55\x39\x12\x7e\x27\x1a\x08\x23\x88\x5b\x1d\
+\x17\x8b\x5d\x1e\x17\x8b\x5d\x1e\x19\x8d\x5e\x1e\x79\xcc\x93\x43\
+\xff\xd1\x92\x3b\xff\xc2\x82\x2a\xff\xbe\x7f\x28\xff\xb8\x7b\x27\
+\xff\xbb\x7e\x2b\xff\xd1\x97\x45\xff\x8c\x5f\x21\xdc\x00\x00\x00\
+\x40\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x01\x91\x62\x20\xb5\xce\x93\x43\xff\xb4\x79\x26\
+\xff\xb0\x76\x25\xff\xb5\x79\x27\xff\xc1\x82\x2b\xff\xd0\x97\x47\
+\xff\x82\x57\x1b\xc5\x00\x00\x00\x15\x00\x00\x00\x01\x00\x00\x00\
+\x04\x00\x00\x00\x0c\x00\x00\x00\x04\x8e\x5f\x1f\x07\x9e\x6c\x26\
+\xce\xd3\x99\x49\xff\xbe\x7f\x28\xff\xb8\x7b\x27\xff\xb3\x77\x26\
+\xff\xaf\x75\x25\xff\xc4\x88\x35\xff\xc2\x87\x36\xff\x40\x2b\x0e\
+\x8b\x00\x00\x00\x27\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x40\x2b\x0d\x13\xa5\x72\x2d\xf6\xd0\x91\x3b\xff\xa8\x71\x24\
+\xff\xac\x73\x25\xff\xb2\x77\x26\xff\xcd\x92\x40\xff\xb1\x7a\x2e\
+\xf0\x35\x23\x0b\x67\x00\x00\x00\x07\x00\x00\x00\x00\x39\x26\x0c\
+\x2f\x18\x10\x05\x41\x00\x00\x00\x0f\x00\x00\x00\x00\x90\x60\x1e\
+\x64\xc1\x8b\x3e\xf8\xc7\x89\x33\xff\xb4\x78\x26\xff\xaf\x75\x25\
+\xff\xab\x73\x25\xff\xb3\x79\x29\xff\xd4\x97\x43\xff\x62\x42\x15\
+\xc2\x08\x06\x02\x3b\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x79\x51\x1a\x54\xc4\x88\x35\xff\xb3\x7d\x32\xff\xa3\x6d\x23\
+\xff\xa7\x70\x24\xff\xba\x7f\x2e\xff\xd4\x97\x44\xff\x62\x42\x15\
+\xb1\x0b\x08\x02\x1f\x00\x00\x00\x01\x14\x0e\x04\x08\x7f\x55\x1b\
+\xc8\x3f\x2a\x0d\x9a\x00\x00\x00\x29\x00\x00\x00\x05\x82\x57\x1b\
+\x1b\x98\x66\x20\xc2\xd4\x9a\x4b\xff\xb5\x7a\x28\xff\xab\x72\x24\
+\xff\xa7\x70\x24\xff\xa4\x6e\x23\xff\xc8\x8f\x42\xff\x99\x67\x21\
+\xe2\x1e\x14\x06\x5f\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7c\x53\x1b\x7c\xd3\x94\x3c\xff\xc8\x89\x33\xff\xb9\x7c\x28\
+\xff\xa8\x71\x24\xff\xcd\x90\x3b\xff\xb0\x7c\x33\xff\x2f\x1f\x0a\
+\x55\x00\x00\x00\x09\x00\x00\x00\x01\x42\x2c\x0e\x48\xac\x75\x29\
+\xfc\x87\x5c\x21\xe1\x00\x00\x00\x41\x00\x00\x00\x11\x82\x57\x1b\
+\x02\x7f\x55\x1b\x71\xc8\x8e\x3e\xff\xbf\x85\x35\xff\xa6\x6f\x24\
+\xff\xab\x73\x25\xff\xbb\x7d\x28\xff\xd0\x92\x3d\xff\xb0\x79\x2c\
+\xf0\x24\x18\x07\x72\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7b\x52\x1a\x9d\xdf\xad\x68\xff\xde\xac\x67\xff\xdd\xa8\x5f\
+\xff\xdd\xa8\x60\xff\xce\x9b\x53\xff\x71\x4c\x18\xc0\x00\x00\x00\
+\x25\x00\x00\x00\x12\x1d\x13\x06\x2b\x83\x58\x1c\xc4\xdb\xa4\x57\
+\xff\xbf\x87\x3a\xff\x3a\x27\x0c\x8c\x00\x00\x00\x2a\x00\x00\x00\
+\x03\x7d\x54\x1b\x07\x9a\x6b\x2a\xe0\xda\xa0\x51\xff\xd8\x9d\x4b\
+\xff\xdc\xa5\x5b\xff\xdd\xa9\x61\xff\xdf\xaf\x6b\xff\xba\x8b\x49\
+\xf9\x26\x1a\x08\x80\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7b\x52\x1a\xaa\xe2\xb6\x79\xff\xdf\xae\x6a\xff\xdd\xaa\x64\
+\xff\xe0\xb0\x6f\xff\xb4\x7e\x34\xf6\x49\x31\x10\x9f\x2a\x1c\x09\
+\x65\x2a\x1c\x09\x61\x4a\x31\x10\x92\xb8\x87\x43\xf7\xdd\xa7\x5e\
+\xff\xdd\xa8\x5e\xff\x64\x43\x15\xc8\x0d\x08\x02\x4b\x00\x00\x00\
+\x0a\x00\x00\x00\x00\x86\x5a\x1e\x84\xcd\x9e\x5c\xff\xdd\xaa\x63\
+\xff\xdd\xa8\x60\xff\xde\xab\x65\xff\xe0\xaf\x6d\xff\xbf\x94\x58\
+\xff\x29\x1b\x08\x83\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x7d\x54\x1b\x89\xe0\xb1\x70\xff\xe1\xb2\x71\xff\xe0\xb0\x6d\
+\xff\xe3\xb8\x7b\xff\xc3\x8e\x44\xff\xbb\x88\x42\xff\xbc\x89\x42\
+\xff\xbc\x89\x42\xff\xc5\x90\x46\xff\xde\xac\x67\xff\xdb\xa3\x57\
+\xff\xde\xac\x67\xff\xba\x86\x3e\xf6\x31\x21\x0a\x88\x00\x00\x00\
+\x1f\x00\x00\x00\x03\x7d\x54\x1b\x29\xad\x77\x2c\xd3\xe1\xb4\x75\
+\xff\xde\xac\x67\xff\xdf\xae\x6b\xff\xe1\xb4\x75\xff\xb7\x86\x42\
+\xf2\x25\x19\x08\x6e\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x80\x56\x1b\x6d\xdb\xa5\x5a\xff\xe1\xb5\x76\xff\xe0\xb2\x70\
+\xff\xe0\xb2\x72\xff\xe0\xb1\x70\xff\xe0\xb0\x6e\xff\xe0\xaf\x6c\
+\xff\xdf\xae\x6b\xff\xdf\xae\x69\xff\xde\xab\x65\xff\xdc\xa7\x5c\
+\xff\xdd\xa8\x5f\xff\xd6\xa5\x61\xff\x76\x4f\x19\xc5\x00\x00\x00\
+\x38\x00\x00\x00\x0c\x7d\x54\x1b\x05\x8b\x5d\x1e\x94\xdf\xae\x6b\
+\xff\xe0\xb1\x70\xff\xe0\xb1\x6f\xff\xe2\xb7\x7a\xff\xb3\x7c\x32\
+\xea\x23\x18\x07\x5e\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x85\x59\x1d\x2f\xc6\x92\x4b\xff\xe4\xbc\x85\xff\xe1\xb3\x73\
+\xff\xe0\xb1\x70\xff\xe0\xb0\x6d\xff\xdf\xae\x6a\xff\xde\xad\x68\
+\xff\xde\xac\x66\xff\xde\xab\x65\xff\xde\xaa\x64\xff\xdd\xaa\x63\
+\xff\xdd\xaa\x62\xff\xe2\xb6\x79\xff\xbb\x8b\x47\xff\x29\x1b\x09\
+\x6f\x00\x00\x00\x21\x00\x00\x00\x01\x84\x59\x1d\x24\xc0\x8d\x46\
+\xf8\xe4\xbc\x83\xff\xe1\xb5\x77\xff\xe6\xbf\x8a\xff\x86\x5a\x1d\
+\xcf\x15\x0e\x04\x36\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x87\x5b\x1d\x04\xa8\x7a\x3a\xdd\xe6\xc1\x8d\xff\xe2\xb6\x79\
+\xff\xe1\xb4\x76\xff\xe1\xb2\x73\xff\xe0\xb1\x70\xff\xe0\xb0\x6d\
+\xff\xdf\xaf\x6c\xff\xdf\xae\x6a\xff\xde\xad\x69\xff\xde\xad\x69\
+\xff\xde\xad\x69\xff\xe1\xb4\x76\xff\xe2\xb6\x79\xff\x59\x3c\x13\
+\xb4\x05\x03\x01\x3b\x00\x00\x00\x07\x00\x00\x00\x00\xa3\x72\x2d\
+\xba\xe4\xbc\x83\xff\xe5\xbd\x86\xff\xe3\xb8\x7e\xff\x58\x3b\x13\
+\xa2\x03\x02\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x99\x66\x21\x6b\xd4\xab\x73\xf8\xe7\xc3\x91\
+\xff\xe3\xb7\x7c\xff\xe2\xb6\x79\xff\xe2\xb5\x77\xff\xe1\xb3\x74\
+\xff\xe1\xb3\x73\xff\xe1\xb2\x71\xff\xe0\xb1\x70\xff\xe0\xb1\x6f\
+\xff\xe0\xb1\x6f\xff\xe0\xb2\x71\xff\xe8\xc5\x95\xff\xb7\x84\x3d\
+\xed\x26\x19\x08\x75\x00\x00\x00\x17\x00\x00\x00\x02\x8f\x5f\x1e\
+\x4d\xca\xa0\x66\xef\xec\xce\xa5\xff\xb4\x87\x48\xf5\x24\x18\x08\
+\x3e\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x8f\x60\x1f\x20\xac\x79\x33\xca\xec\xce\xa6\
+\xff\xe5\xbe\x88\xff\xe3\xb9\x7e\xff\xe3\xb8\x7c\xff\xe2\xb7\x7a\
+\xff\xe2\xb6\x79\xff\xe1\xb5\x76\xff\xe1\xb5\x76\xff\xe1\xb4\x75\
+\xff\xe1\xb4\x75\xff\xe1\xb4\x75\xff\xe4\xbb\x81\xff\xdb\xb3\x7e\
+\xfe\x65\x44\x16\xac\x00\x00\x00\x2f\x00\x00\x00\x08\x8e\x5f\x1f\
+\x17\xab\x77\x2f\xc1\xda\xb6\x85\xff\x7c\x55\x20\xab\x00\x00\x00\
+\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\x39\xcb\x95\x4b\
+\xee\xeb\xcc\xa2\xff\xe6\xc0\x8b\xff\xe4\xbb\x81\xff\xe4\xba\x80\
+\xff\xe3\xb9\x7e\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\
+\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe3\xb8\x7d\xff\xe9\xc8\x9a\
+\xff\xb8\x88\x48\xf7\x14\x0e\x04\x59\x00\x00\x00\x1a\x00\x00\x00\
+\x00\x94\x63\x20\x5e\x86\x5a\x1c\xbf\x1c\x13\x06\x2e\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9f\x6b\x23\
+\x79\xce\x9a\x53\xf5\xed\xd1\xac\xff\xe7\xc2\x8f\xff\xe4\xbc\x84\
+\xff\xe4\xbb\x83\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe4\xbb\x81\
+\xff\xe4\xba\x81\xff\xe4\xbb\x82\xff\xe4\xbb\x82\xff\xe8\xc5\x95\
+\xff\xdf\xb5\x7a\xff\x51\x36\x11\x9c\x00\x00\x00\x30\x00\x00\x00\
+\x04\x96\x64\x20\x0e\x65\x44\x15\x34\x00\x00\x00\x03\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9d\x69\x22\
+\x04\x9b\x68\x21\x52\xc9\x93\x49\xe3\xe5\xc4\x98\xff\xeb\xce\xa6\
+\xff\xe8\xc5\x94\xff\xe5\xbf\x89\xff\xe5\xbe\x87\xff\xe5\xbe\x87\
+\xff\xe5\xbd\x86\xff\xe5\xbd\x86\xff\xe5\xbe\x87\xff\xe7\xc3\x90\
+\xff\xef\xd6\xb4\xff\xa3\x74\x33\xdf\x21\x16\x07\x59\x00\x00\x00\
+\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\
+\x02\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x0e\x00\x00\x00\
+\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xa1\x6c\x22\x40\xaf\x7c\x35\xb2\xd7\xab\x6f\
+\xff\xed\xd2\xad\xff\xf0\xd8\xb9\xff\xed\xd3\xad\xff\xeb\xcd\xa2\
+\xff\xeb\xcd\xa2\xff\xec\xce\xa5\xff\xee\xd4\xb0\xff\xf0\xda\xbb\
+\xff\xea\xcc\xa1\xff\xb8\x82\x37\xee\x32\x21\x0a\x57\x00\x00\x00\
+\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x0e\x00\x00\x00\
+\x13\x00\x00\x00\x05\xa4\x6e\x23\x29\x72\x4c\x18\x75\x1f\x15\x06\
+\x52\x02\x02\x00\x33\x00\x00\x00\x15\x00\x00\x00\x06\x00\x00\x00\
+\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\
+\x41\xa8\x71\x24\x8f\xc7\x8e\x3f\xc7\xd0\xa5\x6a\xe0\xd4\xb0\x7e\
+\xeb\xd6\xb7\x8b\xeb\xd2\xac\x77\xea\xcc\x9e\x5f\xdc\xa4\x70\x2a\
+\xb8\x69\x46\x16\x82\x54\x38\x12\x23\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x05\x00\x00\x00\x0f\x26\x19\x08\x45\x72\x4c\x18\x9d\x1e\x14\x06\
+\x30\x00\x00\x00\x06\xb3\x78\x26\x12\xb1\x77\x26\xa4\x90\x60\x1e\
+\xd5\x59\x3c\x13\x9d\x00\x00\x00\x44\x00\x00\x00\x2b\x00\x00\x00\
+\x12\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xa4\x6e\x23\x1d\xa3\x6d\x23\x36\xa2\x6d\x23\
+\x41\xa2\x6d\x23\x41\xa2\x6d\x23\x40\x9e\x6a\x22\x32\x51\x36\x11\
+\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x0e\x14\x0d\x04\
+\x2f\x54\x38\x12\x81\x99\x67\x21\xdc\x98\x66\x20\xba\x00\x00\x00\
+\x13\x00\x00\x00\x02\x00\x00\x00\x00\xbc\x7e\x28\x16\xc5\x89\x35\
+\xc9\xd7\x9b\x4a\xfd\xa6\x78\x37\xe5\x5e\x3f\x15\xa5\x25\x19\x08\
+\x69\x09\x06\x02\x42\x00\x00\x00\x27\x00\x00\x00\x1a\x00\x00\x00\
+\x0d\x00\x00\x00\x08\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\
+\x04\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\
+\x04\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00\x0c\x00\x00\x00\
+\x16\x0c\x08\x02\x28\x29\x1b\x09\x57\x61\x41\x15\x91\xb3\x80\x3a\
+\xec\xd2\x9a\x4b\xff\xaa\x73\x26\xc0\x24\x18\x08\x32\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc5\x84\x2a\
+\x30\xcd\x90\x3b\xbb\xe0\xb0\x6e\xff\xd4\xa9\x6e\xfd\xb0\x7c\x35\
+\xe5\x76\x50\x19\xc8\x50\x36\x11\x97\x32\x21\x0a\x6b\x00\x00\x00\
+\x40\x00\x00\x00\x37\x00\x00\x00\x2d\x00\x00\x00\x27\x00\x00\x00\
+\x23\x00\x00\x00\x1d\x00\x00\x00\x1c\x00\x00\x00\x21\x00\x00\x00\
+\x26\x00\x00\x00\x2c\x0b\x07\x02\x39\x33\x22\x0b\x5b\x57\x3a\x12\
+\x93\x7b\x52\x1a\xc0\xba\x86\x3e\xe7\xd5\xa8\x69\xfd\xdf\xae\x6a\
+\xff\xc0\x86\x35\xc3\x28\x1a\x08\x30\x00\x00\x00\x03\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\xd3\x8e\x2e\x07\xd2\x90\x34\x90\xde\xab\x64\xe8\xe7\xc3\x90\
+\xff\xe6\xc1\x8e\xff\xda\xa3\x58\xff\xc6\x8f\x42\xff\xad\x7a\x34\
+\xf9\xa3\x71\x2b\xe7\x90\x60\x1f\xc9\x91\x62\x1f\xc8\x76\x4f\x19\
+\xb0\x70\x4b\x18\xa9\x79\x51\x1a\xb0\x8f\x60\x1f\xc4\x95\x65\x21\
+\xcf\xa1\x6f\x29\xe1\xb1\x7e\x37\xfb\xc5\x8e\x41\xff\xde\xaa\x64\
+\xff\xe6\xc0\x8c\xff\xe5\xbf\x89\xff\xdb\xa8\x62\xed\x94\x63\x20\
+\x81\x2e\x1f\x0a\x18\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xd3\x8f\x30\x09\xd3\x91\x34\x45\xd9\x9f\x4f\
+\xc7\xe2\xb5\x78\xff\xeb\xcc\xa2\xff\xe8\xc5\x94\xff\xe2\xb5\x78\
+\xff\xde\xab\x65\xff\xd8\x9f\x4f\xff\xd8\x9b\x48\xff\xd1\x97\x48\
+\xff\xcf\x97\x48\xff\xd1\x98\x48\xff\xd7\x9c\x49\xff\xd9\xa1\x53\
+\xff\xde\xab\x64\xff\xe3\xb8\x7d\xff\xe7\xc3\x91\xff\xea\xca\x9e\
+\xff\xe1\xb3\x74\xff\xcf\x94\x41\xc0\x6b\x49\x19\x58\x1f\x14\x06\
+\x09\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8f\x31\
+\x07\xd3\x90\x33\x41\xd6\x99\x43\xae\xdf\xad\x68\xdc\xe7\xc2\x90\
+\xff\xe9\xc6\x97\xff\xe9\xc8\x9b\xff\xea\xc9\x9c\xff\xe9\xc6\x97\
+\xff\xe8\xc4\x94\xff\xe8\xc6\x97\xff\xe9\xc6\x97\xff\xea\xc9\x9b\
+\xff\xe8\xc6\x97\xff\xe6\xc0\x8c\xfa\xdc\xac\x69\xdd\xb3\x7c\x2f\
+\xa7\x7f\x55\x1c\x50\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xd4\x92\x36\x0b\xd4\x91\x35\x32\xd5\x96\x3d\
+\x63\xd7\x9a\x46\x9e\xda\xa1\x53\xc9\xdd\xa8\x60\xeb\xde\xab\x65\
+\xff\xde\xab\x65\xff\xde\xab\x65\xff\xdc\xa6\x5b\xe5\xd9\xa0\x51\
+\xcb\xd0\x94\x41\x9b\x97\x67\x25\x63\x43\x2d\x0f\x35\x5d\x3e\x14\
+\x0a\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xff\xc0\x03\xff\xff\x80\x01\xff\xfe\x00\x00\
+\x7f\xfc\x00\x00\x3f\xf8\x00\x00\x1f\xf8\x00\x00\x1f\xf0\x00\x00\
+\x0f\xf0\x00\x00\x0f\xe0\x00\x00\x07\xe0\x04\x40\x07\xe0\x00\x00\
+\x07\xe0\x00\x00\x07\xe0\x00\x00\x07\xe0\x00\x10\x07\xe0\x00\x00\
+\x07\xe0\x00\x00\x07\xe0\x00\x00\x07\xe0\x00\x04\x0f\xf0\x00\x00\
+\x0f\xf0\x00\x00\x0f\xf8\x00\x02\x1f\xfc\x00\x00\x3f\x9c\x00\x01\
+\xf8\x0f\x00\x01\xf0\x01\xc0\x03\xc0\x00\x70\x0f\x00\x80\x00\x00\
+\x01\xc0\x00\x00\x03\xe0\x00\x00\x07\xf0\x00\x00\x0f\xfc\x00\x00\
+\x7f\xff\x00\x00\xff\x28\x00\x00\x00\x18\x00\x00\x00\x30\x00\x00\
+\x00\x01\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x08\x00\x00\x00\x19\x00\x00\x00\x29\x00\x00\x00\x32\x00\x00\x00\
+\x32\x00\x00\x00\x29\x00\x00\x00\x19\x00\x00\x00\x08\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x07\x1c\x12\x06\x36\x68\x46\x16\
+\x9c\xb1\x83\x44\xd9\xc6\x9f\x69\xf8\xcc\xa7\x73\xff\xc6\x9f\x69\
+\xf8\xaf\x82\x43\xdc\x65\x43\x15\xaa\x19\x11\x05\x58\x00\x00\x00\
+\x24\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x13\x0d\x04\x0e\x6e\x4a\x17\x95\xcf\xa1\x61\xfa\xe6\xc0\x8c\
+\xff\xde\xac\x66\xff\xd8\x9d\x4a\xff\xd6\x97\x3f\xff\xd8\x9d\x4a\
+\xff\xde\xac\x66\xff\xe6\xc0\x8c\xff\xcf\xa1\x61\xfa\x67\x45\x16\
+\xac\x06\x04\x01\x3e\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x0d\x04\
+\x0d\x8e\x64\x2a\xb8\xe2\xb5\x77\xff\xd6\x9d\x4d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\
+\xff\xd2\x8d\x2d\xff\xd2\x8d\x2d\xff\xd7\x9d\x4e\xff\xe2\xb6\x79\
+\xff\x83\x5d\x28\xca\x05\x03\x01\x40\x00\x00\x00\x0a\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x88\x5b\x1d\
+\xaa\xdd\xa9\x62\xff\xc6\x87\x30\xff\xc9\x86\x2b\xff\xd7\x9c\x4a\
+\xff\xda\xa6\x5f\xff\xd1\xa0\x5c\xff\xd1\xa0\x5c\xff\xd1\xa0\x5c\
+\xff\xde\xac\x66\xff\xd1\x8d\x2e\xff\xc9\x87\x2b\xff\xc7\x88\x31\
+\xff\xdd\xaa\x62\xff\x77\x4f\x19\xc2\x00\x00\x00\x30\x00\x00\x00\
+\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x2b\x0e\x54\xca\x90\x3f\
+\xff\xbc\x80\x2c\xff\xba\x7d\x28\xff\xc7\x86\x2d\xff\xcc\x95\x49\
+\xff\x55\x39\x12\x7d\x56\x3a\x12\x25\x8b\x5d\x1e\x22\x8b\x5d\x1e\
+\x26\xac\x78\x2f\xdc\xd3\x96\x41\xff\xc2\x82\x2a\xff\xbc\x7e\x28\
+\xff\xbd\x80\x2c\xff\xca\x90\x40\xff\x32\x22\x0b\x82\x00\x00\x00\
+\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x01\x9f\x6b\x23\xd0\xc6\x8b\x39\
+\xff\xaf\x75\x25\xff\xb5\x79\x27\xff\xd2\x96\x44\xff\x85\x59\x1c\
+\xc8\x00\x00\x00\x0f\x00\x00\x00\x01\x00\x00\x00\x0e\x00\x00\x00\
+\x04\x8b\x5d\x1e\x54\xcd\x93\x43\xff\xbe\x7f\x28\xff\xb5\x79\x27\
+\xff\xaf\x75\x25\xff\xc7\x8b\x39\xff\x92\x63\x21\xdc\x00\x00\x00\
+\x30\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x71\x4c\x18\x24\xbe\x88\x3d\xff\xac\x73\x25\
+\xff\xa9\x71\x24\xff\xb9\x7d\x29\xff\xc1\x8a\x3d\xff\x2a\x1c\x09\
+\x50\x00\x00\x00\x02\x40\x2b\x0e\x2c\x3b\x28\x0d\x82\x00\x00\x00\
+\x15\x00\x00\x00\x00\xa5\x71\x29\xd4\xc9\x8d\x3c\xff\xaf\x75\x25\
+\xff\xaa\x72\x24\xff\xae\x75\x25\xff\xbe\x88\x3d\xff\x1e\x14\x06\
+\x5a\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x7c\x53\x1b\x5b\xd3\x94\x3d\xff\xb7\x7b\x27\
+\xff\xa9\x71\x24\xff\xce\x93\x41\xff\x7b\x52\x1a\xc3\x00\x00\x00\
+\x0e\x00\x00\x00\x02\x7b\x52\x1a\xa8\x8e\x61\x23\xe2\x00\x00\x00\
+\x37\x00\x00\x00\x03\x80\x56\x1b\x60\xcf\x95\x44\xff\xac\x73\x25\
+\xff\xa8\x71\x24\xff\xb6\x7a\x27\xff\xd3\x94\x3d\xff\x36\x24\x0b\
+\x88\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x7b\x52\x1a\x78\xe0\xaf\x6d\xff\xdd\xaa\x62\
+\xff\xdd\xa9\x61\xff\xc1\x8e\x47\xff\x24\x18\x08\x60\x00\x00\x00\
+\x18\x2f\x1f\x0a\x45\xbf\x88\x3d\xfc\xcb\x91\x42\xff\x2e\x1f\x0a\
+\x81\x00\x00\x00\x12\x7d\x54\x1b\x04\xa8\x75\x2f\xe0\xda\xa2\x54\
+\xff\xdc\xa6\x5b\xff\xdd\xaa\x62\xff\xdf\xae\x6a\xff\x3b\x27\x0c\
+\x9d\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x7c\x53\x1b\x74\xe2\xb5\x77\xff\xdf\xad\x69\
+\xff\xe1\xb3\x73\xff\xa7\x70\x24\xf5\x72\x4c\x18\xc1\x73\x4d\x19\
+\xbe\x92\x63\x21\xe2\xde\xad\x68\xff\xdf\xac\x67\xff\x83\x59\x1f\
+\xd7\x00\x00\x00\x30\x00\x00\x00\x02\x80\x56\x1c\x70\xde\xac\x66\
+\xff\xdd\xaa\x63\xff\xdf\xad\x69\xff\xe0\xb0\x6e\xff\x3b\x27\x0c\
+\x96\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x80\x56\x1b\x56\xde\xab\x65\xff\xe0\xb1\x70\
+\xff\xe1\xb4\x75\xff\xe1\xb3\x73\xff\xe1\xb2\x71\xff\xe0\xb1\x70\
+\xff\xe0\xb1\x6f\xff\xdd\xa9\x60\xff\xdd\xa8\x5e\xff\xcb\x98\x52\
+\xff\x23\x17\x07\x6e\x00\x00\x00\x0e\x7d\x54\x1b\x0c\xb9\x88\x45\
+\xf0\xe0\xb1\x71\xff\xe0\xb1\x6f\xff\xdd\xaa\x62\xff\x36\x24\x0c\
+\x7a\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x85\x5a\x1d\x1c\xcb\x9d\x5e\xff\xe2\xb7\x7a\
+\xff\xe1\xb2\x72\xff\xe0\xb0\x6e\xff\xdf\xae\x6a\xff\xde\xac\x67\
+\xff\xde\xab\x65\xff\xde\xab\x64\xff\xdd\xaa\x63\xff\xe3\xb8\x7d\
+\xff\x7d\x55\x1e\xca\x00\x00\x00\x2b\x00\x00\x00\x01\x99\x67\x21\
+\x8c\xe5\xbd\x85\xff\xe2\xb7\x7a\xff\xc4\x98\x5a\xff\x1c\x12\x06\
+\x3e\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x77\x30\xbc\xe9\xc7\x97\
+\xff\xe2\xb6\x79\xff\xe1\xb4\x75\xff\xe1\xb2\x72\xff\xe0\xb1\x70\
+\xff\xe0\xb0\x6e\xff\xdf\xaf\x6c\xff\xdf\xaf\x6c\xff\xe1\xb4\x76\
+\xff\xcb\x9e\x60\xfc\x1f\x14\x06\x63\x00\x00\x00\x0a\x8a\x5d\x1e\
+\x20\xd0\xa3\x66\xfb\xea\xcb\x9f\xff\x94\x68\x2b\xcd\x00\x00\x00\
+\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8f\x60\x1f\x3c\xd9\xb2\x7c\
+\xff\xe5\xbf\x89\xff\xe3\xb9\x7d\xff\xe2\xb7\x7a\xff\xe2\xb6\x79\
+\xff\xe1\xb4\x76\xff\xe1\xb4\x75\xff\xe1\xb4\x75\xff\xe1\xb4\x75\
+\xff\xe9\xc7\x98\xff\x6d\x49\x17\xba\x00\x00\x00\x25\x00\x00\x00\
+\x00\xa8\x76\x30\xac\xd6\xaf\x79\xff\x34\x23\x0b\x58\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\x71\x24\
+\x8c\xe8\xc5\x95\xff\xe6\xc0\x8c\xff\xe4\xba\x81\xff\xe3\xb9\x7f\
+\xff\xe3\xb9\x7e\xff\xe3\xb9\x7d\xff\xe3\xb9\x7d\xff\xe3\xb9\x7d\
+\xff\xe6\xc0\x8c\xff\xc9\x9d\x60\xfa\x17\x0f\x05\x57\x00\x00\x00\
+\x08\x94\x63\x20\x3d\x6e\x4a\x17\x99\x00\x00\x00\x08\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x99\x67\x21\
+\x04\xa9\x73\x28\xa4\xe7\xc3\x91\xff\xe9\xc7\x99\xff\xe5\xbd\x86\
+\xff\xe5\xbd\x85\xff\xe5\xbd\x85\xff\xe4\xbc\x83\xff\xe5\xbc\x85\
+\xff\xe5\xbd\x85\xff\xea\xca\x9e\xff\x62\x42\x15\xab\x00\x00\x00\
+\x1e\x00\x00\x00\x00\x4c\x33\x10\x04\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x03\x00\x00\x00\x0f\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xa2\x6d\x23\x68\xcc\xa0\x62\xf0\xec\xd0\xa9\
+\xff\xed\xd2\xad\xff\xea\xcb\x9f\xff\xea\xc9\x9c\xff\xea\xcb\x9f\
+\xff\xed\xd2\xad\xff\xed\xd1\xab\xff\xb2\x7f\x38\xe6\x00\x00\x00\
+\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x03\x00\x00\x00\x10\x00\x00\x00\x06\xa5\x6f\x23\
+\x3e\x57\x3a\x12\x7f\x06\x04\x01\x3d\x00\x00\x00\x17\x00\x00\x00\
+\x04\x00\x00\x00\x00\x00\x00\x00\x00\xa2\x6d\x23\x10\xa7\x70\x24\
+\x64\xbf\x88\x3c\xb0\xc6\x9c\x61\xdd\xc9\xa4\x71\xe2\xc6\x9c\x61\
+\xde\xb9\x84\x3b\xb4\x4f\x35\x11\x68\x45\x2e\x0f\x15\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\
+\x12\x3e\x2a\x0d\x66\x5c\x3e\x14\x7f\x00\x00\x00\x07\xb9\x7c\x28\
+\x04\xb6\x7a\x27\xbc\xb1\x7c\x33\xe8\x42\x2c\x0e\x8e\x06\x04\x01\
+\x3f\x00\x00\x00\x1f\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x03\x00\x00\x00\x0a\x00\x00\x00\x19\x39\x26\x0c\x62\x9f\x6f\x2c\
+\xd0\xb5\x7a\x28\xe9\x20\x15\x07\x28\x00\x00\x00\x00\x00\x00\x00\
+\x00\xc3\x83\x2a\x0c\xcd\x91\x3d\xbc\xe0\xb0\x6d\xff\xb7\x87\x46\
+\xee\x6a\x47\x17\xb6\x3a\x27\x0c\x7e\x0f\x0a\x03\x47\x00\x00\x00\
+\x30\x00\x00\x00\x25\x00\x00\x00\x1f\x00\x00\x00\x18\x00\x00\x00\
+\x17\x00\x00\x00\x1e\x00\x00\x00\x24\x0c\x08\x02\x33\x35\x23\x0b\
+\x61\x5e\x3f\x14\x9f\xaf\x7e\x3b\xe1\xda\xaa\x68\xff\xcc\x93\x45\
+\xde\x2d\x1e\x09\x35\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xd3\x8e\x2e\x04\xd3\x91\x36\x80\xe1\xb3\x73\
+\xfb\xe8\xc6\x97\xff\xdb\xa8\x61\xff\xc2\x8e\x47\xff\xb1\x7c\x32\
+\xee\xa0\x6b\x22\xd6\x96\x64\x20\xce\x82\x57\x1c\xbf\x8f\x60\x1e\
+\xc7\xa1\x6c\x23\xd5\xae\x78\x2e\xe8\xbe\x8a\x43\xfc\xd8\xa4\x5c\
+\xff\xe6\xc1\x8d\xff\xe3\xb9\x7e\xff\xb6\x80\x35\xad\x34\x23\x0b\
+\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x92\x36\
+\x24\xd7\x9a\x45\xa8\xe3\xb8\x7d\xfb\xea\xc9\x9d\xff\xe8\xc4\x93\
+\xff\xe3\xb8\x7d\xff\xe0\xb1\x6f\xff\xdf\xaf\x6b\xff\xe0\xb0\x6d\
+\xff\xe2\xb6\x79\xff\xe7\xc2\x8e\xff\xea\xca\x9e\xff\xe4\xbc\x83\
+\xff\xd1\x98\x4a\xc5\x60\x41\x14\x49\x00\x00\x00\x02\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xd4\x92\x36\x18\xd4\x92\x36\x6c\xd9\x9e\x4c\
+\xb0\xde\xab\x66\xd9\xe3\xb7\x7b\xfb\xe3\xb8\x7c\xff\xe3\xb8\x7c\
+\xff\xdf\xad\x68\xe2\xd5\x9d\x4f\xb8\x97\x68\x26\x80\x7a\x52\x1a\
+\x2a\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\
+\x41\xfc\x00\x3f\x41\xf8\x00\x1f\x41\xf0\x00\x0f\x41\xe0\x00\x07\
+\x41\xe0\x00\x07\x41\xc0\x00\x03\x41\xc0\x04\x03\x41\xc0\x00\x03\
+\x41\xc0\x00\x03\x41\xc0\x00\x03\x41\xc0\x00\x03\x41\xc0\x00\x03\
+\x41\xe0\x00\x07\x41\xe0\x00\x87\x41\xf0\x00\x0f\x41\xf0\x00\x5f\
+\x41\x1c\x00\x78\x41\x06\x00\xe0\x41\x00\xff\x01\x41\x80\x00\x01\
+\x41\xc0\x00\x07\x41\xf0\x00\x0f\x41\xfc\x00\x3f\x41\x28\x00\x00\
+\x00\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x01\x0b\x07\x02\x17\x2d\x1e\x0a\
+\x54\x37\x25\x0c\x75\x33\x22\x0b\x6c\x13\x0d\x04\x3c\x00\x00\x00\
+\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x13\x0d\x04\x06\x5b\x41\x1c\x80\xca\x9e\x61\xee\xdf\xad\x69\
+\xff\xdf\xaf\x6b\xff\xdf\xaf\x6b\xff\xd6\xa8\x68\xfc\x86\x63\x34\
+\xbe\x11\x0b\x03\x43\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x10\x05\
+\x05\x87\x62\x2f\xa8\xd8\xa4\x5b\xff\xd2\x8f\x33\xff\xd8\x9d\x4a\
+\xff\xd9\x9f\x4e\xff\xd9\x9e\x4d\xff\xd2\x8f\x31\xff\xd5\x9d\x4f\
+\xff\xb5\x88\x49\xe7\x14\x0d\x04\x4b\x00\x00\x00\x03\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5f\x41\x16\
+\x73\xca\x8f\x3d\xff\xbe\x7f\x28\xff\xcf\x95\x44\xff\x76\x51\x1d\
+\x95\xa1\x6c\x22\x6c\xa4\x6f\x27\x8f\xd2\x96\x43\xff\xc2\x82\x29\
+\xff\xc3\x87\x35\xff\x93\x66\x28\xd6\x00\x00\x00\x1f\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x23\x17\x07\x05\xb5\x7e\x33\
+\xea\xae\x74\x25\xff\xbd\x81\x2e\xff\x8e\x63\x27\xc7\x00\x00\x00\
+\x07\x16\x0f\x05\x20\x1c\x13\x06\x07\xb5\x7f\x35\xca\xbc\x7f\x2a\
+\xff\xaf\x75\x25\xff\xc3\x88\x36\xff\x2b\x1d\x09\x6c\x00\x00\x00\
+\x02\x00\x00\x00\x00\x00\x00\x00\x00\x7a\x52\x1a\x34\xc5\x89\x35\
+\xff\xab\x72\x25\xff\xc3\x89\x38\xff\x27\x1a\x08\x4c\x1e\x14\x06\
+\x15\x7c\x54\x1c\xd0\x00\x00\x00\x20\x8a\x5d\x1e\x54\xc4\x8a\x3a\
+\xff\xa9\x71\x24\xff\xbe\x83\x32\xff\x63\x43\x17\xa8\x00\x00\x00\
+\x06\x00\x00\x00\x00\x00\x00\x00\x00\x7b\x52\x1a\x52\xe0\xaf\x6d\
+\xff\xde\xab\x64\xff\x8f\x65\x2c\xd5\x15\x0e\x04\x3f\x68\x49\x1d\
+\x9e\xd5\x9e\x53\xff\x2a\x1c\x09\x72\x19\x10\x05\x05\xb2\x81\x3d\
+\xd8\xdc\xa5\x5a\xff\xde\xad\x68\xff\x72\x55\x2c\xbf\x00\x00\x00\
+\x08\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x55\x1b\x3d\xdf\xaf\x6c\
+\xff\xe1\xb3\x73\xff\xcf\x9e\x59\xff\xcd\x9c\x57\xff\xd8\xa5\x5f\
+\xff\xdc\xa8\x5e\xff\x8e\x67\x31\xd1\x00\x00\x00\x1a\x97\x67\x23\
+\x65\xe0\xb0\x6e\xff\xe1\xb2\x72\xff\x6d\x4d\x21\xaa\x00\x00\x00\
+\x04\x00\x00\x00\x00\x00\x00\x00\x00\x85\x5a\x1c\x0d\xce\xa2\x66\
+\xf6\xe1\xb4\x75\xff\xe0\xb0\x6f\xff\xdf\xae\x6a\xff\xde\xac\x67\
+\xff\xde\xab\x66\xff\xd8\xab\x6c\xff\x22\x16\x07\x60\x2c\x1d\x09\
+\x0b\xcb\x9e\x5e\xec\xe4\xba\x81\xff\x3d\x29\x0d\x70\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb1\x82\x41\
+\x93\xe7\xc2\x8f\xff\xe2\xb7\x7a\xff\xe2\xb5\x77\xff\xe1\xb3\x73\
+\xff\xe1\xb2\x72\xff\xe3\xb9\x7f\xff\x87\x65\x36\xc3\x00\x00\x00\
+\x14\xab\x7c\x3b\x85\xbd\x98\x65\xe8\x0b\x07\x02\x15\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\x63\x20\
+\x0e\xc9\x9a\x59\xd7\xe7\xc3\x92\xff\xe4\xba\x81\xff\xe4\xba\x80\
+\xff\xe3\xb9\x7f\xff\xe4\xba\x7f\xff\xda\xb2\x7c\xfd\x19\x11\x05\
+\x50\x4a\x31\x10\x1c\x4a\x31\x10\x49\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\
+\x00\x9c\x68\x21\x15\xc3\x94\x53\xb5\xe6\xc4\x95\xff\xea\xca\x9e\
+\xff\xe8\xc5\x94\xff\xe9\xc7\x99\xff\xec\xd0\xa8\xff\x6b\x4b\x1f\
+\x9f\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x04\x00\x00\x00\x07\x98\x66\x20\x55\x43\x2d\x0e\x7e\x00\x00\x00\
+\x23\x00\x00\x00\x07\x00\x00\x00\x00\xa5\x6f\x23\x34\xc1\x8e\x48\
+\x7f\xc8\xa2\x6c\x96\xc3\x97\x59\x8e\x71\x4d\x1b\x53\x2e\x1f\x0a\
+\x09\x00\x00\x00\x00\x00\x00\x00\x05\x1d\x13\x06\x31\x72\x4d\x18\
+\x9e\x09\x06\x02\x13\xbb\x7e\x28\x05\xcc\x8f\x3b\xac\xae\x84\x4a\
+\xe1\x55\x3b\x16\x96\x20\x16\x07\x51\x00\x00\x00\x23\x00\x00\x00\
+\x17\x00\x00\x00\x12\x00\x00\x00\x11\x00\x00\x00\x17\x11\x0b\x03\
+\x2a\x37\x25\x0c\x64\x86\x63\x31\xb3\xc9\x93\x49\xeb\x45\x2e\x0f\
+\x49\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x8e\x2e\x02\xd7\x9a\x46\
+\x72\xe2\xb6\x79\xf1\xdc\xb1\x74\xff\xc4\x93\x4f\xf7\xb4\x7f\x35\
+\xe4\xa1\x72\x30\xd6\xac\x79\x32\xdd\xbc\x88\x40\xeb\xd0\xa2\x61\
+\xfe\xe4\xba\x80\xff\xbe\x91\x51\xc1\x4d\x34\x10\x29\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\xd3\x90\x33\x12\xda\xa0\x51\x72\xe0\xb1\x70\xc0\xe3\xb7\x7a\
+\xed\xe3\xb8\x7d\xff\xe3\xb7\x7c\xf9\xdf\xb1\x71\xd9\xa7\x80\x4a\
+\x9c\x81\x58\x1f\x40\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\xf0\x0f\xac\x41\xe0\x07\xac\x41\xc0\x03\xac\
+\x41\xc0\x03\xac\x41\x80\x01\xac\x41\x80\x01\xac\x41\x80\x01\xac\
+\x41\x80\x01\xac\x41\x80\x03\xac\x41\xc0\x03\xac\x41\xc0\x07\xac\
+\x41\x20\x0c\xac\x41\x08\x10\xac\x41\x00\x01\xac\x41\x80\x03\xac\
+\x41\xe0\x07\xac\x41\
+\x00\x00\x10\xbf\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x2a\x04\xcb\
+\x2f\x9a\x51\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
+\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
+\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x10\x4e\x49\x44\x41\x54\
+\x78\xda\xad\x5a\x6b\x8c\x5c\xe5\x79\x7e\xcf\x65\xee\xb3\xbb\xb3\
+\xb3\xbb\xf6\x7a\xcd\xa6\xbe\x60\x70\x28\xa6\x24\x32\x26\x4e\x6b\
+\x48\xe2\x82\x94\x36\x6d\x23\x0b\x44\x14\x45\xca\x9f\x36\x15\x48\
+\x51\x5a\x8a\x52\x2a\x25\x7f\xaa\x44\xa2\x29\xca\x1f\xa4\xf6\x47\
+\xa5\x4a\xf9\xd1\x94\xa4\x28\x52\x4b\x44\x9b\x26\x40\xb0\x53\xb9\
+\x86\x00\x69\x31\xc4\x26\xc6\x98\xd8\x6b\xaf\xf7\x32\x3b\xbb\x33\
+\x3b\xf7\x73\x4e\x9f\xe7\x3d\xdf\x77\xe6\xec\xae\x93\x1a\x92\x33\
+\x3a\x3a\xf7\xef\x7b\xde\xfb\xe5\x1b\xe7\xa5\x17\x5f\x94\xcd\x9b\
+\xe3\x38\xe2\x79\x9e\x44\x51\x24\x61\x18\x8a\xeb\xba\x7a\xe4\x35\
+\xcf\xf9\x3c\x08\x02\xbd\xe6\x86\x67\x59\xbc\xff\x7e\x9c\xfe\x56\
+\x36\x9b\xbd\x05\xcf\xf7\xe0\x7a\x14\xc7\x92\x79\xbe\x3e\x18\x0c\
+\xd6\xf0\xfe\xf9\x6e\xb7\xfb\x06\x6e\xfd\x0f\xae\x7f\x8a\xb1\x7a\
+\x76\x0c\x8e\x6b\xe7\xb3\x18\x38\x07\x8f\x66\x0c\xb9\xd6\xe6\xcb\
+\x7b\xd8\x38\x11\x76\x7e\x7b\x24\x9f\xcf\xdf\x57\x28\x16\x8f\x16\
+\x8b\xc5\x1b\x0b\xf9\xbc\x97\xcd\xe5\x24\x93\xc9\x88\x47\x42\x5d\
+\x3b\x79\xa4\x60\xfa\xbd\x9e\x74\xba\x5d\x69\xb7\x5a\xc1\xfa\xfa\
+\xfa\xb9\x56\xab\xf5\x6c\xa7\xd3\x79\x0a\x63\x9d\xc0\x3e\x78\x2f\
+\x58\x9c\x77\x23\x01\xee\x78\xe6\x63\x7b\x00\x80\x3f\x3f\x56\xa9\
+\xdc\x39\x32\x32\x22\x38\xdf\xf0\x7d\xb7\xd7\x97\x7e\xbf\x0f\xb0\
+\x3d\xbd\xce\xe7\xb2\x92\x05\x51\xd9\x6c\x66\xc8\x04\x8c\xd5\x68\
+\x36\x65\x6d\x75\x55\xea\xf5\xfa\x29\x10\xf4\x04\xa4\xf2\x2d\x12\
+\xc2\xf9\xaf\x57\x02\xd7\x4d\x00\x07\xc3\xf1\x5e\x00\xfe\xca\xc4\
+\xc4\xc4\x1d\x63\x95\x31\xbc\x13\x0b\x70\x69\xa5\x2e\x6f\x5f\xbc\
+\x2c\xef\x60\x5f\x58\xae\xc9\x6a\xa3\x29\x3d\x10\x10\x04\xf1\xa4\
+\x94\x06\xc1\x8f\x8d\x94\x65\xdb\x64\x55\x76\xdd\x30\x23\xbb\x66\
+\x67\x64\x72\xbc\x12\x13\x0c\xa9\xd4\x6a\x35\x59\x5a\x5c\x7c\xa9\
+\xd1\x68\x7c\x09\x73\xfe\xa7\x9d\xf3\x57\x26\xc0\x00\x2f\x43\xb7\
+\x1f\xdb\xb6\x6d\xdb\x43\xd5\x89\x09\x8a\x40\xd5\xe2\xf4\xd9\x73\
+\xf2\xf2\xe9\x9f\x02\xfc\x9c\xac\xb7\xda\xfc\x52\x7c\xdf\x93\x0c\
+\xbe\xe5\xf7\xe9\xc9\xb9\xf7\x31\xd6\x60\x10\x90\xff\x52\x2e\x16\
+\x40\xc8\x4e\x39\x78\xdb\x2d\xf2\x9b\x37\xef\x15\x17\xef\xb6\xdb\
+\x6d\x59\x58\x58\x88\x16\xae\x5e\xfd\xbb\x5e\xaf\xf7\x28\xbe\x69\
+\xfe\x4a\x04\x98\x89\xf7\x57\xc6\xc7\xbf\xb1\x63\x7a\xfa\x50\xa9\
+\x5c\xd6\xe7\xaf\x9d\x7d\x4b\x5e\x38\xf5\xb2\x5c\x9c\x9b\x27\x66\
+\x29\x40\xef\xa1\xff\x92\x33\xaa\xe2\x79\x6e\x6c\x03\x76\x72\x23\
+\xc9\x01\xed\x00\x92\xe9\x76\xfb\xd2\xea\x74\xa4\xdd\xe9\xea\xf3\
+\xd9\x99\xed\xf2\x91\x0f\x1d\x94\x03\x20\x84\xdb\xf2\xd2\x92\xcc\
+\x5d\xbe\xfc\xe2\x6a\xbd\xfe\x59\x5c\x9e\xe1\x38\xef\x9a\x00\x8a\
+\x10\xdb\x61\xa8\xcb\x53\xd3\x3b\x76\xcc\x40\x02\x52\x87\x6a\x3c\
+\xf3\xdc\x8f\x94\x00\x72\xac\x5c\x2a\xea\x5e\x04\x78\x3f\xe3\xeb\
+\xbd\x5f\xb6\x59\x8f\xc3\x63\x7f\x30\x00\x01\x1d\x69\xac\xb7\xa4\
+\xd9\x6c\x41\x26\x91\xdc\x7a\xd3\x5e\xf9\xc4\xd1\x23\xaa\x6a\x50\
+\x25\x99\xbb\x74\xe9\xf2\xf2\xf2\xf2\x7d\x00\x7f\xd2\x7e\x7b\x5d\
+\x04\x70\x83\x9a\x1c\x9e\x9a\x9a\xfa\x2e\xc0\x57\x29\x8d\x73\x17\
+\x2e\xc9\xbf\xfe\xe0\x05\x59\x5e\x59\x95\x91\x72\x49\x46\x31\x09\
+\xd5\xc0\xf7\x5c\x55\x1d\x4b\xb8\xe5\xba\xb3\x89\x18\xe3\xb9\xb6\
+\x10\x42\x15\x6d\x42\xfd\x68\x37\x8d\xe6\xba\x4c\x8c\x8f\xc9\x27\
+\xef\xb9\x5b\x6e\xdc\x35\x4b\x6f\x25\x3f\xbf\x78\xb1\xb6\xb8\xb0\
+\xf0\x09\xbc\x77\xd2\xb9\x06\x83\x7e\x91\x04\x6e\xde\xbe\x7d\xfb\
+\xb3\x3b\x66\x66\x76\x12\xfc\xff\x9e\x39\x27\x4f\xff\xe0\xb8\x04\
+\x98\x90\xdc\xa9\xc0\xf3\x64\xb3\x34\x60\x17\x92\x8a\x41\x93\xfb\
+\x74\x9b\x8e\x6c\x25\x20\x8a\xd1\x26\xc7\x50\x89\x09\xd5\x8e\x2c\
+\x61\x3d\x78\xae\x3a\xb8\xbe\xba\xd6\xd4\x31\xff\x08\x44\xdc\xb6\
+\xff\x46\x81\xab\x95\x77\x2e\x5c\x98\x5b\x5c\x5c\x3c\x8a\xaf\xcf\
+\x6e\x61\xf4\x35\xa8\x2a\xc3\x50\xbf\xb9\x7d\x7a\x5a\xc1\x9f\x7e\
+\xf3\x2d\x79\xfa\xd9\x13\xe2\xc2\x70\x27\x47\x47\x94\xf3\xbc\xcf\
+\x49\x5c\x87\xbe\xde\x8d\xc1\x3b\x96\x90\x84\x0b\x69\xf6\x27\x07\
+\x92\xc1\x63\x08\x02\xa2\x30\x26\x86\x52\xe0\x98\x39\x78\xaa\x3c\
+\x54\x75\x65\xad\x01\x69\x1f\x57\x56\x1c\x00\x11\xb3\xb3\xb3\x3b\
+\xe1\x62\xbf\xb9\xb2\xb2\x72\x37\x6e\x35\x37\x10\xb0\x59\xcc\xe5\
+\x72\xf9\x6b\xd3\xd3\xd3\x1f\x64\x30\x7a\xfb\xd2\x65\xf9\xf7\x1f\
+\x9e\x54\xc3\xac\x42\xb4\xa3\xe5\xb2\x1a\x28\x81\x2b\x01\xc6\x50\
+\xd3\x04\x0c\x19\x92\x66\xcc\x50\x65\x62\x5a\x62\xa3\xe6\x29\x89\
+\x08\xe8\x30\x28\x91\xc0\xc3\x3c\x15\xf1\xc0\xac\x95\xfa\xaa\x3c\
+\xf3\xc2\x49\xb5\xb1\xdd\x70\xb9\x33\x33\x33\x1f\x44\xd0\xfb\x1a\
+\x24\xf2\x50\x9a\xe9\x2e\x41\x58\x20\x00\xfd\xb1\x6d\xdb\xb7\x3f\
+\x58\x28\x14\x64\x0d\xfa\xf8\xbd\xe3\xa7\x70\xdf\x95\xc9\xea\xb8\
+\x54\xe1\xf7\x49\x48\xc6\xc7\x0e\x83\xcd\xf8\xe9\x3d\x93\x9c\xab\
+\x1b\xd5\x73\x2f\x7e\x6f\xd3\xbb\xd9\x0c\xdf\xf1\x35\x5a\x27\xef\
+\xe3\x5e\xd6\xdc\x63\xbc\xe0\x5c\x13\x98\x93\x98\xfe\xe3\xf8\x7f\
+\x03\x4b\x4b\xa0\x15\x02\x37\xfe\x20\x24\xf5\xb1\xb4\x41\xfb\xc6\
+\xdb\x90\x3b\xb9\x4a\xa5\xf2\x38\x76\xbd\xfe\xe1\xa9\x57\xd5\xb8\
+\x26\xc0\x91\x6a\x65\xd4\x70\x3e\xe6\x7a\x9c\x26\xa4\xf4\x9f\x9a\
+\x6f\x98\x40\xe6\x0c\xed\x60\x83\x06\x0d\xd5\xc7\xe4\x55\x91\xb1\
+\x87\xf8\x1a\x01\x33\x8c\xcf\x3d\x37\x52\x22\xb8\xd1\x69\x3c\x7f\
+\xf2\x65\xd8\xc4\x11\x01\x73\x65\x6d\x6d\xed\x71\xa8\xd2\x61\xe0\
+\xe8\x26\x36\xc0\x81\x40\xfd\x31\xb8\xcc\x0f\x10\xe0\xb9\x77\x2e\
+\x21\x38\x5d\x91\xca\xe8\x28\x06\x1a\x51\xae\x69\x6c\x20\xe8\x44\
+\x85\xdc\xc4\x0e\x2c\xd7\xe9\xe7\x09\x33\xf1\x44\x86\x10\x35\xdf\
+\x94\xfe\xa7\x81\x87\x06\x74\x68\x09\xc0\x18\x01\x08\xf0\x30\x36\
+\x19\xc7\x67\xe7\xa1\xca\x3f\x03\xa6\x7d\xbf\x71\x83\xc0\x33\x7e\
+\xa0\xd9\x6c\x1e\x83\x4d\xfc\x33\xe7\xf1\xcd\x60\x3e\x52\x84\x87\
+\x19\xa8\x18\xfe\x5f\x79\xfd\x67\x1a\x94\x2a\xa3\x65\xe4\x31\xb9\
+\x24\xb0\x79\xe4\xb6\xe7\x26\x52\xd0\x63\xde\x95\xb3\xe3\x67\x64\
+\xa1\xb0\x20\x47\x1a\x47\x24\xdb\xca\x4b\xd8\x0f\x63\xbd\x4f\x44\
+\x60\x88\x48\xb9\x53\x6b\xc0\x36\x4a\x07\x86\x90\x80\x4c\xe1\x31\
+\x70\xa4\x80\xf1\x2b\x70\x1c\x7d\x44\xef\x97\x4f\x9f\x95\x3d\xb0\
+\x85\xf1\x6a\x55\x96\x96\x96\x1e\x46\x7c\xf8\x17\xcc\x3f\x50\xfd\
+\x01\xb8\xc3\x48\xcc\x0e\x12\xe8\x85\xb9\x2b\x52\x5b\x5d\x83\xc1\
+\xc6\x41\x8a\xc0\x55\x7f\x3d\xa3\xab\xd4\xd3\x4c\x66\xa8\xdf\x05\
+\x5f\x16\x8b\x0b\xf2\xed\x27\x9f\x94\x2f\xf7\xfe\x4a\xde\x1c\x39\
+\x2b\xb9\x7c\x4e\x0d\x71\xe8\x97\x87\x31\x42\x89\xc6\x58\xbe\xb5\
+\x8d\x64\xcc\xd8\x3e\x68\x0b\xbe\x17\x3f\xe3\x7b\xe5\x52\x01\x58\
+\x4a\x8a\x89\x5a\x91\x03\x43\xa1\xe6\x07\x89\x59\x8d\x98\x83\x16\
+\x0b\x85\x4f\x95\x4a\x9a\xba\x43\x7d\xe6\xd4\x95\x95\x91\x61\xaa\
+\xc1\x71\x32\xaa\x48\x76\xa3\x41\xd2\x10\x15\xcc\x00\xd7\x61\x56\
+\x42\x3f\x90\xda\x33\x75\x79\xfc\xf8\x63\xf2\x37\x85\xaf\xca\xdc\
+\xd4\x9c\x4e\x26\xc3\x04\x74\x6b\xbc\x71\x63\xc9\xfa\x7e\x6a\x6c\
+\x43\x8c\xef\xc7\x73\x93\xb8\x11\x30\x92\x98\xa8\xda\xdc\xc6\xc6\
+\xc6\x04\x8e\xe6\x53\x4a\x00\xf6\x2c\xc0\xdf\xc3\x0f\x56\xe1\x79\
+\x96\xeb\x6b\x72\x03\x72\x13\x46\x5b\xcf\xf5\x8c\xe7\x00\xc7\xbd\
+\x78\x50\x0f\xc4\x38\xee\xd0\x8d\x39\x91\x13\x6b\x0b\x0d\xba\x8c\
+\xf3\x96\x23\x6f\x3e\x75\x4e\xfe\xfa\xe4\x97\xe5\x1f\x27\xfe\x41\
+\x3a\xb9\x36\xfc\x7b\x56\x9c\x5f\x96\x66\x38\x71\x41\xe3\x67\x86\
+\x5e\xcc\x12\x41\x55\x1d\x81\x36\xcc\xce\x4c\x23\xeb\x5d\x55\xef\
+\x48\x66\x13\x33\xc6\xcc\xba\xae\xe7\xdd\x82\x82\x64\x37\xc7\xb9\
+\xba\x54\x53\x4f\x33\x59\xad\xc8\x18\x08\xf0\x8d\xda\xf8\x86\x4b\
+\xd6\x63\x6d\x89\x86\xf0\xdf\x09\x16\x0f\x4e\xa1\x02\x43\x9c\x0f\
+\xe5\xf8\x3f\x9d\x90\x47\x2e\x7f\x41\x9e\x9e\xfa\x37\x89\x0a\x81\
+\x06\xaa\xd8\xf0\x1d\xf9\x45\xe4\xa8\xad\x25\xdc\x8f\xd5\x88\xf1\
+\x67\x0a\x98\xf8\xdd\x3c\x31\xe2\x1e\x08\xd8\x0d\x3c\xb7\x20\xc0\
+\xfa\xb7\x43\xd4\xbe\x75\x59\x85\x42\x5e\x39\x40\xdd\xcb\xe5\x32\
+\x9a\xeb\x90\xeb\x9b\x67\x74\x62\xfd\x53\x6f\xe1\x86\x5b\x09\x73\
+\x33\xf8\xae\xe2\x4a\xef\xad\x81\x7c\xfb\x7b\x4f\xca\x23\xf2\xe7\
+\xf2\xc2\xf4\x09\xe9\x96\x50\xe4\x78\x92\x04\xc0\x6b\x25\x80\x24\
+\x92\x92\xa0\xeb\xa6\xea\x50\x85\xc8\x40\xa8\xba\x2c\xd5\x56\xf5\
+\x1d\x30\x1d\x34\x7a\xb7\xfb\x10\xef\x7e\xaa\x08\xdd\x59\xb3\xd5\
+\x91\x12\x12\x34\xcf\x44\x59\x9e\x77\x90\xfa\x6e\x91\xb8\xe1\xa0\
+\xe6\x30\xfd\x81\x34\x36\x46\xf7\xd8\xdb\xe4\x90\xff\x03\x6c\x90\
+\xc3\xf7\x90\x4a\xfd\xf5\x55\x79\xa2\xf4\xb7\xb2\xf7\xd6\x9b\xe5\
+\xd8\xe8\xfd\xb2\x6f\xfd\x66\xc9\x37\x72\x2a\x3d\xd7\xb8\xf2\x68\
+\xe3\x24\xca\xfd\x7c\x21\xab\x04\xe1\x2d\xe0\xc9\x4b\x73\xbd\xad\
+\xef\xd2\xbe\x90\x21\xef\xf7\x11\x80\xf6\x90\xc3\xbd\x7e\xa0\x7e\
+\x7c\x04\x12\xb0\xfa\x4a\xdd\xe5\xcb\x2c\x11\x2d\x68\xfb\x8c\xae\
+\xad\x3f\xe8\x63\x70\xf8\x7f\xd9\x48\xa4\x03\xce\x85\x79\xb8\xc7\
+\x1c\x9e\x94\x90\xa4\x8d\x2c\x4a\x36\xcc\x49\x34\x88\xe4\xe7\xaf\
+\x5c\x90\xc7\x4b\x5f\x95\x43\x37\xfd\xb6\xfc\xde\xb6\x3f\x94\xe9\
+\xd6\xb4\x14\x1a\x90\x3a\xb2\x1a\x8e\x1c\xa6\x32\xd5\x9c\x89\xcc\
+\x5a\x15\xe2\x29\x41\x33\x63\xa5\xab\x27\xd3\xd9\x3c\x60\x7d\x3b\
+\x4a\x85\x88\x23\xa0\xab\x29\x80\x0d\x3f\xdc\x8a\xf8\x88\xb5\xc4\
+\x20\x18\x28\xa7\x02\x93\x39\xc6\x41\x0b\xfa\x0f\x75\xf0\xa2\xd8\
+\x65\xfa\x3d\x54\x6a\xb9\x40\xc2\x4c\x20\xdd\x62\x5b\xda\xa3\x0d\
+\x69\x95\x1b\xe2\x64\x3d\x19\x78\x03\x89\xfa\x91\x2c\xe7\x2e\xcb\
+\xe4\xe2\x4e\x79\xfd\xb9\xd3\x72\x66\xc7\xeb\xf2\xe1\x03\x77\xc9\
+\xbd\x99\x8f\x4b\xa9\x5f\x04\x21\x05\x10\xe2\xe9\x1c\x54\xe3\x02\
+\xdc\xb1\x25\x88\xa0\xe8\x62\x89\x61\x10\x06\x31\xd6\x4c\x66\x94\
+\xa9\x44\xd1\x72\x34\xce\x89\xac\x3e\x3b\x89\x28\x69\x0f\x2c\xf7\
+\x5a\xa8\xa0\xa8\x32\xe4\x8e\x63\xfd\xbb\xeb\x6c\xca\x3c\x31\x56\
+\xae\x17\xdf\x62\xad\xe0\xa3\xb6\x2d\x76\x35\x6d\x8c\x20\x28\xbf\
+\x99\x55\xa2\x80\x52\xb2\x97\x0a\xf2\x5f\x6f\xfc\x48\x4e\x1e\x38\
+\x21\xf7\xdc\xf6\x71\xb9\xcb\xfd\xa8\x8c\x2d\xc1\x45\xe6\xb3\x5a\
+\x24\xc9\xa6\x22\x46\x55\x09\x63\xf6\xa1\x2d\x99\x1c\x1d\x82\x5b\
+\xdc\x60\x7d\x7e\xaa\x8e\x4d\x23\xe2\x87\x45\xa3\x5a\x14\x9f\xaa\
+\x92\xeb\x6c\x79\x37\x00\xe7\x23\xeb\x90\xf0\xbc\xef\xb5\xf5\xa8\
+\xce\xda\x8d\xf3\xa4\xb4\x33\xb0\x92\xe3\x96\xe9\x43\x89\x6a\x59\
+\x7d\x87\xe0\x39\xfe\x96\x1a\x8c\xa9\x83\x62\x94\xe4\x19\x53\x89\
+\x16\x4f\xb2\x99\xd8\x65\x6e\x60\xa5\x05\x06\xe9\x50\xc5\xc6\x11\
+\xd6\xa9\x97\x0d\x14\x19\x2a\x85\x6b\xf9\x76\xdc\xf7\x06\x48\x8f\
+\xa1\x72\x11\xb4\x2c\xd3\x05\x28\x10\xd5\xcf\x76\x25\xd3\x81\x1d\
+\x80\x01\x99\x0e\xc0\xae\xe4\x64\x7d\x77\x5d\x7e\x7f\xf7\x31\x39\
+\x2a\xbf\x2b\x63\x57\x47\x25\xef\xe7\xa0\xb2\x79\x2d\xfe\xbd\xc8\
+\x78\xa8\x28\x19\x36\x26\xd4\x64\xb1\x26\x25\x69\xf9\xec\x98\x29\
+\x37\x10\xb4\xb8\xdb\x2a\x29\x66\x5b\x6c\x54\xaa\x5e\x34\x62\xd6\
+\x0b\x70\x69\x14\x71\x63\x3d\x56\x29\xe6\x34\x96\x0c\x66\x26\xa1\
+\x17\xe8\xd1\xef\x21\x97\x6a\x4c\xc9\xaa\xb7\x28\x51\x27\xd4\xeb\
+\x08\xa2\xcf\xd7\xca\xe2\x00\xc4\xa1\x3b\x3f\x24\xc7\x6a\xf7\x4b\
+\xa5\x36\x0e\x2d\x83\x9a\xc2\x45\xd2\x75\x0e\x06\xa1\xf6\x8c\xf0\
+\xaa\x5e\xa7\x19\x4a\x5c\xb1\x7b\x85\x9d\xc0\x81\x10\x3b\x49\x39\
+\xaf\xad\x13\xcf\x55\x09\xd0\xe3\x00\x93\xb8\x26\xe1\xea\x07\x83\
+\x44\x7c\x36\x93\x24\x17\xa6\xaa\x39\xed\xfd\x74\x31\x53\xd6\xf5\
+\x13\x15\x52\x2d\xe9\xc2\x96\x7c\x80\x76\x32\x32\x16\x4d\x49\xab\
+\xb0\x26\x5e\x0b\xee\x10\x69\xc5\x4d\xfb\x0e\xc8\xe7\x2e\x3f\x28\
+\x33\xad\x1d\xe2\x97\x3c\x29\xb0\x93\x01\x8e\x06\x51\x68\xfa\x48\
+\x91\xea\x19\xb3\x53\x76\x30\x7c\x93\x53\xd9\xee\x1e\x3d\x23\xed\
+\xa0\xdb\x51\x5b\x3c\xef\xf7\x7a\xbd\x33\xa0\x44\x5d\x54\xa9\x00\
+\xb1\xb6\x3b\xc8\x0c\x21\x66\x1a\x0b\x53\x5b\x8a\xd3\xf5\x62\x11\
+\x3a\x51\xd2\xec\x22\x50\x66\xaa\x95\xb1\x9c\xec\xa9\xef\xd1\xe7\
+\x69\x8d\xf2\x5a\x19\x55\x17\x67\x15\x91\x14\x93\x4e\xdd\x31\x25\
+\x7f\xd2\xff\x53\xb9\xad\x77\xab\x64\x67\xe2\xd6\xa3\xb6\x6e\x82\
+\xb8\xdd\x12\x4b\xde\x64\xab\x71\xad\x86\xfb\xac\xda\x02\x0d\xa6\
+\x81\x12\x10\x2a\x46\x4e\xd3\xeb\x76\x49\xe0\x19\x1f\x1f\xff\x04\
+\x44\x0c\x18\x8d\x99\xf5\x5d\x5d\xae\x2b\xe8\x50\xc5\x39\x30\x06\
+\x13\xa5\x72\x79\x31\x35\x6d\x64\x6c\x03\x46\xee\x39\xb2\xc5\x1c\
+\x60\x59\x21\x82\xe6\xe8\x87\x4b\xf2\xf0\xc4\x5f\xc8\xe1\xd1\x43\
+\x0a\x98\x63\xc6\x75\xb0\x4d\xa3\x83\xa4\xd4\x4c\x82\x59\x34\x34\
+\x41\xbe\xef\x42\x74\x81\x21\x92\x18\xb9\xa1\xbc\x04\xf4\xe0\x27\
+\x3e\x0a\x88\x37\xe0\x22\xdf\x46\x3d\xb0\xaf\x82\x82\x9d\x56\xde\
+\x87\xab\x24\x87\x02\x13\x1b\x92\xea\x49\xed\x80\x13\xbb\x0a\x58\
+\xf9\x13\xc4\xc5\x79\xa2\xa9\x70\x8f\xce\x1a\xe2\xe6\xfe\x48\x3e\
+\x73\xfb\x67\xe4\x01\xff\x3e\x66\x8e\xda\x3e\xec\xf6\x7a\xc3\xb1\
+\x0c\x47\xc3\x54\x97\x82\xf0\xe3\xeb\x30\x21\x86\x18\xc0\x5f\xc5\
+\x44\x49\xb0\x46\xe1\xbb\xeb\xeb\xeb\x6f\x83\x01\x6f\xb0\x22\xeb\
+\x81\x80\xef\xe3\x62\x5f\x11\xe2\x61\x1d\xc0\x66\x93\xab\xdd\xb5\
+\xd8\x95\x25\xdc\x31\x44\xb8\x0a\x80\xee\x31\x56\xa7\x3e\xd2\x05\
+\x12\xe1\x36\xa0\x6a\x3b\x06\xf2\xd1\xa3\x77\xcb\xe7\xe4\x8f\x65\
+\xb2\x5c\x55\xd0\xad\x76\x7b\x23\x97\xad\x04\x2c\xd0\x54\x7b\x25\
+\x32\x13\x46\x32\xec\x1f\xf5\x20\x85\x1e\x8c\x96\xdc\x2f\x22\xb8\
+\xb1\xd5\x82\xfd\xfb\x7c\xe4\xf3\x95\x76\xab\xf5\x64\xa7\xdd\x7e\
+\xa8\x88\x34\x75\xfb\xc4\xb8\x76\xe0\x54\x35\x68\xb8\xac\x57\x99\
+\x85\x46\x71\x07\x21\xa2\x1d\x38\x92\x02\x14\x4a\x3b\x6c\x8b\xdb\
+\xf7\x64\xd7\x1f\xbc\x4f\x1e\x6d\xff\xa5\xec\xaa\xcc\x6a\xe7\x6d\
+\xbd\xd5\x4a\x7b\xd7\x0d\xdc\xb7\xe0\x43\x5b\x99\xa5\x25\x61\x54\
+\xd4\xce\x61\xab\xb6\xed\x53\xe3\x3a\x56\xb3\xd1\x60\x60\x7d\x52\
+\x4b\x4a\x27\xd6\xb3\x93\x28\x96\x7f\x0c\x02\x0e\x4e\x8c\x8f\x4a\
+\xe9\x6a\x5e\xbd\x91\xeb\xe6\x54\x19\x1d\x1d\x9c\xc5\xbb\x29\x05\
+\xd5\xc3\xc2\x63\x84\xae\x8a\xf6\x86\xce\x4e\xf9\xb3\x4f\x7e\x41\
+\x3e\xe2\xdd\x25\xce\x98\x63\x1a\xbd\x71\xde\x34\x0c\xa6\x51\xe2\
+\xa2\x43\x19\x72\xdd\x12\x13\x5a\x06\x59\x42\x4c\xe1\x1f\x9a\x36\
+\x64\x09\x81\x74\x12\x85\x3e\x6d\xa2\x5e\xaf\xff\x98\x9d\x3a\xd3\
+\x51\x51\x6f\x30\x68\x34\x9b\x5f\xef\x76\x3a\xaa\xf3\xb3\xd3\xdb\
+\xb4\x8b\xac\xab\x30\x26\x16\x58\x8e\x84\x91\x2d\xc4\x63\xf5\x61\
+\x6a\xb1\x33\x3b\x2b\x47\x9c\xdf\x51\x75\xe1\x02\xc6\x40\xbd\x57\
+\x68\x8e\xf1\xae\xe7\xa6\x78\x4f\x7f\xaf\xbf\x48\x0c\xe0\x14\xf7\
+\xcd\xbc\xfa\x3d\xb0\xcc\x4e\x4f\x69\x46\x00\x46\x0b\x8a\xfa\xaf\
+\xc3\xbd\x0e\xb4\xc1\x66\x39\xd5\xef\xf5\xbe\x03\xca\x5e\xe5\xf5\
+\x54\x75\x4c\x26\x2a\x23\x30\x9e\xde\x90\x6b\x61\x68\x38\x63\xc5\
+\x6c\x38\x29\xf4\xdf\x03\x78\x85\x9e\xf1\x2c\xc6\x40\x93\x62\x3d\
+\x34\xe7\xa6\x68\x0f\x62\xee\xc6\x5c\x37\x52\x49\xae\x25\x69\xb1\
+\xd8\x67\x8c\x05\xc4\xc2\x82\x86\xdc\x5f\x5e\x5a\x7a\x15\xf7\xbe\
+\x63\xb3\x00\xd7\xea\x17\x00\x75\x57\xea\xf5\x47\x5a\xeb\xeb\xfa\
+\x80\x1d\x00\x56\x50\x1c\xc0\x12\xa1\x60\x6c\x47\x81\xdf\x05\x31\
+\xa0\x40\x27\x0b\x0c\xc0\x30\x69\x97\xf0\x59\x42\x90\x25\x2e\x0a\
+\x13\xe2\x62\xf0\xc6\x1e\x42\x89\xc7\xd1\x3d\x0e\x5c\x04\xcc\xaa\
+\x8c\x58\xb8\xd5\x96\x97\xa9\x3e\x8f\x10\xab\xc5\xed\xa6\xbb\xc6\
+\x83\x7e\xff\xb9\xc5\xc5\xc5\xbf\xe7\x87\x5c\x16\xda\x8b\x0f\xc9\
+\x8d\xbe\xfa\xee\x61\x3f\x27\xd8\x60\x70\x86\xd3\x86\xc0\x30\xb2\
+\xe0\x02\xc3\xd5\xcd\xe7\x69\xc3\x35\xcc\x23\x81\x51\x68\x24\x11\
+\x25\xe0\x39\xf6\xde\xf7\xed\x54\x2c\x70\x9b\x5c\xfc\x20\xb6\xe7\
+\x44\x86\x0e\xc1\x4d\xf7\x34\x69\x0f\x78\xf1\x8b\xcb\xcb\xcb\xaf\
+\xf0\xe1\xf8\xd8\x88\x52\x4f\x15\x51\x49\x18\xe3\x4a\xf7\x72\x6c\
+\x3f\xc7\x4e\x6e\xef\x05\x46\x8d\x82\xd4\x7b\x09\xe7\xa3\xa1\x04\
+\x12\x86\x6c\x02\xcf\x39\x39\x77\x75\x2c\x56\xe5\xf9\x2b\x57\x5e\
+\x81\xeb\xfc\xa2\x9b\x74\x00\xe3\xdd\xbf\xc6\xc2\x41\x13\x7a\xf6\
+\x69\x64\x7c\xcf\x56\x27\x26\x76\xd2\xad\x52\xdb\xce\x5f\xba\xa2\
+\x79\x38\x93\x29\xd7\x25\xf5\x26\x5b\x34\x75\x6d\xa4\xa7\xf1\x58\
+\x49\xc9\xee\x24\x0e\x28\x69\x6c\x49\x2a\xe2\x0e\x5d\xa5\x95\x28\
+\xb5\x60\xa0\x04\x92\xf3\x9c\x9b\x46\x3c\x3f\x3f\x3f\x57\xab\xd5\
+\x3e\x4d\x6c\x9b\x33\xe0\x6b\x2e\xb3\x62\xc0\xb3\x4b\x4b\x4b\xf7\
+\xa3\xdc\xfc\xee\xf8\xf8\x78\x75\x1b\x06\x62\x0a\x7b\xfe\xe2\xbc\
+\x2e\x95\xfa\x19\x16\x13\x16\x23\x39\x98\xea\x4e\x4b\x2a\x27\x8a\
+\x36\x8c\x99\x1c\x6d\x90\x4a\x62\x09\x5d\x41\x10\xab\x6a\xd6\xcf\
+\xc8\xee\xd9\x9d\xca\x79\x05\x7f\xe5\x4a\x0d\x0c\xbd\x9f\x98\xae\
+\x85\xd5\x39\xfd\xda\x6b\x5b\x6f\x6a\x0b\x91\x9c\x76\x0f\x4f\x4e\
+\x4c\x3c\x35\x5e\xad\xce\xf0\x1e\x97\x4d\xdf\x99\x9b\x97\xfa\x5a\
+\x53\x9b\xb9\xba\x90\xe7\x3a\x5b\xea\x65\x67\x38\xd0\x30\x8a\xa5\
+\xe8\x49\x47\x65\x55\x9b\x41\x6c\x23\x4c\x13\x76\xed\x9c\x56\x9d\
+\xa7\xca\x5e\x9d\x9f\xbf\x0c\x46\xea\x12\x53\x7a\x61\xfd\xba\x08\
+\xb0\xad\x41\xe4\x4a\xfb\xab\xd5\xea\x37\x26\x26\x27\x0f\xd9\xd4\
+\x96\x0d\xa6\x2b\x0b\xcb\x5a\x0f\x38\xa6\x5f\xca\x52\x34\xee\x4c\
+\x1b\xe0\xd1\x46\x15\x12\x67\x93\x14\xac\xed\xe0\x9c\xe9\xc1\x8e\
+\x6d\x13\x32\x39\x1e\x77\xa4\xb9\xb4\x34\x7f\xf5\xea\x8b\x2b\xb5\
+\xda\x67\x31\xfe\x19\x9b\xd4\xbd\x6b\x02\xb4\x84\x64\x36\xe8\x79\
+\xe5\x42\x3e\xff\x18\x88\x78\xa8\x54\x2a\x29\x2c\x4e\x5e\x5b\x6d\
+\x28\x31\x6c\xc3\x6b\x2e\xef\x0c\x97\x9b\x92\xa2\x79\x68\x04\x89\
+\xce\x6b\xd5\x06\xa2\xb9\xc6\xc6\xe8\xca\x0e\xb8\x5d\x4c\x87\x9b\
+\xd4\x65\x56\x24\x7f\x8f\x82\xeb\x4d\xfb\x17\x84\xf7\x4c\x40\x68\
+\x96\x7f\xe8\x41\x00\xec\xde\xd1\xb1\xb1\xaf\x54\x2a\x95\x3b\xf2\
+\x2c\xba\xcd\xc6\xe5\xd2\x35\xae\x36\x82\x10\x9e\xb3\x82\xb3\x6b\
+\x61\xdc\xac\xb1\xb3\xe3\xc1\x4e\x03\x81\x8f\xe8\xea\x66\x2e\x91\
+\x08\xa2\x2b\xfd\xfc\x4b\x88\xb4\x5f\x02\x70\x5d\xe8\x8e\x53\xe9\
+\x5f\x13\x01\xd6\x85\xf2\xaf\x06\xa8\xc8\x1e\x40\xde\xf4\x79\xa4\
+\xe0\x77\x92\x10\x3e\xb7\x5b\xba\xef\xcf\x49\xb9\xc5\x6d\xc9\xd8\
+\x4d\xa7\x3b\x71\xd4\x73\x66\x96\x6b\xab\xab\xa7\x90\xca\x3c\x81\
+\x38\xf4\x2d\xfb\x9f\x09\xce\xfd\x6b\x27\x40\xcc\xbf\x55\xb8\x41\
+\x22\x4c\xc5\x8f\x14\xf8\x67\x8f\x42\xe1\x68\xbe\x50\xb8\x31\x9b\
+\xcd\x7a\xb6\x21\xbb\xb9\xc2\xb1\x4b\xaa\xf1\x42\x77\x37\x80\x9e\
+\xeb\x9f\x3d\x90\x55\xea\x9f\x3d\x30\xd6\xc0\xaa\xa6\x6d\x18\x5c\
+\x0f\x01\xef\xe9\xdf\x2a\xc6\x65\x72\xc4\xe7\xdb\x9d\xce\xf3\x08\
+\x7e\x44\xfe\x7e\x70\x57\xff\x6e\x23\xfc\xbb\x8d\xeb\x8e\xc2\x53\
+\xc5\x7f\xb7\x09\x82\x75\x80\x5f\x0b\xcd\xdf\x6d\x20\xc9\xe4\xef\
+\x36\xa9\xf1\xae\x09\xf0\xff\xdb\xfe\x0f\x0f\x1d\x3a\xca\xed\x25\
+\x35\x52\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x11\x31\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
+\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
+\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
+\x46\x00\x00\x10\xb7\x49\x44\x41\x54\x78\xda\xac\x9a\x59\xac\x24\
+\x67\x75\xc7\x7f\xdf\x52\x55\x5d\xdd\xd5\xcb\xed\xbe\xcb\xcc\x1d\
+\x5f\x3c\x9e\x19\x33\x8b\x8d\xc7\x46\x63\x4f\x2c\x61\x1c\x98\x18\
+\x29\x12\x12\x11\x32\x02\x21\x45\x3c\xe4\x09\x4b\x28\x12\x42\x88\
+\x07\x1e\x79\x40\x08\xf1\x82\x04\xcf\x48\x89\x81\xe0\x38\x02\x13\
+\x02\xd8\x86\x60\xa2\x4c\x6c\x30\x90\xd8\xe0\x19\x33\x9e\xf1\x30\
+\xfb\xdc\xbd\x6f\xef\xf5\x2d\x79\xa8\xe5\xf6\x9d\x05\x1b\x44\x4b\
+\xad\xae\xee\xae\xfa\xbe\xb3\xfc\xcf\x39\xff\x73\xaa\xc4\x2f\x5e\
+\x7a\x89\x1b\x5f\x42\x08\x94\x52\x78\xef\x71\xce\x21\xa5\xc4\x39\
+\x87\xf7\x1e\x29\x25\x42\x08\xac\xb5\x78\xef\x01\x70\xce\x85\x4a\
+\xa9\xc3\xc0\xd1\x30\x0c\x8f\x08\x21\xf6\x29\xa5\x1a\x42\x88\x5a\
+\xfe\x7f\xdf\x18\xd3\xf5\xde\x9f\x1d\x8f\xc7\xbf\x03\xfe\xd7\x18\
+\xf3\x9a\x94\x72\x52\xac\x21\xa5\x2c\xf7\x2b\x64\xb0\xd6\x22\x84\
+\x28\xf6\xe0\x56\x2f\xcd\x9f\xf1\xf2\xde\xe3\xbd\xd7\xc0\x23\x95\
+\x4a\xe5\xf1\xb8\x5a\x3d\x51\xad\x56\x0f\xc4\x95\x8a\x0a\xa3\x88\
+\x20\x08\x50\x52\x22\x64\xb1\xb9\xc7\x5a\x4b\x3a\x99\x30\x1a\x8f\
+\x19\x0e\x06\xb6\xdf\xef\x9f\x19\x0c\x06\xcf\x8f\x46\xa3\xa7\xbc\
+\xf7\x3f\xf7\xde\x9b\x3f\x47\x16\xfd\xa7\x0a\xee\x9c\xd3\x5a\xeb\
+\x8f\x26\x49\xf2\xa9\x66\xab\x75\xbc\x5e\xaf\x53\xad\x56\x77\x9c\
+\x37\x9e\xa4\x0c\xc7\x13\x46\xe3\x09\x00\x95\x28\x24\x0c\x02\x92\
+\x7a\x9d\xa4\x5e\x07\x50\xde\xb9\x83\x5b\xbd\xde\xc1\xee\xe6\xe6\
+\x13\x1b\x1b\x1b\x2f\xf6\xfb\xfd\xaf\x1a\x63\xbe\xed\xbd\x37\x85\
+\xd5\xff\xa2\x0a\x78\xef\x51\x4a\x7d\xa0\x5e\xaf\x7f\xa1\xd3\xe9\
+\x3c\xd8\x6c\x35\x51\x2a\xbb\x7c\x65\x7d\x83\x73\x17\x2e\x73\xfe\
+\xc2\x65\xae\xaf\xae\xb1\xb9\xd5\x63\x92\xa6\x58\x9b\xb9\x5d\x49\
+\x49\x18\x06\x34\xeb\x09\xf3\xb3\x6d\xf6\xde\xb1\xc8\xde\xa5\x45\
+\x66\x67\x5a\x34\x1a\x0d\xe6\xe6\xe7\x8f\xaf\xad\xad\x1d\x5f\x59\
+\x5e\xfe\xc7\xad\xad\xad\xcf\x3b\xe7\x7e\xfc\x76\x95\x10\x6f\x15\
+\x03\xd6\x5a\xa4\x94\x49\x18\x86\x5f\x9c\x9f\x9f\x7f\xa2\xdd\xe9\
+\x08\xad\x35\xce\x79\x5e\x3d\x7d\x86\x97\x5f\x7d\x8d\x73\x17\x2e\
+\xd1\x1f\x0c\x01\x81\xd6\x8a\x40\x29\x94\x52\x3b\xf0\xeb\x9c\x23\
+\xb5\x16\x63\x2c\xe0\x49\xaa\x31\x7b\xef\xd8\xc3\xb1\xfb\x8e\x70\
+\xcf\xc1\xfd\x48\x21\x18\x0e\x87\x5c\xbf\x7e\xdd\x5f\xbf\x76\xed\
+\x6b\x93\xc9\xe4\x73\xce\xb9\xde\x5b\xc5\xc0\x1f\x55\x20\xdf\xf8\
+\x50\x6b\x66\xe6\x1b\xbb\x77\xed\x7a\xa8\x96\x24\x00\xbc\x72\xfa\
+\x0d\x7e\xf6\xe2\xcb\x5c\xb8\x74\x15\x04\xc4\x51\x44\x5c\xa9\x10\
+\xe5\x50\x51\x4a\x66\x31\x50\x6c\x9e\x07\xa7\xb1\x96\x34\x4d\x19\
+\x8f\x53\x06\xa3\x11\xc3\xd1\x18\x80\xa5\xc5\x05\xfe\xfa\xaf\x8e\
+\xf1\xae\x83\xfb\x01\x58\x5d\x59\xe1\xd2\xe5\xcb\x2f\x6d\x6e\x6c\
+\x7c\x02\x38\x25\x84\xf8\xd3\x15\x90\x52\x02\x3c\xdc\xe9\x74\x9e\
+\xda\xb5\x7b\xf7\x62\x18\x86\x6c\x6c\xf5\xf8\xc1\x4f\xfe\x8b\x57\
+\x4e\xbf\x81\x14\x82\xa4\x56\x25\xa9\x55\xa9\x56\x2a\xe8\x40\x23\
+\xdf\xc2\xed\x45\xc6\xf1\xde\x93\x1a\xc3\x70\x34\x62\xab\x3f\xa0\
+\xd7\x1b\xe0\xf1\xdc\xfb\xce\xfd\x7c\xf0\xc4\x23\x34\xeb\x09\x5b\
+\x5b\x5b\x5c\xba\x78\xf1\xf2\xea\xea\xea\xe3\xce\xb9\x93\xc5\xb5\
+\x6f\x2b\x06\xbc\xf7\x08\x21\x1e\x9e\x9b\x9b\xfb\xfe\xae\xdd\xbb\
+\xdb\x4a\x29\xce\xbc\x79\x91\xef\x3e\xf7\x33\x56\xd7\x37\x69\xd4\
+\x13\x1a\xf5\x84\xa4\x1a\xa3\x95\x04\x44\xa9\x78\x61\xf5\x1b\x31\
+\x9c\x67\xae\xf2\x58\x6b\x4d\x25\x8a\x68\x24\x09\xbd\xfa\x90\xcd\
+\xad\x1e\xaf\xbe\x7e\x96\x2b\xcb\xab\xfc\xdd\x63\x8f\x72\x60\xef\
+\x12\x77\xde\x79\xe7\xa2\x90\xf2\xfb\xcb\xd7\xaf\x7f\xd0\x39\x77\
+\xf2\x56\x71\x71\x3b\x0f\x1c\x5c\x58\x58\x78\x7e\xf7\xe2\xe2\x1e\
+\xa5\x14\xff\x77\xea\x0c\xcf\x3c\xf7\x02\xd6\x7b\x9a\xf5\x84\x56\
+\xbd\x4e\x18\x6a\x40\x22\x65\x26\xb4\x14\x02\x21\x05\x82\x9b\x15\
+\xf0\x99\xd4\xe5\xa7\xf3\x1e\xef\x1d\xce\xf9\x52\xb1\xc9\x24\x65\
+\x63\x6b\x8b\xcd\x6e\x0f\x29\x05\x1f\x7a\xec\x51\xee\x3b\x74\x80\
+\xc1\x60\xc0\xf9\x37\xdf\xbc\xb4\xbc\xbc\x7c\x02\x38\x7d\x93\x07\
+\x6e\xa1\x55\xd2\xee\x74\x9e\x5c\xd8\xb5\x6b\x8f\x52\x8a\x57\x5f\
+\x7f\x83\x67\x9e\xff\x39\x52\x6b\x66\x1b\x75\x1a\xf5\x04\xa5\x14\
+\x52\x0a\xa4\x90\x08\x29\x33\xe1\x45\xa1\x48\x69\x85\x69\xf3\x97\
+\x1f\x1e\x8f\xf7\xe0\xbc\xc3\xbb\x4c\x19\x6b\x2d\x4a\x29\xa2\x30\
+\xa0\x12\x86\xac\x77\xb7\xf8\xee\x73\x2f\x20\x80\x77\x1d\x3a\xc0\
+\xd2\xd2\xd2\x1e\x63\xcc\x93\xeb\xeb\xeb\x8f\x02\xbd\xdb\x42\xc8\
+\x7b\x4f\x92\x24\x5f\xda\xb5\x6b\xd7\xbb\x83\x20\xe0\xdc\xc5\xcb\
+\xfc\xc7\x7f\x9e\x24\x0c\x02\xda\x33\x4d\x1a\x49\x82\x52\x12\x29\
+\x32\xcb\x17\x55\x79\x5a\x81\x6d\x83\x4c\x1b\xc6\x4f\xc1\x28\xfb\
+\xee\x9c\xc3\x03\xde\x79\xac\x52\x38\xef\x70\x56\xd1\x9e\x69\xa1\
+\xb4\x66\x7d\x63\x93\x1f\xfc\xec\x24\x49\xad\xca\x5d\x4b\x8b\x2c\
+\x2e\x2e\xbe\x7b\x34\x1a\x7d\x69\x30\x18\x3c\x31\x6d\x74\x29\xa5\
+\x2c\x05\x09\x82\xe0\xfd\xf3\x0b\x0b\x9f\x8c\xe3\x98\x6e\xaf\xcf\
+\x8f\x5e\x78\x11\x21\x24\xb3\xed\x19\xda\xad\x26\x61\x10\x10\xe8\
+\x80\x20\xd0\x04\x7a\xfa\x1d\x94\xc7\x5a\xab\xfc\x58\x65\xe7\xdd\
+\x70\x6e\x18\x68\xb4\xd6\x04\x41\xb0\x7d\x7e\xa0\x09\xf3\xdf\xc2\
+\x30\xa0\xdd\x6a\xd2\x69\xcf\x20\x84\xe0\x87\x2f\xfc\x0f\xdd\xde\
+\x80\x76\xa7\xc3\xfc\xfc\xfc\x27\x95\x52\xef\x9f\x0e\x68\x9d\x67\
+\x1b\xbc\xf7\x51\xab\xd5\xfa\x72\xab\xd5\x02\xe0\x3f\x5f\xfc\x35\
+\xbd\xc1\x90\xce\x4c\x8b\x76\xab\x91\x5b\x3e\xb3\x7a\x46\x13\xa6\
+\xf0\x8f\x40\xe4\x46\x10\x82\xa9\x38\xd8\x81\xa0\x6d\xf8\xe4\xbc\
+\xca\xe7\xf1\x90\x7d\x77\x48\x97\x1d\x2b\xe9\x69\xb7\x9a\x59\x4a\
+\x5d\xdf\xe4\xa7\x27\x5f\xe6\x43\x8f\x3d\xc2\xfc\xc2\x02\xdd\x6e\
+\xf7\xcb\xeb\xeb\xeb\x0f\x4b\x29\xc7\x65\x0c\x78\xef\x09\x82\xe0\
+\xc3\x9d\x4e\xe7\x01\x29\x25\x67\xce\x5f\xe4\xdc\x85\x2b\xb4\x1a\
+\x0d\xda\xad\x3a\x61\xa0\xb3\xda\x20\x25\xb2\x84\x90\x2c\xe3\xa0\
+\xb0\xba\xb1\x59\x91\x2a\x33\x51\xae\x88\xc7\xc3\x14\xfe\xa7\x05\
+\x77\xb9\xd0\xae\x50\xc0\x5a\xac\xf4\x28\x29\x68\xb7\x1a\x38\xe7\
+\x38\x7b\xf1\x32\xbf\x3f\x7f\x91\xbb\xef\xbc\x83\xb9\xb9\xb9\x07\
+\x7a\xbd\xde\x87\x8d\x31\xdf\x14\x42\x20\xf3\xc5\x74\xbd\x5e\xff\
+\x74\x2d\x49\xb0\xd6\xf1\xab\xdf\xfe\x9e\x28\x0a\x69\x35\x12\x2a\
+\x51\x84\x52\xaa\x74\x79\xa0\x33\x58\x84\x81\x26\x0c\x02\xe2\x4a\
+\xc4\x66\x77\x93\x37\xff\x70\x9e\xb8\x52\x21\x8e\xe3\x12\x52\x3a\
+\xc8\xde\x81\x0e\xb2\xcf\x20\x28\x61\x15\xea\xec\xfa\x30\x5f\x6b\
+\xfa\x9a\x40\x67\x95\x3c\x8e\x22\x5a\x8d\x3a\x51\x18\xf2\xf2\xab\
+\xa7\xb1\xce\x31\xd3\x6e\x53\xaf\xd7\x3f\xed\x9c\xd3\xde\x7b\x24\
+\x80\x52\xea\xe1\x66\xab\x75\x4c\x08\xc1\x9b\x97\xae\xb0\xb6\xd9\
+\xa5\x91\x64\x45\x4a\xa9\x1c\xd3\x2a\xc7\x6a\x10\x64\xb1\x90\x0b\
+\x12\x45\x21\xfd\x7e\x9f\x7f\x7d\xfa\xdf\xf8\xee\x33\xcf\xb0\xbc\
+\xbc\x4c\x54\x89\x50\x7a\x2a\x3f\x88\xed\x1a\x21\xa5\x44\x29\x95\
+\x0b\xaa\xa7\xd6\xcc\x8d\xa2\x35\x5a\x65\xff\x29\xa5\x48\x6a\x31\
+\x8d\xa4\xc6\xda\x66\x97\x73\x17\xae\x10\x45\x11\xad\x56\xeb\x98\
+\x52\xea\x61\x00\x29\x84\xa0\x1a\xc7\x1f\xab\xd5\x6a\x00\x9c\x39\
+\x7f\x89\x4a\x18\x92\x54\xab\x59\xc0\x29\x95\x41\x24\xdc\x19\x90\
+\x5a\xeb\xac\x5a\xe7\xb4\x43\x08\xc1\x2b\xbf\xfd\x2d\xff\xf4\xe4\
+\x37\x79\xf6\xb9\xe7\xe9\xf5\xfa\x44\x51\x54\x54\xf4\x5b\xd3\x00\
+\x99\x5d\xab\xf5\xd4\xda\xb9\x32\x5a\x67\x7b\x87\x41\x40\xbd\x56\
+\xa5\x12\x86\x9c\x39\x7f\x11\x80\x66\xb3\x49\x1c\xc7\x1f\x2b\xd2\
+\x68\x58\xab\xd5\x1e\xd3\x5a\xb3\xd9\xeb\xb3\xba\xd1\xe5\x8e\xc5\
+\x85\x0c\xef\x42\x94\x0a\x94\xf8\xbf\x8d\x40\x42\x08\xa2\x30\x24\
+\x4d\x53\xfe\xfb\xe4\x49\x5e\x3b\x75\x8a\xe3\x0f\x3d\xc8\xd1\xfb\
+\xee\xa3\x12\x45\x4c\xd2\x94\xdb\xd1\x01\x04\x65\x5c\x09\x6b\xf3\
+\x64\xe1\x48\x85\xc0\x5a\x43\x3d\xa9\x52\xad\xc6\x5c\x5b\x5e\xa5\
+\xdb\xeb\x93\xd4\x6a\xd4\x6a\xb5\xc7\x86\xc3\x61\x28\xa5\x52\x47\
+\xe2\x6a\xf5\x2e\x80\x6b\x2b\x6b\x48\x21\x98\x6d\xb7\x68\x26\x35\
+\x74\x0e\x1b\x9d\x5b\xe9\xb6\xc2\x4f\x1d\x4b\x29\x89\xa2\x88\xad\
+\xad\x2d\x7e\xf8\xa3\x67\xf9\xe7\x27\xbf\xc9\x6b\xa7\x4e\xa3\xa4\
+\x24\x0a\x83\x3c\xf0\x05\xb7\x63\x4d\x4a\x29\x54\x69\xfd\x0c\x46\
+\x8d\x24\x61\xae\xdd\x42\x0a\xc1\xd5\x95\x35\xa4\x52\xd4\x6a\xb5\
+\xbb\xa4\x94\x47\xa4\xd6\xfa\xfe\x28\x8a\x74\x91\xb2\xe2\xb8\x42\
+\xa0\x35\x49\x2d\x26\x8a\x02\xb4\x92\x28\xad\xb8\x71\x47\x91\xe1\
+\x0f\x25\x05\xb7\xe2\x28\x4a\x29\xe2\xb8\xc2\xf2\xca\x2a\xdf\xfb\
+\xf7\x1f\xf0\xf4\xf7\x9e\xe1\xdc\xf9\x0b\x59\x6e\x9a\xa2\x1f\xb7\
+\x22\x80\x52\x66\xb4\x5c\x29\x49\x25\x0c\xa9\xd7\xaa\x68\xad\xa9\
+\xc6\x31\x2b\x6b\x9b\x00\xc4\xd5\xaa\x56\x4a\xdd\xaf\xa3\x30\x3c\
+\x14\x06\x01\xce\x79\x7a\x83\x11\xb5\x6a\x8c\xca\xab\x6c\xad\x1a\
+\x33\x1a\xa7\xb7\x84\x8b\xc8\x2b\xeb\x24\x35\x4c\x26\xe9\x6d\x2c\
+\x19\x64\xf1\x21\x25\x17\x2e\x5e\xe2\xf2\x95\x6b\x1c\xd8\x77\x17\
+\x47\xef\x7b\x17\xb3\x9d\x4e\x26\xac\xc8\x0c\xe1\x0b\xae\xb4\xbd\
+\x09\x4a\x29\x2a\x71\x88\x94\x02\x85\xa4\x56\xad\xd0\xeb\x0f\xf1\
+\xde\x13\x45\x11\x61\x18\x1e\xd2\x42\xca\x7d\x4a\x2b\x26\xa9\xc5\
+\x58\x4b\x3d\xae\x94\x16\x8d\xc2\x10\xef\x3d\xe3\x49\x5a\x0a\x5d\
+\xfc\x97\x1a\x4b\x6a\x52\xa4\xd4\xd8\x1b\xb8\xba\x94\x12\xa5\x75\
+\x06\x85\x20\x40\x4a\x4d\x18\x81\xb3\x8e\xd7\xcf\xbc\xc1\x9b\x7f\
+\xf8\x03\x87\x0f\xbe\x93\x7b\xef\xb9\x87\x7a\x92\x80\xf0\x68\x29\
+\x11\x79\xef\x50\xd0\x8e\x28\xaf\xcc\xce\x39\x24\x22\x83\x66\xaf\
+\x8f\xb5\x8e\x30\x08\x10\x42\xec\xd3\x5a\xeb\x06\x88\xbc\x02\x4a\
+\x02\xad\xca\xf2\x03\x50\x8d\x22\x9c\x03\x63\x0d\x52\x08\x6c\xce\
+\x1c\xb3\xa2\x05\x5a\xed\xe4\x6d\x85\x02\x52\x69\xb4\x0e\xd0\x2a\
+\x40\x69\x8d\x90\x12\x67\x2d\x52\x29\xd2\xc9\x88\x5f\xbe\xfc\x32\
+\xaf\xbf\xfe\x3a\xf7\x1f\x3d\xca\xa1\x43\x87\x20\x0c\x4b\x48\x5a\
+\xef\x09\xb4\x26\xae\x44\xa5\x42\x08\x08\x75\xd6\x73\x18\x67\x33\
+\x59\x83\xa0\x21\xa5\x94\xd5\xc2\xa2\x19\x27\x92\x3b\x43\x53\x88\
+\x2c\x1e\x02\xcd\xc4\x18\x86\xa3\x31\xc6\xda\xdc\x1b\x59\x2a\xbc\
+\x49\x03\x51\x90\x3c\x85\x54\x0a\x1d\x04\xe8\x20\xcc\xbd\x21\x51\
+\x3a\x20\xaa\xc4\x6c\x6e\x76\x79\xf6\xb9\xe7\xf8\x97\xef\x7c\x87\
+\xdf\xbf\xf1\x06\xe3\x34\x65\x62\x2c\x61\xa0\x49\xaa\xf1\x4d\x81\
+\x2e\xa5\x40\x29\x49\x9a\xda\x9c\x09\xc8\xea\x8e\xb4\xa2\xa7\xfa\
+\xd8\x69\x26\x29\xa5\xa0\x9a\x43\xcb\x5a\x97\x09\x7f\x9b\xe0\xdd\
+\x01\xb5\xa9\x02\x76\x33\x5b\xdd\xd6\x5b\xe4\x6f\xeb\x1c\x42\x40\
+\xb5\x52\x41\x48\x81\xbf\x79\xf1\x5c\x46\xca\xff\xb4\xf7\x7e\x00\
+\x10\x06\x59\xca\xbc\x91\x02\x03\x58\x63\x71\xce\x31\xd3\xa8\x13\
+\x05\x01\x5b\x83\x41\xd1\xb5\xdd\x6e\x66\x84\xcf\x1b\x79\xe7\x2c\
+\xc6\x18\xa4\x74\x38\x9b\xad\x63\x8d\x61\x3c\x1a\xd1\x68\x34\x79\
+\xe0\xfe\xa3\x1c\x3a\x74\x30\xab\xea\x41\x40\x35\xaa\x90\x5a\x8b\
+\xf2\x79\x86\xf2\x3b\x09\x61\x41\x69\xf2\x7d\x06\xda\x18\xd3\xcd\
+\x46\x1f\x0a\x25\x55\xd9\x25\x91\xb1\x0c\x9c\xf7\x19\xbc\x84\x40\
+\x78\x4f\x52\xab\x12\x57\x42\xb6\xfa\x43\x06\xa3\x31\xde\xf9\x9b\
+\x5c\x9d\x75\x5b\x0e\xeb\x0c\xd2\x48\xbc\xcf\x3c\xe1\xac\x65\x34\
+\x1a\x11\x45\x01\x0f\x3d\x78\x8c\x23\x87\x0f\x93\xd4\x13\xb4\x14\
+\x24\x71\x8c\xd6\x0a\x63\x1c\xde\x39\x52\x0b\x5a\xab\x1d\x06\xcd\
+\x5a\xd1\x8c\x27\x59\x93\x62\x8c\xe9\x6a\xbc\x3f\x6b\xad\x45\x2a\
+\x49\x10\x68\xc6\x93\x14\xe7\x41\x7a\x8f\x77\x9e\xd4\x9a\xd2\x7d\
+\x05\x93\x0c\x82\x80\xb9\x76\x56\x5d\xc7\x69\x16\x50\xd3\xaf\x6c\
+\x24\x68\xc8\x73\x2d\xd2\x2a\xd2\xd4\xa0\xb4\xe2\x9d\x07\xf6\x73\
+\xff\x7d\xf7\x32\x37\x37\x8b\x56\x8a\x38\x0a\x09\xb5\xc6\x7a\x97\
+\xcf\x91\x3c\x20\x70\xde\x91\xa6\x29\x3a\xe7\x54\xc5\x74\x2f\x0a\
+\x43\x94\x92\x8c\x47\x06\xef\xfd\x59\x3d\x99\x4c\x4e\x19\x63\x88\
+\xa2\x88\x5a\x1c\xd1\x1f\x8e\xb0\xce\x11\x28\x49\x6a\x6d\xd6\xee\
+\x49\x95\xb9\x50\xf8\x72\x7e\x29\x80\x4a\x14\xd1\x6a\x46\x5c\x9f\
+\x69\xe5\xdb\x4e\x2b\x61\x00\xcf\x78\x3c\x46\x4a\xc5\xdd\xfb\xf7\
+\x71\xfc\xc1\x63\xec\x5d\xba\x83\x30\xcc\x46\x8f\x4a\x29\x9c\xcd\
+\xc6\x2d\x99\xe7\x73\x08\x66\xbd\x1a\xc6\x3a\x3c\x16\xad\x24\xd6\
+\x79\xac\x75\xd4\xe2\x08\x01\x4c\xc6\x63\xd2\x34\x3d\xa5\x8d\xb5\
+\xbf\x99\x4c\x26\x26\x8a\x22\xdd\x48\x6a\x5c\x5b\xdd\xc0\x5a\x8b\
+\xd3\x0a\x63\x4c\x1e\x30\x7e\x8a\xcb\x93\xf7\xb4\x3e\x8f\x0d\x7f\
+\x4b\x6a\x90\xa6\x29\xc3\xe1\x88\xfd\xfb\xf6\xf2\xb7\x8f\x9d\xe0\
+\xde\x23\x87\x51\x2a\x5b\x33\xeb\x83\x0b\x98\xd9\xb2\xd5\x2c\x8b\
+\x99\xdf\x0e\xc1\x2c\x7e\x82\x4c\x26\xe7\x69\x24\x19\xe9\x1c\x8d\
+\x46\xc6\x5a\xfb\x1b\xed\xac\xfd\xdd\x70\x38\x3c\x57\xaf\xd7\xef\
+\x6e\xd5\x13\xb4\xca\xdd\x2d\x25\x36\xaf\x0d\x65\xf7\x24\x04\xc2\
+\x3b\x9c\x93\x08\x01\x8e\x4c\x90\x69\x92\xe6\x9c\x63\x32\x99\x30\
+\x37\x3b\xcb\xdf\xbc\xef\x51\xde\xf3\xf0\x71\xe2\x38\x66\x3c\x1e\
+\x33\x9e\x4c\xb6\xd7\xca\x2d\xea\xa6\xa6\x14\x9e\xbc\xd1\xf1\xae\
+\x54\xc6\x3a\xc7\x64\x62\x48\x53\x83\x56\x92\x56\x23\xc1\x7b\x47\
+\xbf\xdf\x3f\xe7\x9c\xfb\x9d\x16\x42\x4c\x86\xc3\xe1\xb3\xce\xb9\
+\xbb\xab\x71\x44\x23\xa9\xb2\xd5\x1f\x20\x95\x44\xe5\xa9\xac\xb4\
+\x4e\xae\x84\xf4\x1e\xe7\x04\xc8\xa2\x9b\xf2\x79\xc5\x9e\x90\xd4\
+\x6a\xbc\xef\xbd\xef\xe1\xfd\x8f\xbe\x97\xd9\x4e\x9b\xf1\x64\xc2\
+\x60\x38\xdc\x69\xe5\xc2\x03\x85\xa0\x53\xe3\x15\xef\xb7\x47\x30\
+\x85\x61\x26\xc6\x30\x31\x29\x8d\xa4\x46\xb5\x12\x31\x18\x0c\x18\
+\x0c\x06\xcf\x02\x13\xed\x81\xe1\x60\xf0\xad\xd1\x70\xf8\x44\xb5\
+\x56\x63\xa1\x33\xc3\xc6\x56\xaf\x84\x86\x77\x0e\x2f\x65\xd6\x12\
+\x3a\x8f\x17\x1e\x2f\x98\x12\x28\x0b\x36\xe7\x1c\x0f\xbe\xfb\x01\
+\x3e\x70\xe2\x7d\xec\x7d\xc7\x12\xa9\x31\xf4\x07\x83\x1d\x93\x95\
+\x69\xeb\x17\xc2\x3b\xe7\x4b\x2f\x94\x9e\xc8\x21\x5a\xec\x51\xcc\
+\x56\x17\xe6\x66\x00\xe8\x6d\x6d\x31\x1c\x0e\xbf\x25\x84\x40\x8b\
+\x0c\x67\x27\xbb\xdd\xee\x2f\xab\xb5\xda\xb1\xce\x4c\x83\xda\xb5\
+\x0a\xe3\x49\x8a\x94\x51\xd6\xe3\x7a\x8f\xf3\x02\x49\x66\x2d\x27\
+\x01\x1c\x38\x49\x9a\x1a\x9a\xcd\x06\xff\xf0\x89\xbf\xe7\xde\x23\
+\x87\x11\x42\xe4\x83\xde\x2c\x75\x6e\xa3\xcb\x97\x29\xda\xb1\x6d\
+\xf5\x42\x19\x57\x18\xa8\x50\x24\x6f\xfc\x5d\x3e\x86\xac\xc5\x15\
+\x66\x5b\x4d\x8c\x31\x6c\x6c\x6c\xfc\xd2\x5a\x7b\x32\xaf\xc6\x12\
+\xa5\x94\xd9\xea\xf5\xbe\x32\x1e\x8d\x50\x52\xb2\xb4\x6b\x1e\x63\
+\xb2\x0c\xe4\x8b\xe1\x6c\x39\x45\x28\x1a\xf1\x0c\x3e\x93\xd4\x30\
+\xdb\x99\xe3\xf0\xa1\x83\x8c\xf3\x1b\x18\xc6\x5a\x6c\x9e\x5d\x6c\
+\xfe\x36\xd6\x62\xf3\xe6\x7d\xfa\x7a\x47\x21\xbc\xdb\x69\xfd\x7c\
+\x5f\x6b\x2d\xd6\x58\x96\x76\xcd\x21\xa5\xa0\xdb\xed\xd2\xeb\xf5\
+\xbe\xa2\xb5\x36\x4a\xa9\xac\x5a\x09\x21\x48\x27\x93\xa7\x37\x36\
+\x36\x7e\x0d\x30\xd7\x6e\xd2\x69\xd5\x99\x4c\x26\xdb\x56\x2b\xb0\
+\xee\x0a\x37\xe7\x96\xc4\x61\xad\x61\x34\x9a\xe4\x99\x25\x0f\x50\
+\x57\x64\x19\x97\x1f\x67\x6b\x58\x9b\x57\x69\x9f\x07\xb0\x9b\xfe\
+\x4e\x39\x62\x29\xfe\x4b\xd3\x94\x4e\xab\xce\x5c\xbb\x85\x31\x86\
+\xd5\x95\x95\x5f\xa7\x69\xfa\x74\xc1\x02\x64\x81\x2f\xef\xfd\x78\
+\x7d\x63\xe3\x33\x83\x7e\x1f\x80\x7d\x4b\x8b\x44\x61\x90\xe3\x3b\
+\x0f\x3c\xe7\xb0\xb9\x12\xce\x39\x9c\xcd\x04\xb2\xde\xe3\x5c\x4e\
+\x13\xac\x2b\xc7\x25\xd6\xfa\x6d\x85\x0a\xe5\xbc\x2b\x95\xcb\x84\
+\xcf\xe3\xc1\x91\xad\xe3\x7d\xae\x88\xc7\x18\x43\x18\x68\xf6\x2d\
+\x2d\x02\xb0\xb6\xba\xca\xc6\xc6\xc6\x67\xbc\xf7\xe3\x42\x6e\x39\
+\x3d\x35\x36\x69\xfa\x93\xe5\xe5\xe5\xaf\x1b\x63\xa8\x44\x21\xfb\
+\x97\x16\xb3\x20\x35\x26\x73\x73\xbe\x81\xdd\x11\x70\xb9\xa5\x73\
+\x05\x9d\xdf\xe6\x40\x99\x55\x6f\x3c\x9e\x0e\xdc\xdc\x78\xce\x63\
+\xbd\xcb\x3d\xe1\x4b\xe1\xbd\x77\xec\x7f\xc7\x1e\x2a\xf9\xe4\xe3\
+\xfa\xf5\xeb\x5f\x37\xc6\xfc\x64\x9a\x73\xc9\x69\x96\x28\xa5\xa4\
+\xdf\xef\x7f\x76\x75\x75\xf5\x57\xde\x7b\x66\x9a\x75\xf6\x2d\x2d\
+\x62\xad\xc9\x3c\xb1\x83\xa4\xb9\xac\x3a\xe6\xde\x28\x36\x2f\x7e\
+\xb3\x39\x8c\xec\xd4\x79\xa5\xe5\xfd\xb6\x07\x4a\x83\xdc\x20\xbc\
+\xb5\x86\x7d\x4b\x8b\xb4\x9b\x19\x94\xaf\x5e\xb9\xf2\xab\xc1\x60\
+\xf0\x59\x59\x4e\x00\xb3\xb7\xbe\xc5\xa4\xa0\xb7\xba\xb2\xf2\xf1\
+\x40\xeb\xe7\xdb\x9d\xce\x9e\x85\xce\x0c\x02\x38\x7b\xf1\x0a\x69\
+\x6a\xd1\x5a\x21\xa5\xc7\x17\x6c\x31\xef\x6b\xbd\x00\x21\xfc\x8e\
+\x89\x5c\x59\x9e\xf3\xa9\x5c\x79\x3c\x15\xa4\xd3\x23\x46\xef\xc1\
+\xa4\x06\x97\x5b\x7e\xa1\x33\x83\xb5\x96\xab\x57\xaf\x5e\x5a\x5b\
+\x5b\xfb\x38\xd0\xbb\x91\x01\xdf\xee\x06\xc7\xe9\x95\x95\x95\x8f\
+\x08\x29\xbf\x3f\x33\x33\xd3\x9e\xef\xcc\x10\x68\xcd\xd9\x0b\x57\
+\x49\x27\x93\xbc\x31\x29\x64\xf4\x58\x3f\xc5\xf7\xa7\x27\xeb\xfe\
+\xd6\x77\x67\xfc\x34\xed\xce\x95\x73\x36\x83\x6a\xa8\x03\xee\x5a\
+\xda\x43\xbb\x59\xcf\x84\xbf\x72\x65\x6d\x75\x65\xe5\x23\xde\xfb\
+\xd3\x6f\xfb\x0e\x8d\x10\x02\xe7\xfd\xc9\xe5\xe5\xe5\x0f\x7a\xe7\
+\x9e\x9a\x69\xb7\x17\x67\x9a\x75\xee\xa9\x44\x9c\xbf\x74\x95\x8d\
+\x6e\x0f\x97\x93\xb1\xac\xb1\x01\xe1\xa7\x67\xa2\x37\x74\x2c\x85\
+\xf0\x3b\x7a\x86\xa9\x19\xa9\xc9\x62\x64\xa6\x91\xb0\x77\xcf\x2e\
+\x2a\x51\x36\x5f\xba\x76\xf5\xea\xe5\x95\x95\x95\xc7\xbd\xf7\x27\
+\x8b\x19\xee\x4d\xb2\xbe\xfa\xca\x2b\xb7\xbe\xc9\x57\xd0\x58\x6b\
+\x0f\xb5\xdb\xed\x6f\x74\x66\x67\x1f\x2a\xa8\xed\xca\xfa\x26\x57\
+\xae\xaf\x32\x18\x8d\xb3\xd8\x51\x59\x2b\x9a\x37\x61\x94\x2d\xd3\
+\xb4\x27\xc4\x0d\x5e\x28\x62\xc7\x7b\xaa\x95\x88\xdd\xf3\x1d\x66\
+\x67\xb2\x89\xf4\x70\x30\xe0\xea\xb5\x6b\x2f\xad\xaf\xad\x7d\x42\
+\x08\x71\xaa\x20\x75\xb7\x52\xe0\x8f\xde\x27\xce\x2d\x7a\x6a\x63\
+\x73\xf3\xc4\x78\x3c\xfe\x62\x67\x76\xf6\x89\x5a\xad\x26\x66\x67\
+\x9a\xb4\x9b\x75\xd6\x36\xb7\x58\x59\xdf\xa4\x37\x18\x62\x4c\x0a\
+\x62\x7b\xde\x93\x6b\x32\x1d\x04\x25\xe6\xf1\x1e\xa5\x24\x8d\xa4\
+\xca\x6c\xab\x49\xbb\x55\x2f\x1f\x67\xd8\xd8\xd8\xf0\xd7\xaf\x5d\
+\xfb\xda\x78\x3c\xfe\x5c\x81\xf9\xdb\x4e\xf4\xde\xca\x03\x45\x17\
+\xa5\x94\xc2\x3a\x87\x14\xe2\x03\x8d\x66\xf3\x0b\xad\x56\xeb\xc1\
+\x4a\xa5\x52\x9e\x3f\x1c\x8d\xe9\xf6\x07\xf4\x06\x43\x86\xa3\x31\
+\xa9\xb1\xe5\xbd\xb0\x7c\x00\x0b\x42\x10\x68\x45\x5c\x89\x48\xaa\
+\x31\xf5\x5a\x95\x6a\x25\x2a\x3d\xd2\xeb\xf5\x58\x5b\x5d\xfd\x45\
+\xb7\xdb\xfd\xbc\xb5\xf6\xc7\x52\xca\x9c\x4a\x67\x6c\xf8\x76\x1e\
+\x78\xdb\x0a\x14\x29\x54\x08\xa1\x83\x20\xf8\x68\xb5\x56\xfb\x54\
+\xbd\x5e\x3f\x5e\xa9\x54\x50\x6a\xbb\xf5\x9b\x9e\xfb\x1b\x93\x75\
+\x73\xd9\x58\x32\x4b\xd3\xd3\x93\xb8\x34\x4d\x19\x0c\x06\x74\x37\
+\x37\x5f\xdc\xea\xf5\xbe\x6a\xd2\xf4\xdb\xc5\x33\x13\x42\x88\xbf\
+\xbc\x02\xe4\x4f\xab\x90\xf1\x74\x2d\x84\x78\x24\xae\x54\x1e\x8f\
+\xe3\xf8\x44\x25\x8e\x0f\x84\x61\xa8\xb4\xd6\x59\x8b\x79\x8b\xdb\
+\xac\xb6\xbc\xd1\x3d\xb6\xc3\xc1\xe0\xcc\x60\x30\x78\x7e\x38\x1c\
+\x3e\xe5\xbd\xff\xb9\x10\xc2\x14\xfd\x44\x31\x30\x78\x3b\x0a\xfc\
+\x59\x4f\xab\xe4\x29\xd3\xe0\xfd\x4f\x87\xa3\xd1\x4f\xfb\xfd\x7e\
+\xa8\xb4\x3e\x2c\x85\x38\x1a\x86\xe1\x11\x84\xd8\xa7\xa4\x6c\x08\
+\x29\x6b\x79\x22\xe8\x5b\x6b\xbb\x2e\x7f\xdc\xc6\x3b\x57\x3e\x6e\
+\x33\x7d\x7f\xf9\x8f\x61\xfd\x76\xaf\xff\x1f\x00\x54\x46\xd5\x89\
+\x5c\xa3\x2a\xa1\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+\
+\x00\x00\x11\x6f\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
+\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
+\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
+\x46\x00\x00\x10\xf5\x49\x44\x41\x54\x78\xda\xac\x9a\x59\xac\x24\
+\x57\x79\xc7\x7f\x67\xa9\xaa\xae\xde\x97\x3b\xf7\xde\xb9\xe3\x8b\
+\xc7\xe3\xc1\x1b\x01\x0c\x31\x38\x7e\x70\x40\x1e\x8c\x95\x08\x44\
+\x64\x19\x41\x78\xe1\x35\x58\x42\x48\x16\x20\x2c\xa1\x28\x91\x90\
+\x82\x1c\x84\x84\x2c\x41\x78\xe4\x85\x80\xb0\x2c\x10\x0e\x24\x78\
+\xc1\x9b\x32\xd8\xc4\x06\xe2\x01\x3c\xf6\x78\xc6\x13\xcf\x7a\xd7\
+\xbe\xbd\x77\xd5\x59\xf2\x50\x55\x7d\xfb\x8e\xc7\xd8\x20\x4a\x3a\
+\xea\xaa\xee\xea\x3a\xdf\xff\xdb\xce\xf7\xfd\x4f\x89\x5f\x3e\xfb\
+\x2c\x97\x1e\x42\x08\x94\x52\x78\xef\x71\xce\x21\xa5\xc4\x39\x87\
+\xf7\x1e\x29\x25\x42\x08\xac\xb5\x78\xef\x01\x70\xce\x85\x4a\xa9\
+\xeb\x81\x77\x87\x61\x78\x83\x10\xe2\x90\x52\xaa\x2e\x84\xa8\xe4\
+\xbf\x0f\x8d\x31\x3d\xef\xfd\xc9\xe9\x74\xfa\x3b\xe0\x37\xc6\x98\
+\xdf\x4b\x29\x93\xe2\x19\x52\xca\xd9\x7c\x85\x0c\xd6\x5a\x84\x10\
+\xc5\x1c\x5c\xee\xd0\xfc\x09\x87\xf7\x1e\xef\xbd\x06\x6e\x2d\x95\
+\x4a\x77\xc5\xe5\xf2\x91\x72\xb9\x7c\x38\x2e\x95\x54\x18\x45\x04\
+\x41\x80\x92\x12\x21\x8b\xc9\x3d\xd6\x5a\xd2\x24\x61\x32\x9d\x32\
+\x1e\x8d\xec\x70\x38\x3c\x31\x1a\x8d\x1e\x9d\x4c\x26\x0f\x78\xef\
+\x9f\xf2\xde\x9b\x3f\x45\x16\xfd\xc7\x0a\xee\x9c\xd3\x5a\xeb\x4f\
+\x54\xab\xd5\xcf\x36\x9a\xcd\x9b\x6b\xb5\x1a\xe5\x72\x79\xcf\x7d\
+\xd3\x24\x65\x3c\x4d\x98\x4c\x13\x00\x4a\x51\x48\x18\x04\x54\x6b\
+\x35\xaa\xb5\x1a\x80\xf2\xce\x5d\xdb\x1f\x0c\xae\xed\xed\xec\xdc\
+\xdd\xed\x76\x9f\x19\x0e\x87\xf7\x1b\x63\xbe\xef\xbd\x37\x85\xd6\
+\xff\xac\x00\xbc\xf7\x28\xa5\x3e\x5c\xab\xd5\xbe\xd2\xe9\x74\xde\
+\xd7\x68\x36\x50\x2a\xfb\xfb\xc6\x76\x97\x53\xaf\x9d\xe3\xf4\x6b\
+\xe7\x58\xdb\xdc\x62\xa7\x3f\x20\x49\x53\xac\xcd\xcc\xae\xa4\x24\
+\x0c\x03\x1a\xb5\x2a\x8b\x0b\x6d\x0e\x5e\xb1\xc2\xc1\xd5\x15\x16\
+\x5a\x4d\xea\xf5\x3a\xfb\x16\x17\x6f\xde\xda\xda\xba\x79\x63\x7d\
+\xfd\x73\xfd\x7e\xff\xcb\xce\xb9\x9f\xbd\x55\x10\xe2\xcd\x62\xc0\
+\x5a\x8b\x94\xb2\x1a\x86\xe1\x57\x17\x17\x17\xef\x6e\x77\x3a\x42\
+\x6b\x8d\x73\x9e\x63\xc7\x4f\xf0\xdc\xb1\xdf\x73\xea\xb5\xb3\x0c\
+\x47\x63\x40\xa0\xb5\x22\x50\x0a\xa5\xd4\x1e\xff\x75\xce\x91\x5a\
+\x8b\x31\x16\xf0\x54\xcb\x31\x07\xaf\x38\xc0\x4d\xef\xba\x81\x77\
+\x5c\x7b\x35\x52\x08\xc6\xe3\x31\x6b\x6b\x6b\x7e\xed\xe2\xc5\x6f\
+\x26\x49\xf2\x25\xe7\xdc\xe0\xcd\x62\xe0\x0f\x02\xc8\x27\xbe\xae\
+\xd9\x6a\x7d\x67\xff\xf2\xf2\xfb\x2b\xd5\x2a\x00\x2f\x1c\x7f\x85\
+\x27\x9e\x79\x8e\xd7\xce\x5e\x00\x01\x71\x14\x11\x97\x4a\x44\xb9\
+\xab\x28\x25\xb3\x18\x28\x26\xcf\x83\xd3\x58\x4b\x9a\xa6\x4c\xa7\
+\x29\xa3\xc9\x84\xf1\x64\x0a\xc0\xea\xca\x12\x1f\xfc\xab\x9b\x78\
+\xe7\xb5\x57\x03\xb0\xb9\xb1\xc1\xd9\x73\xe7\x9e\xdd\xe9\x76\x3f\
+\x0d\xbc\x28\x84\xf8\xe3\x01\x48\x29\x01\x6e\xe9\x74\x3a\x0f\x2c\
+\xef\xdf\xbf\x12\x86\x21\xdd\xfe\x80\x9f\x3c\xf6\x34\x2f\x1c\x7f\
+\x05\x29\x04\xd5\x4a\x99\x6a\xa5\x4c\xb9\x54\x42\x07\x1a\xf9\x26\
+\x66\x2f\x32\x8e\xf7\x9e\xd4\x18\xc6\x93\x09\xfd\xe1\x88\xc1\x60\
+\x84\xc7\xf3\x17\xd7\x5c\xcd\x47\x8e\xdc\x4a\xa3\x56\xa5\xdf\xef\
+\x73\xf6\xcc\x99\x73\x9b\x9b\x9b\x77\x39\xe7\x8e\x16\xff\x7d\x4b\
+\x31\xe0\xbd\x47\x08\x71\xcb\xbe\x7d\xfb\x1e\x5a\xde\xbf\xbf\xad\
+\x94\xe2\xc4\xab\x67\xf8\xd1\x23\x4f\xb0\xb9\xbd\x43\xbd\x56\xa5\
+\x5e\xab\x52\x2d\xc7\x68\x25\x01\x31\x03\x5e\x68\xfd\x52\x1f\xce\
+\x33\xd7\xec\x5c\x6b\x4d\x29\x8a\xa8\x57\xab\x0c\x6a\x63\x76\xfa\
+\x03\x8e\xbd\x74\x92\xf3\xeb\x9b\xfc\xdd\xed\x1f\xe0\xf0\xc1\x55\
+\xae\xbc\xf2\xca\x15\x21\xe5\x43\xeb\x6b\x6b\x1f\x71\xce\x1d\xbd\
+\x5c\x5c\xbc\x91\x05\xae\x5d\x5a\x5a\x7a\x74\xff\xca\xca\x01\xa5\
+\x14\xff\xfb\xe2\x09\x7e\xfc\xc8\x93\x58\xef\x69\xd4\xaa\x34\x6b\
+\x35\xc2\x50\x03\x12\x29\x33\xa1\xa5\x10\x08\x29\x10\xbc\x1e\x80\
+\xcf\xa4\x9e\x7d\x3a\xef\xf1\xde\xe1\x9c\x9f\x01\x4b\x92\x94\x6e\
+\xbf\xcf\x4e\x6f\x80\x94\x82\x8f\xdd\xfe\x01\xde\x75\xdd\x61\x46\
+\xa3\x11\xa7\x5f\x7d\xf5\xec\xfa\xfa\xfa\x11\xe0\xf8\xeb\x2c\x70\
+\x19\x54\xd5\x76\xa7\xf3\xdd\xa5\xe5\xe5\x03\x4a\x29\x8e\xbd\xf4\
+\x0a\x3f\x7e\xf4\x29\xa4\xd6\x2c\xd4\x6b\xd4\x6b\x55\x94\x52\x48\
+\x29\x90\x42\x22\xa4\xcc\x84\x17\x05\x90\x99\x16\xe6\xd5\x3f\xfb\
+\xf0\x78\xbc\x07\xe7\x1d\xde\x65\x60\xac\xb5\x28\xa5\x88\xc2\x80\
+\x52\x18\xb2\xdd\xeb\xf3\xa3\x47\x9e\x44\x00\xef\xbc\xee\x30\xab\
+\xab\xab\x07\x8c\x31\xdf\xdd\xde\xde\xfe\x00\x30\x78\x43\x17\xf2\
+\xde\x53\xad\x56\xef\x5b\x5e\x5e\x7e\x6f\x10\x04\x9c\x3a\x73\x8e\
+\x9f\x3e\x7e\x94\x30\x08\x68\xb7\x1a\xd4\xab\x55\x94\x92\x48\x91\
+\x69\xbe\x58\x95\xe7\x01\xec\x2a\x64\x5e\x31\x7e\xce\x8d\xb2\x6b\
+\xe7\x1c\x1e\xf0\xce\x63\x95\xc2\x79\x87\xb3\x8a\x76\xab\x89\xd2\
+\x9a\xed\xee\x0e\x3f\x79\xe2\x28\xd5\x4a\x99\xab\x56\x57\x58\x59\
+\x59\x79\xef\x64\x32\xb9\x6f\x34\x1a\xdd\x3d\xaf\x74\x29\xa5\x9c\
+\x09\x12\x04\xc1\x6d\x8b\x4b\x4b\x9f\x89\xe3\x98\xde\x60\xc8\x7f\
+\x3d\xf9\x0c\x42\x48\x16\xda\x2d\xda\xcd\x06\x61\x10\x10\xe8\x80\
+\x20\xd0\x04\x7a\x7e\x04\xb3\x73\xad\x55\x7e\xae\xb2\xfb\x2e\xb9\
+\x37\x0c\x34\x5a\x6b\x82\x20\xd8\xbd\x3f\xd0\x84\xf9\x77\x61\x18\
+\xd0\x6e\x36\xe8\xb4\x5b\x08\x21\xf8\xcf\x27\x7f\x41\x6f\x30\xa2\
+\xdd\xe9\xb0\xb8\xb8\xf8\x19\xa5\xd4\x6d\xf3\x01\xad\xf3\x6c\x83\
+\xf7\x3e\x6a\x36\x9b\x5f\x6b\x36\x9b\x00\x3c\xfe\xcc\xaf\x18\x8c\
+\xc6\x74\x5a\x4d\xda\xcd\x7a\xae\xf9\x4c\xeb\x59\x99\x30\xe7\xff\
+\x08\x44\xae\x04\x21\x98\x8b\x83\x3d\x1e\xb4\xeb\x3e\x79\x5d\xe5\
+\xf3\x78\xc8\xae\x1d\xd2\x65\xe7\x4a\x7a\xda\xcd\x46\x96\x52\xb7\
+\x77\xf8\xf9\xd1\xe7\xf8\xd8\xed\xb7\xb2\xb8\xb4\x44\xaf\xd7\xfb\
+\xda\xf6\xf6\xf6\x2d\x52\xca\xe9\x2c\x06\xbc\xf7\x04\x41\x70\x67\
+\xa7\xd3\x79\x8f\x94\x92\x13\xa7\xcf\x70\xea\xb5\xf3\x34\xeb\x75\
+\xda\xcd\x1a\x61\xa0\xb3\xb5\x41\x4a\xe4\xcc\x85\x76\xdd\x28\x03\
+\x96\x07\xb0\x00\x21\x15\x48\x89\x50\x2a\x13\xdc\x5a\x70\x16\xef\
+\x5c\x16\x07\x73\x82\xbb\x5c\x68\x57\x00\xb0\x16\x2b\x3d\x4a\x0a\
+\xda\xcd\x3a\xce\x39\x4e\x9e\x39\xc7\xcb\xa7\xcf\xf0\xf6\x2b\xaf\
+\x60\xdf\xbe\x7d\xef\x19\x0c\x06\x77\x1a\x63\xfe\x5d\x08\x81\xce\
+\x1f\xa6\x6b\xb5\xda\x3d\x95\x6a\x15\x6b\x1d\xcf\xff\xf6\x65\xa2\
+\x28\xa4\x59\xaf\x52\x8a\xa2\xd9\xc2\xa6\x84\xc8\x00\xe4\x56\x28\
+\x84\x17\x79\x40\xa3\x15\xa2\x14\x43\x32\x45\xec\x74\xa1\xb7\x93\
+\x79\x7f\xbd\x81\x68\x34\x21\x8c\xf0\x93\x31\xce\x98\x59\x00\x17\
+\xab\xb4\xcd\x81\x58\x29\x90\xce\x61\xad\x20\x96\x92\x66\xbd\x46\
+\x6a\x2c\xcf\x1d\x3b\xce\xa1\xd5\x15\x5a\xed\x36\x1b\x1b\x1b\xf7\
+\x6c\x6e\x6e\xfe\x40\x4a\x69\x34\x80\x52\xea\x96\x46\xb3\x79\x93\
+\x10\x82\x57\xcf\x9e\x63\x6b\xa7\x47\xbd\x9a\x2d\x52\x4a\x29\x54\
+\x2e\xb0\x52\x32\xcf\x40\x7b\xb5\x8f\x10\x10\xc7\x88\xed\x6d\xd4\
+\x43\x0f\x22\xff\xfb\x49\xc4\xab\x27\x31\x93\x09\xa9\x80\xb4\x54\
+\xc2\x1d\x3c\x84\xb8\xe5\xaf\x89\x8e\xfc\x0d\xba\xd5\xc2\x8f\xc7\
+\x39\x88\x2c\x1b\xd9\x02\x88\x15\x18\xeb\x90\xc2\x61\x9d\xa3\x5a\
+\x89\x99\x26\x29\x5b\x3b\x3d\x4e\xbd\x76\x9e\xc3\x57\x1e\xa0\xd9\
+\x6c\xde\xd4\xed\x76\x6f\xf1\xde\x3f\xa5\x85\x10\x94\xe3\xf8\x93\
+\x95\x4a\x05\x80\x13\xa7\xcf\x52\x0a\x43\xaa\xe5\x32\x61\xa0\x91\
+\x22\x13\x5c\xeb\x02\x48\x06\xa0\x28\x95\x11\x02\x1f\x97\xd1\x4f\
+\x3d\x46\xf0\x6f\xdf\x40\x9e\x7c\x19\x2f\x15\x3e\x08\xd0\x52\xe0\
+\x00\x3b\x1e\x61\xd6\x2e\x32\xfd\xc5\xd3\x0c\x7e\xf8\x03\x6a\xff\
+\xf0\x39\x4a\xb7\xde\x06\xe3\x11\xca\x0b\x90\x20\xbd\xc3\x59\x97\
+\x2b\xc7\x62\xac\x43\x58\x8b\x94\x82\x5a\xa5\x4c\x92\xa6\x9c\x38\
+\x7d\x86\xc3\x57\x1e\xa0\xd1\x68\x10\xc7\xf1\x27\x47\xa3\xd1\x53\
+\x1a\x08\x2b\x95\xca\xed\x5a\x6b\x76\x06\x43\x36\xbb\x3d\xae\x58\
+\x59\xca\x5c\x44\x08\xb4\x52\x33\xe1\x0b\xf7\xd9\x73\xc4\x65\x82\
+\x07\xbf\x47\x78\xff\xbf\x42\x9a\x42\xa5\xba\x27\x89\x2a\x40\x09\
+\x90\x5a\xa3\x80\xf4\xff\x4e\xb1\xf1\x8f\x5f\xa0\xf1\xd9\x2f\x50\
+\xbf\xf3\x93\xb8\xd1\x10\x04\xb3\xb8\x12\xd6\xe6\x31\xe5\x48\x85\
+\xc0\x5a\x43\xad\x5a\xa6\x5c\x8e\xb9\xb8\xbe\x49\x6f\x30\xa4\x5a\
+\xa9\x50\xa9\x54\x6e\x1f\x8f\xc7\xa1\x94\x4a\xdd\x10\x97\xcb\x57\
+\x01\x5c\xdc\xd8\x42\x0a\xc1\x42\xbb\x49\xa3\x5a\x41\xab\x2c\xc5\
+\x65\x20\xf4\x65\x84\x8f\x51\x4f\x3e\x46\x78\xff\x7d\xe0\x1c\xa2\
+\x54\x42\x5c\xb2\x02\x88\x4b\x86\x8c\x4a\xe0\x1c\x1b\xf7\xdf\xc7\
+\xe0\xc9\x47\x91\x71\xbc\xe7\x91\x4a\x29\x94\xce\xe6\x0c\x03\x8d\
+\x52\x8a\x7a\xb5\xca\xbe\x76\x13\x29\x04\x17\x36\xb6\x90\x4a\x51\
+\xa9\x54\xae\x92\x52\xde\x20\xb5\xd6\x37\x46\x51\xa4\x8b\x94\x15\
+\xc7\x25\x02\xad\xa9\x56\x62\xa2\x28\x40\x2b\x89\xd2\x6a\xaf\x54\
+\xd9\x4c\xc8\xee\x36\xc1\xb7\xbf\x01\xa9\x81\x20\x78\xcb\x4d\x88\
+\x08\x02\xbc\x49\x59\xfb\xf6\x37\x70\xdd\xed\x59\xb6\xda\x5d\x9c\
+\xb2\xb2\x5c\x29\x49\x29\x0c\xa9\x55\xca\x68\xad\x29\xc7\x31\x1b\
+\x5b\x3b\xb9\xe1\xcb\x5a\x29\x75\xa3\x8c\xc2\xf0\xba\x30\x08\x70\
+\xce\x33\x18\x4d\xa8\x94\x63\x54\x1e\xa0\x95\x72\x9c\xa5\xc4\xcb\
+\x55\xab\x71\x8c\x7a\xe4\xa7\x70\xf2\x65\x7c\xa9\x34\x5b\x6f\xfd\
+\x6c\xdd\xdd\x5d\x83\xdd\x25\xbf\x79\x40\x44\x25\xc6\x27\x5f\x66\
+\xfb\xe1\x9f\xa2\xe3\xf8\x75\xfa\x21\xcf\x7c\xd5\x4a\x19\x29\x05\
+\x4a\x49\x2a\xe5\x12\x83\xe1\x18\xef\x3d\x51\x14\x11\x86\xe1\x75\
+\x52\x48\x79\x48\x69\x85\xb1\x16\x63\x2d\xa5\x28\x9c\x95\x03\x51\
+\x18\x12\x85\x7a\x56\x45\x0a\xc8\xcb\x06\x49\x3a\x18\x62\x9f\x7e\
+\x02\x94\x7a\x9d\x70\xf3\x82\xdb\xfc\xd3\x5d\x06\x08\x52\xb1\xf5\
+\xf4\xe3\x4c\x07\xc3\x59\x4d\x35\x5f\xd6\x44\xf9\xca\x0c\x20\x11\
+\x44\x51\x44\x6a\x2d\xd6\x3a\xc2\x20\x40\x08\x71\x48\x6a\xad\xeb\
+\x20\xf2\x15\x50\x12\x68\x95\x6b\x23\x13\xa5\x1c\x45\x04\x3a\x7f\
+\x88\x10\x38\xef\x99\xa4\x29\xd3\xf5\x35\xdc\xe9\x93\xb8\xdc\x75\
+\xfc\x65\x86\x05\x8c\x98\x03\xe1\x77\x7f\x73\x00\x41\xc0\xf8\xf4\
+\x49\x86\xeb\x6b\x4c\x52\x83\xf3\x1e\x95\x83\x08\xb4\x26\x2e\x45\
+\x7b\x82\x29\xd4\x59\xcf\x61\x9c\x45\x4a\x49\x10\x04\x75\x29\xa5\
+\x2c\x03\xa4\xc6\xe6\x35\x91\xdc\x5b\x8c\x09\x91\xc5\x43\xa0\x49\
+\x8c\x61\x3c\x99\x62\xbc\x47\xf4\x7b\x98\x64\x42\x22\x24\xe9\x9c\
+\x76\x0b\xad\xa7\x40\x2a\xc0\x14\x40\xe6\xac\x61\xf1\x19\x20\x21\
+\x71\xd3\x09\xae\xd7\xc3\x7a\xcf\x78\x32\x25\x31\x86\x30\xd0\x54\
+\xcb\xaf\x77\xab\xc2\x95\xd2\xd4\xe6\xe9\x56\x96\xf7\xa4\x15\x3d\
+\xd7\xc7\xce\x7b\xb1\x94\x82\x72\x5c\xca\xb9\x1a\x97\x3d\x58\x40\
+\x02\x24\x22\x1b\x53\xb2\x91\x00\xd3\xfc\xbb\x24\x17\x3c\x05\x8c\
+\xcf\x00\xcc\x40\xe4\xd7\x00\x85\xce\xac\x75\x08\x21\x28\x97\x4a\
+\x08\x29\xf0\x97\x89\x8b\x4c\xc6\x5d\x57\xd5\xde\xfb\x11\x40\x18\
+\x64\x29\xf3\xd2\x12\x18\xc0\x1a\x8b\x73\x8e\x56\xbd\x46\x14\x04\
+\xf4\xa7\x09\xae\x56\xc7\x44\x25\xfc\x64\x8c\x45\x22\xc5\x6e\xa2\
+\xb2\x73\x2e\x64\x81\x74\x4e\x78\x8b\x9f\x81\xf0\xde\x21\xa3\x12\
+\xb2\x56\x47\x02\xb5\x7a\x85\x72\x54\x22\xb5\x16\xe5\xb3\x32\xbd\
+\x10\xa3\x28\x08\x8b\x2a\x36\x2f\x06\x47\xda\x18\xd3\xcb\xa8\x0f\
+\x85\x92\x6a\xd6\x25\x81\x9c\x35\xe4\xa9\xc9\x16\x17\xe1\x3d\xd5\
+\x4a\x99\x38\x2e\xd1\x8f\x42\xb6\xde\x76\x15\xe6\xf9\x35\x94\xd2\
+\xc8\xcb\x64\x1e\x9b\x6b\xda\xe5\x96\x30\xf8\xec\x33\x07\xe4\x92\
+\x94\xf8\xca\xab\xa8\x2d\x2d\x53\x8d\x4b\x68\x29\x30\xc6\xe1\x9d\
+\x23\xb5\xa0\xb5\xda\xf3\xd4\xac\x15\xcd\x18\x0f\x6b\x52\x8c\x31\
+\x3d\x89\xf7\x27\xad\xb5\x48\x25\x09\x02\x8d\xb5\x36\x0b\x36\xef\
+\xf1\xce\x93\xa4\x66\xae\x97\xcd\xbe\x0f\x94\x62\x71\xff\x32\xed\
+\x0f\xdd\xc1\x24\xb5\x24\x1e\xa6\x1e\x92\x4b\x46\xea\xe7\x5c\x28\
+\x17\x3e\xcd\xc1\x38\xc0\x5a\xcb\x15\x1f\xba\x83\xc5\xe5\x25\x02\
+\x25\x71\x6e\x96\x64\x33\xc5\xa5\xe9\x1c\x7d\x99\x75\x6e\x05\xeb\
+\x61\x8c\xc1\x7b\x7f\x52\x26\x49\xf2\xa2\x31\x06\x29\x04\x95\x38\
+\xc2\x58\x8b\x75\x99\x9f\x67\x29\xcb\x82\x67\xd6\x49\x65\xd5\xab\
+\xc3\x8e\xc7\x2c\x7c\xf4\x4e\xc2\x6b\xae\x65\x32\x19\x93\xe6\xfe\
+\x9f\x14\x01\x3c\x1b\x3e\x1f\x99\xe6\x4d\x6e\x11\x3b\x19\x53\xbb\
+\xe6\x5a\x56\x3f\x7a\x27\x76\x32\xce\x2d\x9f\x2b\x2e\xcf\x55\xc6\
+\x3a\x52\x93\x45\x8a\x75\x1e\x6b\x1d\x95\x38\x42\x00\xc9\x74\x4a\
+\x9a\xa6\x2f\x4a\x63\xed\xaf\x93\x24\x31\x00\xf5\x6a\x65\x46\x66\
+\x39\xef\x31\xc6\xec\x36\x22\xf8\x59\x4b\xe8\x3d\x38\x93\x22\x9b\
+\x6d\xae\xb8\xe7\x5e\x6c\x10\x30\x4d\x93\x99\xc0\xc9\x4c\x68\xbf\
+\x0b\xc4\x33\xcb\x56\x2e\x4d\x11\x41\xc0\xf5\xf7\xdc\x8b\x6e\xb6\
+\xb1\x69\x3a\x27\xfc\xde\x05\xc5\x18\x33\xeb\x9b\x9d\xf3\xd4\xab\
+\x59\xd1\x39\x99\x4c\x8c\xb5\xf6\xd7\xd2\x59\xfb\xbb\xf1\x78\x7c\
+\x0a\xa0\x59\xab\xa2\x95\x22\x4d\x0d\x49\x6a\xb0\x73\x0d\x88\xf7\
+\x7e\x16\x78\x45\x9c\x98\xe1\x80\xe6\x6d\x77\x70\xf0\xde\x7f\x22\
+\x91\x82\xf1\x64\x34\xb3\xc2\x6c\xcc\xb9\x52\xa1\x79\x2f\x05\xef\
+\xb8\xf7\x9f\x59\xbc\xed\x0e\xd2\xd1\x20\x9f\xc3\xe1\xc9\x7b\x04\
+\xef\x66\x60\xac\x73\x24\x89\x21\x4d\x0d\x5a\x49\x9a\xf5\x2a\xde\
+\x3b\x86\xc3\xe1\x29\xe7\xdc\xef\xb4\x10\x22\x19\x8f\xc7\x0f\x3b\
+\xe7\xde\x5e\x8e\x23\xea\xd5\x32\xfd\xe1\x08\xa9\x24\x2a\x4f\x65\
+\x7b\x40\x08\x81\xf4\x1e\xe7\x04\x48\x47\x3a\xe8\xb3\xff\xef\x3f\
+\x4d\xb0\xb8\xcc\x4b\x5f\xff\x17\xfa\xc7\x7f\x0f\x4a\x21\x82\x10\
+\x2f\xb2\xff\x3b\xef\x71\x49\x82\x77\x96\xda\x35\xd7\x73\xfd\x3d\
+\xf7\xb2\x78\xe4\xc3\xa4\x83\xfe\x2c\xd6\x8a\xe7\x7b\xbf\x4b\xc1\
+\x14\xfe\x9f\x18\x43\x62\x52\xea\xd5\x0a\xe5\x52\xc4\x68\x34\x62\
+\x34\x1a\x3d\x0c\x24\xda\x03\xe3\xd1\xe8\x7b\x93\xf1\xf8\xee\x72\
+\xa5\xc2\x52\xa7\x45\xb7\x3f\xc0\x39\x8f\x14\x22\x6b\x03\xa5\xcc\
+\xe2\xc0\x79\xbc\xf0\x78\x51\x00\x12\x38\x67\x49\x7a\x3d\x3a\x1f\
+\xbc\x9d\xf7\xdd\xf8\x97\x9c\xfb\x8f\x1f\x72\xf1\xf1\x47\xe9\xbf\
+\xf2\x12\x76\x3c\x46\x00\x41\x1c\x53\xbd\xfa\x1a\x96\x3e\x78\x84\
+\x95\xbf\xfd\x18\xba\xd5\x26\xe9\xf7\x72\xb6\xbb\xd0\xfa\x2e\x5f\
+\x94\xd1\x2e\xbb\x80\x8a\xae\x6d\x69\x5f\x0b\x80\x41\xbf\xcf\x78\
+\x3c\xfe\x9e\x10\x02\xf1\xdb\x63\xc7\x70\xce\xe9\x85\x85\x85\xa3\
+\xcb\xfb\xf7\xdf\x64\x9d\xe3\x37\x2f\xbe\xc2\x34\x49\x89\xa2\x08\
+\x29\xd8\xd3\x3e\x16\xcd\x8c\x14\x39\x2f\x24\x04\x52\xe6\x05\x9e\
+\x0e\xd0\x71\x8c\x9f\x4e\x49\xbb\xdb\x24\x3b\xdb\xe0\x21\x6c\xb6\
+\x08\x9a\x2d\x44\x14\x61\xc6\x63\xac\x49\x67\x5a\x77\x79\x57\xe6\
+\x3c\xf8\xbc\x37\xde\xed\x93\x3d\xd6\x79\xa6\x49\x42\x29\x0c\xb8\
+\xf1\xba\xc3\x38\x67\x39\xf9\xca\x2b\xff\xb3\xb1\xb1\x71\x8b\x94\
+\xd2\x48\x29\x25\x4a\x29\xd3\x1f\x0c\xbe\x3e\x9d\x4c\x50\x52\xb2\
+\xba\xbc\x88\x31\x59\x06\x9a\xb9\x80\xdf\xcd\x40\xd9\x04\x39\x9b\
+\xe0\x1c\xd6\x66\xd7\x36\x4d\x48\xfa\x3d\x4c\x9a\x22\xeb\x0d\xca\
+\x07\xaf\x26\x3e\x78\x08\x59\x6f\x60\xd2\x94\x69\xaf\x87\x4d\x93\
+\x3d\xff\x77\xec\x0a\xbf\x47\xfb\xf9\xbc\xd6\x5a\xac\xb1\xac\x2e\
+\xef\x43\x4a\x41\xaf\xd7\x63\x30\x18\x7c\x5d\x6b\x6d\x94\x52\xd9\
+\xfa\x23\x84\x20\x4d\x92\x07\xbb\xdd\xee\xaf\x00\xf6\xb5\x1b\x74\
+\x9a\x35\x92\x24\x99\x05\x6c\xa1\x91\x4c\x73\x2e\x0f\x6a\x8f\xc3\
+\xcd\x02\xbb\x18\xd6\x5a\x4c\x92\x90\x4e\xc6\xa4\x93\x09\x26\x49\
+\xf2\x2c\x92\x81\x9d\x69\x3a\xb7\xc0\xee\x35\x33\x8a\xa5\xf8\x2d\
+\x4d\x53\x3a\xcd\x1a\xfb\xda\x4d\x8c\x31\x6c\x6e\x6c\xfc\x2a\x4d\
+\xd3\x07\x8b\x92\x47\x16\xfe\xe5\xbd\x9f\x6e\x77\xbb\x9f\x1f\x0d\
+\x87\x00\x1c\x5a\x5d\x21\x0a\x03\xd2\x34\x9d\x81\xb0\xce\x61\x73\
+\x10\xce\x65\x3d\xac\xb5\x1e\xeb\x3d\xce\x15\x02\xba\x19\x5d\x52\
+\x58\xc6\xe5\x39\xbc\x70\x97\xa2\x81\xcf\x84\x2f\x1a\x7b\xb2\xe7\
+\x78\x9f\x03\xc9\xd2\x78\x18\x68\x0e\xad\xae\x00\xb0\xb5\xb9\x49\
+\xb7\xdb\xfd\xbc\xf7\x7e\x5a\xc8\x2d\xe7\x59\x63\x93\xa6\x8f\xad\
+\xaf\xaf\x7f\xcb\x18\x43\x29\x0a\xb9\x7a\x75\x05\xef\x1d\xa9\x31\
+\xb3\xd5\xd9\x79\x3f\x9b\x28\xb3\x44\xce\x26\xe4\x00\x9d\x2f\x84\
+\xb3\xb9\x56\x2f\x3d\x9f\x0f\xdc\x5c\x79\xce\x63\xbd\xcb\x2d\xe1\
+\x67\xc2\x7b\xef\xb8\xfa\x6d\x07\x28\x45\x21\xc3\xe1\x90\xb5\xb5\
+\xb5\x6f\x19\x63\x1e\x9b\x67\xbb\xe5\x3c\xa7\x29\xa5\x64\x38\x1c\
+\x7e\x71\x73\x73\xf3\x79\xef\x3d\xad\x46\x8d\x43\xab\x2b\x58\x6b\
+\x32\x4b\xf8\xdc\xdc\x73\x5c\x8e\x9d\x8b\x85\x22\xe8\x76\x87\xcb\
+\xc7\xee\xb5\xdb\x03\x72\x4e\x21\x97\x08\x6f\xad\xe1\xd0\xea\x0a\
+\xed\x46\xe6\xca\x17\xce\x9f\x7f\x7e\x34\x1a\x7d\x51\xce\x18\xc0\
+\x6c\xe8\xcb\x6c\x1c\x0c\x36\x37\x36\x3e\x15\x68\xfd\x68\xbb\xd3\
+\x39\xb0\xd4\x69\x21\x80\x93\x67\xce\x93\xa6\x16\xad\x15\x52\x66\
+\x29\xb4\xe0\x84\xa4\x10\x78\x01\x42\x14\x9d\x9b\xd8\xcb\xef\xe6\
+\xac\xf4\xec\x7c\x2e\x48\xe7\x29\x46\xef\xc1\xa4\x06\x97\x6b\x7e\
+\xa9\xd3\xc2\x5a\xcb\x85\x0b\x17\xce\x6e\x6d\x6d\x7d\x0a\x18\x5c\
+\x5a\xee\xbf\xd1\x06\xc7\xf1\x8d\x8d\x8d\x8f\x0b\x29\x1f\x6a\xb5\
+\x5a\xed\xc5\x4e\x8b\x40\x6b\x4e\xbe\x76\x81\x34\x49\xd0\x41\x40\
+\x41\x50\x08\x3c\xd6\xcf\xb1\xd3\xf3\xcc\xba\xbf\xfc\xee\x8c\x9f\
+\x73\x81\x82\x72\x77\x36\x73\xd5\x50\x07\x5c\xb5\x7a\x80\x76\xa3\
+\x96\x09\x7f\xfe\xfc\xd6\xe6\xc6\xc6\xc7\xbd\xf7\xc7\xdf\xf2\x0e\
+\x8d\xc8\x5a\xc7\xa3\xeb\xeb\xeb\x1f\xf1\xce\x3d\xd0\x6a\xb7\x57\
+\x5a\x8d\x1a\xef\x28\x45\x9c\x3e\x7b\x81\x6e\x6f\x80\xcb\xd2\x6f\
+\xb6\xa9\x21\x40\x64\xbb\x3a\x7b\x89\xf5\x4b\xd8\x5d\xbf\x67\xb7\
+\x66\x8e\x23\x35\x59\x8c\xb4\xea\x55\x0e\x1e\x58\xa6\x14\x85\xa4\
+\x69\xca\xc5\x0b\x17\xce\x6d\x6c\x6c\xdc\xe5\xbd\x3f\x5a\x70\xb8\
+\xaf\x93\xf5\xd8\x0b\x2f\x5c\x7e\x93\x4f\x67\xd8\x9c\xb5\xd7\xb5\
+\xdb\xed\xef\x74\x16\x16\xde\xaf\x75\xb1\xad\xba\xc3\xf9\xb5\x4d\
+\x46\x93\x69\x16\x3b\x2a\x6b\x45\x45\xd1\xd4\x14\x8d\xc8\xbc\x25\
+\xc4\x25\x56\x28\x62\xc7\x7b\xca\xa5\x88\xfd\x8b\x1d\x16\x5a\x19\
+\x23\x3d\x1e\x8d\xb8\x70\xf1\xe2\xb3\xdb\x5b\x5b\x9f\x16\x42\xbc\
+\x58\x14\x75\x97\x03\xf0\x07\xf7\x89\x73\x8d\xbe\xd8\xdd\xd9\x39\
+\x32\x9d\x4e\xbf\xda\x59\x58\xb8\xbb\x52\xa9\x88\x85\x56\x83\x76\
+\xa3\xc6\xd6\x4e\x9f\x8d\xed\x1d\x06\xa3\x31\xc6\xa4\x20\x76\xb7\
+\x9b\x66\x7d\xe7\x6e\x10\xcc\x7c\x1e\xef\x51\x4a\x52\xaf\x96\x59\
+\x68\x36\x68\x37\x6b\xb3\xd7\x19\xba\xdd\xae\x5f\xbb\x78\xf1\x9b\
+\xd3\xe9\xf4\x4b\x85\xcf\xbf\xd1\x06\xdf\x9b\x5a\x40\x08\x81\xcb\
+\xb7\x7f\xac\x73\x48\x21\x3e\x5c\x6f\x34\xbe\xd2\x6c\x36\xdf\x57\
+\xca\xb9\x20\x80\xf1\x64\x4a\x6f\x38\x62\x30\x1a\x33\x9e\x4c\xb3\
+\x1a\x3e\x77\x8f\x82\xcd\x40\x08\x02\xad\x88\x4b\x11\xd5\x72\x4c\
+\xad\x52\xa6\x9c\xb3\x0e\xde\x7b\x06\x83\x01\x5b\x9b\x9b\xbf\xec\
+\xf5\x7a\x5f\xb6\xd6\xfe\x4c\xca\xac\x69\x29\xde\xa1\x78\x23\x0b\
+\xbc\x65\x00\x45\x0a\x15\x42\xe8\x20\x08\x3e\x51\xae\x54\x3e\x5b\
+\xab\xd5\x6e\x2e\x95\x4a\xa8\x39\x66\x6d\x9e\xf7\x2f\xfa\x89\x8c\
+\x96\x9c\x63\xb3\xf3\x23\x4d\x53\x46\xa3\x11\xbd\x9d\x9d\x67\xfa\
+\x83\xc1\xfd\x26\x4d\xbf\x5f\xbc\x33\x21\x84\xf8\xf3\x03\x20\x7f\
+\x5b\x25\xeb\x90\x9c\x16\x42\xdc\x1a\x97\x4a\x77\xc5\x71\x7c\xa4\
+\x14\xc7\x87\xc3\x30\x54\x5a\x6b\x54\x56\xdd\xbd\x6e\x9b\xd5\xce\
+\x36\xba\xa7\x76\x3c\x1a\x9d\x18\x8d\x46\x8f\x8e\xc7\xe3\x07\xbc\
+\xf7\x4f\x09\x21\x4c\xb1\x23\x9f\x6f\xf3\xbe\x25\x00\x7f\xd2\xdb\
+\x2a\x79\xca\x34\x78\xff\xf3\xf1\x64\xf2\xf3\xe1\x70\x18\x2a\xad\
+\xaf\x97\x42\xbc\x3b\x0c\xc3\x1b\x10\xe2\x90\x92\xb2\x2e\xa4\xac\
+\xe4\x89\x60\x68\xad\xed\xb9\xfc\x75\x1b\xef\xdc\xec\x75\x9b\xf9\
+\xfd\xe5\x3f\xe4\xeb\x6f\x74\xfc\xff\x00\x57\x74\x47\xd3\xc3\x02\
+\x5f\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x14\xc9\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\
+\x25\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\
+\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\
+\x46\x00\x00\x14\x4f\x49\x44\x41\x54\x78\xda\xb4\x5a\x79\x90\x5d\
+\x55\x9d\xfe\xce\x72\xef\x7d\xfb\xd2\x5b\xd2\xf4\x9a\xee\x4e\xc8\
+\xda\xdd\x61\x6b\xa2\x35\xc8\xa2\x23\x12\x12\x20\xea\xb8\xa2\x58\
+\xae\x85\x32\xe0\x8c\x16\xe8\x68\x39\x8c\x8e\x25\x35\x8b\x8e\xe8\
+\x08\xd6\xcc\xc8\x80\x0a\x5a\x25\x60\x10\x15\x23\x1a\x03\xa8\x91\
+\x48\xf6\xf4\x96\xde\x5e\xa7\x97\xa4\xdf\xd6\x6f\xbb\xeb\x39\x67\
+\xfe\xb8\xef\xbd\x74\x02\x0a\x3a\xf8\xaa\x5e\x77\x55\xf7\x7d\xf7\
+\xfe\xce\xf9\xfd\x7e\xdf\xef\xfb\xbe\xf3\xc8\x2d\x37\xbf\x1b\xaf\
+\xd6\x4b\x29\x05\x4d\xd3\xde\xdb\xd9\xd5\xf5\x51\xc6\x98\x20\x20\
+\x20\x94\x80\x52\x0a\xa5\x14\x9b\x38\x79\xf2\xeb\x8e\xe3\xfc\x2f\
+\x21\xe4\x55\x7b\x26\xff\x73\x3e\x24\xa5\x84\x10\xa2\x93\x31\x56\
+\xa1\x94\xa6\x57\x2e\xc0\x30\x8c\xee\xd7\x5d\x75\xd5\xa5\x89\x44\
+\x12\xc2\xf3\xa0\xa0\xea\x9f\x99\x9b\x9b\xeb\xb6\x2c\x0b\x8c\xb1\
+\xfa\xbd\x84\x10\x4d\x9e\xeb\x86\xb8\xa6\xa5\x56\xfe\xfd\x95\xbe\
+\xe8\x9f\xba\xc3\xae\xe3\x04\x74\x5d\xbf\x63\x70\xeb\xd6\x03\xc9\
+\x86\x86\x7b\x84\x10\x2b\x2f\x80\x52\xca\x65\x94\x21\x14\x0c\x42\
+\xd3\x34\x40\x01\xc2\xf3\x50\xbd\xce\x5d\x79\x3f\xd7\x75\xd1\xd6\
+\xde\x7e\xcf\x8d\x6f\x7e\xcb\x81\x68\x34\x7a\x87\x69\x9a\x01\x29\
+\xe5\x5f\x66\x01\x9e\xe7\x41\x29\xb5\xb3\xa7\xaf\xef\x37\x37\xee\
+\xda\xf5\xe5\x1b\x77\xed\x6a\x1e\xdc\xba\xf5\x66\xa5\xd4\x50\x3d\
+\x7e\x00\x20\x04\x94\x51\x50\xea\xbf\x19\x63\x60\x8c\x81\x33\x86\
+\x97\x28\x9c\xa1\x37\xfc\xf5\x1b\x6f\xfe\xab\x2b\xae\x68\xbe\xe3\
+\xef\x3f\xf1\xe5\xd7\xbf\xe1\x0d\xbf\x21\x84\xec\xb4\x6d\xfb\x55\
+\x2f\xa1\x4b\x56\xb7\xb6\x7e\x7a\x70\x70\xf0\xa6\x8b\x2f\xbd\x14\
+\x5d\x5d\x5d\x30\x8c\x00\x4c\xd3\xd2\x46\x86\x87\xbf\x74\x6a\x76\
+\xf6\x6a\x4d\xd3\x54\xfd\xa6\x9c\x83\x73\x0e\xc6\x18\xa4\x94\x50\
+\x4a\x81\x10\x82\x95\xb5\x6f\x59\x16\xb9\xf8\x92\x4b\xbe\xd4\xd3\
+\xdb\xa3\x1d\x3a\x74\x10\xb1\x68\x14\xd7\x6d\xdf\x3e\x38\x74\xf9\
+\xb6\x1f\xfe\xe8\x89\xdd\x8f\x8d\x8d\x8e\x7e\x51\x29\x75\xe0\xe5\
+\xfa\xe5\x65\x33\x20\x84\x40\x34\x1a\xbd\xf3\xda\x37\xbd\xe9\xa6\
+\x1b\x6f\xda\x85\xf5\xeb\x37\xc0\x30\x02\x90\x52\xa2\xbb\xbb\x1b\
+\x43\xdb\xb6\x5d\xa9\x69\xda\xdb\x95\xf2\xe3\x27\x00\x18\xe3\x60\
+\x1a\x07\xaf\xee\x7e\xed\x8d\x6a\x0e\xa4\x94\x08\x85\x42\x6f\x7f\
+\xe3\xb5\x6f\xba\xf2\xcc\x99\x33\xa0\x94\xa2\x58\x2a\xe1\xd0\xa1\
+\x43\x10\xc2\xc3\x6d\xb7\xdf\x7e\x53\x6b\x6b\xeb\x9d\x9e\xe7\xfd\
+\xff\x4b\x88\x31\x86\x7c\x3e\xff\x85\x89\x89\x09\x53\xac\xa8\x4f\
+\xa5\x14\x82\xc1\x20\x2e\xbe\xf8\x12\xac\xdf\xb8\xf1\xf3\xae\xe3\
+\x84\xa5\x94\xf0\x84\x70\x19\x63\xe0\x94\x81\x6b\x9a\x5f\x3e\x9c\
+\x83\x71\x0e\xcf\x73\x5d\x21\x04\x6c\xcb\x0a\x5f\x79\xd5\xd5\x9f\
+\x8f\xc5\xe3\x58\x5e\x5e\x06\x63\x0c\x94\xfa\xa1\x30\xce\x71\xf0\
+\x85\x83\xe6\xec\xec\xec\x17\x38\x7f\xf9\x02\x61\x83\x03\xfd\xaf\
+\xa4\x79\x4f\x67\x32\xe9\x64\x73\x4b\xcb\x6b\x3a\x3b\xbb\x6a\xb0\
+\x08\xa5\x14\xc2\xe1\x30\x34\x5d\x6b\x18\x1d\x19\x89\x72\xce\xfb\
+\xd6\xac\x59\x73\xf3\xe6\xcd\x5b\x5a\xc3\x91\x08\xa4\x92\x90\xd5\
+\xeb\x02\x81\x00\x96\x96\xd2\x4d\xcb\xcb\x79\x3d\x99\x4c\xfe\xcd\
+\xdb\xdf\xf9\xce\x6b\xe7\xe6\xe6\x20\x84\x00\x21\xc4\x47\xb0\x40\
+\x00\x5d\x9d\x5d\x78\xf0\x81\x6f\x7d\xb5\x50\x28\x3c\xf4\x4a\x50\
+\x89\xbc\xd4\x1c\x10\x42\xe0\xfc\x0f\x3b\x8e\xd3\xd4\x3f\x38\x78\
+\xe8\x3d\xb7\xdc\xd2\xd6\xba\xba\x15\x4a\x29\x78\x9e\x07\x42\x08\
+\xb2\xd9\x2c\x8e\x1c\x3e\x84\x60\x30\x84\x64\x32\x89\x40\x30\x88\
+\x78\x2c\x06\xdb\x71\xe0\xba\x2e\x6c\xcb\x42\x24\x1a\x45\x34\x1a\
+\x45\xb1\x50\x80\xeb\x3a\x30\x2d\x0b\xf3\x73\xf3\x20\xd4\x0f\xde\
+\x71\x1c\x6c\xd8\xb0\x11\x2f\xfc\xfe\xc0\xdc\xf7\x1e\xfe\xee\x60\
+\x30\x18\x3a\x07\x9e\x3d\xcf\x03\xe7\x1c\xe7\xf7\xc4\x4b\x95\xd0\
+\xd6\x44\x22\xf1\x1d\x21\xc4\xe0\x4a\x88\xe4\x9c\xa7\x47\x47\x46\
+\xee\x3e\x7c\xf0\x60\x3d\xf0\x5a\x26\xe2\xf1\x38\xd6\x5d\xb8\x1e\
+\xed\x1d\x1d\x68\x6c\x6a\x42\x3c\x16\x03\x21\x04\xbc\x5a\x3e\x9a\
+\xa6\xc1\xac\x54\x90\xc9\x64\xa0\x1b\x06\xc2\x91\x28\x32\x99\x0c\
+\x18\x63\xf5\xdd\x4f\x26\x1b\x00\xa5\xf0\xd4\x4f\x7e\x72\xb7\xae\
+\x1b\xe9\x95\x50\x2b\x84\x18\xec\xec\xea\xfa\x0e\x21\x64\xf0\x8f\
+\x96\x90\xe7\xba\x68\x59\xbd\xfa\xfe\xeb\x77\xec\x78\x4b\x34\x16\
+\x7b\x6f\x36\x93\x49\x54\x2a\x95\xc3\x84\x90\x32\xa5\x14\xae\x6d\
+\x1f\x2e\x96\x4a\x57\xf4\xf6\xf5\x76\x27\x93\xc9\xfa\xc3\x01\x20\
+\x18\x0a\x21\x1c\x89\x40\x37\x8c\x7a\xf6\x08\x25\x80\x52\x00\x01\
+\x84\x27\x60\x55\x2a\xc8\xe7\xf3\xc8\xe7\xb2\x80\x52\x90\x4a\x01\
+\x0a\x90\x4a\xa2\xb7\xb7\x0f\xbb\x1f\x7f\x6c\xef\xd8\xe8\xe8\xc7\
+\x0d\xc3\x50\x42\x08\x58\x96\xd5\xd2\xd9\xd5\xf5\x8f\xef\xbe\xf9\
+\xe6\xfb\xdf\xfa\xb6\xb7\x5f\x74\xf2\xe4\x78\xeb\xdc\xdc\xdc\x23\
+\x2b\x7b\x83\xaf\x4c\x13\x65\x6c\xfb\xc0\xd6\xad\x3b\x37\x6c\xdc\
+\x84\x8d\x9b\x36\x07\xd7\xaf\xdf\xf0\x89\xfd\xfb\x7f\xfb\xb6\x91\
+\x13\x27\xfe\xd9\xb2\xac\x6f\x02\x10\x85\xe5\xe5\xd9\xf9\xb9\x79\
+\xb4\x34\xb7\x20\x1c\x89\x9c\x6d\x3e\xc6\x60\x59\x16\xf2\xf9\x3c\
+\x0a\x85\x02\x1c\xc7\x06\x01\x39\xdb\xc4\x8c\x41\x37\x0c\x80\x90\
+\x7a\x89\x72\xa5\x60\x7b\x36\x5a\x5a\x5a\x10\x89\x44\x90\x4a\xa5\
+\x66\x01\x88\x4a\xa5\x42\x12\x89\xc4\x87\xde\xfc\x96\xb7\xfe\xc3\
+\xd0\xb6\x6d\x1d\x67\x4e\x9f\xc6\xc8\xc8\x30\x76\xde\x70\xc3\x0d\
+\xa3\x23\x23\xdb\xa5\x94\x4f\xd6\x9f\x5b\xcb\x80\xeb\xba\x81\xde\
+\xb5\x6b\x1f\xf9\xab\x2b\x5e\xb7\x2a\x99\x4c\x20\x10\x08\xa1\xad\
+\xed\x02\xf4\xad\x5d\x1b\x6f\x69\x59\x75\x7d\xa9\x54\xba\x82\x51\
+\x76\xcd\xce\x1b\x6e\xbc\xb9\xa1\xb1\x11\xd1\x68\x14\x81\x40\x00\
+\x84\x10\xb8\xae\x8b\xa9\xa9\x29\x8c\x8f\x8f\x21\x9d\x5e\xaa\x36\
+\x6d\x10\x01\xc3\x80\xa6\x6b\xf5\x81\x16\x08\x06\x10\x8b\xc5\x61\
+\xe8\x3a\x1c\xc7\x86\x10\x02\x52\x49\x84\x82\x21\xb4\x77\x74\x60\
+\xcb\x96\xfe\x81\x91\xe1\xe1\xde\xfe\x81\xc1\xdb\xde\xff\xc1\x0f\
+\xde\xd6\xda\xda\x1a\x1f\x1e\x19\xc6\xd2\xd2\x19\x14\x0b\x05\xac\
+\xe9\xe9\x85\xe3\x3a\x9b\xc7\xc7\xc6\x1e\xd0\x34\xcd\xab\x37\xb1\
+\x94\x12\x86\x61\xdc\xbe\xf3\xc6\x1b\xbf\xb2\x79\xcb\x16\xe8\xba\
+\x0e\x4d\xd3\xa1\xeb\x3a\x0c\x43\x87\x10\x12\x8b\x8b\x0b\x38\x73\
+\xfa\x0c\xa2\xd1\x28\xda\xdb\xdb\x91\x48\x26\x01\x00\x4b\x4b\x4b\
+\x38\x7e\xfc\x18\x00\xa0\xbd\xbd\x03\x4d\x4d\x4d\xd0\x75\x1d\x4a\
+\x29\x48\x29\x21\x85\x80\x27\x3c\x38\x8e\x0b\xe1\x79\x60\x8c\x21\
+\x1a\x8d\x02\x84\x60\x7e\x7e\x0e\xe9\xa5\x25\xd8\x8e\x83\xa6\xa6\
+\x66\x6c\xd9\xb2\x05\xa6\xe9\x97\xd9\xcc\xcc\x0c\xb2\x99\x4c\xbd\
+\xcf\x84\x10\x88\x44\xfc\x67\x7f\xf9\xdf\xfe\xf5\x8e\x62\xb1\xf8\
+\x1f\x8c\x31\x3f\x03\x9e\xe7\x75\xf5\x0f\x0e\x3e\x36\xb8\xf5\x22\
+\x2d\x60\x18\x55\x7c\x3a\x3b\x39\x19\xe3\x48\x24\x12\x48\x24\x12\
+\x88\x44\x22\x88\xc5\xe3\x20\x84\x60\x6a\x6a\x12\xc7\x8e\x1d\x45\
+\x5b\x5b\x1b\x06\x06\xb6\xa2\xa9\xa9\x09\x86\x61\x40\x4a\x09\xd7\
+\x75\xe1\xba\x2e\x56\x72\x1b\x21\x25\x6c\xdb\x42\xa1\x58\x84\xc6\
+\x39\x2e\x68\x6b\x03\x21\x04\xc5\x62\x11\xc5\x42\x11\xe5\x4a\x19\
+\x99\x4c\x06\x53\x53\x53\xb0\x6d\x1b\xb4\xda\xe4\xb5\x12\x37\xcd\
+\x0a\x7a\x7a\x7b\xa0\x6b\xfa\xeb\x8e\x1e\x39\xf2\x6d\xce\xf9\x32\
+\x1b\x1c\xe8\x07\x21\x24\xe4\x38\x4e\x0b\x80\xcd\xd1\x68\x94\x05\
+\x82\x41\x50\x4a\x40\x08\xad\xa2\x8d\x1f\x80\xa6\xeb\x08\x04\x03\
+\xa0\x94\x61\x72\x72\x12\xe3\xe3\xe3\xd8\xb2\xa5\x1f\x3d\x3d\xbd\
+\x60\x8c\xc1\xf3\x3c\x2c\x2e\x2e\x62\x6e\xee\x14\xe6\xe7\xe6\x91\
+\xcd\x64\x50\xa9\x54\x20\xa5\x44\x38\x12\x86\xa6\xe9\x90\xd2\x87\
+\xc4\xc2\xb2\x0f\xa7\x1d\x9d\x9d\xa0\x94\xa0\xb0\x5c\x40\xb1\x54\
+\x44\xb9\x5c\x06\xa5\xf4\x9c\xc0\xa5\x10\x48\x26\x93\x58\xb7\x6e\
+\x1d\x26\x27\x26\xdc\x67\x7e\xf5\xab\xef\x94\x4a\xa5\x3d\x94\xd2\
+\x52\x6d\x01\xa5\x72\xb9\xbc\x7b\x72\x62\xe2\x17\x0b\xf3\x0b\x5d\
+\x7a\xc0\xe8\x49\x24\x12\xd0\xb8\x56\xbf\x19\xa5\xb5\x6c\x50\x2c\
+\x2d\x9d\xc1\xc8\xc8\x30\x36\x6f\xde\x82\xf6\xf6\x76\x00\x40\x2e\
+\x9f\xc3\xf1\xe3\xc7\x30\x3a\x32\x82\xc9\x89\x09\xf3\xcc\xe9\xc5\
+\xd1\xa5\xd3\xa7\x9f\x59\x58\x98\x1f\x3e\xbd\xb0\x28\xf3\xf9\xe5\
+\x68\x32\x99\xd0\x56\xad\x5a\x0d\x21\x04\x5c\xc7\x41\xb9\x5c\x86\
+\xeb\x38\x68\x6f\xef\x40\xa5\x52\x46\xa5\x5c\xa9\x23\x98\x52\x0a\
+\xc2\xf3\x10\x08\x06\xd1\xdb\xdb\x0b\x42\x19\x7e\xf4\xc4\xee\xa7\
+\x7f\xf4\xc4\x13\xb7\x2c\x2f\x2f\x7f\x8d\x73\x5e\x3a\xa7\x89\xab\
+\xec\x71\x36\x9b\xcd\x3c\x38\x36\x3a\x7a\x32\x9f\xcf\x6f\x8e\xc7\
+\x13\x8d\xc1\x40\x00\x8c\x71\x50\x4a\xc0\x18\x85\xe3\xb8\x38\x76\
+\xec\x18\x5a\x5b\x2f\x40\x4f\x4f\x0f\x08\x21\x58\x5c\x5c\xc0\xe1\
+\x43\x87\x30\x3e\x3a\xea\x66\xb3\xb9\xaf\x72\xce\x3e\x44\x29\xfd\
+\x7c\x2c\x1e\xff\x7e\x20\x10\xf8\x7e\xa9\x54\xfe\x46\x3e\x97\xfd\
+\xc1\xc8\xf0\xb0\xeb\x38\xee\x25\xeb\xd7\xaf\x67\x84\x52\x58\xa6\
+\x89\x52\xa9\x04\x10\x82\xc6\xa6\x26\xe4\xb2\xb9\xfa\x8c\x01\x80\
+\xd5\xab\x57\xa3\xa9\xa9\x19\xcf\x3d\xfb\xec\xd8\xf7\x1e\xfe\xee\
+\xed\xd3\xd3\xd3\x9f\xd4\x75\x7d\x76\xe5\x90\x7d\x11\x95\x60\x8c\
+\x41\x29\x75\x74\x36\x95\xfa\xf6\xe8\xc8\x48\xb8\xbd\xa3\x63\x28\
+\x1a\x8b\x82\x73\x0d\x9a\xc6\x31\x37\x77\x0a\x95\x4a\x05\x9b\x36\
+\x6d\x04\xe7\x1a\x32\x99\x0c\x8e\x1c\x39\x82\x93\x63\xe3\x39\x42\
+\xe9\x0d\xb1\x68\xec\x3e\xc6\x59\x5a\x4a\xe9\xeb\x81\x2a\x15\x37\
+\x0c\x3d\xad\x69\xfa\x53\xa7\x66\x53\xbf\x4e\xa7\x97\x76\xf4\xf7\
+\x0f\x04\x1d\xc7\x81\xeb\x38\xa8\x94\xcb\x88\xc5\xe2\x00\x14\x0a\
+\x85\x02\x08\x21\x58\x77\xe1\x85\x20\x84\xe2\xde\xaf\x7c\xf9\xde\
+\x83\x2f\xbc\xf0\x2e\x42\xc8\xf3\x9a\xa6\xbd\x78\x12\xd7\x3a\x7c\
+\xe5\x5b\x4a\x09\x42\x48\xde\x75\xdd\x63\x91\x68\xa4\x8e\x02\x96\
+\x65\x23\x93\xc9\xa0\xad\xad\x1d\x9a\xa6\xc3\xb6\x6d\x4c\x4d\x4d\
+\xe1\x54\x6a\xd6\x32\x0c\x63\x57\x3c\x16\x7b\x5a\x4a\x51\x1f\x6e\
+\xe7\x8b\x21\x7f\xe2\x26\x9f\x9e\x4d\xa5\x76\x3d\xf7\xec\x33\x56\
+\x63\x63\x23\xb4\x2a\x62\xe5\xb2\x59\xc4\x62\xb1\x3a\x08\x40\x2a\
+\xe8\xba\x06\xd3\x34\x8f\x31\x4a\xf3\x2f\x15\xa7\x52\x0a\x5c\xd3\
+\xf5\x5b\x82\x81\x40\x3b\x00\xb5\x72\x75\x9e\xe7\xc9\x35\xbd\xbd\
+\x6f\x6b\x6c\x6c\x02\xe0\x43\x62\xa5\xe2\xd7\x68\x43\x43\x03\x94\
+\x52\xc8\x64\x32\x38\x73\x7a\x11\xae\xeb\xdc\xdf\xdc\xdc\xb2\x57\
+\x4a\x09\xc6\x3c\x78\x2b\x55\xda\x39\xd9\xa5\x60\x9c\xa3\xb1\xb1\
+\x71\xef\xf0\x89\x13\xf7\x77\x76\x76\xdd\x1e\x8d\x44\x20\x3c\x0f\
+\xa6\x69\x42\x37\x0c\x04\x83\x41\x38\xb6\x83\x6c\x36\x8b\x35\x3d\
+\x6b\xb0\x79\xcb\x96\x5b\xc7\x46\x47\x1b\xb9\xa6\x51\x28\x5f\xa0\
+\x2a\x7f\x82\x13\xd3\xac\x9c\xe2\x7d\x7d\x7d\x7f\x77\xf5\x35\xaf\
+\xdf\xe2\xba\xae\xcf\xdb\x39\x83\xc6\xb5\xb3\x93\x53\x29\x48\xa1\
+\xe0\xb9\x2e\x4c\xd3\x44\x38\x14\x86\xae\x6b\xf0\x3c\x81\x52\xa9\
+\x84\xe5\xfc\x72\x39\x14\x0a\x7d\x4d\x08\x0f\x4a\xa1\x5a\x36\x04\
+\x2b\xb9\xbc\x52\xaa\xae\x09\x64\x95\x7d\x72\x8d\x7f\x6d\x64\xf8\
+\xc4\x07\xb6\x5e\x74\x71\x98\x6b\x1a\x5c\xc7\x81\x59\x31\xc1\x19\
+\x83\xa6\x6b\xa8\x98\x15\x64\xd2\x19\x5c\x7b\xdd\x75\x03\x57\x5d\
+\x7d\xcd\x80\x94\x02\x42\x48\x78\x9e\x07\x21\x3c\xe8\xba\x81\xa7\
+\x7f\xbe\xe7\x28\xd7\x34\xad\x4c\x29\x85\x69\x9b\xd0\x98\x06\x1d\
+\x7e\x4a\x35\xe5\xd7\xaf\xf0\x3c\x18\x86\x01\x4a\x29\xa4\x10\x08\
+\x04\x83\x90\x52\x01\x70\xe1\xb9\x2e\x84\xf0\x26\x19\x63\x27\x5d\
+\x77\x85\xdc\x25\xd5\x5e\xaa\x4a\x98\xda\x30\x5a\xb9\x28\x4e\xd9\
+\xc9\xa5\xa5\xa5\xc9\xe5\x42\x61\x0b\xe7\x0c\x52\x30\xd8\xb6\x05\
+\x29\x25\x74\x4d\x83\xed\x38\x98\x9a\x9a\x02\xc8\xd9\xf2\xab\xa9\
+\x3b\x29\x04\x12\x89\x04\x28\xa5\x65\x4e\x08\xf1\xd3\x22\x15\x3c\
+\x78\x20\x0e\x01\xf4\xda\x2c\xf3\x87\x99\x57\x9d\xa0\x94\x52\xe8\
+\x9a\x56\x15\xe8\xaa\xfa\x3f\x71\x32\x18\x0c\x9d\x5b\xf7\x04\xd0\
+\xb8\x56\x2f\x3d\xc3\x30\x6a\x8a\x79\x45\x39\x71\x14\x8b\xc5\x93\
+\xf9\x7c\x6e\x4b\x4b\x73\x0b\xb8\xa6\xc1\x71\x1d\x38\xae\x07\x4a\
+\x29\x38\xf7\xe5\x68\x2d\xe8\x97\xa2\xfc\x42\x08\x50\x1f\x3e\x49\
+\xfd\x42\x4f\x78\x10\x9e\xdf\xc8\x5e\xd5\x4d\xf0\x3c\x0f\x9e\xe7\
+\x41\xd7\x75\xd0\xea\xc0\x52\x0a\x88\xc6\xa2\x60\x8c\xbe\xf8\x01\
+\xaa\xfa\xc3\x57\xf9\x2f\x25\xe6\x41\x08\xa0\x94\x84\x65\x5a\x20\
+\x94\x42\xd7\xfd\x8d\xf1\x84\x07\xc6\x38\x38\xf3\x75\x35\x01\x59\
+\x31\x8b\xfc\xdf\x84\x52\x7f\x71\x4a\x81\x33\xc6\x6b\xd0\x79\x76\
+\x80\x08\x01\xca\x28\x04\x11\x20\x1e\xa9\x13\x36\x23\x10\x80\xa6\
+\x71\x78\xae\x0b\x4a\x29\xe2\xf1\x38\xc2\xe1\x70\x9f\x65\x5a\x60\
+\x9c\xad\x74\x57\xe0\xba\x0e\x02\x81\x20\x08\x01\x6c\xc7\x01\x65\
+\xf4\x45\xa8\x44\x08\xed\x03\xf1\xfb\xc6\x1f\x5c\x02\x4a\x4a\x70\
+\xce\x21\xa5\xcf\x58\x15\xf7\xe3\x01\x45\xdd\x1c\x58\xb1\x03\xe0\
+\x42\x78\xe1\x70\x28\x8c\xb6\xb6\x36\xbf\xd1\x28\x03\xe3\x1c\x8c\
+\x51\x18\x86\xe1\x63\x7d\x3a\x0d\xd7\x75\x41\x08\x81\xa1\x1b\x28\
+\x95\x4a\xa0\x8c\x21\x12\x49\xa0\xa9\xa9\xb9\x67\x66\x7a\xba\x2f\
+\x10\x0c\x9e\x04\x54\x3d\x78\x5f\x2a\x56\x8d\x30\x25\xc1\x29\x07\
+\xab\x71\x12\x42\x60\x5b\x56\x1f\xe3\xac\x27\x12\x89\xc0\x30\x74\
+\xd8\xb6\x03\xdb\xb1\xa1\x6b\x7a\x75\x01\x12\x8c\x31\x38\x84\xc2\
+\xb1\x6d\x40\x2a\x08\xa5\xa0\xa4\x9f\x59\xcf\xf5\x20\x84\x17\xe6\
+\x27\x8e\x1f\xff\xf7\xe9\xe9\xe9\x76\x42\x88\x22\x35\x12\x07\xc0\
+\x71\x5d\xb9\x61\xc3\x86\xb7\x7d\xe4\xd6\x5b\x07\xca\xa5\x22\x4c\
+\xd3\x84\x63\xdb\xd0\xaa\x42\xc6\x75\x1d\x38\x8e\x83\x8d\x9b\x36\
+\x85\x27\x26\x26\x3e\x46\x29\xb9\x43\x29\xc0\xb2\x2c\x48\x29\xce\
+\x19\x38\xa4\xea\x44\x40\x29\x68\x9a\x0e\x42\x08\x2a\xe5\xca\xc7\
+\x5a\x5a\x57\x87\xe3\xb1\x38\x18\x67\x90\x15\x01\xdb\xb6\x91\x88\
+\x27\x40\xab\xfd\x26\x25\xb0\xe7\x67\x3f\x3b\x9c\x9a\x99\xfe\xde\
+\x4a\x18\x85\x9f\x09\x62\x59\xd6\x29\x6e\xdb\xf6\x03\x66\xa5\x52\
+\x4f\x89\x5a\xd1\x24\x87\x6d\x3b\x93\xc9\x64\xee\x8f\xc5\x13\xc8\
+\x66\x73\x30\x4d\x13\xa8\x0a\xf4\x52\xa9\x84\x72\xb9\x8c\x96\x96\
+\x16\x6c\xd8\xb8\xf1\xc3\xc7\x8e\x1e\x79\x3c\x16\x8b\xed\x15\x42\
+\xf8\x4a\xec\xfc\x9a\x3f\x6b\x49\xa2\x54\x2a\x5e\xc9\x75\xed\xc3\
+\xad\xad\xad\x68\x68\x68\x00\x05\xc5\x72\x61\x19\x84\x10\x44\xa2\
+\x51\x78\x55\x44\xcb\x66\xb3\x98\x9c\x9c\xf8\x4f\xb3\x52\xf9\x66\
+\x4d\xc0\xd4\x36\x59\x29\xe5\xd3\x1f\x42\x48\xb5\x64\xfc\xd2\xe1\
+\x9c\x83\x52\x0a\x21\x44\x22\x18\x0c\x6e\xb6\x6d\x1b\x84\x00\xb2\
+\x2a\xbc\x97\x0b\x05\x44\xab\x9a\xd7\x71\x1c\x14\x0a\x05\x5c\x36\
+\x34\x14\x68\x59\xb5\xea\xd1\x6c\x2e\x77\xcd\xf9\x06\xd6\xd9\xa6\
+\x25\xa0\x94\xa1\x58\x28\x5c\xe3\x38\xce\xa3\x9d\x9d\x9d\x81\xee\
+\xee\x35\xd0\x0d\x03\xae\xeb\x62\x71\x71\x11\x0d\x0d\x0d\x30\x0c\
+\x03\x8c\x73\x28\x28\x94\x2b\x65\x70\xc6\x36\x2b\xa5\x12\x35\x51\
+\x54\xcb\x4e\x4d\x4f\x9f\xc3\x85\x94\x52\xb0\x6d\x1b\x81\x40\xe0\
+\x5d\xdb\xaf\xbf\xfe\xdb\xef\x7c\xd7\xbb\xaf\x2b\x15\x4b\x48\xcd\
+\xa6\x00\x02\x50\x42\xe1\xb9\x2e\x6a\x14\xa0\x5c\x2a\x41\x29\x09\
+\x42\x08\x7a\x7a\x7a\x82\x66\xa5\xf2\x8e\xc5\xc5\xc5\xa4\x92\x72\
+\x06\x40\x3a\x18\x08\x80\x50\x0a\xdb\xb6\xe1\x79\x62\x7d\xb1\x58\
+\xfc\x34\xd3\xf8\x57\xd7\xf4\xf4\x84\x37\x6d\xda\x84\x96\x96\x16\
+\x28\xa5\x30\x39\x39\x09\xb3\x52\x41\x6f\x6f\x1f\x08\x21\x90\x4a\
+\x81\x51\x86\x50\x28\x8c\xae\xae\xae\x21\x29\xe5\xae\x4c\x26\x93\
+\x73\x5d\xf7\xe8\xf9\x6e\x49\xdd\x56\x71\x1c\x07\x84\x90\xd7\x5e\
+\x7a\xd9\x65\x77\x5f\xb7\xfd\xfa\x6b\x82\xa1\x20\xa6\xa7\xa7\x61\
+\x9a\x26\x02\x46\x00\x9a\x5e\x35\xa9\x18\x47\x63\x63\x23\x3a\x3a\
+\x3b\x91\x5e\x5a\x82\x65\x59\x30\x02\x81\xea\xd4\x96\xbe\xb4\x1c\
+\x1d\x33\x8b\xa5\xe2\x84\xe7\xba\xa3\xd5\xed\xbf\x90\x10\xd2\x9b\
+\x6c\x48\x06\xdb\xdb\x3b\xb0\xb6\x6f\x2d\x1a\x1a\x1b\x21\xa5\x44\
+\x2a\x35\x83\xd1\x91\x11\x6c\xd8\xb8\x11\x0d\xc9\x06\x38\x8e\x03\
+\xcf\xf3\xe0\x7a\x2e\x1c\xdb\x85\xeb\xb9\x30\xcd\x0a\xa6\x26\x27\
+\xf1\xfb\x03\x07\x9e\x4e\xa5\x52\x9f\x23\xc0\x73\xb5\x85\xf0\x6a\
+\xbd\xb7\xf6\xf4\xf4\x7c\x61\xfb\x8e\x1d\x37\x77\x77\xaf\xd1\x66\
+\x66\x66\x90\x39\x99\xa9\x3a\x6a\xcc\x87\x55\x71\x16\xef\x73\xb9\
+\x1c\xb8\xa6\xa1\xa1\xa1\x01\x52\x49\x78\x9e\x4f\x43\xb8\xc6\xd1\
+\xd9\xd1\x89\x78\x3c\x1e\xcc\xa4\xd3\x9b\x8b\xa5\xd2\xe6\x1a\x2b\
+\x4d\x24\x12\x68\x6e\x6e\xc6\xaa\x55\xab\xeb\xb0\x3d\x37\x77\x0a\
+\x23\xc3\xc3\x58\xd3\xd3\x8b\xe6\xe6\x16\x5f\x72\x72\xff\x7f\x42\
+\x4a\x30\x26\x20\x84\x8f\x7c\xbd\xbd\x7d\x58\xb5\x7a\xf5\x35\x63\
+\x63\x63\x57\x1c\x39\x78\xf0\xa1\x6c\x36\xfb\x19\x4a\xe9\x02\xaf\
+\x0a\x7a\x7d\x68\xdb\xb6\x77\x5c\xb8\x7e\x83\xf6\xec\x33\xfb\xc0\
+\xb9\xbf\xd3\x84\x90\xfa\x84\x86\x03\xac\x6e\x6d\x05\x65\x14\xc5\
+\xe5\x02\x32\xe9\x34\x94\x94\x88\x44\xa3\xb0\x6d\x0b\x42\xf8\xd3\
+\x3a\x18\x0a\x21\x14\x0e\x63\x55\xcb\x2a\x48\x25\x41\x08\x05\xe7\
+\x1c\x86\x61\xd4\x7b\xc3\x75\x5d\x8c\x8e\x8c\x20\x95\x9a\x41\x5f\
+\xdf\x5a\x74\x76\x75\x41\x4a\x89\xe5\x42\x01\x94\x12\x84\xc3\xe1\
+\x3a\x75\x60\xd5\x69\xcc\x18\x43\x38\x14\xc6\xe6\x4d\x9b\xb4\x7c\
+\x36\xfb\x8e\xa5\xa5\xa5\x7f\xa2\x94\xa2\xa6\xc8\x96\x67\x53\xa9\
+\xd2\xa5\x97\x0d\x5d\x6b\x5b\xb6\xaf\x47\x57\x88\xe9\x64\x32\x89\
+\xde\xbe\x5e\x34\x37\x35\xa3\xb5\xb5\x15\xa5\x62\x11\xb6\x6d\xc3\
+\x71\x1c\x28\x29\xa1\x71\xad\xee\xc2\x29\x00\x1a\x67\xe0\x5c\x83\
+\xae\xeb\xd0\x75\x03\x8c\x71\x28\x25\x51\x31\x2b\x98\x4d\xa5\x70\
+\xe4\xc8\x61\x94\xcb\x65\xf4\xf7\x0f\xa0\xbd\xa3\xa3\x8e\x38\x33\
+\xd3\xd3\x98\x9f\x9f\x87\x54\x12\xd1\x48\xa4\x9e\x29\xdf\x3f\xf2\
+\xf9\xd0\xec\x6c\x0a\xbf\x7e\xee\xb9\x4f\x4a\x29\x9f\xaa\x37\x31\
+\xa5\x14\x85\x42\xe1\x30\x65\x74\xe7\xb6\x6d\xaf\x59\x75\xea\xd4\
+\x29\x08\x21\x10\x0c\x04\xb0\x7e\xc3\x46\x84\xc3\x61\xec\x7e\xfc\
+\xb1\x5f\xee\x7e\xfc\xf1\x67\x37\x6c\xdc\x34\xd0\xd1\xd9\x09\xb3\
+\x52\x81\x6d\xdb\xd5\x91\x2e\x01\x28\x54\xca\x15\xe4\xb2\x59\x64\
+\x32\x19\x2c\x2f\xe7\x91\xcb\xe5\x91\xc9\xa4\xb1\xb0\x30\x8f\xa9\
+\xa9\x29\x4c\x4d\x4e\xa2\x54\x2a\xa2\xa3\xb3\x13\xfd\x03\x83\x88\
+\xc7\xe3\x75\x5a\x3e\x39\x31\x81\x74\x7a\x09\x3f\xfd\xf1\x8f\x1f\
+\x3a\x39\x3e\x9e\x25\x84\xac\x89\x44\x7c\xeb\x86\x56\x61\xb3\x58\
+\x2c\xe0\x99\x7d\xfb\x8e\x2c\x2e\x2e\x7e\x84\x73\xee\x9d\xa3\xc8\
+\x18\x63\xde\xcc\xf4\xf4\xec\x25\x97\x5d\xf6\xce\x70\x28\x84\xc6\
+\xc6\x26\x74\x77\xaf\xc1\xf3\xbf\xdb\x3f\xfb\x3f\xff\xfd\x5f\x77\
+\x8e\x8d\x8e\xde\xbe\xbc\xbc\xfc\xd8\xc8\xf0\x89\xde\x4b\x87\x86\
+\x06\x4a\xe5\x52\xad\xf1\x51\x83\xe2\x40\x20\x80\x40\x30\x00\xc0\
+\x87\x58\xcb\xb2\xea\xd7\xc4\xe3\x09\x74\x77\x77\x63\xdd\x85\xeb\
+\xd1\xdc\xdc\x5c\x67\xab\x3e\x80\xd8\x98\x4e\x4d\xe3\xa7\x4f\xfe\
+\xf8\xa1\x5c\x36\xfb\x9e\x7c\x3e\xff\xe0\xf8\xd8\xd8\xc2\x52\x3a\
+\x3d\xa0\xe9\x5a\x3c\x1a\x8d\x41\x4a\x81\x23\x47\x8e\xe0\xe0\x81\
+\x03\xef\x67\x8c\x0d\xd7\xca\xb1\xee\xcc\x51\x4a\x61\x59\xd6\x93\
+\x8f\xff\xe0\xd1\xdd\x7f\xfb\xf1\x8f\xef\x3c\x7c\xf0\xa0\xf9\xe0\
+\x03\x0f\x7c\x7d\x7a\x7a\xea\x5f\x0c\xc3\x38\x13\x0a\x85\x60\x56\
+\x2a\xac\xbd\xa3\xa3\xc3\x34\x2b\xc8\xe5\x72\xd0\xb8\x56\x27\x7d\
+\x8c\x31\x50\x42\xa0\x1b\x3a\xa2\xd1\x18\xb4\xaa\xb5\x5e\x23\x61\
+\xb5\xf9\x52\xab\x6d\xdf\xf1\xf0\xf9\x5e\x32\xd9\x80\xce\x8e\x2e\
+\x44\x63\xb1\x8e\x5c\x2e\xc7\x34\x4d\x13\x9e\xe7\xdd\x7f\xe4\xe0\
+\xc1\xc7\xa6\xa7\xa6\x3e\x39\x38\x38\xf8\xd1\xee\x35\x6b\x82\x47\
+\x0f\x1f\xfe\xa1\x90\xf2\xc9\x95\xd6\xe2\x39\x73\x80\x73\x8e\x74\
+\x3a\x3d\x7a\xf0\x85\xdf\x47\x7f\xbe\x67\xcf\xfb\x0b\x85\xc2\x43\
+\x81\x40\xa0\x5c\x1d\x6c\x88\x44\x22\x1f\x78\xcf\x2d\xef\xbb\x2d\
+\x93\xa9\x72\x23\x4a\xea\x01\x26\x12\x89\xaa\xb5\xe2\xfa\x7c\xa5\
+\x1a\x60\xcd\x3f\x35\x4d\x13\xf9\x7c\x1e\x0b\x0b\x0b\xc8\x65\xb3\
+\x48\x36\x24\xeb\xd9\x53\x4a\x21\x1a\x8d\xc2\xf5\xdc\xee\xf1\xb1\
+\xb1\x53\x52\x88\x17\xaa\xc3\xaa\x6c\x5b\xd6\x9e\x54\x2a\xf5\x64\
+\x6a\x66\x26\x96\xcb\xe5\xbe\x48\x29\x5d\xfc\xa3\xe7\x03\x4a\xa9\
+\xc5\x5c\x2e\xf7\x28\xe7\x7c\x71\xe5\xd0\xb0\x6d\xbb\x69\xfb\x8e\
+\x1d\x8f\xb4\x77\x74\xc4\xe6\xe7\xe7\x7d\x94\xa2\x67\x1f\xbe\xbc\
+\xbc\x0c\xcb\x32\xc1\x35\x1d\x8e\xe3\x40\xd3\xb5\xb3\xe5\xc5\x18\
+\x66\x66\x66\x30\x3f\x3f\x87\xc5\x85\x05\xcc\xcf\xcf\xa3\xa9\xa9\
+\x19\xe1\x70\xf8\x9c\xcd\x0b\x85\xc2\x58\x98\x9f\xbb\x68\x61\x61\
+\xe1\x41\xc6\x58\x65\x85\x5b\xb2\x68\x9a\xe6\xa3\xe7\x07\xff\x92\
+\x67\x64\x84\x90\xba\x9b\xb0\xd2\xe2\xee\xe8\xe8\xb8\x6b\xe8\xf2\
+\xcb\xdb\xa6\xa6\xa6\x6a\x54\x03\x84\x12\x18\x55\x2a\xf0\xcd\xfb\
+\xef\xbb\xd7\xb6\xac\xf1\xbe\xb5\x6b\xdf\x77\xfd\x8e\x9d\x5b\xc3\
+\x91\x30\x84\xa0\x55\x0e\xef\x33\xd4\xa7\xf7\xec\x39\x78\x6a\x76\
+\xf6\x5b\x94\xb1\xb5\xc9\x86\x86\xdb\x9a\x9a\x9a\xea\x1c\x5f\x29\
+\x85\xd5\xab\x57\x63\xe8\xf2\xcb\xdb\xa6\xa7\xa6\xee\x2a\x97\xcb\
+\x9f\x38\xc7\x3e\xf9\x03\x87\x1d\xf4\x15\x9c\xce\x80\x10\x32\x70\
+\xed\x75\xdb\x6f\xcd\xe7\xf3\x3e\xa1\x03\x20\xa4\x80\xe7\x7a\x88\
+\x46\xa3\x78\x66\xdf\xbe\x89\xf4\xd2\xd2\xa7\xca\xe5\xf2\xbd\x53\
+\x93\x93\x0f\x3b\x8e\x53\x17\x27\x35\xf7\xc0\x75\x5c\xcc\x9d\x3a\
+\xf5\xb0\x65\x9a\xf7\x96\x8a\xc5\x4f\xfd\xfa\xd9\x67\x27\xe6\xe6\
+\x4e\xbd\x68\xe3\xfa\xfb\x07\x30\xb8\x75\xeb\xad\x00\x06\x5e\x95\
+\x63\x56\xe1\x79\x68\x6b\x6b\xff\x4c\x7f\x7f\x7f\x50\x54\x4d\xa7\
+\x9a\xf5\x12\x08\x06\xb1\x74\x66\x09\xfb\x7f\xfb\x9b\xcf\xea\xba\
+\x5e\xae\x5a\xe9\x9a\xcf\x3a\x25\x84\xeb\x55\xa5\x9f\x0f\xb5\x8c\
+\x73\x8d\x52\x0a\x4d\xd3\xca\xe3\x63\x63\x9f\x7d\xfe\x77\xbf\x83\
+\x65\x59\xe7\x56\x00\xa5\x58\xd3\xd3\x13\x8c\x27\x12\x9f\x11\x7f\
+\xc0\xdd\xf8\xd3\x0e\xf9\x38\xc7\xe9\xd3\x8b\xf7\xdc\x7f\xdf\x37\
+\x1e\x13\x42\x62\xdd\xba\x75\x08\x06\x83\x90\x52\x22\x1a\x8d\xe2\
+\x97\xbf\xf8\xc5\x5e\xd3\x34\x1f\xa9\xd1\x5d\x05\x05\xd7\x73\x21\
+\xab\x9a\xd5\xf3\x7c\x9b\xa5\x2e\x3b\x49\x5d\xe1\x3d\xb2\xff\xb7\
+\xbf\xdd\x7b\xf2\xe4\xb8\xdf\x63\x8e\x8d\x91\xe1\x61\x3c\xf1\xc3\
+\x1f\x62\xcf\x53\x4f\x3d\x56\x2a\x16\xef\x79\x25\x67\x64\x2f\x7b\
+\x0c\x58\xc5\xdb\x03\x27\x8e\x1f\xdf\x75\x72\x7c\x7c\xe7\x65\x43\
+\x43\x77\x6f\x7b\xcd\x6b\x07\x2f\xb8\xa0\x0d\x27\x8e\x1f\x77\x87\
+\x8f\x1f\xbb\xcb\x30\x0c\xb5\x52\x0f\x0b\x4f\xd4\xcf\xb4\x84\xe7\
+\x41\x78\x1e\xce\x3f\x81\xd7\x34\x4d\xcd\xcf\xcd\xdd\xf5\xfc\xfe\
+\xfd\xcf\x84\x82\x41\xed\xf8\xf1\xe3\xf8\xdd\xfe\xfd\x87\x52\x33\
+\x33\x9f\x93\x52\xee\x7e\x25\x27\x94\x7f\xd2\x77\x25\x0c\xc3\x80\
+\x52\x6a\xf7\x33\xfb\xf6\xfd\xec\xd8\xd1\xa3\x1f\x19\xba\x7c\xdb\
+\xa7\x8f\x1f\x3b\xf6\x04\x08\xd9\xff\x47\x1c\x03\x28\x05\xa8\x3f\
+\xe0\x2c\x10\x42\xf6\x1f\x3e\x74\xe8\xa1\xa5\xa5\xa5\x1d\xe3\x63\
+\x63\x5f\x34\x4d\xf3\x3e\xce\xb9\xf5\x4a\x83\xff\x93\xbf\xec\x41\
+\x08\x41\x20\x10\xb0\x4a\xa5\xd2\x57\x9e\xfa\xe9\x4f\x1e\xd5\x34\
+\xad\xb2\xf2\x61\x55\xb5\xa4\x81\x00\xa6\x65\xc2\xb4\x2c\x30\x46\
+\x51\x2a\x73\x7f\x41\x84\x68\xe7\xfb\xb0\xb9\x5c\xee\xce\x74\x3a\
+\x7d\x37\x63\x2c\x75\x3e\xfa\xfd\xc5\xbe\xad\x52\x75\xd9\x52\x2f\
+\x5a\x20\xa5\xb0\x2d\x6b\x7a\xdf\xde\xbd\xcf\x13\x42\xc4\x4a\x4f\
+\x47\x29\xc5\x1c\xc7\x99\x7e\x91\x39\x4b\x69\xba\xd6\x3f\x7f\xce\
+\xeb\xff\x06\x00\x7e\xe5\xec\x94\xcb\x86\x8c\xb1\x00\x00\x00\x00\
+\x49\x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x0f\xf6\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x00\x57\x02\xf9\x87\
+\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xda\x08\x11\x06\x33\x0c\x5e\
+\xf4\xbb\x7b\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\
+\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x04\x67\x41\x4d\x41\
+\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x0f\x85\x49\x44\x41\x54\
+\x78\xda\xad\x5a\x5b\x6c\x5c\x57\xb9\xfe\x67\xcf\x9e\xab\xc7\xf6\
+\x78\xec\xc4\xb7\x1a\xda\x26\x34\x21\x27\x81\x34\x04\xd2\x3c\xa4\
+\x84\x54\xa4\x2f\x48\x45\x08\x04\xe2\x85\x67\x2a\xf1\x82\x10\xe2\
+\x81\x47\x1e\x10\x42\xbc\x20\xc1\x33\x2f\x5c\x44\x85\x44\x41\xc0\
+\x39\xa4\x6d\xd2\x1e\x29\x4d\x7a\xda\x02\x0d\xc5\x86\x24\x4d\x1a\
+\x3b\xbe\xc4\x97\x19\x7b\x3c\x9e\xdb\xde\xfb\x7c\xdf\xbf\xd6\xda\
+\xb3\xc7\xf1\x39\xa4\x85\x6d\x2f\xed\xfb\x5a\xdf\x7f\xbf\xec\x49\
+\xbd\x76\xf5\xaa\xec\xde\x52\xa9\x94\xa4\xd3\x69\x89\xa2\x48\xc2\
+\x30\x14\xcf\xf3\x74\xcf\x73\x1e\xf3\x7e\x10\x04\x7a\xce\x0d\xf7\
+\xb2\x78\xfe\xc3\x38\xfc\x68\x36\x9b\x3d\x82\xfb\x8f\xe2\x7c\x08\
+\xfb\x01\x7b\x7f\xbb\xdb\xed\x6e\xe2\xf9\x9b\xad\x56\xeb\x6d\x5c\
+\xfa\x33\xce\xff\x86\xb9\xda\x6e\x0e\xce\xeb\xd6\x73\x18\xb8\x06\
+\xf7\x76\x0e\xd9\x6b\xf3\xe5\x7d\x6c\x5c\x08\x83\xef\x9e\xc9\xe7\
+\xf3\x9f\x2f\x14\x8b\x4f\x15\x8b\xc5\x83\x85\x7c\x3e\x9d\xcd\xe5\
+\x24\x93\xc9\x48\x9a\x84\x7a\x6e\xf1\x48\xc1\x74\xda\x6d\x69\xb6\
+\x5a\xb2\xd3\x68\x04\xdb\xdb\xdb\xd7\x1b\x8d\xc6\x0b\xcd\x66\xf3\
+\x39\xcc\xf5\x0a\x46\xf7\xfd\x60\x49\xbd\x17\x09\x70\xe0\x9e\x8f\
+\xed\x8b\x00\xfc\xb5\xe1\x72\xf9\xd4\xe0\xe0\xa0\xe0\xb8\xef\xfd\
+\x56\xbb\x23\x9d\x4e\x07\x60\xdb\x7a\x9e\xcf\x65\x25\x0b\xa2\xb2\
+\xd9\x4c\x8f\x09\x98\x6b\xab\x5e\x97\xcd\x5a\x4d\xaa\xd5\xea\x15\
+\x10\xf4\x43\x48\xe5\x17\x24\x84\xeb\x3f\xa8\x04\x1e\x98\x00\x4e\
+\x86\xfd\x79\x00\xfe\xce\xe8\xe8\xe8\xc7\x87\xcb\xc3\x78\xc6\x08\
+\x70\x75\xa3\x2a\xef\xdc\xb9\x2b\xb7\x31\x56\xd6\xd6\xa5\xb6\x55\
+\x97\x36\x08\x08\x02\xb3\x28\xa5\x41\xf0\xc3\x83\x25\xd9\x3f\x56\
+\x91\x87\x1f\x9a\x92\x87\x67\xa6\x64\x6c\xa4\x6c\x08\x86\x54\xd6\
+\xd7\xd7\x65\xf5\xde\xbd\xd7\xb6\xb6\xb6\xbe\x8d\x35\xff\xcb\xad\
+\xf9\x2f\x13\x60\x81\x97\xa0\xdb\xdf\xdd\xbf\x7f\xff\xb3\x95\xd1\
+\x51\x8a\x40\xd5\xe2\xda\xdc\x75\x79\xfd\xda\xdf\x00\x7e\x41\xb6\
+\x1b\x3b\x7c\x53\x7c\x3f\x2d\x19\xbc\xcb\xf7\x93\x8b\x73\x74\x30\
+\x57\xb7\x1b\x90\xff\x52\x2a\x16\x40\xc8\xb4\x9c\xfc\xc8\x11\xf9\
+\x8f\x43\x07\xc4\xc3\xb3\x3b\x3b\x3b\xb2\xb2\xb2\x12\xad\x2c\x2f\
+\xff\xa8\xdd\x6e\x7f\x0b\xef\xd4\xff\x25\x02\xec\xc2\x87\xcb\x23\
+\x23\x3f\x99\x9c\x98\xf8\xc4\x40\xa9\xa4\xf7\xdf\x9a\xbb\x21\x97\
+\xae\xbc\x2e\x77\x16\x96\x88\x59\x0a\xd0\x7b\xe8\xbf\xe4\xac\xaa\
+\xa4\xd3\x9e\xb1\x01\xb7\xb8\x95\x64\x97\x76\x00\xc9\xb4\x5a\x1d\
+\x69\x34\x9b\xb2\xd3\x6c\xe9\xfd\x99\xa9\x71\x39\xfb\xc4\x49\x39\
+\x06\x42\xb8\xad\xad\xae\xca\xc2\xdd\xbb\x57\x6b\xd5\xea\x57\x70\
+\x3a\xcb\x79\xde\x33\x01\x14\x21\xb6\xd3\x50\x97\xe7\x26\x26\x27\
+\xa7\x20\x01\xa9\x42\x35\x7e\xf7\xe2\x7f\x2b\x01\xe4\x58\x69\xa0\
+\xa8\xa3\x08\xf0\x7e\xc6\xd7\x6b\xff\xdf\xe6\x3c\x0e\xf7\x9d\x6e\
+\x17\x04\x34\x65\x6b\xbb\x21\xf5\x7a\x03\x32\x89\xe4\xe8\x63\x07\
+\xe4\x33\x4f\x9d\x51\x55\x83\x2a\xc9\xc2\xfc\xfc\xdd\xb5\xb5\xb5\
+\xcf\x03\xfc\x65\xf7\xee\x03\x11\xc0\x0d\x6a\x72\x7a\xdf\xbe\x7d\
+\xbf\x05\xf8\x0a\xa5\x71\xfd\xd6\xbc\xfc\xfa\xc2\x25\x59\xdb\xa8\
+\xc9\x60\x69\x40\x86\xb0\x08\xd5\xc0\x4f\x7b\xaa\x3a\x8e\x70\xc7\
+\xf5\xd4\x2e\x62\xac\xe7\xba\x8f\x10\xaa\x68\x1d\xea\x47\xbb\xd9\
+\xaa\x6f\xcb\xe8\xc8\xb0\x7c\xf6\xd3\x9f\x94\x83\x0f\xcf\xd0\x5b\
+\xc9\xbb\x77\xee\xac\xdf\x5b\x59\xf9\x0c\x9e\xbb\x9c\xda\x83\x41\
+\xff\x97\x04\x0e\x8d\x8f\x8f\xbf\x30\x39\x35\x35\x4d\xf0\x7f\x99\
+\xbd\x2e\xbf\xb9\xf0\xb2\x04\x58\x90\xdc\x29\xc3\xf3\x64\xb3\x34\
+\x60\x0f\x92\x32\xa0\xc9\x7d\xba\xcd\x94\xdc\x4f\x40\x64\xd0\xc6\
+\xfb\x50\x89\x09\xd5\x8e\x1c\x61\x6d\x78\xae\x2a\xb8\x5e\xdb\xac\
+\xeb\x9c\xcf\x80\x88\x8f\x1c\x3e\x28\x70\xb5\x72\xfb\xd6\xad\x85\
+\x7b\xf7\xee\x3d\x85\xb7\xe7\xee\x63\xf4\x1e\x54\x95\x60\xa8\x3f\
+\x1d\x9f\x98\x50\xf0\xd7\xfe\x7e\x43\x7e\xf3\xc2\x2b\xe2\xc1\x70\
+\xc7\x86\x06\x95\xf3\xbc\xce\x45\xbc\x14\x7d\xbd\x67\xc0\xa7\x1c\
+\x21\x31\x17\x92\xec\x8f\x77\x24\x83\xfb\x10\x04\x44\xa1\x21\x86\
+\x52\xe0\x9c\x39\x78\xaa\x3c\x54\x75\x63\x73\x0b\xd2\x7e\x59\x59\
+\x71\x0c\x44\xcc\xcc\xcc\x4c\xc3\xc5\xfe\x74\x63\x63\xe3\x93\xb8\
+\x54\xef\x23\x60\xb7\x98\x4b\xa5\xd2\xf7\x26\x26\x26\x4e\x30\x18\
+\xbd\x33\x7f\x57\x7e\x7f\xf1\xb2\x1a\x66\x05\xa2\x1d\x2a\x95\xd4\
+\x40\x09\x5c\x09\xb0\x86\x9a\x24\xa0\xc7\x90\x24\x63\x7a\x2a\x63\
+\x68\x31\x46\xcd\x43\x12\x11\xd0\x61\x50\x22\x41\x1a\xeb\x94\x25\
+\x0d\x66\x6d\x54\x6b\xf2\xbb\x4b\x97\xd5\xc6\x1e\x81\xcb\x9d\x9a\
+\x9a\x3a\x81\xa0\xf7\x3d\x48\xe4\xd9\x24\xd3\x3d\x82\x70\x40\x00\
+\xfa\xdc\xfe\xf1\xf1\xaf\x16\x0a\x05\xd9\x84\x3e\xfe\xe7\xcb\x57\
+\x70\xdd\x93\xb1\xca\x88\x54\xe0\xf7\x49\x48\xc6\xc7\x80\xc1\x66\
+\xfc\xe4\xc8\xc4\xc7\xea\x46\xf5\x38\x6d\x9e\xdb\xf5\x6c\x36\xc3\
+\x67\x7c\x8d\xd6\xf1\xf3\xb8\x96\xb5\xd7\x18\x2f\xb8\xd6\x28\xd6\
+\x24\xa6\x3f\xbc\xfc\x2a\xb0\x34\x04\x5a\x21\x70\xe3\x5f\x85\xa4\
+\xce\x25\x0d\xda\xb7\xde\x86\xdc\xc9\x95\xcb\xe5\xef\x63\xe8\xf9\
+\xc5\x2b\x6f\xaa\x71\x8d\x82\x23\x95\xf2\x90\xe5\xbc\xe1\xba\x49\
+\x13\x12\xfa\x4f\xcd\xb7\x4c\x20\x73\x7a\x76\xd0\xa7\x41\x3d\xf5\
+\xb1\x79\x55\x64\xed\xc1\x9c\x23\x60\x86\xe6\x38\xed\x45\x4a\x04\
+\x37\x3a\x8d\x97\x2e\xbf\x0e\x9b\x38\x23\x60\xae\x6c\x6e\x6e\x7e\
+\x1f\xaa\x74\x1a\x38\x5a\xb1\x0d\x70\x22\x50\xff\x39\xb8\xcc\xc7\
+\x09\xf0\xfa\xed\x79\x04\xa7\x45\x29\x0f\x0d\x61\xa2\x41\xe5\x9a\
+\xc6\x06\x82\x8e\x55\xa8\xa7\x46\x86\x30\x6b\xc0\xf8\x27\x67\x07\
+\xf3\xbe\xe4\x32\x46\x43\xa9\xe3\x8c\x01\xb5\x9d\x0e\xf6\xfd\xc0\
+\x43\x0b\x3a\x74\x04\xe0\xb9\x00\x04\xa4\x31\x1f\x19\xc7\x7b\x37\
+\xa1\xca\xff\x00\xa6\x0f\x7d\xf0\x21\x81\x67\x7c\xbc\x5e\xaf\x7f\
+\x0e\x36\xf1\x33\x62\xf2\xed\x64\x3e\x52\x84\xaf\x33\x50\x31\xfc\
+\xbf\xf1\xd7\x7f\x68\x50\x2a\x0f\x95\x90\xc7\xe4\xe2\xc0\x96\x26\
+\xd0\xb4\x17\x4b\xc1\x81\x4f\x59\x83\xf6\xe1\x99\x46\x06\x10\x13\
+\x52\x26\x3d\x40\x7e\xa3\x60\x91\xf0\xc9\x00\xf2\x25\xa8\xb3\x54\
+\x1b\x2d\x04\xb1\x76\x6c\xc0\x2e\x4a\x07\x96\x90\x80\x73\x71\x1f\
+\xa4\xa4\x80\xf9\xcb\x70\x1c\x1d\x44\xef\xd7\xaf\xcd\xc9\xa3\xb0\
+\x85\x91\x4a\x45\x56\x57\x57\xbf\x8e\xf8\xf0\x4b\xac\xdf\x55\x16\
+\x01\xdc\x69\x24\x66\x27\x09\xf4\xd6\xc2\x5d\x59\xaf\x6d\xc2\x60\
+\x4d\x90\x52\xe0\x16\xb0\x46\x58\xf5\x40\x3d\xee\x67\xb0\x2f\xe6\
+\x7c\xd5\xdd\x10\xc1\xe9\xfa\xdc\xac\x2c\x2f\x2f\xc3\x2d\xb6\x75\
+\x74\x34\x27\x0a\x84\x81\x70\x72\x72\x52\x8e\x1e\x3d\x2a\xa5\x72\
+\x49\xaa\xdb\x4d\xa4\x15\x61\xec\x8d\x02\x47\x08\x80\x77\xc1\x44\
+\x2f\x15\xea\xb5\xd2\x40\x41\x93\x43\x62\xa2\x56\x1c\xfc\xe0\xb4\
+\x40\xcd\x4f\x22\x01\x3c\xcd\x2c\x56\x55\xa8\x58\x28\x7c\x69\x60\
+\x40\x53\x77\xa8\xcf\x82\xba\xb2\x12\x38\x96\xd5\xe8\x6a\x80\xd3\
+\xd8\x0c\x21\x86\x00\x72\x3d\x03\xf3\x19\xc8\xa6\xd5\xf8\xe6\xe7\
+\xe7\x65\x6e\x6e\xae\x2f\x9f\xe7\x75\x17\xac\x18\x59\xe1\xcb\xe5\
+\xda\xb5\x6b\xf2\xc4\x13\x4f\xc8\xa1\x43\x87\x34\x5b\xdd\x6e\x43\
+\x65\xf0\x8a\xa7\x5e\x28\xb4\xcc\x09\x94\x88\x94\xe6\x61\x29\x19\
+\x04\x23\x99\x1c\x52\xb5\x49\xc0\xf0\xf0\xb0\xc0\xd1\x7c\x09\x1e\
+\xe9\x15\x4a\x20\x0b\xf0\x9f\xa6\x67\xa8\xc1\xf3\xac\x55\x37\xe5\
+\x21\xe4\x26\x69\xab\x1e\x3e\x38\xee\xc0\x3b\xf5\x31\xc6\x03\xd5\
+\x48\xa7\xd4\xa3\xcc\xce\xce\xca\xed\xdb\xb7\x8d\x6a\xe1\x79\xeb\
+\xde\xfa\xdc\xb3\x53\x15\xaa\xd5\x85\x0b\x17\x34\xfb\x24\x21\x24\
+\x6e\x1b\x86\xed\xec\x4a\x41\xab\x4d\x21\xf9\xd3\x94\xba\x8b\xc8\
+\x8f\x74\x05\x51\x7f\xf9\xde\x9a\x7a\xc7\x12\x98\x4d\xcc\x48\xfe\
+\xb2\x9e\x97\x4e\x1f\x41\x41\xf2\x08\x17\x5a\x5e\x5d\xd7\x97\xc7\
+\x2a\x65\x19\x46\xba\x40\xf0\x74\x71\x86\x08\xbf\x0f\x54\xce\x53\
+\xc3\x97\x77\xdf\x7d\x57\x6e\xdd\xba\xa5\x1c\xe7\x33\xe9\xc4\xb3\
+\x8e\x20\xdf\xf7\xe3\x7b\x7c\x87\xcf\x5e\x45\x06\xf0\xd6\x5b\x6f\
+\x21\x09\xcc\xc1\x8b\xf5\xdc\xa2\xaa\xac\x6f\xd6\xa4\x06\xf0\x9c\
+\xf1\x67\x1f\x30\x11\xdb\x12\x31\xe2\x1a\x08\x78\x04\xf3\x1f\x41\
+\x80\xf5\x8f\xe7\x72\x39\xdf\xb9\xac\x42\x21\xaf\x5e\x84\xba\x97\
+\xcb\x65\x34\xd7\x49\x43\x02\xb2\x2b\x60\x13\x1c\xf5\x9b\x6a\xe3\
+\x8c\xdc\x81\x76\xc0\xdd\x88\xaf\xd9\xf7\x5c\xaa\x7e\xe9\xd2\x25\
+\x95\x48\x31\x93\xda\x35\xb7\x49\xcb\xa9\xba\x54\x67\xaa\x10\x19\
+\x00\x55\x97\xd5\xf5\x9a\x3e\x03\xa6\x83\xc6\xf4\x71\x2f\x97\xcd\
+\x1e\x66\x80\xa2\x3b\xab\x37\x9a\xf0\x14\x05\x75\x61\x5c\x88\xc7\
+\x29\x2f\xbd\x57\xae\x04\xe2\x72\x72\xe3\xc6\x0d\x35\xd4\xdd\x49\
+\x9c\xcb\x66\xbd\x44\x4a\xad\x7b\xd6\x08\x00\xee\xee\x33\xcf\x79\
+\xf5\xd5\x57\x15\x58\x5a\xa2\xdd\x8b\x28\xa1\x74\x24\x24\x88\xc4\
+\x0c\x14\xf3\x52\xdf\xde\x51\xe2\xb9\x3e\x1c\xc3\x61\xa8\x9e\xf7\
+\x28\x39\xdc\xb5\xbe\x9a\xe5\x9f\x5b\x14\xc4\x61\xf8\x71\xf6\xc8\
+\xab\x14\x23\xdd\x29\x7c\xb1\xaa\x4f\x52\xad\xdc\x7b\x89\xe0\xd8\
+\x17\xb4\x74\x9e\xc4\xf3\xdc\x68\x3f\x35\x94\x95\x59\xa8\x64\x32\
+\x1d\x57\x90\x89\x32\x94\xc1\x92\xa0\x59\x14\xd1\xd5\x67\x8d\x2a\
+\x3e\x4a\x15\x1a\x22\x34\x13\x01\x3d\x4d\x01\x52\x89\xfc\xa5\xc8\
+\x22\xdd\xb7\x93\xb0\xb0\x60\xe6\xd8\xa8\xd1\x17\x2b\x07\x23\xcb\
+\xd1\xbd\x36\xe3\x16\x83\x3e\x22\x92\x00\xb9\x51\x85\x16\x17\x17\
+\x65\x6b\xb3\x86\xe7\x02\x65\x0e\x37\xaa\x31\xed\xa3\xc7\x1d\xd1\
+\x74\x83\x18\xba\x61\x60\x5c\x78\x26\x33\xc4\x5c\x48\x2b\x72\x06\
+\x0b\x23\x72\xaf\xf7\x86\x15\xa5\xda\x03\x0c\xaa\xcd\x22\x04\xa0\
+\xbb\x50\x1b\x06\x2a\xda\x80\xf3\xf5\x49\x2e\x07\xb6\xf2\xe2\x40\
+\xc4\xbc\x8f\x90\xe4\xb1\x23\x82\xcf\x6e\xae\x2e\xeb\x1a\x34\x5e\
+\xd6\x1a\xbb\xd9\xe2\x54\xa9\xd3\x09\x9c\x8a\x16\xfb\xb2\x51\x3f\
+\x51\xc7\xf6\x36\xd3\x0b\x2a\xc2\xb8\x77\xe0\xb7\xdb\xf5\x6a\xcc\
+\x99\xa4\xfe\xdb\x74\xa4\x4f\xa5\x1c\xf8\x64\x40\x4b\x12\x14\xab\
+\xa6\x7d\x9f\x4c\x29\x31\x2e\x21\x72\x33\xce\x84\xc1\xfd\x76\x61\
+\x30\x4a\x6c\x31\x4c\x25\x1a\x3c\xc8\x66\x8c\xcb\x4c\x02\x77\x5b\
+\xd0\x35\x1c\x1b\x41\x58\x1f\xc0\x23\xb5\xea\x86\x82\x25\x08\x82\
+\x4b\xea\xbb\x6b\x50\xb9\x3a\xd6\x3d\xe3\xc0\x07\xaa\xc3\x3d\x09\
+\xf0\x39\xa6\x1a\xdc\x90\x32\x4b\x01\x2e\x93\x7a\x9e\x8e\x4c\x9a\
+\xee\x60\x38\xed\x73\x59\xac\x95\x76\xc3\x67\xc7\x8c\x37\x18\x61\
+\x39\x5c\x95\x44\xb3\x51\x3d\xd6\xfa\xd5\x04\x17\x7a\x90\xe2\x20\
+\xea\x02\x54\x64\xb5\x5a\x55\x5d\x1b\x72\xf4\x58\x75\x08\xd0\x79\
+\x1e\x47\x90\x03\xcb\x7b\x4e\xad\x38\x1c\x01\xec\x29\x31\xc5\x18\
+\x19\x19\x91\x20\x95\x46\xc4\x0d\xb5\x67\x04\x2d\x51\x57\x9a\x64\
+\x28\x9f\x37\xee\x35\x0d\xa6\xaa\x7a\x6e\x92\x5d\x37\xb5\x75\x92\
+\xf6\x54\x02\xba\x60\x64\x8b\x0f\x1c\xb4\x3b\xdd\x44\x0d\x2b\x9a\
+\x74\x75\xe1\xf4\x26\xa7\x1f\x92\x63\xc7\x8e\x69\x2b\x84\xa2\xe7\
+\x9e\xc4\x24\x87\xb3\x13\xee\x55\x8d\xec\x35\xa7\x46\x1c\x27\x4e\
+\x9c\x60\xc5\x25\x91\xe7\x6b\xa6\x6a\x58\x6e\x9c\x85\xb3\x2d\xe3\
+\x10\x8c\xed\xb8\xae\x07\xe7\x60\xab\xd2\xc3\xc4\xb3\xca\x39\x70\
+\x6d\xa0\x90\x53\x57\xca\x24\x8a\x96\xd0\xb1\xe2\x96\x48\xe2\x4a\
+\xca\x64\xaf\x48\x09\x9a\x1d\x39\x75\xea\x14\x23\xa2\x82\x27\xc0\
+\x24\x70\x37\xe2\x73\x18\x7f\xd2\x16\x38\xc8\xfd\x73\xe7\xce\x49\
+\xbd\x05\xe9\x04\xa6\x56\x50\xc6\x99\x5a\x4d\xf3\xa1\x8e\xf6\x91\
+\x0c\xe3\xe8\x3e\x89\x91\xd8\xda\x86\x39\xb3\x1e\x00\xff\x09\x13\
+\x6b\x5f\x72\x08\xe9\x43\xec\x25\xac\x4a\x58\xd8\xb6\x18\x71\x8b\
+\xd0\x40\x01\x24\x5b\x94\xf3\xe7\xcf\x2b\x27\x9d\x24\x08\x78\xb7\
+\x34\x08\x7e\x87\x04\x26\xc0\x73\x7b\xe6\x99\x67\x24\x95\x2b\x5a\
+\x4e\x3b\xf0\x56\x08\x51\xc2\x11\xc4\x9e\x2b\x52\x8c\xdc\x30\x2f\
+\xa0\x07\x7f\xf2\x50\x40\xbc\x8d\x05\xdf\xe1\xc5\x32\x0a\x76\x5a\
+\x79\x07\x6a\x43\xd5\x09\x54\x4f\x7b\x2d\x91\x50\xd5\xa8\xd7\x4d\
+\x68\xc3\xfd\x1d\x38\x7a\x5c\xce\x9e\x3d\xab\x20\xe8\x0e\x1d\x01\
+\xc9\xd1\xb4\xee\xd4\x19\x32\xb7\xa7\x9f\x7e\x5a\x8e\x3c\x7e\x52\
+\xdd\xb2\x59\x23\x54\x26\x85\x56\xc2\x8e\x98\x40\xbd\x58\x57\x31\
+\x31\xad\x61\x8d\xc2\x67\xb1\xd6\x3b\x60\xdc\xdb\x4c\xa7\xdb\x58\
+\xe4\x8f\x38\xf9\x50\x11\xe2\x61\x1d\xc0\x66\x93\xa7\xdd\xb5\x94\
+\x61\x46\x92\x08\xa6\x01\x6a\xa0\x10\x24\x32\xc6\x26\x22\xf2\x93\
+\x67\x3f\x25\x43\xa8\xde\x9e\x7f\xfe\xf9\xd8\xa8\xfb\x1c\x71\xa2\
+\x6d\x4e\xb5\x21\xe7\x4f\x7c\xec\x63\x52\xad\x6f\xc7\xb6\xd6\x8b\
+\x23\x12\xb7\x60\xdc\x3c\x8c\x0d\x6d\x48\x9c\xdc\x2f\xc2\x85\x33\
+\x80\x62\xfc\x91\xb7\x7c\x3e\x02\x2e\xfc\xbc\xb9\xb3\xf3\x6c\x11\
+\xfa\x3c\x3e\x3a\xa2\x1d\x38\x72\x99\x76\x41\x8f\x10\xd1\xb7\x47\
+\xa6\x83\x10\xa5\x38\x1c\x41\x29\x8d\x9e\xec\xe5\x1c\xfb\xe8\x71\
+\x79\xec\xb1\xc7\xe4\xe2\xc5\x8b\x9a\x65\xa2\x76\x8d\x01\xd0\x33\
+\x55\x50\x49\xd1\xe8\x9f\x3c\xf3\xa4\xe4\xb1\x4e\x75\xab\x61\x2b\
+\x32\xc7\xf5\x5e\xbf\xc8\xb4\x5d\x7a\x04\x39\x83\x1f\xdf\x37\xa2\
+\xf3\xd5\x51\x5b\x80\xe9\x3f\xd7\x18\xf4\x57\x14\x18\xb8\xe9\x8f\
+\x8d\x8d\x5d\x9e\x98\x9c\x3c\x49\x91\xfd\x79\xf6\x86\x56\x41\xcc\
+\x3d\xd8\xe7\x49\x96\x8f\xae\x98\xd1\xe3\x94\x6d\xab\x78\xb6\x9f\
+\xca\x2c\x36\x9f\x55\x17\xd7\x80\x64\xb6\x2c\x11\xa8\xf6\xa4\x00\
+\xd0\xdd\x4c\x5e\xda\x3b\x4d\xbd\xef\xb8\x6e\x08\x08\x8d\xe7\xb3\
+\xb5\x71\xaf\x4e\x8e\xd4\x78\x5b\xb0\x9f\x3c\x72\xa2\xe3\x87\x0f\
+\x2a\xc3\x6e\xde\xb8\xf1\x3f\x48\x65\x58\xd8\x77\x3d\x9b\xde\x76\
+\xb7\xea\xf5\x1f\xb4\x20\x7e\x82\x9d\x99\xd8\xaf\x5d\x64\x8d\x96\
+\x36\x16\x38\x8e\xf4\x16\xb0\x89\x9a\xfa\x7a\x73\xde\x85\xae\xd7\
+\xea\x0d\xd9\x6e\xc1\x48\xf3\x25\x19\x9e\x9c\xd1\x11\xe5\x06\xf4\
+\xda\xce\xe6\x96\x04\x9d\x76\xdf\xfb\xfa\x67\xc1\xf7\x71\xdf\xae\
+\xab\x46\x0f\x2c\x33\x13\xfb\x34\x95\xa0\x64\x91\x48\xfe\x00\x31\
+\xa8\xab\xa9\xba\x0b\xe5\x9d\x76\xfb\x57\xa8\x33\xdf\xe4\xf9\xbe\
+\xca\xb0\x8c\x96\x07\xd5\xed\x85\x31\xa7\x0c\x47\x0c\xe7\x42\x6b\
+\xd4\x91\x02\x88\xfa\x88\x32\x8b\x76\xd5\xef\xef\xa8\xef\xef\xda\
+\x48\x1c\x5a\x62\x63\x4e\x5b\x09\xf4\xce\x25\x6e\xb1\xb8\x7b\x74\
+\x0e\xc4\xc2\x82\x86\x0e\x60\x6d\x75\xf5\x4d\x5c\xfb\x55\x9c\xf9\
+\x3a\xfd\x02\xa0\xd6\x46\xb5\xfa\x8d\x06\x3c\x09\x37\x76\x00\xd8\
+\xea\x33\x51\xd3\x26\x69\x04\x60\x89\xd0\xf7\x02\x03\x28\xd0\xc5\
+\x1c\xc0\x30\x6e\x97\x38\xc9\x84\xd6\x87\x3b\x75\x71\x05\xbc\x01\
+\x6f\xbd\x0e\x6c\x3c\x88\x6d\xc1\x04\xae\xae\x4d\xec\x88\x85\xdb\
+\xfa\xda\x1a\xbf\xe6\x7c\x83\x58\x1d\x6e\x2f\x99\xe6\x42\x05\x5e\
+\x44\xe1\xfd\x63\xbe\xc8\xba\xe0\x00\x5e\x24\x37\x3a\xea\x8b\x25\
+\xee\xe7\x04\x7d\x06\x67\xbb\x09\x96\xc0\x30\x72\xe0\x02\xcb\xd5\
+\xdd\xc7\x49\xc3\xb5\xcc\x23\x81\x51\x68\x25\x11\xc5\xe0\x39\xf7\
+\x81\x0f\x4c\x2b\x16\xba\xe8\x95\x95\x15\x62\x7b\xd1\x79\x36\xfd\
+\xe8\x98\xec\x69\xd2\x1e\xf0\xe0\x37\xd7\xd6\xd6\xde\xe0\xcd\x91\
+\xe1\x41\xa5\x9e\x85\xb5\x4a\x22\xb2\xe2\x4e\xf4\x72\x82\x84\x2d\
+\x38\xa3\xeb\x8d\xd0\x8e\xde\x79\xd8\x47\x64\x82\x21\xbb\xc0\x73\
+\x4d\xae\x5d\x19\x36\xaa\xbc\xb4\xb8\xf8\x06\x5c\xe7\x37\xbd\xb8\
+\x03\x68\x86\xbf\xc7\x87\x83\x3a\xf4\xec\xcb\xc8\xf8\x5e\xa8\x8c\
+\x8e\x4e\xd3\xad\x52\xdb\x6e\xce\x2f\x6a\x1e\xce\x64\xca\xf3\x8c\
+\x0b\xd5\x6c\xd1\x36\x77\x23\x3d\x74\x95\x9b\xab\x25\x7a\x89\x6d\
+\x14\xa7\x95\xbd\x88\xdb\x73\x95\x4e\xa2\xd4\x82\xae\x12\x48\xce\
+\x73\x6d\xda\xce\xd2\xd2\xd2\xc2\xfa\xfa\xfa\x97\x89\x6d\x77\xba\
+\xbf\xe7\x67\x56\x4c\x38\x07\x37\xf5\x05\x94\x9b\xbf\x45\x96\x58\
+\xd9\x8f\x89\x98\xc2\xde\xbc\xb3\xa4\x9f\x4a\x7d\xcd\xfb\x1d\x46\
+\x72\x30\xd1\x9d\x96\x44\x67\x3d\xea\x9b\x33\xde\xbb\x20\x15\xa7\
+\x26\x74\x06\x81\x51\xd5\x2c\xaa\xbf\x47\x66\xa6\x95\xf3\x0a\x7e\
+\x71\x71\x1d\x0c\xfd\x02\x31\xed\x85\x35\x75\x0d\x41\xe7\xbe\x8b\
+\xda\x42\xd4\x6e\xc2\xe9\xb1\xd1\xd1\xe7\x46\x2a\x95\x29\x5e\x63\
+\x23\xea\xf6\xc2\x92\x54\x11\xb8\x52\xb6\xbb\x90\xf2\x92\xc0\x53\
+\x7d\x8c\xdf\xdd\xdd\x8d\x12\xc4\x44\x09\x9b\x0a\xbb\xc6\x46\x98\
+\x26\x3c\x3c\x3d\xa1\x3a\x4f\x95\x5d\x5e\x5a\xba\x0b\x46\xea\x27\
+\xa6\x64\x01\xf4\x40\x04\x30\x28\x69\x1a\x1b\x04\x87\x11\x45\x7f\
+\x32\x3a\x36\xf6\x09\xdf\x77\x9f\x55\x6b\xb2\xb8\xb2\x26\x8d\x66\
+\xcb\x12\x6b\x4a\x51\xd3\x99\xb6\xc0\xa3\x7e\x15\x92\xd4\x2e\x29\
+\x84\xbd\x82\x9f\xe9\xc1\xe4\xfe\x51\x19\x1b\x31\x1d\x69\xe6\x47\
+\x4b\xcb\xcb\x57\x37\xd6\xd7\xbf\x82\xf9\x67\x5d\x52\xf7\x9e\x09\
+\xd0\x8f\xcd\x4c\xb5\xd3\xe9\x52\x21\x9f\xff\x2e\x88\x78\x16\xe9\
+\xb3\xc2\xe2\xe2\xeb\xb5\x2d\x25\x86\x6d\x78\xfd\x26\x9c\xea\x7d\
+\x6e\xb2\x94\x24\x8d\x20\xd6\x79\xb2\x9f\x39\x3d\xeb\xde\xb1\xf2\
+\xb0\x76\xc0\xdd\xc7\x74\xb8\x49\xfd\xcc\x8a\xcc\xf6\x5b\xe0\x7a\
+\xdd\x55\x78\xef\x9b\x80\xd0\x7e\xfe\xa1\x07\x01\xb0\xf3\x43\xc3\
+\xc3\xdf\x29\x97\xcb\x1f\x77\x65\xa0\x72\x0c\x92\xd8\xe4\xd7\x46\
+\x10\xc2\x63\xcd\xe1\xad\x7a\x70\x73\xc6\xce\x8e\x07\xeb\x69\x02\
+\x1f\xd4\xaf\x9b\xb9\x58\x22\x6c\xd3\xc0\xcf\xbf\x86\x48\xfb\x6d\
+\x00\xd7\x0f\xdd\xae\xc2\xfb\xb7\x10\xe0\x5c\x28\x7f\x6a\x80\xba\
+\xf4\x8b\x48\xfc\xbe\x36\x38\x38\x78\x8a\x84\xb8\x7e\xa8\x4a\x26\
+\xd1\xf7\x77\xa9\xb3\x69\x35\xf6\xbe\x25\xb8\x8d\x7a\xce\xcc\x72\
+\xb3\x56\xbb\x82\x54\xe6\x87\x88\x43\xbf\x70\xbf\x99\xe0\xda\xff\
+\x76\x02\xc4\xfe\x5a\x85\x1b\x24\xc2\x54\xfc\x4c\x81\x3f\xf6\x28\
+\x14\x9e\xca\x17\x0a\x07\xb3\xd9\x6c\x5a\x7b\xa0\x26\xbb\xeb\x9b\
+\x33\xd9\x6e\x81\x7a\x04\xd0\x73\xfd\xb1\x07\xb2\x4a\xfd\xb1\x07\
+\xe6\xea\x3a\xd5\x74\xc5\xfe\x83\x10\xf0\xbe\x7e\xad\x62\x5d\x26\
+\x67\x7c\x69\xa7\xd9\x7c\x09\xc1\x8f\xc8\x3f\x0c\xee\xea\xcf\x6d\
+\x84\x3f\xb7\xf1\xbc\x21\x78\x2a\xf3\x73\x9b\x20\xd8\x06\xf8\xcd\
+\xd0\xfe\xdc\x06\x92\x8c\x7f\x6e\x93\x98\x6f\x4f\x80\xff\x6c\xfb\
+\x5f\xb5\xb8\x45\x3e\xe2\x04\x60\xdc\x00\x00\x00\x00\x49\x45\x4e\
+\x44\xae\x42\x60\x82\
+"
+
+qt_resource_name = "\
+\x00\x05\
+\x00\x6f\xa6\x53\
+\x00\x69\
+\x00\x63\x00\x6f\x00\x6e\x00\x73\
+\x00\x0e\
+\x0d\x8d\xf4\xe7\
+\x00\x73\
+\x00\x74\x00\x6f\x00\x70\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x08\
+\x0b\x63\x58\x07\
+\x00\x73\
+\x00\x74\x00\x6f\x00\x70\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x0c\
+\x00\x3e\x02\x9f\
+\x00\x50\
+\x00\x79\x00\x43\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x69\x00\x63\x00\x6f\
+\x00\x0e\
+\x0e\xdf\xf7\x87\
+\x00\x70\
+\x00\x6c\x00\x61\x00\x79\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x08\
+\x02\x8c\x59\xa7\
+\x00\x70\
+\x00\x6c\x00\x61\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x0a\
+\x06\x88\x40\x07\
+\x00\x72\
+\x00\x65\x00\x63\x00\x6f\x00\x72\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x0b\
+\x0c\x4d\x7c\x67\
+\x00\x70\
+\x00\x72\x00\x6f\x00\x63\x00\x65\x00\x73\x00\x73\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x0f\
+\x0c\x1d\x7e\x67\
+\x00\x72\
+\x00\x65\x00\x63\x00\x6f\x00\x72\x00\x64\x00\x5f\x00\x67\x00\x72\x00\x65\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\
+"
+
+qt_resource_struct = "\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x02\
+\x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x20\x64\
+\x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x99\x71\
+\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xa6\
+\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x10\x2e\
+\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x00\xd0\xe6\
+\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x01\x00\x00\xbc\x19\
+\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x00\x66\x00\x00\x00\x00\x00\x01\x00\x00\x88\xae\
+"
+
+_registered = False
+
+
+def _resource_blobs():
+ if isinstance(qt_resource_struct, str):
+ s = qt_resource_struct.encode('latin1')
+ else:
+ s = qt_resource_struct
+ if isinstance(qt_resource_name, str):
+ n = qt_resource_name.encode('latin1')
+ else:
+ n = qt_resource_name
+ if isinstance(qt_resource_data, str):
+ d = qt_resource_data.encode('latin1')
+ else:
+ d = qt_resource_data
+ return s, n, d
+
+
+def qInitResources():
+ global _registered
+ if _registered:
+ return
+ QtCore.qRegisterResourceData(0x01, *_resource_blobs())
+ _registered = True
+
+
+def qCleanupResources():
+ global _registered
+ if not _registered:
+ return
+ QtCore.qUnregisterResourceData(0x01, *_resource_blobs())
+ _registered = False
diff --git a/run_pycorder.bat b/run_pycorder.bat
new file mode 100644
index 0000000..a29aa6e
--- /dev/null
+++ b/run_pycorder.bat
@@ -0,0 +1,4 @@
+@echo off
+setlocal
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0run_pycorder.ps1" %*
+exit /b %ERRORLEVEL%
diff --git a/run_pycorder.ps1 b/run_pycorder.ps1
new file mode 100644
index 0000000..7a772eb
--- /dev/null
+++ b/run_pycorder.ps1
@@ -0,0 +1,58 @@
+param(
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$AppArgs
+)
+
+$ErrorActionPreference = "Stop"
+
+$RootDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+Set-Location $RootDir
+
+if (Get-Command py -ErrorAction SilentlyContinue) {
+ $PythonCommand = "py"
+ $PythonPrefix = @("-3")
+} elseif (Get-Command python3 -ErrorAction SilentlyContinue) {
+ $PythonCommand = "python3"
+ $PythonPrefix = @()
+} elseif (Get-Command python -ErrorAction SilentlyContinue) {
+ $PythonCommand = "python"
+ $PythonPrefix = @()
+} else {
+ throw "[error] Python interpreter not found (py/python3/python)."
+}
+
+function Invoke-HostPython {
+ param([string[]]$Args)
+ & $PythonCommand @PythonPrefix @Args
+ if ($LASTEXITCODE -ne 0) {
+ throw "Python command failed: $PythonCommand $($PythonPrefix + $Args -join ' ')"
+ }
+}
+
+if (-not (Test-Path ".venv\Scripts\python.exe")) {
+ Write-Host "[setup] Creating virtual environment at .venv"
+ Invoke-HostPython @("-m", "venv", "--clear", ".venv")
+}
+
+$VenvPython = ".venv\Scripts\python.exe"
+if (-not (Test-Path $VenvPython)) {
+ throw "[error] Virtual environment is missing: $VenvPython"
+}
+
+$ProbeScript = @"
+import importlib
+for name in ("numpy", "scipy", "lxml", "PySide6", "pyqtgraph"):
+ importlib.import_module(name)
+"@
+
+& $VenvPython "-c" $ProbeScript *> $null
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[setup] Installing dependencies from requirements.txt"
+ & $VenvPython "-m" "pip" "install" "--upgrade" "pip" "setuptools" "wheel"
+ if ($LASTEXITCODE -ne 0) { throw "pip bootstrap failed" }
+ & $VenvPython "-m" "pip" "install" "-r" "requirements.txt"
+ if ($LASTEXITCODE -ne 0) { throw "dependency installation failed" }
+}
+
+& $VenvPython "main.py" @AppArgs
+exit $LASTEXITCODE
diff --git a/run_pycorder.sh b/run_pycorder.sh
new file mode 100755
index 0000000..ca7adf6
--- /dev/null
+++ b/run_pycorder.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$ROOT_DIR"
+
+if command -v python3 >/dev/null 2>&1; then
+ PYTHON_BIN="python3"
+elif command -v python >/dev/null 2>&1; then
+ PYTHON_BIN="python"
+else
+ echo "[error] Python interpreter not found (python3/python)." >&2
+ exit 1
+fi
+
+if [[ ! -x ".venv/bin/python" ]] || ! .venv/bin/python -V >/dev/null 2>&1; then
+ echo "[setup] Creating virtual environment at .venv"
+ "$PYTHON_BIN" -m venv --clear .venv
+fi
+
+if ! .venv/bin/python - <<'PY' >/dev/null 2>&1
+import importlib
+for name in ("numpy", "scipy", "lxml", "PySide6", "pyqtgraph"):
+ importlib.import_module(name)
+PY
+then
+ echo "[setup] Installing dependencies from requirements.txt"
+ .venv/bin/pip install --upgrade pip setuptools wheel
+ .venv/bin/pip install -r requirements.txt
+fi
+
+exec .venv/bin/python main.py "$@"
diff --git a/storage.py b/storage.py
index d8ff8a8..e9d9fea 100644
--- a/storage.py
+++ b/storage.py
@@ -1,1009 +1,989 @@
-# -*- coding: utf-8 -*-
-'''
-Storage Module for Vision EEG file format
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-20 14:00:34 +0200 (Do, 20 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 206 $
-'''
-import ctypes as ct
-import os
-import platform
-from modbase import *
-from res import frmStorageVisionOnline
-from res import frmStorageVisionConfig
-
-'''
-------------------------------------------------------------
-STORAGE MODULE
-------------------------------------------------------------
-'''
-
-class StorageVision(ModuleBase):
- ''' Vision Date Exchange Format
- - Storage class using ctypes
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- ModuleBase.__init__(self, queuesize=50, name="StorageVision", **keys)
-
- # XML parameter version
- # 1: initial version
- # 2: minimum required disk space added
- self.xmlVersion = 2
-
- # get OS architecture (32/64-bit)
- self.x64 = ("64" in platform.architecture()[0])
-
- # load C library
- try:
- self.libc = ct.cdll.msvcrt # Windows
- except:
- self.libc = ct.CDLL("libc.so.6") # Linux
-
- # set error handling for C library
- def errcheck(res, func, args):
- if not res:
- raise IOError
- return res
- self.libc._wfopen.errcheck = errcheck
- if self.x64:
- self.libc._wfopen.restype = ct.c_int64
- self.libc.fwrite.argtypes = [ct.c_void_p, ct.c_size_t, ct.c_size_t, ct.c_int64]
- self.libc.fclose.argtypes = [ct.c_int64]
- else:
- self.libc._wfopen.restype = ct.c_void_p
- self.libc.fwrite.argtypes = [ct.c_void_p, ct.c_size_t, ct.c_size_t, ct.c_void_p]
- self.libc.fclose.argtypes = [ct.c_void_p]
-
- self.data = None
- self.dataavailable = False
- self.params = None
- self.last_impedance = None #: last received impedance EEG block
- self.last_impedance_config = None #: last received impedance configuration EEG block
- self.moduledescription = "" #: description of connected modules
-
- # configuration data
- self.setDefault()
-
- # output files
- self.file_name = None #: output file name
- self.data_file = 0 #: clib data file handle
- self.header_file = 0 #: header file handle
- self.marker_file = 0 #: marker file handle
- self.marker_counter = 0 #: total number of markers written
- self.start_sample = 0 #: sample counter of first sample written to file
- self.marker_newseg = False #: request for new segment marker
-
- self.next_samplecounter = -2 #: verify sample counter of next EEG block
- self.total_missing = 0 #: number of total samples missing
- self.samples_written = 0 #: number of samples written to file
- self.write_error = False #: write to disk failed
- self.min_disk_space = 1.0 #: minimum free disk space in GByte
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- self.default_path = "" #: default data storage path
- self.default_prefix = "" #: prefex for data files e.g. "EEG_"
- self.default_numbersize = 6 #: number of digits to append to file name
- self.default_autoname = False #: create auto file name
-
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- '''
- # create online configuration pane
- self.online_cfg = _OnlineCfgPane(self)
- # connect recording button
- self.connect(self.online_cfg.pushButtonRecord, Qt.SIGNAL("clicked(bool)"), self.set_recording_file)
- return self.online_cfg
-
- def get_configuration_pane(self):
- ''' Get the configuration pane if available
- - Qt widgets are not reusable, so we have to create it every time
- '''
- config = _ConfigurationPane(self)
- return config
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
- e.g.
-
- D:\EEG
- ...
-
- '''
- E = objectify.E
- cfg = E.StorageVision(E.d_path(self.default_path),
- E.d_autoname(self.default_autoname),
- E.d_prefix(self.default_prefix),
- E.d_numbersize(self.default_numbersize),
- E.mindiskspace(self.min_disk_space),
- version=str(self.xmlVersion),
- instance=str(self._instance),
- module="storage")
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search my configuration data
- storages = xml.xpath("//StorageVision[@module='storage' and @instance='%i']"%(self._instance) )
- if len(storages) == 0:
- # configuration data not found, leave everything unchanged
- return
-
- # we should have only one instance from this type
- cfg = storages[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- self.default_path = cfg.d_path.pyval
- self.default_autoname = cfg.d_autoname.pyval
- self.default_prefix = cfg.d_prefix.pyval
- self.default_numbersize = cfg.d_numbersize.pyval
- if version > 1:
- self.min_disk_space = cfg.mindiskspace.pyval
- else:
- self.min_disk_space = 1.0
-
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
- def get_free_space(self, path):
- ''' Get the total and free available disk space
- @param path: complete data file path
- @return: tuple folder/drive free and total space (in bytes)
- '''
- if platform.system() == 'Windows':
- folder = os.path.splitdrive(path)[0]
- free_bytes = ct.c_ulonglong(0)
- total_bytes = ct.c_ulonglong(0)
- ct.windll.kernel32.GetDiskFreeSpaceExW(ct.c_wchar_p(folder),\
- ct.pointer(free_bytes),\
- ct.pointer(total_bytes),\
- None)
- free = free_bytes.value - self.min_disk_space * 1024**3
- return free, total_bytes.value
- else:
- folder = os.path.split(path)[0]
- diskinfo = os.statvfs(folder)
- total_bytes = diskinfo.f_blocks * diskinfo.f_bsize
- free = (diskinfo.f_bavail * diskinfo.f_bsize) - self.min_disk_space * 1024**3
- return free, total_bytes
-
-
-
- def check_free_space(self, freespace):
- ''' Check for a minimum available free space
- - If disk runs out of space during recording, stop recording
- @param freespace: available space in bytes
- @return: False if disk is out of space
- '''
- if freespace > 0:
- return True
- if self.data_file != 0:
- # stop recording
- self.write_error = True
- self._close_recording()
- # notify application
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- "out of disk space (<%.2fGB), recording stopped"%(self.min_disk_space),
- severity=ErrorSeverity.NOTIFY))
- return False
-
-
- def _get_auto_filename(self, searchdir):
- ''' Search for next auto file number
- @param searchdir: Qt.Qdir
- @return: the generated filename
- '''
- if not self.default_autoname:
- return ""
- numberstring = "?"
- for n in range(1, self.default_numbersize):
- numberstring += "?"
- searchdir.setNameFilters(Qt.QStringList("%s%s.eeg"%(self.default_prefix, numberstring)))
- searchdir.setFilter(Qt.QDir.Files)
- flist = searchdir.entryList()
- # extract numbers
- flist.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
- if len(self.default_prefix) > 0:
- flist.replaceInStrings(self.default_prefix, "", Qt.Qt.CaseInsensitive)
- numbers = []
- for f in flist:
- num,ok = f.toInt()
- if ok and (num < 10**self.default_numbersize-1):
- numbers.append(num)
- if len(numbers) > 0:
- # get the highest number
- numbers.sort()
- fn = numbers[-1] + 1
- else:
- fn = 1
- name = "%s%0*d.eeg"%(self.default_prefix, self.default_numbersize, fn)
- return name
-
- def _get_unique_filename(self, filename):
- ''' if file already exists, append the next free number to the filename
- @param filename: filename without path and extension, has to be unicode
- @return: fully qualified path and filename for the next, not yet existing eeg file
- '''
- # get rid of path and extension from filename
- fnx = os.path.split(filename)[1]
- fn = os.path.splitext(fnx)[0]
- # take path from configuration
- pn = self.default_path
- eegdir = Qt.QDir(pn)
- if not eegdir.exists():
- raise Exception("path '%s' does not exist"%pn)
- eegdir.setFilter(Qt.QDir.Files)
- eegdir.setNameFilters(Qt.QStringList(u"%s*.eeg"%(fn)))
- allfiles = eegdir.entryList()
- eegdir.setNameFilters(Qt.QStringList(u"%s_*.eeg"%(fn)))
- numberedfiles = eegdir.entryList()
-
- if allfiles.count() == 0:
- return os.path.join(pn, filename + ".eeg")
-
- # extract numbers
- numberedfiles.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
- numberedfiles.replaceInStrings(fn+"_", "", Qt.Qt.CaseInsensitive)
- numbers = []
- for f in numberedfiles:
- num,ok = f.toInt()
- if ok:
- numbers.append(num)
- if len(numbers) > 0:
- # get the highest number
- numbers.sort()
- fnum = numbers[-1] + 1
- else:
- fnum = 1
- newfilename = os.path.join(pn, u"%s_%d.eeg"%(filename,fnum))
-
- # verify that the file is not yet existing
- if Qt.QFile.exists(newfilename):
- raise Exception("auto numbering failed, '%s' already exists"%newfilename)
-
- return newfilename
-
-
- def set_recording_file(self):
- ''' SIGNAL recording button clicked: Select data file and prepare recording
- '''
- try:
- # is recording active?
- if self.data_file != 0:
- if self.process_query("Stop"):
- self._close_recording()
- else:
- self.online_cfg.set_recording_state(True)
- elif self.params.recording_mode != RecordingMode.IMPEDANCE:
- dlg = Qt.QFileDialog()
- dlg.setFileMode(Qt.QFileDialog.AnyFile)
- dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
- dlg.setDefaultSuffix("eeg")
- if len(self.default_path) > 0:
- dlg.setDirectory(self.default_path)
- dlg.selectFile(self._get_auto_filename(dlg.directory()))
- namefilters = Qt.QStringList(u"EEG files (*.eeg)")
- if self.default_autoname and (len(self.default_prefix) > 0):
- namefilters.prepend(u"EEG files (%s*.eeg)"%(self.default_prefix))
- dlg.setNameFilters(namefilters)
- ok = False
- if dlg.exec_() == True:
- ok = True
- files = dlg.selectedFiles()
- pf = unicode(files[0])
- # strip leading/trailing spaces from file name
- pn, fn = os.path.split(pf)
- self.file_name = os.path.join(pn, fn.strip())
- # append the extension .eeg, if not already present
- if not self.file_name.lower().endswith(".eeg"):
- self.file_name += ".eeg"
- # additional check for existing files, possibly we have modified the file name
- if self.file_name.replace("\\","/") != pf.replace("\\","/") and os.path.exists(self.file_name):
- ret = Qt.QMessageBox.warning(None, "Save As", "%s already exists.\nDo you want to replace it?"%(os.path.split(self.file_name)[1]),
- Qt.QMessageBox.Ok | Qt.QMessageBox.No, Qt.QMessageBox.No)
- if ret != Qt.QMessageBox.Ok:
- ok = False
-
- if ok and self._prepare_recording():
- self.online_cfg.set_filename(os.path.split(self.file_name)[0],
- os.path.split(self.file_name)[1])
- if not ok:
- self.online_cfg.set_recording_state(False)
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.STOP)
-
-
- def _getImpedanceValueText(self, impedance):
- ''' evaluate the impedance value and get the text for the header file
- @return: text
- '''
- if impedance > CHAMP_IMP_INVALID:
- valuetext = "Disconnected!"
- else:
- v = impedance / 1000.0
- if impedance == CHAMP_IMP_INVALID:
- valuetext = "Out of Range!"
- else:
- valuetext = "%.0f"%(v)
- return valuetext
-
-
- def _prepare_recording(self):
- ''' Create and prepare EEG data, header and marker file
- '''
- if self.file_name != None:
- # check for minimum available disk space
- if self.get_free_space(self.file_name)[0] < 0:
- self.online_cfg.set_recording_state(False)
- path = os.path.split(self.file_name)[0]
- Qt.QMessageBox.critical(None, "Storage", "out of disk space (%.2fGB) on %s"%(self.min_disk_space, path))
- return False
-
- fname, ext = os.path.splitext(self.file_name)
- headername = fname + ".vhdr"
- markername = fname + ".vmrk"
- crlf = u"\n"
-
- # create EEG header file
- try:
- self.header_file = open(headername, "w")
- h = u"Brain Vision Data Exchange Header File Version 1.0" + crlf
- h += u"; Data created by the actiCHamp PyCorder" + crlf + crlf
-
- # common infos.
- h += u"[Common Infos]" + crlf
- h += u"Codepage=UTF-8" + crlf
- h += u"DataFile=" + os.path.split(self.file_name)[1] + crlf
- h += u"MarkerFile=" + os.path.split(markername)[1] + crlf
- h += u"DataFormat=BINARY" + crlf
- h += u"; Data orientation: MULTIPLEXED=ch1,pt1, ch2,pt1 ..." + crlf
- h += u"DataOrientation=MULTIPLEXED" + crlf
- h += u"NumberOfChannels=%d"%(len(self.params.channel_properties)) + crlf
- h += u"; Sampling interval in microseconds" + crlf
- usSR = 1000000.0 / self.params.sample_rate
- if int(usSR) == usSR:
- h += u"SamplingInterval=%d"%(usSR) + crlf
- else:
- h += u"SamplingInterval=%.5f"%(usSR) + crlf
- h += crlf
- h += u"[Binary Infos]" + crlf
- h += u"BinaryFormat=IEEE_FLOAT_32" + crlf
- h += crlf
- h += u"[Channel Infos]" + crlf
- h += u"; Each entry: Ch=,," + crlf
- h += u"; ,, Future extensions.." + crlf
- h += u"; Fields are delimited by commas, some fields might be omitted (empty)." + crlf
- h += u"; Commas in channel names are coded as \"\\1\"." + crlf
-
- # channel configuration
- ch = 1
- for channel in self.params.channel_properties:
- lbl = channel.name.replace(",","\\1")
- refLabel = channel.refname.replace(",","\\1")
- if len(channel.unit) > 0:
- unit = channel.unit
- else:
- unit = u"µV"
- h += u"Ch%d=%s,%s,1.0,%s"%(ch, lbl, refLabel, unit) + crlf
- ch += 1
-
- # recorder info
- h += crlf
- h += u"[Comment]" + crlf
- h += self.moduledescription
- h += crlf
-
- # reference channel names
- h += u"Reference channel: %s"%(self.params.ref_channel_name) + crlf
-
- # impedance values if available
- if self.last_impedance != None:
- h += crlf
- h += u"Impedance [KOhm] at %s (recording started at %s)"%(self.last_impedance.block_time.strftime("%H:%M:%S"), \
- datetime.datetime.now().strftime("%H:%M:%S")) + crlf
-
- # impedance for eeg electrodes
- gndImpedance = None
- for idx, ch in enumerate(self.last_impedance_config.channel_properties):
- if not ((ch.inputgroup == ChannelGroup.EEG) and (ch.enable or ch.isReference)):
- continue
- valD = ""
- valR = ""
- # impedance value for data electrode available?
- if self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.DATA] == 1:
- valD = self._getImpedanceValueText(self.last_impedance.eeg_channels[idx, ImpedanceIndex.DATA])
-
- # impedance value for reference electrode available?
- if self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.REF] == 1:
- valR = self._getImpedanceValueText(self.last_impedance.eeg_channels[idx, ImpedanceIndex.REF])
-
- if len(valD) and len(valR):
- impedanceText = "+%s / -%s"%(valD, valR)
- else:
- impedanceText = valD
-
- # take the first available GND impedance
- if gndImpedance == None and self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.GND] == 1:
- gndImpedance = self.last_impedance.eeg_channels[idx, ImpedanceIndex.GND]
-
- if len(impedanceText) > 0:
- h += u"%3d %s: %s"%(ch.input, ch.name, impedanceText) + crlf
-
- # GND electrode
- if gndImpedance != None:
- val = self._getImpedanceValueText(gndImpedance)
- h += u"GND: %s"%(val) + crlf
-
- self.header_file.write(h.encode('utf-8'))
- self.header_file.close()
-
- except Exception as e:
- raise ModuleError(self._object_name, "failed to create %s\n%s"%(headername, str(e)))
-
- # create EEG marker file
- try:
- self.marker_file = open(markername, "w")
- h = u"Brain Vision Data Exchange Marker File, Version 1.0" + crlf
- h += crlf
- # common infos.
- h += u"[Common Infos]" + crlf
- h += u"Codepage=UTF-8" + crlf
- h += u"DataFile=" + os.path.split(self.file_name)[1] + crlf
- h += crlf
- # Marker infos.
- h += u"[Marker Infos]" + crlf
- h += u"; Each entry: Mk=,,," + crlf
- h += u"; , " + crlf
- h += u"; Fields are delimited by commas, some fields might be omitted (empty)." + crlf
- h += u"; Commas in type or description text are coded as \"\\1\"." + crlf
-
- self.marker_file.write(h.encode('utf-8'))
- self.marker_file.flush()
- self.marker_counter = 0
- self.marker_newseg = False
-
- except Exception as e:
- self.header_file.close()
- raise ModuleError(self._object_name, "failed to create %s\n%s"%(markername, str(e)))
-
- # create EEG data file
- try:
- self._thLock.acquire()
- self.data_file = self.libc._wfopen(unicode(self.file_name), u"wb")
- self.write_error = False
- except IOError as e:
- self.header_file.close()
- self.marker_file.close()
- raise ModuleError(self._object_name, "failed to create %s"%(self.file_name))
- finally:
- self._thLock.release()
-
- # show recording state
- self.online_cfg.set_recording_state(True)
-
- # send status to application
- self.send_event(ModuleEvent(self._object_name,
- EventType.STATUS,
- info = self.file_name,
- status_field="Storage"))
- return True
-
- def _close_recording(self):
- ''' Close all EEG files
- '''
- self._thLock.acquire()
- if self.data_file != 0:
- try:
- self.libc.fclose(self.data_file)
- self.marker_file.close()
- except Exception as e:
- print "Failed to close recording files: " + str(e)
- self.data_file = 0
- self.data_file = 0
- self.online_cfg.set_recording_state(False)
- self._thLock.release()
-
-
- def _writeMarkerToFile(self, marker, blockdate):
- ''' Write single marker object to marker file
- @param marker: EEG_Marker object
- @param blockdate: datetime object with start time of the current data block
- '''
- # consecutive marker number
- self.marker_counter += 1
- # Mkn=type,description,position,points,channel
- m = u"Mk%d=%s,%s,%d,%d,%d"%(self.marker_counter,
- marker.type,
- marker.description,
- marker.position,
- marker.points,
- marker.channel)
- if marker.date:
- try:
- m += marker.dt.strftime(",%Y%m%d%H%M%S%f")
- except:
- m += blockdate.strftime(",%Y%m%d%H%M%S%f")
- m += u"\n"
- self.marker_file.write(m.encode('utf-8'))
- self.marker_file.flush()
-
-
- def _write_marker(self, markers, blockdate, blocksamplecounter, sctBreakDiff):
- ''' Write marker to file
- @param markers: list of marker objects (EEG_Marker)
- @param blockdate: datetime object with start time of the current data block
- @param blocksamplecounter: first sample counter value of the current data block
- @param sctBreakDiff: 2-dimensional numpy array with sample counter values at index 0
- and number of missing samples at this counter at index 1
- '''
- # insert "New Segment" marker as first marker and reset internal sample counters
- if self.marker_counter == 0:
- markers.insert(0, EEG_Marker(type="New Segment", date=True, position=blocksamplecounter))
- self.start_sample = blocksamplecounter
- self.total_missing = 0
- self.samples_written = 0
- self.start_time = blockdate
-
- # adjust marker positions and insert new segment markers if necessary
- new_segments = sctBreakDiff[:,:]
- ns_cumulatedMissing = 0
- output_markers = []
-
- for marker in markers:
- # are there a new segments before current marker position?
- if self.marker_newseg and new_segments.shape[1]:
- ns_position = new_segments[0, np.nonzero(new_segments[0] <= marker.position)[0]]
- ns_missing = new_segments[1,np.nonzero(new_segments[0] <= marker.position)[0]]
- # insert new segment markers
- for ns in range(ns_position.size):
- ns_cumulatedMissing += ns_missing[ns]
- mkr = EEG_Marker(type="New Segment", date=True, position=ns_position[ns])
- output_markers.append(copy.deepcopy(mkr))
- # adjust the new segment marker time
- sampletime = (ns_position[ns] - self.start_sample) / self.params.sample_rate
- mkr.dt = self.start_time + datetime.timedelta(seconds=sampletime)
- # adjust position to file sample counter
- mkr.position = ns_position[ns] - self.start_sample - self.total_missing - ns_cumulatedMissing + 1
- # write new segment marker to file
- self._writeMarkerToFile(mkr, blockdate)
- # remove handled new segments
- new_segments = new_segments[:,np.nonzero(new_segments[0] > marker.position)[0]]
-
- output_markers.append(copy.deepcopy(marker))
- # missing samples up to marker position
- miss = np.sum(sctBreakDiff[1, np.nonzero(sctBreakDiff[0] <= marker.position)[0]])
- # adjust position to file sample counter
- marker.position = marker.position - self.start_sample - self.total_missing - miss + 1
- # write marker to file
- self._writeMarkerToFile(marker, blockdate)
-
- # append disregarded new segment markers
- if self.marker_newseg and new_segments.shape[1]:
- ns_position = new_segments[0,:]
- ns_missing = new_segments[1,:]
- # insert new segment markers
- for ns in range(ns_position.size):
- ns_cumulatedMissing += ns_missing[ns]
- mkr = EEG_Marker(type="New Segment", date=True, position=ns_position[ns])
- output_markers.append(copy.deepcopy(mkr))
- # adjust the new segment marker time
- sampletime = (ns_position[ns] - self.start_sample) / self.params.sample_rate
- mkr.dt = self.start_time + datetime.timedelta(seconds=sampletime)
- # adjust position to file sample counter
- mkr.position = ns_position[ns] - self.start_sample - self.total_missing - ns_cumulatedMissing + 1
- # write new segment marker to file
- self._writeMarkerToFile(mkr, blockdate)
-
- return output_markers
-
-
- def process_event(self, event):
- ''' Handle events from attached receivers
- @param event: ModuleEvent
- '''
- # Get info of all connected modules for header file comment
- if event.type == EventType.STATUS and event.status_field == "ModuleInfo":
- self.moduledescription = event.info
-
- # handle remote commands
- if event.type == EventType.COMMAND:
- # check for start, cmd_value contains the EEG filename without extension
- if event.info == "StartSaving":
- # quit if recording is already active or if we are in impendance mode
- if self.data_file != 0 or self.params.recording_mode == RecordingMode.IMPEDANCE:
- return
- try:
- self.file_name = self._get_unique_filename(event.cmd_value)
- if self._prepare_recording():
- self.online_cfg.set_filename(os.path.split(self.file_name)[0],
- os.path.split(self.file_name)[1])
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.STOP)
-
- # check for stop
- if event.info == "StopSaving":
- self._close_recording()
-
-
- def process_update(self, params):
- ''' Calculate recording parameters for updated channels
- '''
- # copy settings
- self.params = copy.copy(params)
- if params.recording_mode == RecordingMode.IMPEDANCE:
- self.last_impedance_config = copy.copy(params)
- numchannels = len(params.channel_properties)
- self.samples_per_second = numchannels * params.sample_rate
- return params
-
- def process_query(self, command):
- ''' Evaluate query commands.
- @param command: command string
- @return: True if user confirms to stop recording to file
- '''
- if self.data_file == 0:
- return True
- if command == "Stop":
- ret = Qt.QMessageBox.question(None, "PyCorder", "Stop Recording?",
- Qt.QMessageBox.Ok | Qt.QMessageBox.Cancel, Qt.QMessageBox.Cancel)
- if ret != Qt.QMessageBox.Ok:
- return False
- if command == "RemoteStop":
- return False
- return True
-
-
- def process_start(self):
- ''' Start data acquisition
- '''
- # reset sample counter check
- self.missing_timer = time.clock()
- self.missing_interval = 0
- self.missing_cumulated = 0
- self.next_samplecounter = -2
- # enable recording button
- if self.params.recording_mode != RecordingMode.IMPEDANCE:
- self.online_cfg.pushButtonRecord.setEnabled(True)
- else:
- self.online_cfg.pushButtonRecord.setEnabled(False)
-
- def process_stop(self):
- ''' Stop data acquisition
- '''
- self._close_recording()
- # disable recording button
- self.online_cfg.pushButtonRecord.setEnabled(False)
-
- def process_input(self, datablock):
- ''' Store data to file
- '''
- self.dataavailable = True
- self.data = datablock
-
- # keep last impedance values for next EEG header file
- if self.data.recording_mode == RecordingMode.IMPEDANCE:
- self.last_impedance = copy.copy(datablock)
- return
-
- # check sample counter
- if self.next_samplecounter < -1:
- self.next_samplecounter = self.data.sample_channel[0][0] - 1 # first block after start
- samples = len(self.data.sample_channel[0])
- missing_precheck = self.data.sample_channel[0][-1] - (self.next_samplecounter + samples)
- self.marker_newseg = True # always write new segment markers if samples are missing
-
- # counter not in expected range ?
- if missing_precheck != 0:
- sct = self.data.sample_channel[0]
- sct_check = np.append((self.next_samplecounter), sct)
- sctDiff = np.diff(sct_check) - 1
- sctBreak = np.nonzero(sctDiff)[0]
- missing_samples = np.sum(sctDiff)
- self.missing_interval += missing_samples
- self.missing_cumulated += missing_samples
- sctBreakDiff = np.array([sct_check[sctBreak+1], sctDiff[sctBreak]]) # samplecounter / missing
- if time.clock() - self.missing_timer > 30:
- self.missing_interval = missing_samples
- #print "samples missing = %i, interval = %i, cumulated = %i"%(missing_samples, self.missing_interval, self.missing_cumulated)
- error = "%d samples missing"%(missing_samples)
- if self.missing_interval > 2:
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR, info=error, severity=ErrorSeverity.NOTIFY))
- self.missing_interval = 0
- self.missing_cumulated = 0
- else:
- self.send_event(ModuleEvent(self._object_name, EventType.LOG, info=error))
- self.missing_timer = time.clock()
- else:
- missing_samples = 0
- sctBreakDiff = np.array([[],[]],dtype=np.int64)
-
- # set counter to the expected start sample number of next data block
- self.next_samplecounter = self.data.sample_channel[0,-1]
-
-
- if (self.data_file != 0) and not self.write_error:
- try:
- t = time.clock()
- # convert data to float and write to data file
- d = datablock.eeg_channels.transpose()
- f = d.flatten().astype(np.float32)
- sizeof_item = f.dtype.itemsize # item size in bytes
- write_items = len(f) # number of items to write
- nitems = self.libc.fwrite(f.tostring(), sizeof_item, write_items, self.data_file)
- if nitems != write_items:
- raise ModuleError(self._object_name, "Write to file %s failed"%(self.file_name))
- # write marker
- #self._write_marker(self.data.markers, self.data.block_time, self.data.sample_channel[0,0])
- self.data.markers = self._write_marker(self.data.markers, self.data.block_time, self.data.sample_channel[0,0], sctBreakDiff)
-
- # update file sample counter
- self.samples_written += samples
-
- writetime = time.clock() - t
- #print "Write file: %.0f ms / %d Bytes / QSize %d"%(writetime*1000.0, nitems, self._input_queue.qsize())
- except Exception as e:
- self.write_error = True # indicate write error
- self._thLock.release() # release the thread lock because it is acquired by _close_recording()
- self._close_recording() # stop recording
- self._thLock.acquire()
- # notify application
- self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
- str(e),
- severity=ErrorSeverity.NOTIFY))
-
- # update the global sample counter missing value
- self.total_missing += missing_samples
-
-
- def process_output(self):
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
-
-
-'''
-------------------------------------------------------------
-STORAGE MODULE ONLINE GUI
-------------------------------------------------------------
-'''
-
-class _OnlineCfgPane(Qt.QFrame, frmStorageVisionOnline.Ui_frmStorageVisionOnline):
- ''' Vision Storage Module online configuration pane
- '''
- def __init__(self, module, *args):
- ''' Constructor
- @param module: parent module
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
- self.module = module
-
- # set default values
- self.pushButtonRecord.setEnabled(False)
- self.set_recording_state(False)
-
- self.filename = ""
- self.pathname = ""
-
- # start display update timer
- self.startTimer(1000)
-
- def set_filename(self, path, file, time=0):
- ''' Show pathname, filename and optional recording time
- @param path: recording path name
- @param file: recording file name
- @param time: time of data written to file in seconds
- '''
- self.pathname = path
- self.filename = file
- if time > 0:
- days, hours, minutes, seconds = self.get_DHMS(time)
- if days == 0:
- timestring = " %02d:%02d:%02d [h:m:s]"%(hours, minutes, seconds)
- else:
- timestring = " %d:%02d:%02d:%02d [d:h:m:s]"%(days, hours, minutes, seconds)
- else:
- timestring = ""
- self.lineEditPath.setText(path)
- self.lineEditFile.setText(file + timestring)
-
- def timerEvent(self,e):
- ''' Display update timer event
- '''
- # calculate available disk size
- path = unicode(self.lineEditPath.text())
- if len(path) > 0:
- free, total = self.module.get_free_space(path)
- if total > 0 and free > 0:
- ratio = free * 100.0 / total
- else:
- ratio = 0
- self.progressBar.setValue(ratio)
- self.module.check_free_space(free)
- else:
- free = total = 0
- self.progressBar.setValue(0)
-
- # estimate required size in Byte per second
- bps = self.module.samples_per_second * np.zeros(1,np.float32).dtype.itemsize
- if bps > 0:
- if free > 0:
- seconds = free / bps
- else:
- seconds = 0
- # Get the days, hours, minutes:
- days, hours, minutes, seconds = self.get_DHMS(seconds)
- self.lineEditDiskSpace.setText("%d:%02d:%02d"%(days, hours, minutes))
- else:
- self.lineEditDiskSpace.setText("--:--:--")
-
- # calculate the time of data written to file
- if (self.module.params != None) and (self.module.params.sample_rate > 0):
- seconds = self.module.samples_written / self.module.params.sample_rate
- self.set_filename(self.pathname, self.filename, time=seconds)
-
-
- def get_DHMS(self, seconds):
- ''' Get days, hours, minutes and seconds from seconds
- @param seconds: total number of seconds
- @return: tuple (Days, Hours, Minutes, Seconds)
- '''
- MINUTE = 60
- HOUR = MINUTE * 60
- DAY = HOUR * 24
- days = int( seconds / DAY )
- hours = int(( seconds % DAY ) / HOUR )
- minutes = int(( seconds % HOUR ) / MINUTE )
- seconds = int( seconds % MINUTE )
- return days, hours, minutes, seconds
-
-
- def set_recording_state(self, on):
- ''' Update display elements to reflect the recording state
- '''
- if on:
- self.progressBar.setEnabled(True)
- palette = self.lineEditFile.palette()
- if self.module.write_error:
- palette.setColor(Qt.QPalette.Base, Qt.Qt.red)
- else:
- palette.setColor(Qt.QPalette.Base, Qt.Qt.green)
- self.lineEditFile.setPalette(palette)
- self.pushButtonRecord.setChecked(True)
- self.pushButtonRecord.setText("Stop Recording")
- else:
- self.progressBar.setEnabled(False)
- palette = self.lineEditFile.palette()
- if self.module.write_error:
- palette.setColor(Qt.QPalette.Base, Qt.Qt.red)
- else:
- palette.setColor(Qt.QPalette.Base, Qt.QColor(240, 240, 240))
- self.lineEditFile.setPalette(palette)
- self.pushButtonRecord.setChecked(False)
- self.pushButtonRecord.setText("Start Recording")
-
-
-'''
-------------------------------------------------------------
-STORAGE MODULE CONFIGURATION GUI
-------------------------------------------------------------
-'''
-
-class _ConfigurationPane(Qt.QFrame, frmStorageVisionConfig.Ui_frmStorageVisionConfig):
- ''' Vision Storage configuration pane
- '''
- def __init__(self, storage, *args):
- ''' Constructor
- @param storage: parent module
- '''
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
-
- # set validators
- validator = Qt.QIntValidator(1, 50, self)
- self.lineEditCounterSize.setValidator(validator)
-
- validator2 = Qt.QDoubleValidator(0.01, 500.0, 2,self)
- self.lineEditSpace.setValidator(validator2)
-
- # setup content
- self.storage = storage
-
- self.lineEditFolder.setText(storage.default_path)
- self.lineEditPrefix.setText(storage.default_prefix)
- self.lineEditCounterSize.setText(str(storage.default_numbersize))
- self.checkBoxAutoFile.setChecked(storage.default_autoname)
- self.lineEditSpace.setText(str(storage.min_disk_space))
- self._showExample()
-
- # actions
- self.connect(self.lineEditFolder, Qt.SIGNAL("editingFinished()"), self._contentChanged)
- self.connect(self.lineEditPrefix, Qt.SIGNAL("editingFinished()"), self._contentChanged)
- self.connect(self.lineEditCounterSize, Qt.SIGNAL("editingFinished()"), self._contentChanged)
- self.connect(self.checkBoxAutoFile, Qt.SIGNAL("clicked()"), self._contentChanged)
- self.connect(self.pushButtonBrowse, Qt.SIGNAL("clicked()"), self._browse)
- self.connect(self.lineEditSpace, Qt.SIGNAL("editingFinished()"), self._contentChanged)
-
- def _contentChanged(self):
- ''' Update parent object vars
- '''
- self.storage.default_path = unicode(self.lineEditFolder.displayText())
- self.storage.default_prefix = unicode(self.lineEditPrefix.displayText()).lstrip()
- self.lineEditPrefix.setText(self.storage.default_prefix)
- self.storage.default_numbersize = self.lineEditCounterSize.displayText().toInt()[0]
- self.storage.default_autoname = self.checkBoxAutoFile.isChecked()
- self.storage.min_disk_space = self.lineEditSpace.displayText().toDouble()[0]
- self._showExample()
-
- def _browse(self):
- ''' Browse for the default data folder
- '''
- dlg = Qt.QFileDialog()
- dlg.setFileMode(Qt.QFileDialog.DirectoryOnly )
- dlg.setOption(Qt.QFileDialog.ShowDirsOnly)
- dlg.setAcceptMode(Qt.QFileDialog.AcceptOpen)
- if dlg.exec_() == True:
- files = dlg.selectedFiles()
- file_name = unicode(files[0])
- self.lineEditFolder.setText(file_name)
- self._contentChanged()
-
- def _showExample(self):
- ''' Show auto file name example
- '''
- example = "%s%0*d.eeg"%(self.storage.default_prefix, self.storage.default_numbersize, 1)
- self.labelExample.setText(example)
-
-
-
-
-
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Storage Module for Vision EEG file format
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-20 14:00:34 +0200 (Do, 20 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 206 $
+'''
+import ctypes as ct
+import os
+import platform
+from modbase import *
+from res import frmStorageVisionOnline
+from res import frmStorageVisionConfig
+
+'''
+------------------------------------------------------------
+STORAGE MODULE
+------------------------------------------------------------
+'''
+
+class StorageVision(ModuleBase):
+ ''' Vision Date Exchange Format
+ - Storage class using ctypes
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ ModuleBase.__init__(self, queuesize=50, name="StorageVision", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ # 2: minimum required disk space added
+ self.xmlVersion = 2
+
+ # get OS architecture (32/64-bit)
+ self.x64 = ("64" in platform.architecture()[0])
+
+ self.data = None
+ self.dataavailable = False
+ self.params = None
+ self.last_impedance = None #: last received impedance EEG block
+ self.last_impedance_config = None #: last received impedance configuration EEG block
+ self.moduledescription = "" #: description of connected modules
+
+ # configuration data
+ self.setDefault()
+
+ # output files
+ self.file_name = None #: output file name
+ self.data_file = None #: python file handle
+ self.header_file = 0 #: header file handle
+ self.marker_file = 0 #: marker file handle
+ self.marker_counter = 0 #: total number of markers written
+ self.start_sample = 0 #: sample counter of first sample written to file
+ self.marker_newseg = False #: request for new segment marker
+
+ self.next_samplecounter = -2 #: verify sample counter of next EEG block
+ self.total_missing = 0 #: number of total samples missing
+ self.samples_written = 0 #: number of samples written to file
+ self.write_error = False #: write to disk failed
+ self.min_disk_space = 1.0 #: minimum free disk space in GByte
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ self.default_path = "" #: default data storage path
+ self.default_prefix = "" #: prefex for data files e.g. "EEG_"
+ self.default_numbersize = 6 #: number of digits to append to file name
+ self.default_autoname = False #: create auto file name
+
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ '''
+ # create online configuration pane
+ self.online_cfg = _OnlineCfgPane(self)
+ # connect recording button
+ self.connect(self.online_cfg.pushButtonRecord, Qt.SIGNAL("clicked(bool)"), self.set_recording_file)
+ return self.online_cfg
+
+ def get_configuration_pane(self):
+ ''' Get the configuration pane if available
+ - Qt widgets are not reusable, so we have to create it every time
+ '''
+ config = _ConfigurationPane(self)
+ return config
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+ e.g.
+
+ D:\EEG
+ ...
+
+ '''
+ E = objectify.E
+ cfg = E.StorageVision(E.d_path(self.default_path),
+ E.d_autoname(self.default_autoname),
+ E.d_prefix(self.default_prefix),
+ E.d_numbersize(self.default_numbersize),
+ E.mindiskspace(self.min_disk_space),
+ version=str(self.xmlVersion),
+ instance=str(self._instance),
+ module="storage")
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search my configuration data
+ storages = xml.xpath("//StorageVision[@module='storage' and @instance='%i']"%(self._instance) )
+ if len(storages) == 0:
+ # configuration data not found, leave everything unchanged
+ return
+
+ # we should have only one instance from this type
+ cfg = storages[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ self.default_path = cfg.d_path.pyval
+ self.default_autoname = cfg.d_autoname.pyval
+ self.default_prefix = cfg.d_prefix.pyval
+ self.default_numbersize = cfg.d_numbersize.pyval
+ if version > 1:
+ self.min_disk_space = cfg.mindiskspace.pyval
+ else:
+ self.min_disk_space = 1.0
+
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+ def get_free_space(self, path):
+ ''' Get the total and free available disk space
+ @param path: complete data file path
+ @return: tuple folder/drive free and total space (in bytes)
+ '''
+ if platform.system() == 'Windows':
+ folder = os.path.splitdrive(path)[0]
+ free_bytes = ct.c_ulonglong(0)
+ total_bytes = ct.c_ulonglong(0)
+ ct.windll.kernel32.GetDiskFreeSpaceExW(ct.c_wchar_p(folder),\
+ ct.pointer(free_bytes),\
+ ct.pointer(total_bytes),\
+ None)
+ free = free_bytes.value - self.min_disk_space * 1024**3
+ return free, total_bytes.value
+ else:
+ folder = os.path.split(path)[0]
+ diskinfo = os.statvfs(folder)
+ total_bytes = diskinfo.f_blocks * diskinfo.f_bsize
+ free = (diskinfo.f_bavail * diskinfo.f_bsize) - self.min_disk_space * 1024**3
+ return free, total_bytes
+
+
+
+ def check_free_space(self, freespace):
+ ''' Check for a minimum available free space
+ - If disk runs out of space during recording, stop recording
+ @param freespace: available space in bytes
+ @return: False if disk is out of space
+ '''
+ if freespace > 0:
+ return True
+ if self.data_file is not None:
+ # stop recording
+ self.write_error = True
+ self._close_recording()
+ # notify application
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ "out of disk space (<%.2fGB), recording stopped"%(self.min_disk_space),
+ severity=ErrorSeverity.NOTIFY))
+ return False
+
+
+ def _get_auto_filename(self, searchdir):
+ ''' Search for next auto file number
+ @param searchdir: Qt.Qdir
+ @return: the generated filename
+ '''
+ if not self.default_autoname:
+ return ""
+ numberstring = "?"
+ for n in range(1, self.default_numbersize):
+ numberstring += "?"
+ searchdir.setNameFilters(Qt.QStringList("%s%s.eeg"%(self.default_prefix, numberstring)))
+ searchdir.setFilter(Qt.QDir.Files)
+ flist = searchdir.entryList()
+ # extract numbers
+ flist.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
+ if len(self.default_prefix) > 0:
+ flist.replaceInStrings(self.default_prefix, "", Qt.Qt.CaseInsensitive)
+ numbers = []
+ for f in flist:
+ num,ok = f.toInt()
+ if ok and (num < 10**self.default_numbersize-1):
+ numbers.append(num)
+ if len(numbers) > 0:
+ # get the highest number
+ numbers.sort()
+ fn = numbers[-1] + 1
+ else:
+ fn = 1
+ name = "%s%0*d.eeg"%(self.default_prefix, self.default_numbersize, fn)
+ return name
+
+ def _get_unique_filename(self, filename):
+ ''' if file already exists, append the next free number to the filename
+ @param filename: filename without path and extension, has to be unicode
+ @return: fully qualified path and filename for the next, not yet existing eeg file
+ '''
+ # get rid of path and extension from filename
+ fnx = os.path.split(filename)[1]
+ fn = os.path.splitext(fnx)[0]
+ # take path from configuration
+ pn = self.default_path
+ eegdir = Qt.QDir(pn)
+ if not eegdir.exists():
+ raise Exception("path '%s' does not exist"%pn)
+ eegdir.setFilter(Qt.QDir.Files)
+ eegdir.setNameFilters(Qt.QStringList(u"%s*.eeg"%(fn)))
+ allfiles = eegdir.entryList()
+ eegdir.setNameFilters(Qt.QStringList(u"%s_*.eeg"%(fn)))
+ numberedfiles = eegdir.entryList()
+
+ if allfiles.count() == 0:
+ return os.path.join(pn, filename + ".eeg")
+
+ # extract numbers
+ numberedfiles.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
+ numberedfiles.replaceInStrings(fn+"_", "", Qt.Qt.CaseInsensitive)
+ numbers = []
+ for f in numberedfiles:
+ num,ok = f.toInt()
+ if ok:
+ numbers.append(num)
+ if len(numbers) > 0:
+ # get the highest number
+ numbers.sort()
+ fnum = numbers[-1] + 1
+ else:
+ fnum = 1
+ newfilename = os.path.join(pn, u"%s_%d.eeg"%(filename,fnum))
+
+ # verify that the file is not yet existing
+ if Qt.QFile.exists(newfilename):
+ raise Exception("auto numbering failed, '%s' already exists"%newfilename)
+
+ return newfilename
+
+
+ def set_recording_file(self):
+ ''' SIGNAL recording button clicked: Select data file and prepare recording
+ '''
+ try:
+ # is recording active?
+ if self.data_file is not None:
+ if self.process_query("Stop"):
+ self._close_recording()
+ else:
+ self.online_cfg.set_recording_state(True)
+ elif self.params.recording_mode != RecordingMode.IMPEDANCE:
+ dlg = Qt.QFileDialog()
+ dlg.setFileMode(Qt.QFileDialog.AnyFile)
+ dlg.setAcceptMode(Qt.QFileDialog.AcceptSave)
+ dlg.setDefaultSuffix("eeg")
+ if len(self.default_path) > 0:
+ dlg.setDirectory(self.default_path)
+ dlg.selectFile(self._get_auto_filename(dlg.directory()))
+ namefilters = Qt.QStringList(u"EEG files (*.eeg)")
+ if self.default_autoname and (len(self.default_prefix) > 0):
+ namefilters.prepend(u"EEG files (%s*.eeg)"%(self.default_prefix))
+ dlg.setNameFilters(namefilters)
+ ok = False
+ if dlg.exec_() == True:
+ ok = True
+ files = dlg.selectedFiles()
+ pf = unicode(files[0])
+ # strip leading/trailing spaces from file name
+ pn, fn = os.path.split(pf)
+ self.file_name = os.path.join(pn, fn.strip())
+ # append the extension .eeg, if not already present
+ if not self.file_name.lower().endswith(".eeg"):
+ self.file_name += ".eeg"
+ # additional check for existing files, possibly we have modified the file name
+ if self.file_name.replace("\\","/") != pf.replace("\\","/") and os.path.exists(self.file_name):
+ ret = Qt.QMessageBox.warning(None, "Save As", "%s already exists.\nDo you want to replace it?"%(os.path.split(self.file_name)[1]),
+ Qt.QMessageBox.Ok | Qt.QMessageBox.No, Qt.QMessageBox.No)
+ if ret != Qt.QMessageBox.Ok:
+ ok = False
+
+ if ok and self._prepare_recording():
+ self.online_cfg.set_filename(os.path.split(self.file_name)[0],
+ os.path.split(self.file_name)[1])
+ if not ok:
+ self.online_cfg.set_recording_state(False)
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.STOP)
+
+
+ def _getImpedanceValueText(self, impedance):
+ ''' evaluate the impedance value and get the text for the header file
+ @return: text
+ '''
+ if impedance > CHAMP_IMP_INVALID:
+ valuetext = "Disconnected!"
+ else:
+ v = impedance / 1000.0
+ if impedance == CHAMP_IMP_INVALID:
+ valuetext = "Out of Range!"
+ else:
+ valuetext = "%.0f"%(v)
+ return valuetext
+
+
+ def _prepare_recording(self):
+ ''' Create and prepare EEG data, header and marker file
+ '''
+ if self.file_name != None:
+ # check for minimum available disk space
+ if self.get_free_space(self.file_name)[0] < 0:
+ self.online_cfg.set_recording_state(False)
+ path = os.path.split(self.file_name)[0]
+ Qt.QMessageBox.critical(None, "Storage", "out of disk space (%.2fGB) on %s"%(self.min_disk_space, path))
+ return False
+
+ fname, ext = os.path.splitext(self.file_name)
+ headername = fname + ".vhdr"
+ markername = fname + ".vmrk"
+ crlf = u"\n"
+
+ # create EEG header file
+ try:
+ self.header_file = open(headername, "w")
+ h = u"Brain Vision Data Exchange Header File Version 1.0" + crlf
+ h += u"; Data created by the actiCHamp PyCorder" + crlf + crlf
+
+ # common infos.
+ h += u"[Common Infos]" + crlf
+ h += u"Codepage=UTF-8" + crlf
+ h += u"DataFile=" + os.path.split(self.file_name)[1] + crlf
+ h += u"MarkerFile=" + os.path.split(markername)[1] + crlf
+ h += u"DataFormat=BINARY" + crlf
+ h += u"; Data orientation: MULTIPLEXED=ch1,pt1, ch2,pt1 ..." + crlf
+ h += u"DataOrientation=MULTIPLEXED" + crlf
+ h += u"NumberOfChannels=%d"%(len(self.params.channel_properties)) + crlf
+ h += u"; Sampling interval in microseconds" + crlf
+ usSR = 1000000.0 / self.params.sample_rate
+ if int(usSR) == usSR:
+ h += u"SamplingInterval=%d"%(usSR) + crlf
+ else:
+ h += u"SamplingInterval=%.5f"%(usSR) + crlf
+ h += crlf
+ h += u"[Binary Infos]" + crlf
+ h += u"BinaryFormat=IEEE_FLOAT_32" + crlf
+ h += crlf
+ h += u"[Channel Infos]" + crlf
+ h += u"; Each entry: Ch=,," + crlf
+ h += u"; ,, Future extensions.." + crlf
+ h += u"; Fields are delimited by commas, some fields might be omitted (empty)." + crlf
+ h += u"; Commas in channel names are coded as \"\\1\"." + crlf
+
+ # channel configuration
+ ch = 1
+ for channel in self.params.channel_properties:
+ lbl = channel.name.replace(",","\\1")
+ refLabel = channel.refname.replace(",","\\1")
+ if len(channel.unit) > 0:
+ unit = channel.unit
+ else:
+ unit = u"µV"
+ h += u"Ch%d=%s,%s,1.0,%s"%(ch, lbl, refLabel, unit) + crlf
+ ch += 1
+
+ # recorder info
+ h += crlf
+ h += u"[Comment]" + crlf
+ h += self.moduledescription
+ h += crlf
+
+ # reference channel names
+ h += u"Reference channel: %s"%(self.params.ref_channel_name) + crlf
+
+ # impedance values if available
+ if self.last_impedance != None:
+ h += crlf
+ h += u"Impedance [KOhm] at %s (recording started at %s)"%(self.last_impedance.block_time.strftime("%H:%M:%S"), \
+ datetime.datetime.now().strftime("%H:%M:%S")) + crlf
+
+ # impedance for eeg electrodes
+ gndImpedance = None
+ for idx, ch in enumerate(self.last_impedance_config.channel_properties):
+ if not ((ch.inputgroup == ChannelGroup.EEG) and (ch.enable or ch.isReference)):
+ continue
+ valD = ""
+ valR = ""
+ # impedance value for data electrode available?
+ if self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.DATA] == 1:
+ valD = self._getImpedanceValueText(self.last_impedance.eeg_channels[idx, ImpedanceIndex.DATA])
+
+ # impedance value for reference electrode available?
+ if self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.REF] == 1:
+ valR = self._getImpedanceValueText(self.last_impedance.eeg_channels[idx, ImpedanceIndex.REF])
+
+ if len(valD) and len(valR):
+ impedanceText = "+%s / -%s"%(valD, valR)
+ else:
+ impedanceText = valD
+
+ # take the first available GND impedance
+ if gndImpedance == None and self.last_impedance_config.eeg_channels[idx, ImpedanceIndex.GND] == 1:
+ gndImpedance = self.last_impedance.eeg_channels[idx, ImpedanceIndex.GND]
+
+ if len(impedanceText) > 0:
+ h += u"%3d %s: %s"%(ch.input, ch.name, impedanceText) + crlf
+
+ # GND electrode
+ if gndImpedance != None:
+ val = self._getImpedanceValueText(gndImpedance)
+ h += u"GND: %s"%(val) + crlf
+
+ self.header_file.write(h.encode('utf-8'))
+ self.header_file.close()
+
+ except Exception as e:
+ raise ModuleError(self._object_name, "failed to create %s\n%s"%(headername, str(e)))
+
+ # create EEG marker file
+ try:
+ self.marker_file = open(markername, "w")
+ h = u"Brain Vision Data Exchange Marker File, Version 1.0" + crlf
+ h += crlf
+ # common infos.
+ h += u"[Common Infos]" + crlf
+ h += u"Codepage=UTF-8" + crlf
+ h += u"DataFile=" + os.path.split(self.file_name)[1] + crlf
+ h += crlf
+ # Marker infos.
+ h += u"[Marker Infos]" + crlf
+ h += u"; Each entry: Mk=,,," + crlf
+ h += u"; , " + crlf
+ h += u"; Fields are delimited by commas, some fields might be omitted (empty)." + crlf
+ h += u"; Commas in type or description text are coded as \"\\1\"." + crlf
+
+ self.marker_file.write(h.encode('utf-8'))
+ self.marker_file.flush()
+ self.marker_counter = 0
+ self.marker_newseg = False
+
+ except Exception as e:
+ self.header_file.close()
+ raise ModuleError(self._object_name, "failed to create %s\n%s"%(markername, str(e)))
+
+ # create EEG data file
+ try:
+ self._thLock.acquire()
+ # Python標準I/Oでバイナリ書き込み
+ self.data_file = open(self.file_name, "wb")
+ self.write_error = False
+ except IOError as e:
+ self.header_file.close()
+ self.marker_file.close()
+ raise ModuleError(self._object_name, "failed to create %s"%(self.file_name))
+ finally:
+ self._thLock.release()
+
+ # show recording state
+ self.online_cfg.set_recording_state(True)
+
+ # send status to application
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.STATUS,
+ info = self.file_name,
+ status_field="Storage"))
+ return True
+
+ def _close_recording(self):
+ ''' Close all EEG files
+ '''
+ self._thLock.acquire()
+ if self.data_file is not None:
+ try:
+ self.data_file.close()
+ self.marker_file.close()
+ except Exception as e:
+ print("Failed to close recording files: " + str(e))
+ self.data_file = None
+ self.data_file = None
+ self.online_cfg.set_recording_state(False)
+ self._thLock.release()
+
+
+ def _writeMarkerToFile(self, marker, blockdate):
+ ''' Write single marker object to marker file
+ @param marker: EEG_Marker object
+ @param blockdate: datetime object with start time of the current data block
+ '''
+ # consecutive marker number
+ self.marker_counter += 1
+ # Mkn=type,description,position,points,channel
+ m = u"Mk%d=%s,%s,%d,%d,%d"%(self.marker_counter,
+ marker.type,
+ marker.description,
+ marker.position,
+ marker.points,
+ marker.channel)
+ if marker.date:
+ try:
+ m += marker.dt.strftime(",%Y%m%d%H%M%S%f")
+ except:
+ m += blockdate.strftime(",%Y%m%d%H%M%S%f")
+ m += u"\n"
+ self.marker_file.write(m.encode('utf-8'))
+ self.marker_file.flush()
+
+
+ def _write_marker(self, markers, blockdate, blocksamplecounter, sctBreakDiff):
+ ''' Write marker to file
+ @param markers: list of marker objects (EEG_Marker)
+ @param blockdate: datetime object with start time of the current data block
+ @param blocksamplecounter: first sample counter value of the current data block
+ @param sctBreakDiff: 2-dimensional numpy array with sample counter values at index 0
+ and number of missing samples at this counter at index 1
+ '''
+ # insert "New Segment" marker as first marker and reset internal sample counters
+ if self.marker_counter == 0:
+ markers.insert(0, EEG_Marker(type="New Segment", date=True, position=blocksamplecounter))
+ self.start_sample = blocksamplecounter
+ self.total_missing = 0
+ self.samples_written = 0
+ self.start_time = blockdate
+
+ # adjust marker positions and insert new segment markers if necessary
+ new_segments = sctBreakDiff[:,:]
+ ns_cumulatedMissing = 0
+ output_markers = []
+
+ for marker in markers:
+ # are there a new segments before current marker position?
+ if self.marker_newseg and new_segments.shape[1]:
+ ns_position = new_segments[0, np.nonzero(new_segments[0] <= marker.position)[0]]
+ ns_missing = new_segments[1,np.nonzero(new_segments[0] <= marker.position)[0]]
+ # insert new segment markers
+ for ns in range(ns_position.size):
+ ns_cumulatedMissing += ns_missing[ns]
+ mkr = EEG_Marker(type="New Segment", date=True, position=ns_position[ns])
+ output_markers.append(copy.deepcopy(mkr))
+ # adjust the new segment marker time
+ sampletime = (ns_position[ns] - self.start_sample) / self.params.sample_rate
+ mkr.dt = self.start_time + datetime.timedelta(seconds=sampletime)
+ # adjust position to file sample counter
+ mkr.position = ns_position[ns] - self.start_sample - self.total_missing - ns_cumulatedMissing + 1
+ # write new segment marker to file
+ self._writeMarkerToFile(mkr, blockdate)
+ # remove handled new segments
+ new_segments = new_segments[:,np.nonzero(new_segments[0] > marker.position)[0]]
+
+ output_markers.append(copy.deepcopy(marker))
+ # missing samples up to marker position
+ miss = np.sum(sctBreakDiff[1, np.nonzero(sctBreakDiff[0] <= marker.position)[0]])
+ # adjust position to file sample counter
+ marker.position = marker.position - self.start_sample - self.total_missing - miss + 1
+ # write marker to file
+ self._writeMarkerToFile(marker, blockdate)
+
+ # append disregarded new segment markers
+ if self.marker_newseg and new_segments.shape[1]:
+ ns_position = new_segments[0,:]
+ ns_missing = new_segments[1,:]
+ # insert new segment markers
+ for ns in range(ns_position.size):
+ ns_cumulatedMissing += ns_missing[ns]
+ mkr = EEG_Marker(type="New Segment", date=True, position=ns_position[ns])
+ output_markers.append(copy.deepcopy(mkr))
+ # adjust the new segment marker time
+ sampletime = (ns_position[ns] - self.start_sample) / self.params.sample_rate
+ mkr.dt = self.start_time + datetime.timedelta(seconds=sampletime)
+ # adjust position to file sample counter
+ mkr.position = ns_position[ns] - self.start_sample - self.total_missing - ns_cumulatedMissing + 1
+ # write new segment marker to file
+ self._writeMarkerToFile(mkr, blockdate)
+
+ return output_markers
+
+
+ def process_event(self, event):
+ ''' Handle events from attached receivers
+ @param event: ModuleEvent
+ '''
+ # Get info of all connected modules for header file comment
+ if event.type == EventType.STATUS and event.status_field == "ModuleInfo":
+ self.moduledescription = event.info
+
+ # handle remote commands
+ if event.type == EventType.COMMAND:
+ # check for start, cmd_value contains the EEG filename without extension
+ if event.info == "StartSaving":
+ # quit if recording is already active or if we are in impendance mode
+ if self.data_file is not None or self.params.recording_mode == RecordingMode.IMPEDANCE:
+ return
+ try:
+ self.file_name = self._get_unique_filename(event.cmd_value)
+ if self._prepare_recording():
+ self.online_cfg.set_filename(os.path.split(self.file_name)[0],
+ os.path.split(self.file_name)[1])
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.STOP)
+
+ # check for stop
+ if event.info == "StopSaving":
+ self._close_recording()
+
+
+ def process_update(self, params):
+ ''' Calculate recording parameters for updated channels
+ '''
+ # copy settings
+ self.params = copy.copy(params)
+ if params.recording_mode == RecordingMode.IMPEDANCE:
+ self.last_impedance_config = copy.copy(params)
+ numchannels = len(params.channel_properties)
+ self.samples_per_second = numchannels * params.sample_rate
+ return params
+
+ def process_query(self, command):
+ ''' Evaluate query commands.
+ @param command: command string
+ @return: True if user confirms to stop recording to file
+ '''
+ if self.data_file is None:
+ return True
+ if command == "Stop":
+ ret = Qt.QMessageBox.question(None, "PyCorder", "Stop Recording?",
+ Qt.QMessageBox.Ok | Qt.QMessageBox.Cancel, Qt.QMessageBox.Cancel)
+ if ret != Qt.QMessageBox.Ok:
+ return False
+ if command == "RemoteStop":
+ return False
+ return True
+
+
+ def process_start(self):
+ ''' Start data acquisition
+ '''
+ # reset sample counter check
+ self.missing_timer = time.perf_counter()
+ self.missing_interval = 0
+ self.missing_cumulated = 0
+ self.next_samplecounter = -2
+ # enable recording button
+ if self.params.recording_mode != RecordingMode.IMPEDANCE:
+ self.online_cfg.pushButtonRecord.setEnabled(True)
+ else:
+ self.online_cfg.pushButtonRecord.setEnabled(False)
+
+ def process_stop(self):
+ ''' Stop data acquisition
+ '''
+ self._close_recording()
+ # disable recording button
+ self.online_cfg.pushButtonRecord.setEnabled(False)
+
+ def process_input(self, datablock):
+ ''' Store data to file
+ '''
+ self.dataavailable = True
+ self.data = datablock
+
+ # keep last impedance values for next EEG header file
+ if self.data.recording_mode == RecordingMode.IMPEDANCE:
+ self.last_impedance = copy.copy(datablock)
+ return
+
+ # check sample counter
+ if self.next_samplecounter < -1:
+ self.next_samplecounter = self.data.sample_channel[0][0] - 1 # first block after start
+ samples = len(self.data.sample_channel[0])
+ missing_precheck = self.data.sample_channel[0][-1] - (self.next_samplecounter + samples)
+ self.marker_newseg = True # always write new segment markers if samples are missing
+
+ # counter not in expected range ?
+ if missing_precheck != 0:
+ sct = self.data.sample_channel[0]
+ sct_check = np.append((self.next_samplecounter), sct)
+ sctDiff = np.diff(sct_check) - 1
+ sctBreak = np.nonzero(sctDiff)[0]
+ missing_samples = np.sum(sctDiff)
+ self.missing_interval += missing_samples
+ self.missing_cumulated += missing_samples
+ sctBreakDiff = np.array([sct_check[sctBreak+1], sctDiff[sctBreak]]) # samplecounter / missing
+ if time.perf_counter() - self.missing_timer > 30:
+ self.missing_interval = missing_samples
+ #print "samples missing = %i, interval = %i, cumulated = %i"%(missing_samples, self.missing_interval, self.missing_cumulated)
+ error = "%d samples missing"%(missing_samples)
+ if self.missing_interval > 2:
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR, info=error, severity=ErrorSeverity.NOTIFY))
+ self.missing_interval = 0
+ self.missing_cumulated = 0
+ else:
+ self.send_event(ModuleEvent(self._object_name, EventType.LOG, info=error))
+ self.missing_timer = time.perf_counter()
+ else:
+ missing_samples = 0
+ sctBreakDiff = np.array([[],[]],dtype=np.int64)
+
+ # set counter to the expected start sample number of next data block
+ self.next_samplecounter = self.data.sample_channel[0,-1]
+
+
+ if (self.data_file is not None) and not self.write_error:
+ try:
+ t = time.perf_counter()
+ # convert data to float and write to data file
+ d = datablock.eeg_channels.transpose()
+ f = d.flatten().astype(np.float32)
+ sizeof_item = f.dtype.itemsize # item size in bytes
+ # Python I/O: write bytes directly
+ nbytes_written = self.data_file.write(f.tobytes())
+ if nbytes_written != sizeof_item * len(f):
+ raise ModuleError(self._object_name, "Write to file %s failed"%(self.file_name))
+ # write marker
+ #self._write_marker(self.data.markers, self.data.block_time, self.data.sample_channel[0,0])
+ self.data.markers = self._write_marker(self.data.markers, self.data.block_time, self.data.sample_channel[0,0], sctBreakDiff)
+
+ # update file sample counter
+ self.samples_written += samples
+
+ writetime = time.perf_counter() - t
+ #print "Write file: %.0f ms / %d Bytes / QSize %d"%(writetime*1000.0, nitems, self._input_queue.qsize())
+ except Exception as e:
+ self.write_error = True # indicate write error
+ self._thLock.release() # release the thread lock because it is acquired by _close_recording()
+ self._close_recording() # stop recording
+ self._thLock.acquire()
+ # notify application
+ self.send_event(ModuleEvent(self._object_name, EventType.ERROR,
+ str(e),
+ severity=ErrorSeverity.NOTIFY))
+
+ # update the global sample counter missing value
+ self.total_missing += missing_samples
+
+
+ def process_output(self):
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+
+
+'''
+------------------------------------------------------------
+STORAGE MODULE ONLINE GUI
+------------------------------------------------------------
+'''
+
+class _OnlineCfgPane(Qt.QFrame, frmStorageVisionOnline.Ui_frmStorageVisionOnline):
+ ''' Vision Storage Module online configuration pane
+ '''
+ def __init__(self, module, *args):
+ ''' Constructor
+ @param module: parent module
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+ self.module = module
+
+ # set default values
+ self.pushButtonRecord.setEnabled(False)
+ self.set_recording_state(False)
+
+ self.filename = ""
+ self.pathname = ""
+
+ # start display update timer
+ self.startTimer(1000)
+
+ def set_filename(self, path, file, time=0):
+ ''' Show pathname, filename and optional recording time
+ @param path: recording path name
+ @param file: recording file name
+ @param time: time of data written to file in seconds
+ '''
+ self.pathname = path
+ self.filename = file
+ if time > 0:
+ days, hours, minutes, seconds = self.get_DHMS(time)
+ if days == 0:
+ timestring = " %02d:%02d:%02d [h:m:s]"%(hours, minutes, seconds)
+ else:
+ timestring = " %d:%02d:%02d:%02d [d:h:m:s]"%(days, hours, minutes, seconds)
+ else:
+ timestring = ""
+ self.lineEditPath.setText(path)
+ self.lineEditFile.setText(file + timestring)
+
+ def timerEvent(self,e):
+ ''' Display update timer event
+ '''
+ # calculate available disk size
+ path = str(self.lineEditPath.text())
+ if len(path) > 0:
+ free, total = self.module.get_free_space(path)
+ if total > 0 and free > 0:
+ ratio = free * 100.0 / total
+ else:
+ ratio = 0
+ self.progressBar.setValue(ratio)
+ self.module.check_free_space(free)
+ else:
+ free = total = 0
+ self.progressBar.setValue(0)
+
+ # estimate required size in Byte per second
+ bps = self.module.samples_per_second * np.zeros(1,np.float32).dtype.itemsize
+ if bps > 0:
+ if free > 0:
+ seconds = free / bps
+ else:
+ seconds = 0
+ # Get the days, hours, minutes:
+ days, hours, minutes, seconds = self.get_DHMS(seconds)
+ self.lineEditDiskSpace.setText("%d:%02d:%02d"%(days, hours, minutes))
+ else:
+ self.lineEditDiskSpace.setText("--:--:--")
+
+ # calculate the time of data written to file
+ if (self.module.params != None) and (self.module.params.sample_rate > 0):
+ seconds = self.module.samples_written / self.module.params.sample_rate
+ self.set_filename(self.pathname, self.filename, time=seconds)
+
+
+ def get_DHMS(self, seconds):
+ ''' Get days, hours, minutes and seconds from seconds
+ @param seconds: total number of seconds
+ @return: tuple (Days, Hours, Minutes, Seconds)
+ '''
+ MINUTE = 60
+ HOUR = MINUTE * 60
+ DAY = HOUR * 24
+ days = int( seconds / DAY )
+ hours = int(( seconds % DAY ) / HOUR )
+ minutes = int(( seconds % HOUR ) / MINUTE )
+ seconds = int( seconds % MINUTE )
+ return days, hours, minutes, seconds
+
+
+ def set_recording_state(self, on):
+ ''' Update display elements to reflect the recording state
+ '''
+ if on:
+ self.progressBar.setEnabled(True)
+ palette = self.lineEditFile.palette()
+ if self.module.write_error:
+ palette.setColor(Qt.QPalette.Base, Qt.Qt.red)
+ else:
+ palette.setColor(Qt.QPalette.Base, Qt.Qt.green)
+ self.lineEditFile.setPalette(palette)
+ self.pushButtonRecord.setChecked(True)
+ self.pushButtonRecord.setText("Stop Recording")
+ else:
+ self.progressBar.setEnabled(False)
+ palette = self.lineEditFile.palette()
+ if self.module.write_error:
+ palette.setColor(Qt.QPalette.Base, Qt.Qt.red)
+ else:
+ palette.setColor(Qt.QPalette.Base, Qt.QColor(240, 240, 240))
+ self.lineEditFile.setPalette(palette)
+ self.pushButtonRecord.setChecked(False)
+ self.pushButtonRecord.setText("Start Recording")
+
+
+'''
+------------------------------------------------------------
+STORAGE MODULE CONFIGURATION GUI
+------------------------------------------------------------
+'''
+
+class _ConfigurationPane(Qt.QFrame, frmStorageVisionConfig.Ui_frmStorageVisionConfig):
+ ''' Vision Storage configuration pane
+ '''
+ def __init__(self, storage, *args):
+ ''' Constructor
+ @param storage: parent module
+ '''
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+
+ # set validators
+ validator = Qt.QIntValidator(1, 50, self)
+ self.lineEditCounterSize.setValidator(validator)
+
+ validator2 = Qt.QDoubleValidator(0.01, 500.0, 2,self)
+ self.lineEditSpace.setValidator(validator2)
+
+ # setup content
+ self.storage = storage
+
+ self.lineEditFolder.setText(storage.default_path)
+ self.lineEditPrefix.setText(storage.default_prefix)
+ self.lineEditCounterSize.setText(str(storage.default_numbersize))
+ self.checkBoxAutoFile.setChecked(storage.default_autoname)
+ self.lineEditSpace.setText(str(storage.min_disk_space))
+ self._showExample()
+
+ # actions
+ self.connect(self.lineEditFolder, Qt.SIGNAL("editingFinished()"), self._contentChanged)
+ self.connect(self.lineEditPrefix, Qt.SIGNAL("editingFinished()"), self._contentChanged)
+ self.connect(self.lineEditCounterSize, Qt.SIGNAL("editingFinished()"), self._contentChanged)
+ self.connect(self.checkBoxAutoFile, Qt.SIGNAL("clicked()"), self._contentChanged)
+ self.connect(self.pushButtonBrowse, Qt.SIGNAL("clicked()"), self._browse)
+ self.connect(self.lineEditSpace, Qt.SIGNAL("editingFinished()"), self._contentChanged)
+
+ def _contentChanged(self):
+ ''' Update parent object vars
+ '''
+ self.storage.default_path = str(self.lineEditFolder.displayText())
+ self.storage.default_prefix = str(self.lineEditPrefix.displayText()).lstrip()
+ self.lineEditPrefix.setText(self.storage.default_prefix)
+ self.storage.default_numbersize = self.lineEditCounterSize.displayText().toInt()[0]
+ self.storage.default_autoname = self.checkBoxAutoFile.isChecked()
+ self.storage.min_disk_space = self.lineEditSpace.displayText().toDouble()[0]
+ self._showExample()
+
+ def _browse(self):
+ ''' Browse for the default data folder
+ '''
+ dlg = Qt.QFileDialog()
+ dlg.setFileMode(Qt.QFileDialog.DirectoryOnly )
+ dlg.setOption(Qt.QFileDialog.ShowDirsOnly)
+ dlg.setAcceptMode(Qt.QFileDialog.AcceptOpen)
+ if dlg.exec_() == True:
+ files = dlg.selectedFiles()
+ file_name = str(files[0])
+ self.lineEditFolder.setText(file_name)
+ self._contentChanged()
+
+ def _showExample(self):
+ ''' Show auto file name example
+ '''
+ example = "%s%0*d.eeg"%(self.storage.default_prefix, self.storage.default_numbersize, 1)
+ self.labelExample.setText(example)
+
+
+
+
+
+
+
+
diff --git a/syscheck.py b/syscheck.py
index 56dbd6e..2f8c4e9 100644
--- a/syscheck.py
+++ b/syscheck.py
@@ -1,321 +1,321 @@
-# -*- coding: utf-8 -*-
-'''
-System Check
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 62 $
-'''
-
-import os, sys, traceback, platform
-import datetime
-from loadlibs import *
-
-__version__ = "0.90.0"
-'''Application Version'''
-
-logentries = ""
-
-def GetExceptionTraceBack():
- ''' Get last trace back info as tuple
- @return: tuple(string representation, filename, line number, module)
- '''
- exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
- tb = traceback.extract_tb(exceptionTraceback)[-1]
- fn = os.path.split(tb[0])[1]
- txt = "%s, line %d, %s"%(fn, tb[1], tb[2])
- return tuple([txt, fn, tb[1], tb[2]])
-
-
-def logIt(logentry):
- ''' Print and collect log entries
- @param logentry: log text
- '''
- global logentries
- print logentry
- logentries += logentry + "\r\n"
-
-def logHeader():
- ''' Create header for log entries
- '''
- logIt("PyCorder System Check")
- logIt("=====================")
- logIt(datetime.datetime.now().strftime("%A, %d. %B %Y %I:%M%p\r\n"))
-
-
-def checkOS():
- ''' Get operating system infos
- '''
- logIt("Operating System: %s"%(platform.platform()))
- logIt("Processor: %s"%(platform.processor()))
- logIt("Python: %s"%(sys.version))
- # required libs available?
- if len(import_log) == 0:
- # yes, print versions
- logIt(" NumPy: %s"%(np.__version__))
- logIt(" SciPy: %s"%(sc.__version__))
- logIt(" PyQt: %s"%(Qt.QT_VERSION_STR))
- logIt(" PyQwt: %s"%(Qwt.QWT_VERSION_STR))
- logIt(" lxml: %s"%(etree.__version__))
- else:
- logIt("One of the following modules are missing or have the wrong version:")
- logIt(import_log)
- raise Exception, "Python Libraries Missmatch"
-
-
-def checkAmplifierBase():
- ''' Check amplifier base functionality
- '''
- amp = None
- logIt("ActiCHamp Base Functionality")
- try:
- log = " Load DLL: "
- amp = ActiChamp()
- logIt(log+"OK")
-
- log = " Connect HW: "
- amp.open()
- logIt(log+"OK")
-
- log = " Initialize HW: "
- rate = CHAMP_RATE_10KHZ
- amp.setup(CHAMP_MODE_NORMAL, rate, 1)
- logIt(log+"OK")
-
- log = " HW Configuration: "
- log += "EEG=%d, AUX=%d, TRG_IN=%d, TRG_OUT=%d "%(amp.properties.CountEeg,
- amp.properties.CountAux,
- amp.properties.TriggersIn,
- amp.properties.TriggersOut)
- if amp.properties.CountEeg in range(32,161,32) and \
- amp.properties.CountAux == 8 and \
- amp.properties.TriggersIn == 8 and \
- amp.properties.TriggersOut == 8:
- logIt(log+"OK")
- else:
- raise Exception, "HW Configuration Missmatch"
-
- log = " Start Acquisition: "
- amp.start()
- logIt(log+"OK")
-
- # read 2s of data
- print " ... collecting data, please wait (~5s)"
- log = " Read Data: "
- eeg, trg, sct, atime, ptime, errors = readAmplifierData(amp, 2.0,
- amp.properties.CountEeg,
- amp.properties.CountAux)
- logIt(log+"OK")
-
- log = " Stop Acquisition: "
- amp.stop()
- logIt(log+"OK")
-
- totalSamples = sct.shape[1]
- sampleTime = ptime / totalSamples
- utilization = sampleTime * sample_rate[rate] * 100
- missingSamples = (sct[0][-1] - sct[0][0] + 1) - totalSamples
-
- # check sample counter
- if missingSamples == 0:
- logIt(" Samplecounter: OK")
- else:
- logIt(" Samplecounter: %d missing samples FAILED"%(missingSamples))
-
- # check device errors
- if errors == 0:
- logIt(" Device Errors: 0 OK")
- else:
- logIt(" Device Errors: %d FAILED"%(errors))
-
- # check processing time
- log = " Processing Time: %.0f%% "%(utilization)
- if utilization < 50.0:
- logIt(log + "OK")
- else:
- logIt(log + "FAILED")
-
- # TEST only
- if False:
- eeg[0] = 0.0
- eeg[2] *= 3.0
- eeg[5] = 0.0
- eeg[16] *= 0.8
- eeg[66] = 0.0
-
- # remove DC
- cut = 5.0 / sample_rate[rate] * 2.0
- b,a = signal.filter_design.butter(2, cut, 'high')
- eeg = signal.lfilter(b, a, eeg)
- eeg = eeg[:,eeg.shape[1]/2:]
-
- # check for channels shorted and abnormal values
- rms = np.sqrt(np.mean(eeg*eeg, 1)) # calculate RMS
- rms_eeg = rms[:amp.properties.CountEeg] # split eeg and aux channels
- rms_aux = rms[amp.properties.CountEeg:]
-
- #print rms_eeg
- #print rms_aux
-
- def checkRms(rms_values, rms_limit):
- # channels shorted (rms < limit)?
- mask = lambda x: (x < rms_limit)
- shorted = np.array(map(mask, rms_values))
-
- # search for abnormal values ( > +/-2*SD)
- channels_ok = np.nonzero(rms_values > rms_limit)
- num_outlier = 0
- while True:
- sd = np.std(rms_values[channels_ok])
- mean = rms_values[channels_ok].mean()
- mask = lambda x: ((x > mean + 3.0*sd) or (x < mean - 3.0*sd)) and x > rms_limit
- outlier = np.array(map(mask, rms_values))
- channels_ok = ~(shorted | outlier)
- if num_outlier == len(outlier):
- break
- num_outlier = len(outlier)
- return shorted, outlier
-
- eeg_shorted, eeg_outlier = checkRms(rms_eeg, 0.1)
- aux_shorted, aux_outlier = checkRms(rms_aux, 0.1)
-
- # create channel display
- def createDispString(shorted, outlier, groupsize):
- channels = len(shorted)
- out_string = []
- for group in range(channels / groupsize):
- disp = []
- for n in range(group * groupsize, (group+1) * groupsize):
- if n in np.nonzero(shorted)[0]:
- disp.append("x")
- else:
- if n in np.nonzero(outlier)[0]:
- disp.append("?")
- else:
- disp.append("-")
- out_string.append("%d %s"%(group+1, "".join(disp)))
- return out_string
-
- log = " Channel Data: "
- if np.any([eeg_shorted, eeg_outlier]) or np.any([aux_shorted, aux_outlier]):
- eeg_disp = createDispString(eeg_shorted, eeg_outlier, 32)
- aux_disp = createDispString(aux_shorted, aux_outlier, 8)
- log += "x=shorted, ?=outlier FAILED"
- log += "\n EEG channels\n 12345678901234567890123456789012\n"
- for n in range(len(eeg_disp)):
- log += " " + eeg_disp[n] + "\n"
- log += " AUX channels\n 12345678\n"
- log += " " + aux_disp[0]
- else:
- log += "OK"
- logIt(log)
-
- #print eeg_shorted, eeg_outlier, aux_shorted, aux_outlier
-
- # close device
- log = " Close Device: "
- amp.close()
- logIt(log+"OK")
- except Exception as e:
- logIt(log + "FAILED")
- try:
- if amp != None:
- amp.close()
- except:
- pass
- raise e
-
-def readAmplifierData(amp, duration, eegChannels, auxChannels):
- ''' Read data stream from amplifier
- @param amp: amplifier object
- @param duration: data size in seconds
- @param eegChannels: number of eeg channels to read
- @param auxChannels: number of aux channels to read
- @return: channel data, trigger, sample counter,
- acquisition time, processing time, device errors
- '''
- dataChunks = int(duration / 0.05)
- # discard first second
- for n in range(0, 20):
- time.sleep(0.05)
- d, disconnected = amp.read(range(eegChannels + auxChannels),
- eegChannels,
- auxChannels)
- if d == None:
- raise Exception, "No data transfer from amplifier"
-
- # get initial error counter
- initialErrors = amp.getDeviceStatus()[1]
-
- # read data
- t = time.clock()
- processingTime = 0
- for n in range(0, dataChunks):
- time.sleep(0.05)
- tp = time.clock()
- d, disconnected = amp.read(range(eegChannels + auxChannels),
- eegChannels,
- auxChannels)
- processingTime += time.clock() - tp
- if d == None:
- raise Exception, "No data transfer from amplifier"
- if n == 0:
- eeg = d[0]
- trg = d[1]
- sct = d[2]
- else:
- eeg = np.append(eeg, d[0], 1)
- trg = np.append(trg, d[1], 1)
- sct = np.append(sct, d[2], 1)
- acquisitionTime = (time.clock()-t)*1000.0
-
- # get device error counter
- deviceErrors = amp.getDeviceStatus()[1] - initialErrors
-
- return eeg, trg, sct, acquisitionTime, processingTime, deviceErrors
-
-
-
-if __name__ == '__main__':
- try:
- logHeader()
- checkOS()
- from actichamp_w import *
- from scipy import signal
- checkAmplifierBase()
-
-
- except Exception as e:
- tb = GetExceptionTraceBack()[0]
- logIt("\nERROR: " + tb + " -> " + str(e))
- logIt("Can't proceed system check")
-
-
-
- raw_input("\nPress RETURN to close this window ..." )
- sys.exit(1)
+# -*- coding: utf-8 -*-
+'''
+System Check
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 62 $
+'''
+
+import os, sys, traceback, platform
+import datetime
+from loadlibs import *
+
+__version__ = "0.90.0"
+'''Application Version'''
+
+logentries = ""
+
+def GetExceptionTraceBack():
+ ''' Get last trace back info as tuple
+ @return: tuple(string representation, filename, line number, module)
+ '''
+ exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
+ tb = traceback.extract_tb(exceptionTraceback)[-1]
+ fn = os.path.split(tb[0])[1]
+ txt = "%s, line %d, %s"%(fn, tb[1], tb[2])
+ return tuple([txt, fn, tb[1], tb[2]])
+
+
+def logIt(logentry):
+ ''' Print and collect log entries
+ @param logentry: log text
+ '''
+ global logentries
+ print logentry
+ logentries += logentry + "\r\n"
+
+def logHeader():
+ ''' Create header for log entries
+ '''
+ logIt("PyCorder System Check")
+ logIt("=====================")
+ logIt(datetime.datetime.now().strftime("%A, %d. %B %Y %I:%M%p\r\n"))
+
+
+def checkOS():
+ ''' Get operating system infos
+ '''
+ logIt("Operating System: %s"%(platform.platform()))
+ logIt("Processor: %s"%(platform.processor()))
+ logIt("Python: %s"%(sys.version))
+ # required libs available?
+ if len(import_log) == 0:
+ # yes, print versions
+ logIt(" NumPy: %s"%(np.__version__))
+ logIt(" SciPy: %s"%(sc.__version__))
+ logIt(" PyQt: %s"%(Qt.QT_VERSION_STR))
+ logIt(" PyQwt: %s"%(Qwt.QWT_VERSION_STR))
+ logIt(" lxml: %s"%(etree.__version__))
+ else:
+ logIt("One of the following modules are missing or have the wrong version:")
+ logIt(import_log)
+ raise Exception("Python Libraries Missmatch")
+
+
+def checkAmplifierBase():
+ ''' Check amplifier base functionality
+ '''
+ amp = None
+ logIt("ActiCHamp Base Functionality")
+ try:
+ log = " Load DLL: "
+ amp = ActiChamp()
+ logIt(log+"OK")
+
+ log = " Connect HW: "
+ amp.open()
+ logIt(log+"OK")
+
+ log = " Initialize HW: "
+ rate = CHAMP_RATE_10KHZ
+ amp.setup(CHAMP_MODE_NORMAL, rate, 1)
+ logIt(log+"OK")
+
+ log = " HW Configuration: "
+ log += "EEG=%d, AUX=%d, TRG_IN=%d, TRG_OUT=%d "%(amp.properties.CountEeg,
+ amp.properties.CountAux,
+ amp.properties.TriggersIn,
+ amp.properties.TriggersOut)
+ if amp.properties.CountEeg in range(32,161,32) and \
+ amp.properties.CountAux == 8 and \
+ amp.properties.TriggersIn == 8 and \
+ amp.properties.TriggersOut == 8:
+ logIt(log+"OK")
+ else:
+ raise Exception("HW Configuration Missmatch")
+
+ log = " Start Acquisition: "
+ amp.start()
+ logIt(log+"OK")
+
+ # read 2s of data
+ print(" ... collecting data, please wait (~5s)")
+ log = " Read Data: "
+ eeg, trg, sct, atime, ptime, errors = readAmplifierData(amp, 2.0,
+ amp.properties.CountEeg,
+ amp.properties.CountAux)
+ logIt(log+"OK")
+
+ log = " Stop Acquisition: "
+ amp.stop()
+ logIt(log+"OK")
+
+ totalSamples = sct.shape[1]
+ sampleTime = ptime / totalSamples
+ utilization = sampleTime * sample_rate[rate] * 100
+ missingSamples = (sct[0][-1] - sct[0][0] + 1) - totalSamples
+
+ # check sample counter
+ if missingSamples == 0:
+ logIt(" Samplecounter: OK")
+ else:
+ logIt(" Samplecounter: %d missing samples FAILED"%(missingSamples))
+
+ # check device errors
+ if errors == 0:
+ logIt(" Device Errors: 0 OK")
+ else:
+ logIt(" Device Errors: %d FAILED"%(errors))
+
+ # check processing time
+ log = " Processing Time: %.0f%% "%(utilization)
+ if utilization < 50.0:
+ logIt(log + "OK")
+ else:
+ logIt(log + "FAILED")
+
+ # TEST only
+ if False:
+ eeg[0] = 0.0
+ eeg[2] *= 3.0
+ eeg[5] = 0.0
+ eeg[16] *= 0.8
+ eeg[66] = 0.0
+
+ # remove DC
+ cut = 5.0 / sample_rate[rate] * 2.0
+ b,a = signal.filter_design.butter(2, cut, 'high')
+ eeg = signal.lfilter(b, a, eeg)
+ eeg = eeg[:,eeg.shape[1]/2:]
+
+ # check for channels shorted and abnormal values
+ rms = np.sqrt(np.mean(eeg*eeg, 1)) # calculate RMS
+ rms_eeg = rms[:amp.properties.CountEeg] # split eeg and aux channels
+ rms_aux = rms[amp.properties.CountEeg:]
+
+ #print rms_eeg
+ #print rms_aux
+
+ def checkRms(rms_values, rms_limit):
+ # channels shorted (rms < limit)?
+ mask = lambda x: (x < rms_limit)
+ shorted = np.array([mask(val) for val in rms_values], dtype=bool)
+
+ # search for abnormal values ( > +/-2*SD)
+ channels_ok = np.nonzero(rms_values > rms_limit)
+ num_outlier = 0
+ while True:
+ sd = np.std(rms_values[channels_ok])
+ mean = rms_values[channels_ok].mean()
+ mask = lambda x: ((x > mean + 3.0*sd) or (x < mean - 3.0*sd)) and x > rms_limit
+ outlier = np.array([mask(val) for val in rms_values], dtype=bool)
+ channels_ok = ~(shorted | outlier)
+ if num_outlier == len(outlier):
+ break
+ num_outlier = len(outlier)
+ return shorted, outlier
+
+ eeg_shorted, eeg_outlier = checkRms(rms_eeg, 0.1)
+ aux_shorted, aux_outlier = checkRms(rms_aux, 0.1)
+
+ # create channel display
+ def createDispString(shorted, outlier, groupsize):
+ channels = len(shorted)
+ out_string = []
+ for group in range(channels / groupsize):
+ disp = []
+ for n in range(group * groupsize, (group+1) * groupsize):
+ if n in np.nonzero(shorted)[0]:
+ disp.append("x")
+ else:
+ if n in np.nonzero(outlier)[0]:
+ disp.append("?")
+ else:
+ disp.append("-")
+ out_string.append("%d %s"%(group+1, "".join(disp)))
+ return out_string
+
+ log = " Channel Data: "
+ if np.any([eeg_shorted, eeg_outlier]) or np.any([aux_shorted, aux_outlier]):
+ eeg_disp = createDispString(eeg_shorted, eeg_outlier, 32)
+ aux_disp = createDispString(aux_shorted, aux_outlier, 8)
+ log += "x=shorted, ?=outlier FAILED"
+ log += "\n EEG channels\n 12345678901234567890123456789012\n"
+ for n in range(len(eeg_disp)):
+ log += " " + eeg_disp[n] + "\n"
+ log += " AUX channels\n 12345678\n"
+ log += " " + aux_disp[0]
+ else:
+ log += "OK"
+ logIt(log)
+
+ #print eeg_shorted, eeg_outlier, aux_shorted, aux_outlier
+
+ # close device
+ log = " Close Device: "
+ amp.close()
+ logIt(log+"OK")
+ except Exception as e:
+ logIt(log + "FAILED")
+ try:
+ if amp != None:
+ amp.close()
+ except:
+ pass
+ raise e
+
+def readAmplifierData(amp, duration, eegChannels, auxChannels):
+ ''' Read data stream from amplifier
+ @param amp: amplifier object
+ @param duration: data size in seconds
+ @param eegChannels: number of eeg channels to read
+ @param auxChannels: number of aux channels to read
+ @return: channel data, trigger, sample counter,
+ acquisition time, processing time, device errors
+ '''
+ dataChunks = int(duration / 0.05)
+ # discard first second
+ for n in range(0, 20):
+ time.sleep(0.05)
+ d, disconnected = amp.read(range(eegChannels + auxChannels),
+ eegChannels,
+ auxChannels)
+ if d == None:
+ raise Exception("No data transfer from amplifier")
+
+ # get initial error counter
+ initialErrors = amp.getDeviceStatus()[1]
+
+ # read data
+ t = time.perf_counter()
+ processingTime = 0
+ for n in range(0, dataChunks):
+ time.sleep(0.05)
+ tp = time.perf_counter()
+ d, disconnected = amp.read(range(eegChannels + auxChannels),
+ eegChannels,
+ auxChannels)
+ processingTime += time.perf_counter() - tp
+ if d == None:
+ raise Exception("No data transfer from amplifier")
+ if n == 0:
+ eeg = d[0]
+ trg = d[1]
+ sct = d[2]
+ else:
+ eeg = np.append(eeg, d[0], 1)
+ trg = np.append(trg, d[1], 1)
+ sct = np.append(sct, d[2], 1)
+ acquisitionTime = (time.perf_counter()-t)*1000.0
+
+ # get device error counter
+ deviceErrors = amp.getDeviceStatus()[1] - initialErrors
+
+ return eeg, trg, sct, acquisitionTime, processingTime, deviceErrors
+
+
+
+if __name__ == '__main__':
+ try:
+ logHeader()
+ checkOS()
+ from actichamp_w import *
+ from scipy import signal
+ checkAmplifierBase()
+
+
+ except Exception as e:
+ tb = GetExceptionTraceBack()[0]
+ logIt("\nERROR: " + tb + " -> " + str(e))
+ logIt("Can't proceed system check")
+
+
+
+ input("\nPress RETURN to close this window ..." )
+ sys.exit(1)
diff --git a/tools/modview.py b/tools/modview.py
index 536f686..4b03dd1 100644
--- a/tools/modview.py
+++ b/tools/modview.py
@@ -1,555 +1,564 @@
-# -*- coding: utf-8 -*-
-'''
-Generic Model/View Table
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 197 $
-'''
-
-from PyQt4 import Qt
-import types
-
-
-
-class GenericTableWidget(Qt.QTableView):
- ''' Generic model/view table widget
- Table view for a list of data objects:
- The view content is defined by a list of column dictionaries
- dictionary: {'variable':'variable name', 'header':'header text',
- 'edit':False/True, 'editor':'default' or 'combobox' or 'plaintext'}
- optional entries: 'min': minum value, 'max': maximum value,
- 'dec': number of decimal places, 'step': spin box incr/decr
- 'indexed' : True, use value as combobox index
- If a column is defined as combobox, the cb list text items can also be defined in a dictionary:
- dictionary: {'variable name':['Item 1', 'Item 2', ...]}
-
- e.g.:
- class data()
- def __init__(self, idx):
- self.intVar = 55
- self.floatVar = 1.25
- self.strVar = "the quick brown fox"
- self.boolVar = False
-
- columns = [
- {'variable':'intVar', 'header':'Index', 'edit':True, 'editor':'default', 'min':5, 'step':5},
- {'variable':'floatVar', 'header':'Float Variable', 'edit':True, 'editor':'combobox'},
- {'variable':'boolVar', 'header':'Bool Variable', 'edit':True, 'editor':'default'},
- {'variable':'strVar', 'header':'String Variable', 'edit':True, 'editor':'default'},
- ]
-
- cblist = {'floatVar':['0.1', '0.22', '1.23', '2', '4.5', '6.44']}
-
- datalist = []
- for r in range(5):
- datalist.append(data())
-
- setData(datalist, columns, cblist)
- '''
- def __init__(self, *args, **kwargs):
- ''' Constructor
- '''
- apply(Qt.QTableView.__init__, (self,) + args)
-
- self.setAlternatingRowColors(True)
- #self.setObjectName("tableViewGeneric")
- #self.horizontalHeader().setCascadingSectionResizes(False)
- self.horizontalHeader().setStretchLastSection(True)
- self.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
- if "RowNumbers" in kwargs:
- self.verticalHeader().setVisible(kwargs["RowNumbers"])
- else:
- self.verticalHeader().setVisible(False)
- self.verticalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
- if "SelectionBehavior" in kwargs:
- self.setSelectionBehavior(kwargs["SelectionBehavior"])
- self.setSelectionMode(Qt.QAbstractItemView.ExtendedSelection)
-
- # table description and content
- self.fnColorSelect = lambda x: None
- self.fnCheckBox = lambda x: None
- self.fnValidate = lambda row, col, data: True
- self.descrition = []
- self.cblist = {}
- self.data = []
-
- # selection info
- self.selectedRow = 0
-
- def _fillTables(self):
- ''' Create and fill data tables
- '''
- self.data_model = _DataTableModel(self.data, self.descrition, self.cblist)
- self.setModel(self.data_model)
- self.setItemDelegate(_DataItemDelegate())
- self.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
- self.data_model.fnColorSelect = self.fnColorSelect
- self.data_model.fnCheckBox = self.fnCheckBox
- self.data_model.fnValidate = self.fnValidate
-
- # actions
- self.connect(self.data_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._table_data_changed)
- self.connect(self.selectionModel(), Qt.SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self._selectionChanged)
-
- def _table_data_changed(self, topLeft, bottomRight):
- ''' SIGNAL data in channel table has changed
- '''
- # look for multiple selected rows
- cr = self.currentIndex().row()
- cc = self.currentIndex().column()
- selectedRows = [i.row() for i in self.selectedIndexes() if i.column() == cc]
- # change column value in all selected rows, but only if value is of type Bool
- if len(selectedRows) > 1:
- val = self.data_model._getitem(cr, cc)
- if val.type() == Qt.QMetaType.Bool:
- for r in selectedRows:
- self.data_model._setitem(r, cc, val)
-
- # notify parent about changes
- self.emit(Qt.SIGNAL('dataChanged()'))
-
-
-
- def _selectionChanged(self, selected, deselected):
- if len(selected.indexes()) > 0:
- self.selectedRow = selected.indexes()[0].row()
- '''
- selectedIdx = [i.row() for i in selected.indexes()]
- deselectedIdx = [i.row() for i in deselected.indexes()]
- print "selected: ",selectedIdx, " deselected: ", deselectedIdx
- '''
-
-
- def setData(self, data, description, cblist):
- ''' Initialize the table view
- @param data: list of data objects
- @param description: list of column description dictionaries
- @param cblist: dictionary of combo box list contents
- '''
- self.data = data
- self.descrition = description
- self.cblist = cblist
- self._fillTables()
-
- def setfnColorSelect(self, lambdaColor):
- ''' Set the background color selection function
- @param lambdaColor: color selction function
- '''
- self.fnColorSelect = lambdaColor
-
- def setfnCheckBox(self, lambdaCheckBox):
- ''' Set the checkbox display function
- @param lambdaCheckBox: function override
- '''
- self.fnCheckBox = lambdaCheckBox
-
- def setfnValidate(self, lambdaValidate):
- ''' Set the row validation function
- @param lambdaValidate: function override
- '''
- self.fnValidate = lambdaValidate
-
- def getSelectedRow(self):
- return self.selectedRow
-
-
-class _DataTableModel(Qt.QAbstractTableModel):
- ''' EEG and AUX table data model for the configuration pane
- '''
- def __init__(self, data, description, cblist, parent=None, *args):
- ''' Constructor
- @param data: list of data objects
- @param description: list of column description dictionaries
- @param cblist: dictionary of combo box list contents
- '''
- Qt.QAbstractTableModel.__init__(self, parent, *args)
- self.arraydata = data
- # list of column description dictionaries
- # dictionary: {'variable':'variable name', 'header':'header text', 'edit':False/True, 'editor':'default' or 'combobox'}
- # optional entries: 'min': minum value, 'max': maximum value, 'dec': number of decimal places,
- # 'step': spin box incr/decr
- # 'indexed' : True, use value as combobox index
- self.columns = description
-
- # dictionary of combo box list contents
- # dictionary: {'variable name':['Item 1', 'Item 2', ...]}
- self.cblist = cblist
-
- # color selection function
- self.fnColorSelect = lambda x: None
- # checkbox modification function
- self.fnCheckBox = lambda x: None
- # row validation function
- self.fnValidate = lambda row, col, data: True
-
- def _getitem(self, row, column):
- ''' Get data item based on table row and column
- @param row: row number
- @param column: column number
- @return: QVariant data value
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return Qt.QVariant()
-
- # get data object
- data = self.arraydata[row]
- # get variable name from column description
- variable_name = self.columns[column]['variable']
- # get variable value
- if hasattr(data, variable_name):
- d = Qt.QVariant(vars(data)[variable_name])
- # get value from combobox list values?
- if self.columns[column].has_key('indexed') and self.cblist.has_key(variable_name):
- idx, ok = d.toInt()
- if ok and idx >=0 and idx < len(self.cblist[variable_name]):
- d = Qt.QVariant(self.cblist[variable_name][idx])
- else:
- d = Qt.QVariant()
- return d
-
- def _setitem(self, row, column, value):
- ''' Set data item based on table row and column
- @param row: row number
- @param column: column number
- @param value: QVariant value object
- @return: True if property value was set, False if not
- '''
- if (row >= len(self.arraydata)) or (column >= len(self.columns)):
- return False
-
- # get data object
- data = self.arraydata[row]
-
- # get variable name from column description
- variable_name = self.columns[column]['variable']
-
- # get index from combobox list values
- if self.columns[column].has_key('indexed') and self.cblist.has_key(variable_name):
- v = value.toString()
- if v in self.cblist[variable_name]:
- value = Qt.QVariant(self.cblist[variable_name].index(v))
- else:
- return False
-
- # set variable value
- if hasattr(data, variable_name):
- t = type(vars(data)[variable_name])
- if t is bool:
- vars(data)[variable_name] = value.toBool()
- return True
- elif t is float:
- vars(data)[variable_name] = value.toDouble()[0]
- return True
- elif t is int:
- vars(data)[variable_name] = value.toInt()[0]
- return True
- elif t in types.StringTypes:
- vars(data)[variable_name] = "%s" % value.toString()
- return True
- else:
- return False
- else:
- return False
-
- def editorType(self, column):
- ''' Get the columns editor type from column description
- @param column: table column number
- @return: editor type as QVariant (string)
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- return Qt.QVariant(self.columns[column]['editor'])
-
- def editorMinValue(self, column):
- ''' Get the columns editor minimum value from column description
- @param column: table column number
- @return: minimum value as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column].has_key('min'):
- return Qt.QVariant(self.columns[column]['min'])
- else:
- return Qt.QVariant()
-
- def editorMaxValue(self, column):
- ''' Get the columns editor maximum value from column description
- @param column: table column number
- @return: minimum value as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column].has_key('max'):
- return Qt.QVariant(self.columns[column]['max'])
- else:
- return Qt.QVariant()
-
- def editorDecimals(self, column):
- ''' Get the columns editor decimal places from column description
- @param column: table column number
- @return: minimum value as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column].has_key('dec'):
- return Qt.QVariant(self.columns[column]['dec'])
- else:
- return Qt.QVariant()
-
- def editorStep(self, column):
- ''' Get the columns editor single step value from column description
- @param column: table column number
- @return: minimum value as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
- if self.columns[column].has_key('step'):
- return Qt.QVariant(self.columns[column]['step'])
- else:
- return Qt.QVariant()
-
-
- def comboBoxList(self, column):
- ''' Get combo box item list for specified column
- @param column: table column number
- @return: combo box item list as QVariant
- '''
- if column >= len(self.columns):
- return Qt.QVariant()
-
- # get variable name from column description
- variable_name = self.columns[column]['variable']
- # lookup list in dictionary
- if self.cblist.has_key(variable_name):
- return Qt.QVariant(self.cblist[variable_name])
- else:
- return Qt.QVariant()
-
- def rowCount(self, parent=Qt.QModelIndex()):
- ''' Get the number of required table rows
- @return: number of rows
- '''
- if parent.isValid():
- return 0
- return len(self.arraydata)
-
- def columnCount(self, parent=Qt.QModelIndex()):
- ''' Get the number of required table columns
- @return: number of columns
- '''
- if parent.isValid():
- return 0
- return len(self.columns)
-
- def data(self, index, role):
- ''' Abstract method from QAbstactItemModel to get cell data based on role
- @param index: QModelIndex table cell reference
- @param role: given role for the item referred to by the index
- @return: the data stored under the given role for the item referred to by the index
- '''
- if not index.isValid():
- return Qt.QVariant()
-
- # get the underlying data
- value = self._getitem(index.row(), index.column())
-
- if role == Qt.Qt.CheckStateRole:
- # display function override?
- data = self.arraydata[index.row()]
- check = self.fnCheckBox((index.column(), data))
- if check != None:
- if check:
- return Qt.Qt.Checked
- else:
- return Qt.Qt.Unchecked
- # use data value
- if value.type() == Qt.QMetaType.Bool:
- if value.toBool():
- return Qt.Qt.Checked
- else:
- return Qt.Qt.Unchecked
-
- elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
- if value.type() != Qt.QMetaType.Bool:
- return value
-
- elif role == Qt.Qt.BackgroundRole:
- # change background color for a specified row
- data = self.arraydata[index.row()]
- color = self.fnColorSelect(data)
- if not self.fnValidate(index.row(), index.column(), self.arraydata):
- color = Qt.QColor(255, 0, 0)
- if color != None:
- return Qt.QVariant(color)
-
- return Qt.QVariant()
-
- def flags(self, index):
- ''' Abstract method from QAbstactItemModel
- @param index: QModelIndex table cell reference
- @return: the item flags for the given index
- '''
- if not index.isValid():
- return Qt.Qt.ItemIsEnabled
- if not self.columns[index.column()]['edit']:
- return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable
- value = self._getitem(index.row(), index.column())
- if value.type() == Qt.QMetaType.Bool:
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
- return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
-
- def setData(self, index, value, role):
- ''' Abstract method from QAbstactItemModel to set cell data based on role
- @param index: QModelIndex table cell reference
- @param value: QVariant new cell data
- @param role: given role for the item referred to by the index
- @return: true if successful; otherwise returns false.
- '''
- if index.isValid():
- left = self.createIndex(index.row(), 0)
- right = self.createIndex(index.row(), self.columnCount())
- if role == Qt.Qt.EditRole:
- if not self._setitem(index.row(), index.column(), value):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
- return True
- elif role == Qt.Qt.CheckStateRole:
- if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
- return False
- self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), left, right)
- return True
- return False
-
- def headerData(self, section, orientation, role):
- ''' Abstract method from QAbstactItemModel to get the column header
- @param section: column or row number
- @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
- @param role: given role for the item referred to by the index
- @return: header
- '''
- if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
- return Qt.QVariant(self.columns[section]['header'])
- if orientation == Qt.Qt.Vertical and role == Qt.Qt.DisplayRole:
- return Qt.QVariant(Qt.QString.number(section+1))
- return Qt.QVariant()
-
-
-class _DataItemDelegate(Qt.QStyledItemDelegate):
- ''' Combobox item editor
- '''
- def __init__(self, parent=None):
- super(_DataItemDelegate, self).__init__(parent)
-
- def createEditor(self, parent, option, index):
- # combo box
- if index.model().editorType(index.column()) == 'combobox':
- combobox = Qt.QComboBox(parent)
- combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
- combobox.setEditable(False)
- self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
- return combobox
-
- # multi line editor (plain text)
- if index.model().editorType(index.column()) == 'plaintext':
- editor = Qt.QPlainTextEdit(parent)
- editor.setMinimumHeight(100)
- return editor
-
- # get default editor
- editor = Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
-
- # set min/max Values for integer values if available
- if isinstance(editor, Qt.QSpinBox):
- min = index.model().editorMinValue(index.column())
- if min.isValid():
- editor.setMinimum(min.toInt()[0])
- max = index.model().editorMaxValue(index.column())
- if max.isValid():
- editor.setMaximum(max.toInt()[0])
- step = index.model().editorStep(index.column())
- if step.isValid():
- editor.setSingleStep(step.toInt()[0])
-
- # set min/max Values for float values if available
- if isinstance(editor, Qt.QDoubleSpinBox):
- min = index.model().editorMinValue(index.column())
- if min.isValid():
- editor.setMinimum(min.toDouble()[0])
- max = index.model().editorMaxValue(index.column())
- if max.isValid():
- editor.setMaximum(max.toDouble()[0])
- dec = index.model().editorDecimals(index.column())
- if dec.isValid():
- editor.setDecimals(dec.toInt()[0])
- step = index.model().editorStep(index.column())
- if step.isValid():
- editor.setSingleStep(step.toDouble()[0])
-
- return editor
-
- def setEditorData(self, editor, index):
- #if index.model().columns[index.column()]['editor'] == 'combobox':
- if isinstance(editor, Qt.QComboBox):
- idx = 0
- # get data
- d = index.model().data(index, Qt.Qt.DisplayRole)
- if d.isValid():
- if d.type() == Qt.QMetaType.QString:
- # find matching list item text
- idx = editor.findText(d.toString())
- if idx == -1:
- idx = 0
- else:
- # find the closest matching index
- closest = lambda a,l:min(enumerate(l),key=lambda x:abs(x[1]-a))
- # get item list
- itemlist = []
- for i in range(editor.count()):
- itemlist.append(editor.itemText(i).toDouble()[0])
- # find index
- idx = closest(d.toDouble()[0], itemlist)[0]
-
- editor.setCurrentIndex(idx)
- return
- Qt.QStyledItemDelegate.setEditorData(self, editor, index)
-
-
- def setModelData(self, editor, model, index):
- #if model.columns[index.column()]['editor'] == 'combobox':
- if isinstance(editor, Qt.QComboBox):
- model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
- #model.reset()
- return
- Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
-
- def emitCommitData(self):
- self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
-
-
+# -*- coding: utf-8 -*-
+'''
+Generic Model/View Table
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2013-06-05 12:04:17 +0200 (Mi, 05 Jun 2013) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 197 $
+'''
+
+from PyQt4 import Qt
+import types
+
+
+
+class GenericTableWidget(Qt.QTableView):
+ ''' Generic model/view table widget
+ Table view for a list of data objects:
+ The view content is defined by a list of column dictionaries
+ dictionary: {'variable':'variable name', 'header':'header text',
+ 'edit':False/True, 'editor':'default' or 'combobox' or 'plaintext'}
+ optional entries: 'min': minum value, 'max': maximum value,
+ 'dec': number of decimal places, 'step': spin box incr/decr
+ 'indexed' : True, use value as combobox index
+ If a column is defined as combobox, the cb list text items can also be defined in a dictionary:
+ dictionary: {'variable name':['Item 1', 'Item 2', ...]}
+
+ e.g.:
+ class data()
+ def __init__(self, idx):
+ self.intVar = 55
+ self.floatVar = 1.25
+ self.strVar = "the quick brown fox"
+ self.boolVar = False
+
+ columns = [
+ {'variable':'intVar', 'header':'Index', 'edit':True, 'editor':'default', 'min':5, 'step':5},
+ {'variable':'floatVar', 'header':'Float Variable', 'edit':True, 'editor':'combobox'},
+ {'variable':'boolVar', 'header':'Bool Variable', 'edit':True, 'editor':'default'},
+ {'variable':'strVar', 'header':'String Variable', 'edit':True, 'editor':'default'},
+ ]
+
+ cblist = {'floatVar':['0.1', '0.22', '1.23', '2', '4.5', '6.44']}
+
+ datalist = []
+ for r in range(5):
+ datalist.append(data())
+
+ setData(datalist, columns, cblist)
+ '''
+ def __init__(self, *args, **kwargs):
+ ''' Constructor
+ '''
+ Qt.QTableView.__init__(self, *args)
+
+ self.setAlternatingRowColors(True)
+ #self.setObjectName("tableViewGeneric")
+ #self.horizontalHeader().setCascadingSectionResizes(False)
+ self.horizontalHeader().setStretchLastSection(True)
+ try:
+ self.horizontalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
+ except AttributeError:
+ self.horizontalHeader().setSectionResizeMode(Qt.QHeaderView.ResizeToContents)
+ if "RowNumbers" in kwargs:
+ self.verticalHeader().setVisible(kwargs["RowNumbers"])
+ else:
+ self.verticalHeader().setVisible(False)
+ try:
+ self.verticalHeader().setResizeMode(Qt.QHeaderView.ResizeToContents)
+ except AttributeError:
+ self.verticalHeader().setSectionResizeMode(Qt.QHeaderView.ResizeToContents)
+ if "SelectionBehavior" in kwargs:
+ self.setSelectionBehavior(kwargs["SelectionBehavior"])
+ self.setSelectionMode(Qt.QAbstractItemView.ExtendedSelection)
+
+ # table description and content
+ self.fnColorSelect = lambda x: None
+ self.fnCheckBox = lambda x: None
+ self.fnValidate = lambda row, col, data: True
+ self.descrition = []
+ self.cblist = {}
+ self.data = []
+
+ # selection info
+ self.selectedRow = 0
+
+ def _fillTables(self):
+ ''' Create and fill data tables
+ '''
+ self.data_model = _DataTableModel(self.data, self.descrition, self.cblist)
+ self.setModel(self.data_model)
+ self.setItemDelegate(_DataItemDelegate())
+ self.setEditTriggers(Qt.QAbstractItemView.AllEditTriggers)
+ self.data_model.fnColorSelect = self.fnColorSelect
+ self.data_model.fnCheckBox = self.fnCheckBox
+ self.data_model.fnValidate = self.fnValidate
+
+ # actions
+ self.connect(self.data_model, Qt.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self._table_data_changed)
+ self.connect(self.selectionModel(), Qt.SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self._selectionChanged)
+
+ def _table_data_changed(self, topLeft, bottomRight):
+ ''' SIGNAL data in channel table has changed
+ '''
+ # look for multiple selected rows
+ cr = self.currentIndex().row()
+ cc = self.currentIndex().column()
+ selectedRows = [i.row() for i in self.selectedIndexes() if i.column() == cc]
+ # change column value in all selected rows, but only if value is of type Bool
+ if len(selectedRows) > 1:
+ val = self.data_model._getitem(cr, cc)
+ if val.type() == Qt.QMetaType.Bool:
+ for r in selectedRows:
+ self.data_model._setitem(r, cc, val)
+
+ # notify parent about changes
+ self.emit(Qt.SIGNAL('dataChanged()'))
+
+
+
+ def _selectionChanged(self, selected, deselected):
+ if len(selected.indexes()) > 0:
+ self.selectedRow = selected.indexes()[0].row()
+ '''
+ selectedIdx = [i.row() for i in selected.indexes()]
+ deselectedIdx = [i.row() for i in deselected.indexes()]
+ print "selected: ",selectedIdx, " deselected: ", deselectedIdx
+ '''
+
+
+ def setData(self, data, description, cblist):
+ ''' Initialize the table view
+ @param data: list of data objects
+ @param description: list of column description dictionaries
+ @param cblist: dictionary of combo box list contents
+ '''
+ self.data = data
+ self.descrition = description
+ self.cblist = cblist
+ self._fillTables()
+
+ def setfnColorSelect(self, lambdaColor):
+ ''' Set the background color selection function
+ @param lambdaColor: color selction function
+ '''
+ self.fnColorSelect = lambdaColor
+
+ def setfnCheckBox(self, lambdaCheckBox):
+ ''' Set the checkbox display function
+ @param lambdaCheckBox: function override
+ '''
+ self.fnCheckBox = lambdaCheckBox
+
+ def setfnValidate(self, lambdaValidate):
+ ''' Set the row validation function
+ @param lambdaValidate: function override
+ '''
+ self.fnValidate = lambdaValidate
+
+ def getSelectedRow(self):
+ return self.selectedRow
+
+
+class _DataTableModel(Qt.QAbstractTableModel):
+ ''' EEG and AUX table data model for the configuration pane
+ '''
+ def __init__(self, data, description, cblist, parent=None, *args):
+ ''' Constructor
+ @param data: list of data objects
+ @param description: list of column description dictionaries
+ @param cblist: dictionary of combo box list contents
+ '''
+ Qt.QAbstractTableModel.__init__(self, parent, *args)
+ self.arraydata = data
+ # list of column description dictionaries
+ # dictionary: {'variable':'variable name', 'header':'header text', 'edit':False/True, 'editor':'default' or 'combobox'}
+ # optional entries: 'min': minum value, 'max': maximum value, 'dec': number of decimal places,
+ # 'step': spin box incr/decr
+ # 'indexed' : True, use value as combobox index
+ self.columns = description
+
+ # dictionary of combo box list contents
+ # dictionary: {'variable name':['Item 1', 'Item 2', ...]}
+ self.cblist = cblist
+
+ # color selection function
+ self.fnColorSelect = lambda x: None
+ # checkbox modification function
+ self.fnCheckBox = lambda x: None
+ # row validation function
+ self.fnValidate = lambda row, col, data: True
+
+ def _getitem(self, row, column):
+ ''' Get data item based on table row and column
+ @param row: row number
+ @param column: column number
+ @return: QVariant data value
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return Qt.QVariant()
+
+ # get data object
+ data = self.arraydata[row]
+ # get variable name from column description
+ variable_name = self.columns[column]['variable']
+ # get variable value
+ if hasattr(data, variable_name):
+ d = Qt.QVariant(vars(data)[variable_name])
+ # get value from combobox list values?
+ if ('indexed' in self.columns[column]) and (variable_name in self.cblist):
+ idx, ok = d.toInt()
+ if ok and idx >=0 and idx < len(self.cblist[variable_name]):
+ d = Qt.QVariant(self.cblist[variable_name][idx])
+ else:
+ d = Qt.QVariant()
+ return d
+
+ def _setitem(self, row, column, value):
+ ''' Set data item based on table row and column
+ @param row: row number
+ @param column: column number
+ @param value: QVariant value object
+ @return: True if property value was set, False if not
+ '''
+ if (row >= len(self.arraydata)) or (column >= len(self.columns)):
+ return False
+
+ # get data object
+ data = self.arraydata[row]
+
+ # get variable name from column description
+ variable_name = self.columns[column]['variable']
+
+ # get index from combobox list values
+ if ('indexed' in self.columns[column]) and (variable_name in self.cblist):
+ v = value.toString()
+ if v in self.cblist[variable_name]:
+ value = Qt.QVariant(self.cblist[variable_name].index(v))
+ else:
+ return False
+
+ # set variable value
+ if hasattr(data, variable_name):
+ t = type(vars(data)[variable_name])
+ if t is bool:
+ vars(data)[variable_name] = value.toBool()
+ return True
+ elif t is float:
+ vars(data)[variable_name] = value.toDouble()[0]
+ return True
+ elif t is int:
+ vars(data)[variable_name] = value.toInt()[0]
+ return True
+ elif isinstance(vars(data)[variable_name], str):
+ vars(data)[variable_name] = "%s" % value.toString()
+ return True
+ else:
+ return False
+ else:
+ return False
+
+ def editorType(self, column):
+ ''' Get the columns editor type from column description
+ @param column: table column number
+ @return: editor type as QVariant (string)
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ return Qt.QVariant(self.columns[column]['editor'])
+
+ def editorMinValue(self, column):
+ ''' Get the columns editor minimum value from column description
+ @param column: table column number
+ @return: minimum value as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if 'min' in self.columns[column]:
+ return Qt.QVariant(self.columns[column]['min'])
+ else:
+ return Qt.QVariant()
+
+ def editorMaxValue(self, column):
+ ''' Get the columns editor maximum value from column description
+ @param column: table column number
+ @return: minimum value as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if 'max' in self.columns[column]:
+ return Qt.QVariant(self.columns[column]['max'])
+ else:
+ return Qt.QVariant()
+
+ def editorDecimals(self, column):
+ ''' Get the columns editor decimal places from column description
+ @param column: table column number
+ @return: minimum value as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if 'dec' in self.columns[column]:
+ return Qt.QVariant(self.columns[column]['dec'])
+ else:
+ return Qt.QVariant()
+
+ def editorStep(self, column):
+ ''' Get the columns editor single step value from column description
+ @param column: table column number
+ @return: minimum value as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+ if 'step' in self.columns[column]:
+ return Qt.QVariant(self.columns[column]['step'])
+ else:
+ return Qt.QVariant()
+
+
+ def comboBoxList(self, column):
+ ''' Get combo box item list for specified column
+ @param column: table column number
+ @return: combo box item list as QVariant
+ '''
+ if column >= len(self.columns):
+ return Qt.QVariant()
+
+ # get variable name from column description
+ variable_name = self.columns[column]['variable']
+ # lookup list in dictionary
+ if variable_name in self.cblist:
+ return Qt.QVariant(self.cblist[variable_name])
+ else:
+ return Qt.QVariant()
+
+ def rowCount(self, parent=Qt.QModelIndex()):
+ ''' Get the number of required table rows
+ @return: number of rows
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.arraydata)
+
+ def columnCount(self, parent=Qt.QModelIndex()):
+ ''' Get the number of required table columns
+ @return: number of columns
+ '''
+ if parent.isValid():
+ return 0
+ return len(self.columns)
+
+ def data(self, index, role):
+ ''' Abstract method from QAbstactItemModel to get cell data based on role
+ @param index: QModelIndex table cell reference
+ @param role: given role for the item referred to by the index
+ @return: the data stored under the given role for the item referred to by the index
+ '''
+ if not index.isValid():
+ return Qt.QVariant()
+
+ # get the underlying data
+ value = self._getitem(index.row(), index.column())
+
+ if role == Qt.Qt.CheckStateRole:
+ # display function override?
+ data = self.arraydata[index.row()]
+ check = self.fnCheckBox((index.column(), data))
+ if check != None:
+ if check:
+ return Qt.Qt.Checked
+ else:
+ return Qt.Qt.Unchecked
+ # use data value
+ if value.type() == Qt.QMetaType.Bool:
+ if value.toBool():
+ return Qt.Qt.Checked
+ else:
+ return Qt.Qt.Unchecked
+
+ elif (role == Qt.Qt.DisplayRole) or (role == Qt.Qt.EditRole):
+ if value.type() != Qt.QMetaType.Bool:
+ return value
+
+ elif role == Qt.Qt.BackgroundRole:
+ # change background color for a specified row
+ data = self.arraydata[index.row()]
+ color = self.fnColorSelect(data)
+ if not self.fnValidate(index.row(), index.column(), self.arraydata):
+ color = Qt.QColor(255, 0, 0)
+ if color != None:
+ return Qt.QVariant(color)
+
+ return Qt.QVariant()
+
+ def flags(self, index):
+ ''' Abstract method from QAbstactItemModel
+ @param index: QModelIndex table cell reference
+ @return: the item flags for the given index
+ '''
+ if not index.isValid():
+ return Qt.Qt.ItemIsEnabled
+ if not self.columns[index.column()]['edit']:
+ return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable
+ value = self._getitem(index.row(), index.column())
+ if value.type() == Qt.QMetaType.Bool:
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsSelectable
+ return Qt.QAbstractTableModel.flags(self, index) | Qt.Qt.ItemIsEditable
+
+ def setData(self, index, value, role):
+ ''' Abstract method from QAbstactItemModel to set cell data based on role
+ @param index: QModelIndex table cell reference
+ @param value: QVariant new cell data
+ @param role: given role for the item referred to by the index
+ @return: true if successful; otherwise returns false.
+ '''
+ if index.isValid():
+ left = self.createIndex(index.row(), 0)
+ right = self.createIndex(index.row(), self.columnCount())
+ if role == Qt.Qt.EditRole:
+ if not self._setitem(index.row(), index.column(), value):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
+ return True
+ elif role == Qt.Qt.CheckStateRole:
+ if not self._setitem(index.row(), index.column(), Qt.QVariant(value == Qt.Qt.Checked)):
+ return False
+ self.emit(Qt.SIGNAL('dataChanged(QModelIndex, QModelIndex)'), left, right)
+ return True
+ return False
+
+ def headerData(self, section, orientation, role):
+ ''' Abstract method from QAbstactItemModel to get the column header
+ @param section: column or row number
+ @param orientation: Qt.Horizontal = column header, Qt.Vertical = row header
+ @param role: given role for the item referred to by the index
+ @return: header
+ '''
+ if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole:
+ return Qt.QVariant(self.columns[section]['header'])
+ if orientation == Qt.Qt.Vertical and role == Qt.Qt.DisplayRole:
+ return Qt.QVariant(Qt.QString.number(section+1))
+ return Qt.QVariant()
+
+
+class _DataItemDelegate(Qt.QStyledItemDelegate):
+ ''' Combobox item editor
+ '''
+ def __init__(self, parent=None):
+ super(_DataItemDelegate, self).__init__(parent)
+
+ def createEditor(self, parent, option, index):
+ # combo box
+ if index.model().editorType(index.column()) == 'combobox':
+ combobox = Qt.QComboBox(parent)
+ combobox.addItems(index.model().comboBoxList(index.column()).toStringList())
+ combobox.setEditable(False)
+ self.connect(combobox, Qt.SIGNAL('activated(int)'), self.emitCommitData)
+ return combobox
+
+ # multi line editor (plain text)
+ if index.model().editorType(index.column()) == 'plaintext':
+ editor = Qt.QPlainTextEdit(parent)
+ editor.setMinimumHeight(100)
+ return editor
+
+ # get default editor
+ editor = Qt.QStyledItemDelegate.createEditor(self, parent, option, index)
+
+ # set min/max Values for integer values if available
+ if isinstance(editor, Qt.QSpinBox):
+ min = index.model().editorMinValue(index.column())
+ if min.isValid():
+ editor.setMinimum(min.toInt()[0])
+ max = index.model().editorMaxValue(index.column())
+ if max.isValid():
+ editor.setMaximum(max.toInt()[0])
+ step = index.model().editorStep(index.column())
+ if step.isValid():
+ editor.setSingleStep(step.toInt()[0])
+
+ # set min/max Values for float values if available
+ if isinstance(editor, Qt.QDoubleSpinBox):
+ min = index.model().editorMinValue(index.column())
+ if min.isValid():
+ editor.setMinimum(min.toDouble()[0])
+ max = index.model().editorMaxValue(index.column())
+ if max.isValid():
+ editor.setMaximum(max.toDouble()[0])
+ dec = index.model().editorDecimals(index.column())
+ if dec.isValid():
+ editor.setDecimals(dec.toInt()[0])
+ step = index.model().editorStep(index.column())
+ if step.isValid():
+ editor.setSingleStep(step.toDouble()[0])
+
+ return editor
+
+ def setEditorData(self, editor, index):
+ #if index.model().columns[index.column()]['editor'] == 'combobox':
+ if isinstance(editor, Qt.QComboBox):
+ idx = 0
+ # get data
+ d = index.model().data(index, Qt.Qt.DisplayRole)
+ if d.isValid():
+ if d.type() == Qt.QMetaType.QString:
+ # find matching list item text
+ idx = editor.findText(str(d.toString()))
+ if idx == -1:
+ idx = 0
+ else:
+ # find the closest matching index
+ closest = lambda a,l:min(enumerate(l),key=lambda x:abs(x[1]-a))
+ # get item list
+ itemlist = []
+ for i in range(editor.count()):
+ try:
+ itemlist.append(editor.itemText(i).toDouble()[0])
+ except AttributeError:
+ itemlist.append(float(editor.itemText(i)))
+ # find index
+ idx = closest(d.toDouble()[0], itemlist)[0]
+
+ editor.setCurrentIndex(idx)
+ return
+ Qt.QStyledItemDelegate.setEditorData(self, editor, index)
+
+
+ def setModelData(self, editor, model, index):
+ #if model.columns[index.column()]['editor'] == 'combobox':
+ if isinstance(editor, Qt.QComboBox):
+ model.setData(index, Qt.QVariant(editor.currentText()), Qt.Qt.EditRole)
+ #model.reset()
+ return
+ Qt.QStyledItemDelegate.setModelData(self, editor, model, index)
+
+ def emitCommitData(self):
+ self.emit(Qt.SIGNAL('commitData(QWidget*)'), self.sender())
+
+
\ No newline at end of file
diff --git a/tutorial/tut_2.py b/tutorial/tut_2.py
index 4656de1..346e229 100644
--- a/tutorial/tut_2.py
+++ b/tutorial/tut_2.py
@@ -1,126 +1,126 @@
-# -*- coding: utf-8 -*-
-'''
-Tutorial Module 2
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 62 $
-'''
-
-from modbase import *
-
-class TUT_2(ModuleBase):
- ''' Tutorial Module 2
-
- Data Processing.
- - Create and use a channel selection mask
- - Demonstrate the effect of for loops
- - Insert markers
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor
- '''
- # initialize the base class, give a descriptive name
- ModuleBase.__init__(self, name="Tutorial 2", **keys)
-
- # initialize module variables
- self.data = None # hold the data block we got from previous module
- self.dataavailable = False # data available for output to next module
-
- def process_update(self, params):
- ''' Evaluate and maybe modify the data block configuration.
- @param params: EEG_DataBlock object. We will get a complete EEG_DataBlock object
- but we are only interested to get the channel configuration and sample rate at
- this time.
- @return: EEG_DataBlock object
- '''
-
- # get a local reference to the parameter object
- self.params = params
-
- # select channels with "_x2" in channel name, these channels will be multiplied by 2
- mask = lambda x: ("_x2" in x.name) # selection function
- mask_ref = np.array(map(mask, self.params.channel_properties)) # create an boolean array with results of the mask function
- self.mask_index = np.nonzero(mask_ref) # create an array of TRUE indices
- print self.mask_index
-
- # search channels with "_loop" in channel name,
- # all channels will be processed within a for-loop
- mask = lambda x: ("_loop" in x.name) # selection function
- mask_ref = np.array(map(mask, self.params.channel_properties)) # create an boolean array with results of the mask function
- self.loop = (np.nonzero(mask_ref)[0].size > 0) # use for loop if any channel name contains _loop
- print self.loop
-
- return self.params # don't forget to pass the configuration down to the next module
-
-
- def process_input(self, datablock):
- ''' Get data from previous module.
- - Multiply selected channels by 2
- - Add an DC offest to all channels.
- - Create and insert 1s tick markers.
- @param datablock: EEG_DataBlock object
- '''
- self.dataavailable = True # signal data availability
- self.data = datablock # get a local reference
-
- # multiply selected channels by 2
- self.data.eeg_channels[self.mask_index] *= 2.0
-
- # add an 100uV DC offset to all channels
- if self.loop:
- # use for-loops to demonstrate the loss of performance
- for channel in range(self.data.eeg_channels.shape[0]):
- for n in range(self.data.eeg_channels.shape[1]):
- self.data.eeg_channels[channel][n] += 100.0
- else:
- # use array function
- self.data.eeg_channels += 100.0
-
- # Create and insert markers
- # search 1s sample counter ticks
- tickmap = (self.data.sample_channel[0] % self.data.sample_rate) == 0
- ticks = np.nonzero(tickmap)[0]
- # add 1s tick markers
- for tick in ticks:
- self.data.markers.append(EEG_Marker(type="1s Tick",
- description="1s",
- position=self.data.sample_channel[0][tick],
- channel=0))
-
- def process_output(self):
- ''' Send data out to next module
- @return: EEG_DataBlock object
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
-
+# -*- coding: utf-8 -*-
+'''
+Tutorial Module 2
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 62 $
+'''
+
+from modbase import *
+
+class TUT_2(ModuleBase):
+ ''' Tutorial Module 2
+
+ Data Processing.
+ - Create and use a channel selection mask
+ - Demonstrate the effect of for loops
+ - Insert markers
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor
+ '''
+ # initialize the base class, give a descriptive name
+ ModuleBase.__init__(self, name="Tutorial 2", **keys)
+
+ # initialize module variables
+ self.data = None # hold the data block we got from previous module
+ self.dataavailable = False # data available for output to next module
+
+ def process_update(self, params):
+ ''' Evaluate and maybe modify the data block configuration.
+ @param params: EEG_DataBlock object. We will get a complete EEG_DataBlock object
+ but we are only interested to get the channel configuration and sample rate at
+ this time.
+ @return: EEG_DataBlock object
+ '''
+
+ # get a local reference to the parameter object
+ self.params = params
+
+ # select channels with "_x2" in channel name, these channels will be multiplied by 2
+ mask = lambda x: ("_x2" in x.name) # selection function
+ mask_ref = np.array([mask(ch) for ch in self.params.channel_properties], dtype=bool) # create an boolean array with results of the mask function
+ self.mask_index = np.nonzero(mask_ref) # create an array of TRUE indices
+ print self.mask_index
+
+ # search channels with "_loop" in channel name,
+ # all channels will be processed within a for-loop
+ mask = lambda x: ("_loop" in x.name) # selection function
+ mask_ref = np.array([mask(ch) for ch in self.params.channel_properties], dtype=bool) # create an boolean array with results of the mask function
+ self.loop = (np.nonzero(mask_ref)[0].size > 0) # use for loop if any channel name contains _loop
+ print self.loop
+
+ return self.params # don't forget to pass the configuration down to the next module
+
+
+ def process_input(self, datablock):
+ ''' Get data from previous module.
+ - Multiply selected channels by 2
+ - Add an DC offest to all channels.
+ - Create and insert 1s tick markers.
+ @param datablock: EEG_DataBlock object
+ '''
+ self.dataavailable = True # signal data availability
+ self.data = datablock # get a local reference
+
+ # multiply selected channels by 2
+ self.data.eeg_channels[self.mask_index] *= 2.0
+
+ # add an 100uV DC offset to all channels
+ if self.loop:
+ # use for-loops to demonstrate the loss of performance
+ for channel in range(self.data.eeg_channels.shape[0]):
+ for n in range(self.data.eeg_channels.shape[1]):
+ self.data.eeg_channels[channel][n] += 100.0
+ else:
+ # use array function
+ self.data.eeg_channels += 100.0
+
+ # Create and insert markers
+ # search 1s sample counter ticks
+ tickmap = (self.data.sample_channel[0] % self.data.sample_rate) == 0
+ ticks = np.nonzero(tickmap)[0]
+ # add 1s tick markers
+ for tick in ticks:
+ self.data.markers.append(EEG_Marker(type="1s Tick",
+ description="1s",
+ position=self.data.sample_channel[0][tick],
+ channel=0))
+
+ def process_output(self):
+ ''' Send data out to next module
+ @return: EEG_DataBlock object
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+
diff --git a/tutorial/tut_3.py b/tutorial/tut_3.py
index dd0427c..594acda 100644
--- a/tutorial/tut_3.py
+++ b/tutorial/tut_3.py
@@ -1,598 +1,599 @@
-# -*- coding: utf-8 -*-
-'''
-Tutorial Module 3
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 62 $
-'''
-
-from modbase import *
-from PyQt4 import QtGui
-from PyQt4 import Qwt5 as Qwt
-import collections
-
-################################################################
-# The module itself
-
-class TUT_3(ModuleBase):
- ''' Tutorial Module 3
-
- Calculate and display a FFT for selected channels. Channel selection will be
- send from display module as command events.
-
- - GUI elements
- - Create and use configuration panes
- - Create and use a FFT signal pane
- - Data processing
- - collect chunks of data and send it to the signal pane
- - calculate and display the FFT
- - Cofiguration management
- - write parameters to XML stream
- - read parameters from XML stream
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor.
- Initialize instance variables and instantiate GUI objects
- '''
- # initialize the base class, give a descriptive name
- ModuleBase.__init__(self, name="Tutorial 3", **keys)
-
- # XML parameter version
- # 1: initial version
- self.xmlVersion = 1
-
- # initialize module variables
- self.data = None #: hold the data block we got from previous module
- self.dataavailable = False #: data available for output to next module
- self.channelFifo = collections.deque() #: channel selection FiFo
- self.params = EEG_DataBlock(0,0) #: default channel configuration
-
- # Plot configuration data
- self.chunk = 1024 #: FFT chunk size
- self.frequency_range = 200 #: Display frequency range [Hz]
- self.plot_items = 4 #: Maximum number of plot items
-
- # instantiate online configuration pane
- self.onlinePane = _OnlineCfgPane()
- # connect the event handler for changes in chunk size
- self.connect(self.onlinePane.comboBoxChunk,
- Qt.SIGNAL("currentIndexChanged(int)"),
- self.onlineValueChanged)
- # connect the event handler for changes in frequency
- self.connect(self.onlinePane.comboBoxFrequency,
- Qt.SIGNAL("currentIndexChanged(int)"),
- self.onlineValueChanged)
-
- # instantiate signal pane
- self.signalPane = _SignalPane()
-
- # set default values
- self.setDefault()
-
- def setDefault(self):
- ''' Set all module parameters to default values
- '''
- self.chunk = 1024
- self.frequency_range = 200
- self.plot_items = 4
- self.onlinePane.setCurrentValues(self.frequency_range, self.chunk)
-
- def process_input(self, datablock):
- ''' Get data from previous module.
- Because we need to exchange data between different threads we will use
- a queue object to send data to the display thread to be thread-safe.
- @param datablock: EEG_DataBlock object
- '''
- self.dataavailable = True # signal data availability
- self.data = datablock # get a local reference
-
- # anything to do ? check channel selection and recording mode.
- # If we are in impedance mode, it makes no sense to calculate the FFT
- if (self.channel_index[0].size == 0) or (self.data.recording_mode == RecordingMode.IMPEDANCE):
- return
-
- # because we need a predefined data chunk size, we have to collect data until
- # at least one data block of data with chunk size is available
- # so first we append data from selected channels to a local buffer
- append = self.data.eeg_channels[self.channel_index]
- self.eeg_channels = np.append(self.eeg_channels, append, 1) # append to axis 1 of local buffer
-
- # slice the local buffer into chunks
- while self.eeg_channels.shape[1] > self.chunk:
- # copy data of chunk size from all selected channels
- bufcopy = self.eeg_channels[:,:self.chunk]
- self.eeg_channels = self.eeg_channels[:,self.chunk:]
- # send channel data to the signal pane queue
- if self.signalPane.data_queue.empty():
- self.signalPane.data_queue.put(bufcopy, False)
- #print bufcopy.shape[1]
-
- # set different color for selected channels
- selected = self.data.channel_properties[self.channel_index]
- for ch in selected:
- ch.color = Qt.Qt.darkYellow
-
-
- def process_output(self):
- ''' Send data out to next module
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
-
- def process_update(self, params):
- ''' Evaluate and maybe modify the channel configuration.
- @param params: EEG_DataBlock object.
- @return: EEG_DataBlock object
- '''
- # keep a reference of the channel configuration
- self.params = params
- # Create a new channel selection array and setup the signal pane
- self.updateSignalPane()
- return params
-
-
- def process_event(self, event):
- ''' Handle events from attached receivers.
- @param event: ModuleEvent
- '''
- # Search for ModuleEvents from display module and update channel selection
- if (event.type == EventType.COMMAND) and (event.info == "ChannelSelected"):
- channel = event.cmd_value
- # if channel is already in selection, remove it
- if channel in self.channelFifo:
- self.channelFifo.remove(channel)
- else:
- self.channelFifo.appendleft(channel)
- # limit selection to max. entries
- while len(self.channelFifo) > self.plot_items:
- self.channelFifo.pop()
- # Create a new channel selection array and setup the signal pane
- self.updateSignalPane()
-
-
- def updateSignalPane(self):
- ''' Create a channel selection array and setup the signal pane
- '''
- # acquire ModuleBase thread lock
- self._thLock.acquire()
-
- # get values from online configuration pane
- self.frequency_range, self.chunk = self.onlinePane.getCurrentValues()
-
- # if channel FiFo contains more than maximum configured items, remove odd
- while len(self.channelFifo) > self.plot_items:
- self.channelFifo.pop()
-
- # create channel selection indices from channel FiFo
- mask = lambda x: (x.name in self.channelFifo)
- channel_ref = np.array(map(mask, self.params.channel_properties))
- self.channel_index = np.nonzero(channel_ref)
-
- # create empty calculation buffers
- if self.params.eeg_channels.shape[0] > 0:
- self.eeg_channels = np.delete(np.zeros_like(self.params.eeg_channels[self.channel_index]),
- np.s_[:],
- 1)
-
- # create FFT plot for each selected channel
- self.signalPane.setupDisplay(self.params.channel_properties[self.channel_index],
- self.params.sample_rate,
- self.chunk,
- self.frequency_range)
- # release ModuleBase thread lock
- self._thLock.release()
-
-
- def get_display_pane(self):
- ''' Get the signal display pane
- @return: a QFrame object or None if you don't need a display pane
- '''
- return self.signalPane
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- @return: a QFrame object or None if you don't need a online configuration pane
- '''
- return self.onlinePane
-
- def get_configuration_pane(self):
- ''' Get the configuration pane
- @return: a QFrame object or None if you don't need a configuration pane
- '''
- cfgPane = _ConfigurationPane(self)
- return cfgPane
-
- def onlineValueChanged(self, int):
- ''' Event handler for changes in frequency and chunk size
- '''
- # Create a new channel selection array and setup the signal pane
- self.updateSignalPane()
-
-
- def getXML(self):
- ''' Get module properties for XML configuration file
- @return: objectify XML element::
- e.g.
-
- 200.0
- 2048
- 4
-
- '''
- E = objectify.E
- cfg = E.Tut_FFT(E.frequency_range(self.frequency_range),
- E.chunk_size(self.chunk),
- E.plot_items(self.plot_items),
- version=str(self.xmlVersion),
- module="FFT",
- instance=str(self._instance))
- return cfg
-
-
- def setXML(self, xml):
- ''' Set module properties from XML configuration file
- @param xml: complete objectify XML configuration tree,
- module will search for matching values
- '''
- # search module configuration data
- storages = xml.xpath("//Tut_FFT[@module='FFT' and @instance='%i']"%(self._instance) )
- if len(storages) == 0:
- # configuration data not found, set default values
- self.setDefault()
- return
-
- # we should have only one instance from this type
- cfg = storages[0]
-
- # check version, has to be lower or equal than current version
- version = cfg.get("version")
- if (version == None) or (int(version) > self.xmlVersion):
- self.send_event(ModuleEvent(self._object_name,
- EventType.ERROR,
- "XML Configuration: wrong version"))
- return
- version = int(version)
-
- # get the values
- try:
- self.frequency_range = cfg.frequency_range.pyval
- self.chunk = cfg.chunk_size.pyval
- self.plot_items = cfg.plot_items.pyval
- self.onlinePane.setCurrentValues(self.frequency_range, self.chunk)
- except Exception as e:
- self.send_exception(e, severity=ErrorSeverity.NOTIFY)
-
-
-
-
-
-################################################################
-# Online Configuration Pane
-
-class _OnlineCfgPane(Qt.QFrame):
- ''' Online configuration pane
- '''
- def __init__(self , *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # make it nice ;-)
- self.setFrameShape(QtGui.QFrame.Panel)
- self.setFrameShadow(QtGui.QFrame.Raised)
-
- # give us a layout and group box
- self.gridLayout = QtGui.QGridLayout(self)
- self.groupBox = QtGui.QGroupBox(self)
- self.groupBox.setTitle("FFT")
-
- # group box layout
- self.gridLayoutGroup = QtGui.QGridLayout(self.groupBox)
- self.gridLayoutGroup.setHorizontalSpacing(10)
- self.gridLayoutGroup.setContentsMargins(20, -1, 20, -1)
-
- # add the chunk size combobox
- self.comboBoxChunk = QtGui.QComboBox(self.groupBox)
- self.comboBoxChunk.setObjectName("comboBoxChunk")
- self.comboBoxChunk.addItem(Qt.QString("128"))
- self.comboBoxChunk.addItem(Qt.QString("256"))
- self.comboBoxChunk.addItem(Qt.QString("512"))
- self.comboBoxChunk.addItem(Qt.QString("1024"))
- self.comboBoxChunk.addItem(Qt.QString("2048"))
- self.comboBoxChunk.addItem(Qt.QString("4096"))
- self.comboBoxChunk.addItem(Qt.QString("8129"))
- self.comboBoxChunk.addItem(Qt.QString("16384"))
- self.comboBoxChunk.addItem(Qt.QString("32768"))
-
- # add the frequency range combobox
- self.comboBoxFrequency = QtGui.QComboBox(self.groupBox)
- self.comboBoxFrequency.setObjectName("comboBoxChunk")
- self.comboBoxFrequency.addItem(Qt.QString("20"))
- self.comboBoxFrequency.addItem(Qt.QString("50"))
- self.comboBoxFrequency.addItem(Qt.QString("100"))
- self.comboBoxFrequency.addItem(Qt.QString("200"))
- self.comboBoxFrequency.addItem(Qt.QString("500"))
- self.comboBoxFrequency.addItem(Qt.QString("1000"))
- self.comboBoxFrequency.addItem(Qt.QString("2000"))
- self.comboBoxFrequency.addItem(Qt.QString("5000"))
-
- # create unit labels
- self.labelChunk = QtGui.QLabel(self.groupBox)
- self.labelChunk.setText("[n]")
- self.labelFrequency = QtGui.QLabel(self.groupBox)
- self.labelFrequency.setText("[Hz]")
-
- # add widgets to layouts
- self.gridLayoutGroup.addWidget(self.comboBoxFrequency, 0, 0, 1, 1)
- self.gridLayoutGroup.addWidget(self.labelFrequency, 0, 1, 1, 1)
- self.gridLayoutGroup.addWidget(self.comboBoxChunk, 0, 2, 1, 1)
- self.gridLayoutGroup.addWidget(self.labelChunk, 0, 3, 1, 1)
-
- self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1)
-
- # set default values
- self.comboBoxFrequency.setCurrentIndex(2)
- self.comboBoxChunk.setCurrentIndex(4)
-
- def getCurrentValues(self):
- ''' Get current selected values for frequency and chunk size
- @return: frequency and chunk size as tuple
- '''
- chunk,ok = self.comboBoxChunk.currentText().toFloat()
- frequency,ok = self.comboBoxFrequency.currentText().toFloat()
- return frequency, chunk
-
- def setCurrentValues(self, frequency, chunk):
- ''' Set the current values for frequency and chunk size
- @param frequency: new frequency value
- @param chunk: new chunk size
- '''
- # find chunk size index
- idx = -1
- for i in range(self.comboBoxChunk.count()):
- if chunk == self.comboBoxChunk.itemText(i).toFloat()[0]:
- idx = i
- # set new combobox index
- if idx >= 0:
- self.comboBoxChunk.setCurrentIndex(idx)
-
- # find frequency index
- idx = -1
- for i in range(self.comboBoxFrequency.count()):
- if frequency == self.comboBoxFrequency.itemText(i).toFloat()[0]:
- idx = i
- # set new combobox index
- if idx >= 0:
- self.comboBoxFrequency.setCurrentIndex(idx)
-
-
-
-################################################################
-# Signal Pane
-
-class _SignalPane(Qt.QFrame):
- ''' FFT display pane
- '''
- def __init__(self , *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # Initialize local variables
- self.data_queue = Queue.Queue(10) # data exchange queue
-
- # Layout display items
- self.setMinimumSize(Qt.QSize(0, 0)) # hide signal pane if there are no plot items
- self.verticalLayout = QtGui.QVBoxLayout(self) # arrange plot items vertically
-
- # list of current plot widgets
- self.plot = []
-
- # start 50ms display timer
- self.startTimer(50)
-
-
- def setupDisplay(self, channels, samplerate, datapoints, frequency_range):
- ''' Rearrange and setup plot widgets
- @param channels: list of selected channels as EEG_ChannelProperties
- @param samplerate: sampling rate in Hz
- @param datapoints: chunk size in samples
- @param frequency_range: show frequencies up to this value [Hz]
- '''
- # flush input data queue
- while not self.data_queue.empty():
- self.data_queue.get_nowait()
-
- # remove previous plot widgets
- for plot in self.plot[:]:
- self.verticalLayout.removeWidget(plot)
- plot.setParent(None)
- self.plot.remove(plot)
- del plot
-
- # create and setup requested display widgets
- pos = 0
- for channel in channels:
- plot = _FFT_Plot(self)
- self.plot.append(plot)
- plot.setupDisplay(channel.name, samplerate, datapoints, frequency_range)
- self.verticalLayout.insertWidget(pos, plot)
- pos += 1
-
-
- def timerEvent(self,e):
- ''' Display timer callback.
- Get data from input queue and distribute it to the plot widgets
- '''
- while not self.data_queue.empty():
- # get data from queue
- channel_data = self.data_queue.get_nowait()
- # distribute data to plot widgets
- for index, plot in enumerate(self.plot):
- plot.calculate(channel_data[index])
-
-
-class _FFT_Plot(Qwt.QwtPlot):
- ''' FFT plot widget
- '''
- def __init__(self, *args):
- Qwt.QwtPlot.__init__(self, *args)
-
- self.setMinimumSize(Qt.QSize(300, 50))
-
- font = Qt.QFont("arial", 11)
- title = Qwt.QwtText('FFT')
- title.setFont(font)
- self.setTitle(title);
- self.setCanvasBackground(Qt.Qt.white)
-
- # grid
- self.grid = Qwt.QwtPlotGrid()
- self.grid.enableXMin(True)
- self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine));
- self.grid.attach(self)
-
- # axes
- font = Qt.QFont("arial", 9)
- titleX = Qwt.QwtText('Frequency [Hz]')
- titleX.setFont(font)
- titleY = Qwt.QwtText('Amplitude')
- titleY.setFont(font)
- self.setAxisTitle(Qwt.QwtPlot.xBottom, titleX);
- self.setAxisTitle(Qwt.QwtPlot.yLeft, titleY);
- self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 10);
- self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 0);
- self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 10);
- self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0);
- self.setAxisFont(Qwt.QwtPlot.xBottom, font)
- self.setAxisFont(Qwt.QwtPlot.yLeft, font)
- self.axisWidget(Qwt.QwtPlot.yLeft).setMinBorderDist(5,10)
-
- # curves
- self.curve1 = Qwt.QwtPlotCurve('Trace1')
- self.curve1.setPen(Qt.QPen(Qt.Qt.blue,2))
- self.curve1.setYAxis(Qwt.QwtPlot.yLeft)
- self.curve1.attach(self)
-
- # set initial display values
- self.setupDisplay("channel", 500, 1024, 200)
-
-
- def setupDisplay(self, channelname, samplerate, datapoints, frequency_range):
- ''' Initialize all display parameters
- @param channelname: channel name as string
- @param samplerate: sampling rate in Hz
- @param datapoints: chunk size in samples
- @param frequency_range: show frequencies up to this value [Hz]
- '''
- self.samplerate = samplerate
- self.datapoints = datapoints
- self.frequency_range = frequency_range
-
- self.dt = 1.0 / samplerate
- self.df = 1.0 / (datapoints * self.dt)
- self.xValues = np.arange(0.0, samplerate, self.df)
- self.yValues = 0.0 * self.xValues
- self.setAxisScale( Qwt.QwtPlot.xBottom, 0.0, frequency_range)
- #self.setAxisScale( Qwt.QwtPlot.yLeft, 0.0, 10000.0)
- self.curve1.setData(self.xValues, self.yValues)
- self.setTitle(channelname);
- self.replot()
-
- def calculate(self, channel_data):
- ''' Do the FFT
- @param channel_data: raw data array of chunk size
- '''
- lenX = channel_data.shape[0]
- window = np.hanning(lenX)
- window = window / sum(window) * 2.0
- A = np.fft.fft(channel_data*window)
- B = np.abs(A)
- self.curve1.setData(self.xValues[:lenX/2], B[:lenX/2])
- self.replot()
-
-
-
-################################################################
-# Configuration Pane
-
-class _ConfigurationPane(Qt.QFrame):
- ''' FFT Module configuration pane.
-
- Tab for global configuration dialog, contains only one item: "Max. number of plot items"
- '''
- def __init__(self, module, *args):
- apply(Qt.QFrame.__init__, (self,) + args)
-
- # reference to our parent module (TUT_3)
- self.module = module
-
- # Set tab name
- self.setWindowTitle("FFT")
-
- # make it nice
- self.setFrameShape(QtGui.QFrame.StyledPanel)
- self.setFrameShadow(QtGui.QFrame.Raised)
-
- # base layout
- self.gridLayout = QtGui.QGridLayout(self)
-
- # item label
- self.label = QtGui.QLabel(self)
- self.label.setText("Max. number of plot items")
-
- # plot item combobox
- self.comboBoxItems = QtGui.QComboBox(self)
- self.comboBoxItems.setObjectName("comboBoxItems")
- # add combobox list items (1-4)
- for n in range(1,5):
- self.comboBoxItems.addItem(Qt.QString(str(n)))
-
- # use spacer items to align label and combox top-left
- spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
- spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
-
- # add all items to the layout
- self.gridLayout.addWidget(self.label, 0, 0)
- self.gridLayout.addWidget(self.comboBoxItems, 0, 1)
- self.gridLayout.addItem(spacerItem1, 1, 0, 1, 1)
- self.gridLayout.addItem(spacerItem2, 0, 2, 1, 1)
-
- # set initial value from parent module
- self.comboBoxItems.setCurrentIndex(self.module.plot_items-1)
-
- # actions
- self.connect(self.comboBoxItems, Qt.SIGNAL("currentIndexChanged(int)"),
- self._ItemsChanged)
-
- def _ItemsChanged(self, index):
- ''' Event handler for changes in "number of plot items"
- '''
- self.module.plot_items = index + 1 # update the TUT_3 module parameter
-
+# -*- coding: utf-8 -*-
+'''
+Tutorial Module 3
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2011-03-24 16:03:45 +0100 (Do, 24 Mrz 2011) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 62 $
+'''
+
+from modbase import *
+from PyQt4 import QtGui
+from PyQt4 import Qwt5 as Qwt
+import collections
+
+################################################################
+# The module itself
+
+class TUT_3(ModuleBase):
+ ''' Tutorial Module 3
+
+ Calculate and display a FFT for selected channels. Channel selection will be
+ send from display module as command events.
+
+ - GUI elements
+ - Create and use configuration panes
+ - Create and use a FFT signal pane
+ - Data processing
+ - collect chunks of data and send it to the signal pane
+ - calculate and display the FFT
+ - Cofiguration management
+ - write parameters to XML stream
+ - read parameters from XML stream
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor.
+ Initialize instance variables and instantiate GUI objects
+ '''
+ # initialize the base class, give a descriptive name
+ ModuleBase.__init__(self, name="Tutorial 3", **keys)
+
+ # XML parameter version
+ # 1: initial version
+ self.xmlVersion = 1
+
+ # initialize module variables
+ self.data = None #: hold the data block we got from previous module
+ self.dataavailable = False #: data available for output to next module
+ self.channelFifo = collections.deque() #: channel selection FiFo
+ self.params = EEG_DataBlock(0,0) #: default channel configuration
+
+ # Plot configuration data
+ self.chunk = 1024 #: FFT chunk size
+ self.frequency_range = 200 #: Display frequency range [Hz]
+ self.plot_items = 4 #: Maximum number of plot items
+
+ # instantiate online configuration pane
+ self.onlinePane = _OnlineCfgPane()
+ # connect the event handler for changes in chunk size
+ self.connect(self.onlinePane.comboBoxChunk,
+ Qt.SIGNAL("currentIndexChanged(int)"),
+ self.onlineValueChanged)
+ # connect the event handler for changes in frequency
+ self.connect(self.onlinePane.comboBoxFrequency,
+ Qt.SIGNAL("currentIndexChanged(int)"),
+ self.onlineValueChanged)
+
+ # instantiate signal pane
+ self.signalPane = _SignalPane()
+
+ # set default values
+ self.setDefault()
+
+ def setDefault(self):
+ ''' Set all module parameters to default values
+ '''
+ self.chunk = 1024
+ self.frequency_range = 200
+ self.plot_items = 4
+ self.onlinePane.setCurrentValues(self.frequency_range, self.chunk)
+
+ def process_input(self, datablock):
+ ''' Get data from previous module.
+ Because we need to exchange data between different threads we will use
+ a queue object to send data to the display thread to be thread-safe.
+ @param datablock: EEG_DataBlock object
+ '''
+ self.dataavailable = True # signal data availability
+ self.data = datablock # get a local reference
+
+ # anything to do ? check channel selection and recording mode.
+ # If we are in impedance mode, it makes no sense to calculate the FFT
+ if (self.channel_index[0].size == 0) or (self.data.recording_mode == RecordingMode.IMPEDANCE):
+ return
+
+ # because we need a predefined data chunk size, we have to collect data until
+ # at least one data block of data with chunk size is available
+ # so first we append data from selected channels to a local buffer
+ append = self.data.eeg_channels[self.channel_index]
+ self.eeg_channels = np.append(self.eeg_channels, append, 1) # append to axis 1 of local buffer
+
+ # slice the local buffer into chunks
+ while self.eeg_channels.shape[1] > self.chunk:
+ # copy data of chunk size from all selected channels
+ bufcopy = self.eeg_channels[:,:self.chunk]
+ self.eeg_channels = self.eeg_channels[:,self.chunk:]
+ # send channel data to the signal pane queue
+ if self.signalPane.data_queue.empty():
+ self.signalPane.data_queue.put(bufcopy, False)
+ #print bufcopy.shape[1]
+
+ # set different color for selected channels
+ selected = self.data.channel_properties[self.channel_index]
+ for ch in selected:
+ ch.color = Qt.Qt.darkYellow
+
+
+ def process_output(self):
+ ''' Send data out to next module
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+
+ def process_update(self, params):
+ ''' Evaluate and maybe modify the channel configuration.
+ @param params: EEG_DataBlock object.
+ @return: EEG_DataBlock object
+ '''
+ # keep a reference of the channel configuration
+ self.params = params
+ # Create a new channel selection array and setup the signal pane
+ self.updateSignalPane()
+ return params
+
+
+ def process_event(self, event):
+ ''' Handle events from attached receivers.
+ @param event: ModuleEvent
+ '''
+ # Search for ModuleEvents from display module and update channel selection
+ if (event.type == EventType.COMMAND) and (event.info == "ChannelSelected"):
+ channel = event.cmd_value
+ # if channel is already in selection, remove it
+ if channel in self.channelFifo:
+ self.channelFifo.remove(channel)
+ else:
+ self.channelFifo.appendleft(channel)
+ # limit selection to max. entries
+ while len(self.channelFifo) > self.plot_items:
+ self.channelFifo.pop()
+ # Create a new channel selection array and setup the signal pane
+ self.updateSignalPane()
+
+
+ def updateSignalPane(self):
+ ''' Create a channel selection array and setup the signal pane
+ '''
+ # acquire ModuleBase thread lock
+ self._thLock.acquire()
+
+ # get values from online configuration pane
+ self.frequency_range, self.chunk = self.onlinePane.getCurrentValues()
+
+ # if channel FiFo contains more than maximum configured items, remove odd
+ while len(self.channelFifo) > self.plot_items:
+ self.channelFifo.pop()
+
+ # create channel selection indices from channel FiFo
+ mask = lambda x: (x.name in self.channelFifo)
+ channel_ref = np.array([mask(ch) for ch in self.params.channel_properties], dtype=bool)
+ self.channel_index = np.nonzero(channel_ref)
+
+ # create empty calculation buffers
+ if self.params.eeg_channels.shape[0] > 0:
+ self.eeg_channels = np.delete(np.zeros_like(self.params.eeg_channels[self.channel_index]),
+ np.s_[:],
+ 1)
+
+ # create FFT plot for each selected channel
+ self.signalPane.setupDisplay(self.params.channel_properties[self.channel_index],
+ self.params.sample_rate,
+ self.chunk,
+ self.frequency_range)
+ # release ModuleBase thread lock
+ self._thLock.release()
+
+
+ def get_display_pane(self):
+ ''' Get the signal display pane
+ @return: a QFrame object or None if you don't need a display pane
+ '''
+ return self.signalPane
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ @return: a QFrame object or None if you don't need a online configuration pane
+ '''
+ return self.onlinePane
+
+ def get_configuration_pane(self):
+ ''' Get the configuration pane
+ @return: a QFrame object or None if you don't need a configuration pane
+ '''
+ cfgPane = _ConfigurationPane(self)
+ return cfgPane
+
+ def onlineValueChanged(self, int):
+ ''' Event handler for changes in frequency and chunk size
+ '''
+ # Create a new channel selection array and setup the signal pane
+ self.updateSignalPane()
+
+
+ def getXML(self):
+ ''' Get module properties for XML configuration file
+ @return: objectify XML element::
+ e.g.
+
+ 200.0
+ 2048
+ 4
+
+ '''
+ E = objectify.E
+ cfg = E.Tut_FFT(E.frequency_range(self.frequency_range),
+ E.chunk_size(self.chunk),
+ E.plot_items(self.plot_items),
+ version=str(self.xmlVersion),
+ module="FFT",
+ instance=str(self._instance))
+ return cfg
+
+
+ def setXML(self, xml):
+ ''' Set module properties from XML configuration file
+ @param xml: complete objectify XML configuration tree,
+ module will search for matching values
+ '''
+ # search module configuration data
+ storages = xml.xpath("//Tut_FFT[@module='FFT' and @instance='%i']"%(self._instance) )
+ if len(storages) == 0:
+ # configuration data not found, set default values
+ self.setDefault()
+ return
+
+ # we should have only one instance from this type
+ cfg = storages[0]
+
+ # check version, has to be lower or equal than current version
+ version = cfg.get("version")
+ if (version == None) or (int(version) > self.xmlVersion):
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.ERROR,
+ "XML Configuration: wrong version"))
+ return
+ version = int(version)
+
+ # get the values
+ try:
+ self.frequency_range = cfg.frequency_range.pyval
+ self.chunk = cfg.chunk_size.pyval
+ self.plot_items = cfg.plot_items.pyval
+ self.onlinePane.setCurrentValues(self.frequency_range, self.chunk)
+ except Exception as e:
+ self.send_exception(e, severity=ErrorSeverity.NOTIFY)
+
+
+
+
+
+################################################################
+# Online Configuration Pane
+
+class _OnlineCfgPane(Qt.QFrame):
+ ''' Online configuration pane
+ '''
+ def __init__(self , *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # make it nice ;-)
+ self.setFrameShape(QtGui.QFrame.Panel)
+ self.setFrameShadow(QtGui.QFrame.Raised)
+
+ # give us a layout and group box
+ self.gridLayout = QtGui.QGridLayout(self)
+ self.groupBox = QtGui.QGroupBox(self)
+ self.groupBox.setTitle("FFT")
+
+ # group box layout
+ self.gridLayoutGroup = QtGui.QGridLayout(self.groupBox)
+ self.gridLayoutGroup.setHorizontalSpacing(10)
+ self.gridLayoutGroup.setContentsMargins(20, -1, 20, -1)
+
+ # add the chunk size combobox
+ self.comboBoxChunk = QtGui.QComboBox(self.groupBox)
+ self.comboBoxChunk.setObjectName("comboBoxChunk")
+ self.comboBoxChunk.addItem(Qt.QString("128"))
+ self.comboBoxChunk.addItem(Qt.QString("256"))
+ self.comboBoxChunk.addItem(Qt.QString("512"))
+ self.comboBoxChunk.addItem(Qt.QString("1024"))
+ self.comboBoxChunk.addItem(Qt.QString("2048"))
+ self.comboBoxChunk.addItem(Qt.QString("4096"))
+ self.comboBoxChunk.addItem(Qt.QString("8129"))
+ self.comboBoxChunk.addItem(Qt.QString("16384"))
+ self.comboBoxChunk.addItem(Qt.QString("32768"))
+
+ # add the frequency range combobox
+ self.comboBoxFrequency = QtGui.QComboBox(self.groupBox)
+ self.comboBoxFrequency.setObjectName("comboBoxChunk")
+ self.comboBoxFrequency.addItem(Qt.QString("20"))
+ self.comboBoxFrequency.addItem(Qt.QString("50"))
+ self.comboBoxFrequency.addItem(Qt.QString("100"))
+ self.comboBoxFrequency.addItem(Qt.QString("200"))
+ self.comboBoxFrequency.addItem(Qt.QString("500"))
+ self.comboBoxFrequency.addItem(Qt.QString("1000"))
+ self.comboBoxFrequency.addItem(Qt.QString("2000"))
+ self.comboBoxFrequency.addItem(Qt.QString("5000"))
+
+ # create unit labels
+ self.labelChunk = QtGui.QLabel(self.groupBox)
+ self.labelChunk.setText("[n]")
+ self.labelFrequency = QtGui.QLabel(self.groupBox)
+ self.labelFrequency.setText("[Hz]")
+
+ # add widgets to layouts
+ self.gridLayoutGroup.addWidget(self.comboBoxFrequency, 0, 0, 1, 1)
+ self.gridLayoutGroup.addWidget(self.labelFrequency, 0, 1, 1, 1)
+ self.gridLayoutGroup.addWidget(self.comboBoxChunk, 0, 2, 1, 1)
+ self.gridLayoutGroup.addWidget(self.labelChunk, 0, 3, 1, 1)
+
+ self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1)
+
+ # set default values
+ self.comboBoxFrequency.setCurrentIndex(2)
+ self.comboBoxChunk.setCurrentIndex(4)
+
+ def getCurrentValues(self):
+ ''' Get current selected values for frequency and chunk size
+ @return: frequency and chunk size as tuple
+ '''
+ chunk,ok = self.comboBoxChunk.currentText().toFloat()
+ frequency,ok = self.comboBoxFrequency.currentText().toFloat()
+ return frequency, chunk
+
+ def setCurrentValues(self, frequency, chunk):
+ ''' Set the current values for frequency and chunk size
+ @param frequency: new frequency value
+ @param chunk: new chunk size
+ '''
+ # find chunk size index
+ idx = -1
+ for i in range(self.comboBoxChunk.count()):
+ if chunk == self.comboBoxChunk.itemText(i).toFloat()[0]:
+ idx = i
+ # set new combobox index
+ if idx >= 0:
+ self.comboBoxChunk.setCurrentIndex(idx)
+
+ # find frequency index
+ idx = -1
+ for i in range(self.comboBoxFrequency.count()):
+ if frequency == self.comboBoxFrequency.itemText(i).toFloat()[0]:
+ idx = i
+ # set new combobox index
+ if idx >= 0:
+ self.comboBoxFrequency.setCurrentIndex(idx)
+
+
+
+################################################################
+# Signal Pane
+
+class _SignalPane(Qt.QFrame):
+ ''' FFT display pane
+ '''
+ def __init__(self , *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # Initialize local variables
+ self.data_queue = Queue.Queue(10) # data exchange queue
+
+ # Layout display items
+ self.setMinimumSize(Qt.QSize(0, 0)) # hide signal pane if there are no plot items
+ self.verticalLayout = QtGui.QVBoxLayout(self) # arrange plot items vertically
+
+ # list of current plot widgets
+ self.plot = []
+
+ # start 50ms display timer
+ self.startTimer(50)
+
+
+ def setupDisplay(self, channels, samplerate, datapoints, frequency_range):
+ ''' Rearrange and setup plot widgets
+ @param channels: list of selected channels as EEG_ChannelProperties
+ @param samplerate: sampling rate in Hz
+ @param datapoints: chunk size in samples
+ @param frequency_range: show frequencies up to this value [Hz]
+ '''
+ # flush input data queue
+ while not self.data_queue.empty():
+ self.data_queue.get_nowait()
+
+ # remove previous plot widgets
+ for plot in self.plot[:]:
+ self.verticalLayout.removeWidget(plot)
+ plot.setParent(None)
+ self.plot.remove(plot)
+ del plot
+
+ # create and setup requested display widgets
+ pos = 0
+ for channel in channels:
+ plot = _FFT_Plot(self)
+ self.plot.append(plot)
+ plot.setupDisplay(channel.name, samplerate, datapoints, frequency_range)
+ self.verticalLayout.insertWidget(pos, plot)
+ pos += 1
+
+
+ def timerEvent(self,e):
+ ''' Display timer callback.
+ Get data from input queue and distribute it to the plot widgets
+ '''
+ while not self.data_queue.empty():
+ # get data from queue
+ channel_data = self.data_queue.get_nowait()
+ # distribute data to plot widgets
+ for index, plot in enumerate(self.plot):
+ plot.calculate(channel_data[index])
+
+
+class _FFT_Plot(Qwt.QwtPlot):
+ ''' FFT plot widget
+ '''
+ def __init__(self, *args):
+ Qwt.QwtPlot.__init__(self, *args)
+
+ self.setMinimumSize(Qt.QSize(300, 50))
+
+ font = Qt.QFont("arial", 11)
+ title = Qwt.QwtText('FFT')
+ title.setFont(font)
+ self.setTitle(title);
+ self.setCanvasBackground(Qt.Qt.white)
+
+ # grid
+ self.grid = Qwt.QwtPlotGrid()
+ self.grid.enableXMin(True)
+ self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine));
+ self.grid.attach(self)
+
+ # axes
+ font = Qt.QFont("arial", 9)
+ titleX = Qwt.QwtText('Frequency [Hz]')
+ titleX.setFont(font)
+ titleY = Qwt.QwtText('Amplitude')
+ titleY.setFont(font)
+ self.setAxisTitle(Qwt.QwtPlot.xBottom, titleX);
+ self.setAxisTitle(Qwt.QwtPlot.yLeft, titleY);
+ self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 10);
+ self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 0);
+ self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 10);
+ self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0);
+ self.setAxisFont(Qwt.QwtPlot.xBottom, font)
+ self.setAxisFont(Qwt.QwtPlot.yLeft, font)
+ self.axisWidget(Qwt.QwtPlot.yLeft).setMinBorderDist(5,10)
+
+ # curves
+ self.curve1 = Qwt.QwtPlotCurve('Trace1')
+ self.curve1.setPen(Qt.QPen(Qt.Qt.blue,2))
+ self.curve1.setYAxis(Qwt.QwtPlot.yLeft)
+ self.curve1.attach(self)
+
+ # set initial display values
+ self.setupDisplay("channel", 500, 1024, 200)
+
+
+ def setupDisplay(self, channelname, samplerate, datapoints, frequency_range):
+ ''' Initialize all display parameters
+ @param channelname: channel name as string
+ @param samplerate: sampling rate in Hz
+ @param datapoints: chunk size in samples
+ @param frequency_range: show frequencies up to this value [Hz]
+ '''
+ self.samplerate = samplerate
+ self.datapoints = datapoints
+ self.frequency_range = frequency_range
+
+ self.dt = 1.0 / samplerate
+ self.df = 1.0 / (datapoints * self.dt)
+ self.xValues = np.arange(0.0, samplerate, self.df)
+ self.yValues = 0.0 * self.xValues
+ self.setAxisScale( Qwt.QwtPlot.xBottom, 0.0, frequency_range)
+ #self.setAxisScale( Qwt.QwtPlot.yLeft, 0.0, 10000.0)
+ self.curve1.setData(self.xValues, self.yValues)
+ self.setTitle(channelname);
+ self.replot()
+
+ def calculate(self, channel_data):
+ ''' Do the FFT
+ @param channel_data: raw data array of chunk size
+ '''
+ lenX = channel_data.shape[0]
+ window = np.hanning(lenX)
+ window = window / sum(window) * 2.0
+ A = np.fft.fft(channel_data*window)
+ B = np.abs(A)
+ self.curve1.setData(self.xValues[:lenX/2], B[:lenX/2])
+ self.replot()
+
+
+
+################################################################
+# Configuration Pane
+
+class _ConfigurationPane(Qt.QFrame):
+ ''' FFT Module configuration pane.
+
+ Tab for global configuration dialog, contains only one item: "Max. number of plot items"
+ '''
+ def __init__(self, module, *args):
+ Qt.QFrame.__init__(self, *args)
+
+ # reference to our parent module (TUT_3)
+ self.module = module
+
+ # Set tab name
+ self.setWindowTitle("FFT")
+
+ # make it nice
+ self.setFrameShape(QtGui.QFrame.StyledPanel)
+ self.setFrameShadow(QtGui.QFrame.Raised)
+
+ # base layout
+ self.gridLayout = QtGui.QGridLayout(self)
+
+ # item label
+ self.label = QtGui.QLabel(self)
+ self.label.setText("Max. number of plot items")
+
+ # plot item combobox
+ self.comboBoxItems = QtGui.QComboBox(self)
+ self.comboBoxItems.setObjectName("comboBoxItems")
+ # add combobox list items (1-4)
+ for n in range(1,5):
+ self.comboBoxItems.addItem(Qt.QString(str(n)))
+
+ # use spacer items to align label and combox top-left
+ spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
+
+ # add all items to the layout
+ self.gridLayout.addWidget(self.label, 0, 0)
+ self.gridLayout.addWidget(self.comboBoxItems, 0, 1)
+ self.gridLayout.addItem(spacerItem1, 1, 0, 1, 1)
+ self.gridLayout.addItem(spacerItem2, 0, 2, 1, 1)
+
+ # set initial value from parent module
+ self.comboBoxItems.setCurrentIndex(self.module.plot_items-1)
+
+ # actions
+ self.connect(self.comboBoxItems, Qt.SIGNAL("currentIndexChanged(int)"),
+ self._ItemsChanged)
+
+ def _ItemsChanged(self, index):
+ ''' Event handler for changes in "number of plot items"
+ '''
+ self.module.plot_items = index + 1 # update the TUT_3 module parameter
+
+
diff --git a/tutorial/tut_4.py b/tutorial/tut_4.py
index 2d6a470..a28375d 100644
--- a/tutorial/tut_4.py
+++ b/tutorial/tut_4.py
@@ -1,237 +1,237 @@
-# -*- coding: utf-8 -*-
-'''
-Tutorial Module 4
-
-PyCorder ActiChamp Recorder
-
-------------------------------------------------------------
-
-Copyright (C) 2010, Brain Products GmbH, Gilching
-
-This file is part of PyCorder
-
-PyCorder 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 PyCorder. If not, see .
-
-------------------------------------------------------------
-
-@author: Norbert Hauser
-@date: $Date: 2011-04-11 11:51:56 +0200 (Mo, 11 Apr 2011) $
-@version: 1.0
-
-B{Revision:} $LastChangedRevision: 69 $
-'''
-
-from modbase import *
-import frmTUT4Online
-
-class TUT_4(ModuleBase):
- ''' Tutorial Module 4
-
- Set the trigger out port with values from the online configuration pane.
- Indicate the "My Button" state. Both, trigger out and "My Button" state are available
- only during data acquisition.
- '''
-
- def __init__(self, *args, **keys):
- ''' Constructor.
- Initialize instance variables and instantiate GUI objects
- '''
- # initialize the base class, give a descriptive name
- ModuleBase.__init__(self, name="Trigger Output", **keys)
- self.data = None
- self.dataavailable = False
-
- # instantiate online configuration pane
- self.online_cfg = _OnlineCfgPane(self)
-
- # connect the signal handler for trigger out settings
- self.connect(self.online_cfg, Qt.SIGNAL("valueChanged(int)"), self.sendTrigger)
- self.online_cfg.groupBox.setEnabled(False)
-
- def get_online_configuration(self):
- ''' Get the online configuration pane
- @return: a QFrame object or None if you don't need a online configuration pane
- '''
- return self.online_cfg
-
- def process_event(self, event):
- ''' Handle events from attached modules.
- @param event: ModuleEvent
- '''
- # Search for "MyButton" ModuleEvents from acquisition module
- # and indicate the button state
- if (event.type == EventType.COMMAND) and (event.info == "MyButton"):
- if "pressed" == event.cmd_value:
- self.online_cfg.MyButton.setChecked(True)
- self.setButtonLED(True)
- else:
- self.online_cfg.MyButton.setChecked(False)
- self.setButtonLED(False)
-
- def process_start(self):
- ''' Data acquisition started.
- Enable the trigger setting group box.
- '''
- self.online_cfg.groupBox.setEnabled(True)
- self.firstblock = True
-
- def process_stop(self):
- ''' Data acquisition stopped.
- Disable the trigger setting group box.
- '''
- self.online_cfg.groupBox.setEnabled(False)
-
- def process_input(self, datablock):
- ''' Get data from previous module.
- @param datablock: EEG_DataBlock object
- '''
- self.dataavailable = True
- self.data = datablock
- # first time initialization of trigger output
- if self.firstblock:
- self.firstblock = False
- self.sendTrigger(self.online_cfg.getCheckboxes())
-
-
- def process_output(self):
- ''' Send data out to next module.
- '''
- if not self.dataavailable:
- return None
- self.dataavailable = False
- return self.data
-
- def sendTrigger(self, triggervalue):
- ''' Signal from online configuration pane,
- if the trigger output value has changed.
- @param triggervalue: binary trigger out value (Bit0=D0, Bit1=D1 ...)
- '''
- # send new trigger output value to acquisition module
- self.send_event(ModuleEvent(self._object_name,
- EventType.COMMAND,
- info = "TriggerOut",
- cmd_value=triggervalue
- ))
-
- def setButtonLED(self, on):
- ''' Switch MyButton LED on/off
- @param on: on=True, off=False
- '''
- period = 300 # let it blink at a rate of 300ms
- duty = 50 # with a 50% duty cycle
- if not on:
- duty = 0 # switch it off
-
- # send LED command to acquisition module
- self.send_event(ModuleEvent(self._object_name,
- EventType.COMMAND,
- info = "SetLED",
- cmd_value = (period, duty)
- ))
-
-
-################################################################
-# Online Configuration Pane
-
-class _OnlineCfgPane(Qt.QFrame, frmTUT4Online.Ui_frmTUT4Online):
- ''' TUT_4 Module online configuration pane
- '''
- def __init__(self, module, *args):
- # initialize designer generated user interface
- apply(Qt.QFrame.__init__, (self,) + args)
- self.setupUi(self)
-
- self.module = module
- self.triggervalue = 0
-
- # connect the event handlers
- self.connect(self.checkBox0, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox1, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox2, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox3, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox4, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox5, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox6, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.checkBox7, Qt.SIGNAL("clicked()"), self.valueChanged)
- self.connect(self.pushButton_ResetAll, Qt.SIGNAL("clicked()"), self.resetAll)
- self.connect(self.pushButton_SetAll, Qt.SIGNAL("clicked()"), self.setAll)
- self.connect(self.MyButton, Qt.SIGNAL("clicked(bool)"), self.myButton)
-
- def myButton(self, checked):
- ''' MyButton signal handler.
- Don't check it manually
- '''
- self.MyButton.setChecked(not checked)
-
- def valueChanged(self):
- ''' Signal handler for value checkboxes
- '''
- # get value from checkboxes
- trigger_out = self.getCheckboxes()
- # send value to parent
- if trigger_out != self.triggervalue:
- self.triggervalue = trigger_out
- self.emit(Qt.SIGNAL('valueChanged(int)'), self.triggervalue)
-
- def resetAll(self):
- ''' "Reset All" button signal handler
- '''
- self.setCheckboxes(0)
- self.valueChanged()
-
- def setAll(self):
- ''' "Set All" button signal handler
- '''
- self.setCheckboxes(0xFF)
- self.valueChanged()
-
- def setCheckboxes(self, value):
- ''' Set checkboxes from trigger out value.
- @param value: binary trigger out value (Bit0=D0, Bit1=D1 ...)
- '''
-
- self.checkBox0.setChecked(value & 0x01)
- self.checkBox1.setChecked(value & 0x02)
- self.checkBox2.setChecked(value & 0x04)
- self.checkBox3.setChecked(value & 0x08)
- self.checkBox4.setChecked(value & 0x10)
- self.checkBox5.setChecked(value & 0x20)
- self.checkBox6.setChecked(value & 0x40)
- self.checkBox7.setChecked(value & 0x80)
-
- def getCheckboxes(self):
- ''' Get trigger out value from checkboxes.
- @return: binary trigger out value (Bit0=D0, Bit1=D1 ...)
- '''
- value = 0
- if self.checkBox0.isChecked():
- value |= 0x01
- if self.checkBox1.isChecked():
- value |= 0x02
- if self.checkBox2.isChecked():
- value |= 0x04
- if self.checkBox3.isChecked():
- value |= 0x08
- if self.checkBox4.isChecked():
- value |= 0x10
- if self.checkBox5.isChecked():
- value |= 0x20
- if self.checkBox6.isChecked():
- value |= 0x40
- if self.checkBox7.isChecked():
- value |= 0x80
- return value
-
-
-
+# -*- coding: utf-8 -*-
+'''
+Tutorial Module 4
+
+PyCorder ActiChamp Recorder
+
+------------------------------------------------------------
+
+Copyright (C) 2010, Brain Products GmbH, Gilching
+
+This file is part of PyCorder
+
+PyCorder 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 PyCorder. If not, see .
+
+------------------------------------------------------------
+
+@author: Norbert Hauser
+@date: $Date: 2011-04-11 11:51:56 +0200 (Mo, 11 Apr 2011) $
+@version: 1.0
+
+B{Revision:} $LastChangedRevision: 69 $
+'''
+
+from modbase import *
+import frmTUT4Online
+
+class TUT_4(ModuleBase):
+ ''' Tutorial Module 4
+
+ Set the trigger out port with values from the online configuration pane.
+ Indicate the "My Button" state. Both, trigger out and "My Button" state are available
+ only during data acquisition.
+ '''
+
+ def __init__(self, *args, **keys):
+ ''' Constructor.
+ Initialize instance variables and instantiate GUI objects
+ '''
+ # initialize the base class, give a descriptive name
+ ModuleBase.__init__(self, name="Trigger Output", **keys)
+ self.data = None
+ self.dataavailable = False
+
+ # instantiate online configuration pane
+ self.online_cfg = _OnlineCfgPane(self)
+
+ # connect the signal handler for trigger out settings
+ self.connect(self.online_cfg, Qt.SIGNAL("valueChanged(int)"), self.sendTrigger)
+ self.online_cfg.groupBox.setEnabled(False)
+
+ def get_online_configuration(self):
+ ''' Get the online configuration pane
+ @return: a QFrame object or None if you don't need a online configuration pane
+ '''
+ return self.online_cfg
+
+ def process_event(self, event):
+ ''' Handle events from attached modules.
+ @param event: ModuleEvent
+ '''
+ # Search for "MyButton" ModuleEvents from acquisition module
+ # and indicate the button state
+ if (event.type == EventType.COMMAND) and (event.info == "MyButton"):
+ if "pressed" == event.cmd_value:
+ self.online_cfg.MyButton.setChecked(True)
+ self.setButtonLED(True)
+ else:
+ self.online_cfg.MyButton.setChecked(False)
+ self.setButtonLED(False)
+
+ def process_start(self):
+ ''' Data acquisition started.
+ Enable the trigger setting group box.
+ '''
+ self.online_cfg.groupBox.setEnabled(True)
+ self.firstblock = True
+
+ def process_stop(self):
+ ''' Data acquisition stopped.
+ Disable the trigger setting group box.
+ '''
+ self.online_cfg.groupBox.setEnabled(False)
+
+ def process_input(self, datablock):
+ ''' Get data from previous module.
+ @param datablock: EEG_DataBlock object
+ '''
+ self.dataavailable = True
+ self.data = datablock
+ # first time initialization of trigger output
+ if self.firstblock:
+ self.firstblock = False
+ self.sendTrigger(self.online_cfg.getCheckboxes())
+
+
+ def process_output(self):
+ ''' Send data out to next module.
+ '''
+ if not self.dataavailable:
+ return None
+ self.dataavailable = False
+ return self.data
+
+ def sendTrigger(self, triggervalue):
+ ''' Signal from online configuration pane,
+ if the trigger output value has changed.
+ @param triggervalue: binary trigger out value (Bit0=D0, Bit1=D1 ...)
+ '''
+ # send new trigger output value to acquisition module
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.COMMAND,
+ info = "TriggerOut",
+ cmd_value=triggervalue
+ ))
+
+ def setButtonLED(self, on):
+ ''' Switch MyButton LED on/off
+ @param on: on=True, off=False
+ '''
+ period = 300 # let it blink at a rate of 300ms
+ duty = 50 # with a 50% duty cycle
+ if not on:
+ duty = 0 # switch it off
+
+ # send LED command to acquisition module
+ self.send_event(ModuleEvent(self._object_name,
+ EventType.COMMAND,
+ info = "SetLED",
+ cmd_value = (period, duty)
+ ))
+
+
+################################################################
+# Online Configuration Pane
+
+class _OnlineCfgPane(Qt.QFrame, frmTUT4Online.Ui_frmTUT4Online):
+ ''' TUT_4 Module online configuration pane
+ '''
+ def __init__(self, module, *args):
+ # initialize designer generated user interface
+ Qt.QFrame.__init__(self, *args)
+ self.setupUi(self)
+
+ self.module = module
+ self.triggervalue = 0
+
+ # connect the event handlers
+ self.connect(self.checkBox0, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox1, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox2, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox3, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox4, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox5, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox6, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.checkBox7, Qt.SIGNAL("clicked()"), self.valueChanged)
+ self.connect(self.pushButton_ResetAll, Qt.SIGNAL("clicked()"), self.resetAll)
+ self.connect(self.pushButton_SetAll, Qt.SIGNAL("clicked()"), self.setAll)
+ self.connect(self.MyButton, Qt.SIGNAL("clicked(bool)"), self.myButton)
+
+ def myButton(self, checked):
+ ''' MyButton signal handler.
+ Don't check it manually
+ '''
+ self.MyButton.setChecked(not checked)
+
+ def valueChanged(self):
+ ''' Signal handler for value checkboxes
+ '''
+ # get value from checkboxes
+ trigger_out = self.getCheckboxes()
+ # send value to parent
+ if trigger_out != self.triggervalue:
+ self.triggervalue = trigger_out
+ self.emit(Qt.SIGNAL('valueChanged(int)'), self.triggervalue)
+
+ def resetAll(self):
+ ''' "Reset All" button signal handler
+ '''
+ self.setCheckboxes(0)
+ self.valueChanged()
+
+ def setAll(self):
+ ''' "Set All" button signal handler
+ '''
+ self.setCheckboxes(0xFF)
+ self.valueChanged()
+
+ def setCheckboxes(self, value):
+ ''' Set checkboxes from trigger out value.
+ @param value: binary trigger out value (Bit0=D0, Bit1=D1 ...)
+ '''
+
+ self.checkBox0.setChecked(value & 0x01)
+ self.checkBox1.setChecked(value & 0x02)
+ self.checkBox2.setChecked(value & 0x04)
+ self.checkBox3.setChecked(value & 0x08)
+ self.checkBox4.setChecked(value & 0x10)
+ self.checkBox5.setChecked(value & 0x20)
+ self.checkBox6.setChecked(value & 0x40)
+ self.checkBox7.setChecked(value & 0x80)
+
+ def getCheckboxes(self):
+ ''' Get trigger out value from checkboxes.
+ @return: binary trigger out value (Bit0=D0, Bit1=D1 ...)
+ '''
+ value = 0
+ if self.checkBox0.isChecked():
+ value |= 0x01
+ if self.checkBox1.isChecked():
+ value |= 0x02
+ if self.checkBox2.isChecked():
+ value |= 0x04
+ if self.checkBox3.isChecked():
+ value |= 0x08
+ if self.checkBox4.isChecked():
+ value |= 0x10
+ if self.checkBox5.isChecked():
+ value |= 0x20
+ if self.checkBox6.isChecked():
+ value |= 0x40
+ if self.checkBox7.isChecked():
+ value |= 0x80
+ return value
+
+
+
From d805ea586b7cecdc82747b5484b421d141f01abb Mon Sep 17 00:00:00 2001
From: yasufumi <52214705+yasufumi-nakata@users.noreply.github.com>
Date: Tue, 24 Feb 2026 20:45:11 +0900
Subject: [PATCH 2/5] Stabilize GUI startup and add dummy acquisition
validation
---
PyQt4/Qt.py | 9 ++
PyQt4/QtCore.py | 28 ++++
PyQt4/Qwt5.py | 20 ++-
README.md | 41 +++++
actichamp_w.py | 220 ++++++++++++++++++++++++---
amplifier.py | 2 +-
display.py | 10 +-
display_fallback.py | 48 ++++++
display_simple.py | 130 ++++++++++++++++
main.py | 55 +++++--
run_pycorder.sh | 30 ++++
tools/gui_dummy_measurement_check.py | 169 ++++++++++++++++++++
12 files changed, 722 insertions(+), 40 deletions(-)
create mode 100644 display_fallback.py
create mode 100644 display_simple.py
create mode 100644 tools/gui_dummy_measurement_check.py
diff --git a/PyQt4/Qt.py b/PyQt4/Qt.py
index bd09ad6..5ea8e7e 100644
--- a/PyQt4/Qt.py
+++ b/PyQt4/Qt.py
@@ -225,6 +225,15 @@ def __init__(self, *args, **kwargs):
QDir = QtCore.QDir
+
+def __getattr__(name):
+ # Fallback lookup so legacy code can access additional Qt classes via
+ # `from PyQt4 import Qt; Qt.QVBoxLayout` style.
+ for module in (QtWidgets, QtGui, QtCore):
+ if hasattr(module, name):
+ return getattr(module, name)
+ raise AttributeError("module 'PyQt4.Qt' has no attribute %r" % (name,))
+
# Re-export submodules for import style from PyQt4 import Qt; then Qt.Qt etc.
__all__ = [
'Qt', 'QApplication', 'QWidget', 'QDialog', 'QMainWindow', 'QMessageBox', 'QFileDialog',
diff --git a/PyQt4/QtCore.py b/PyQt4/QtCore.py
index 1cbe11a..0a8b769 100644
--- a/PyQt4/QtCore.py
+++ b/PyQt4/QtCore.py
@@ -38,3 +38,31 @@ def toStringList(self):
return []
+class QString(str):
+ @staticmethod
+ def number(n):
+ return str(n)
+
+ def toFloat(self):
+ try:
+ return float(self), True
+ except Exception:
+ return 0.0, False
+
+ def toDouble(self):
+ return self.toFloat()
+
+ def toInt(self):
+ try:
+ return int(float(self)), True
+ except Exception:
+ return 0, False
+
+
+class QStringList(list):
+ def __init__(self, *args, **kwargs):
+ if len(args) == 1 and isinstance(args[0], str):
+ super(QStringList, self).__init__([args[0]])
+ else:
+ super(QStringList, self).__init__(*args, **kwargs)
+
diff --git a/PyQt4/Qwt5.py b/PyQt4/Qwt5.py
index 6f17099..46db4dc 100644
--- a/PyQt4/Qwt5.py
+++ b/PyQt4/Qwt5.py
@@ -31,6 +31,8 @@ def __init__(self, text=''):
self._text = str(text)
self._color = QtGui.QColor('black')
self._font = QtGui.QFont()
+ def setText(self, text):
+ self._text = str(text)
def setFont(self, f):
self._font = f
def setColor(self, c):
@@ -42,6 +44,7 @@ def text(self):
class QwtPlot(pg.PlotWidget):
+ xTop = 2
xBottom = 0
yLeft = 1
LeftLegend = 0
@@ -117,8 +120,23 @@ def __init__(self, title=None):
super(QwtPlotCurve, self).__init__(x=[], y=[])
self._title = title
self._style = self.Lines
+ self._pen_color = QtGui.QColor('white')
+ class _PenProxy:
+ def __init__(self, color):
+ self.color = color
+ def pen(self):
+ return self._PenProxy(self._pen_color)
def setPen(self, pen):
- color = pen.color()
+ if hasattr(pen, 'color'):
+ if callable(pen.color):
+ color = pen.color()
+ else:
+ color = pen.color
+ else:
+ color = pen
+ if not isinstance(color, QtGui.QColor):
+ color = QtGui.QColor(color)
+ self._pen_color = color
super(QwtPlotCurve, self).setPen(pg.mkPen(color))
def setYAxis(self, axis):
pass
diff --git a/README.md b/README.md
index 123ad66..b8882f8 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,47 @@ Quick start:
- `python main.py`
- Automated smoke test (headless, no window popup):
- `bash ./run_pycorder.sh --smoketest`
+- Automated GUI measurement check with dummy data (headless/offscreen, validates 1000 Hz by default):
+ - `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py`
+
+GUI startup (recommended):
+- macOS/Linux:
+ 1. `bash ./run_pycorder.sh`
+ 2. Wait until the PyCorder main window opens.
+- Windows PowerShell:
+ 1. `.\run_pycorder.ps1`
+ 2. Wait until the PyCorder main window opens.
+- Windows CMD:
+ 1. `run_pycorder.bat`
+ 2. Wait until the PyCorder main window opens.
+
+Dummy data measurement (no hardware):
+1. Start GUI with one of the commands above.
+2. In the amplifier online pane, click `Default` (start acquisition).
+3. Confirm waveform updates in the display pane.
+4. Click again to stop acquisition.
+
+Validate 1000 Hz dummy acquisition (automated):
+1. Run:
+ - `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py --sample-rate-hz 1000 --duration-ms 2600`
+2. Confirm the output contains:
+ - `gui_measurement_ok=True`
+ - `configured_rate=1000.0`
+ - `reported_rate=1000.0`
+
+You can test other rates similarly, for example:
+- `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py --sample-rate-hz 2000 --duration-ms 2600`
+
+If GUI does not start:
+1. Run `bash ./run_pycorder.sh --smoketest` to verify dependency/runtime startup.
+2. If another instance is still running, stop it:
+ - `pkill -f "python.*main.py"`
+3. Run `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py --sample-rate-hz 1000` to verify dummy acquisition path.
+
+Notes:
+- On macOS, PyCorder uses a stable simple scope (`DISP_ScopeSimple`) by default to avoid Qt/Qwt shim startup crashes while still showing live dummy/hardware waveforms.
+- If the selected scope widget cannot be initialized on your Qt runtime, PyCorder falls back to a minimal pane (`DISP_ScopeLite`) so acquisition can still run.
+- If TCP port `51244` is already in use, the RDA server module is skipped and the GUI still starts.
Current additions:
diff --git a/actichamp_w.py b/actichamp_w.py
index e940cfe..36f0f6e 100644
--- a/actichamp_w.py
+++ b/actichamp_w.py
@@ -492,6 +492,11 @@ def __init__(self):
self.sampleCounterAdjust = 0 #: sample counter wrap around, HW counter is 32bit value but we need 64bit
self.BlockingMode = True #: read data in blocking mode
self.EmulationMode = False #: emulate hardware
+ self._sim_module_count = 2
+ self._sim_sample_counter = 0
+ self._sim_signal_rate = None
+ self._sim_last_trigger = 0
+ self._sim_next_block_time = None
# set default properties
self.properties.CountEeg = 32
@@ -533,6 +538,13 @@ def __init__(self):
self.close()
except:
pass
+ if self.lib is None:
+ try:
+ modules = self.getEmulationMode()
+ if modules > 0:
+ self._sim_module_count = int(modules)
+ except Exception:
+ pass
def _resetDeviceProperties(self):
''' Set channel count to zero
@@ -541,6 +553,78 @@ def _resetDeviceProperties(self):
self.properties.CountAux = 0
self.properties.TriggersIn = 0
self.properties.TriggersOut = 0
+
+ def _apply_simulation_profile(self, modules):
+ '''Apply device properties for pure Python simulation mode.'''
+ try:
+ modules = int(modules)
+ except Exception:
+ modules = 0
+ if modules <= 0:
+ modules = self._sim_module_count
+ modules = max(1, min(modules, 5))
+
+ self._sim_module_count = modules
+ self.EmulationMode = True
+ self.devicehandle = 1
+ self.modulestate.Present = (1 << (modules + 1)) - 1
+ self.modulestate.Enabled = self.modulestate.Present
+ self.properties.CountEeg = modules * 32
+ self.properties.CountAux = 8
+ self.properties.TriggersIn = 8
+ self.properties.TriggersOut = 8
+ self.properties.Rate = sample_rate.get(self.settings.Rate, 10000.0)
+
+ def _simulate_read(self, eegcount, auxcount):
+ '''Generate deterministic sine-wave data for GUI/recording tests.'''
+ sr = float(sample_rate.get(self.settings.Rate, 1000.0))
+ interval = 0.05 if self.BlockingMode else 0.02
+ if self._sim_next_block_time is None:
+ self._sim_next_block_time = time.perf_counter()
+ now = time.perf_counter()
+ wait_s = self._sim_next_block_time - now
+ if wait_s > 0:
+ time.sleep(wait_s)
+ now = time.perf_counter()
+ self._sim_next_block_time = now + interval
+
+ block_samples = max(1, int(round(sr * interval)))
+ start = int(self._sim_sample_counter)
+ sct = np.arange(start, start + block_samples, dtype=np.uint64)
+ self._sim_sample_counter = int(sct[-1] + 1)
+
+ numchannels = int(eegcount + auxcount)
+ if numchannels > 0:
+ if (not hasattr(self, "DummySignals")
+ or len(self.DummySignals) != numchannels
+ or self._sim_signal_rate != sr):
+ sg = SignalGenerator(float)
+ _, self.DummySignals = sg.GetSineWaveBuffers(
+ numchannels,
+ [1.0, 2.0, 3.7, 5.0, 10.0, 17.2, 20.0, 50.0, 100.0, 200.0],
+ 1.0,
+ 100.0,
+ 0.0,
+ sr,
+ )
+ self._sim_signal_rate = sr
+ sc_idx = np.asarray(sct, dtype=np.int64)
+ eeg = np.zeros((numchannels, block_samples), dtype=float)
+ for c in range(numchannels):
+ eeg[c] = np.take(self.DummySignals[c], sc_idx, mode="wrap")
+ else:
+ eeg = np.zeros((0, block_samples), dtype=float)
+
+ trg = np.zeros((1, block_samples), dtype=np.uint32)
+ trigger_period = max(1, int(sr * 10.0))
+ tr_idx = np.nonzero((sct % trigger_period) < 3)[0]
+ if tr_idx.size:
+ trg[0, tr_idx] = 1
+ self._sim_last_trigger = 1
+ else:
+ self._sim_last_trigger = 0
+
+ return [eeg, trg, sct.reshape(1, -1)], None
def loadLib(self):
@@ -571,11 +655,9 @@ def open(self):
if self.running:
return
if self.lib == None:
- # 非Windows・未検出時はエミュレーションにフォールバック
- if platform.system() != 'Windows':
- self.EmulationMode = True
- return
- raise AmpError("library ActiChamp_x86.dll not available")
+ modules = self.getEmulationMode()
+ self._apply_simulation_profile(modules)
+ return
# check if device hardware is available
self._resetDeviceProperties()
@@ -621,9 +703,9 @@ def close(self):
''' Close hardware device
'''
if self.lib == None:
- if platform.system() != 'Windows':
- return
- raise AmpError("library ActiChamp_x86.dll not available")
+ self.running = False
+ self.devicehandle = 0
+ return
if self.devicehandle != 0:
if self.running:
try:
@@ -660,7 +742,18 @@ def setup(self, mode, rate, binning):
self.binning = int(binning)
self.binning_offset = 0
if self.devicehandle == 0:
- raise AmpError("device not open")
+ if self.lib is None:
+ self.open()
+ else:
+ raise AmpError("device not open")
+ if self.lib is None:
+ modules = self.getEmulationMode()
+ self._apply_simulation_profile(modules)
+ self._sim_signal_rate = None
+ self._sim_next_block_time = None
+ trgdelay = trigger_delay[self.settings.Rate]
+ self.trgdelaybuf = np.zeros(trgdelay, np.uint32) + 0xFFFF
+ return
# setup amplifier
ex_settings = self._get_settings_ex(self.settings)
@@ -715,6 +808,15 @@ def start(self):
return
if self.devicehandle == 0:
raise AmpError("device not open")
+ if self.lib is None:
+ self.running = True
+ self.readError = False
+ self.sampleCounterAdjust = 0
+ self._sim_sample_counter = 0
+ self._sim_next_block_time = time.perf_counter()
+ self.BlockTimer = time.perf_counter()
+ self.DummySignals = []
+ return
# start amplifier
err = self.lib.champStart(self.devicehandle)
@@ -754,6 +856,8 @@ def stop(self):
self.running = False
if self.devicehandle == 0:
raise AmpError("device not open")
+ if self.lib is None:
+ return
err = self.lib.champStop(self.devicehandle)
if err != CHAMP_ERR_OK:
raise AmpError("failed to stop device", err)
@@ -768,6 +872,8 @@ def read(self, indices, eegcount, auxcount):
'''
if not self.running or (self.devicehandle == 0) or self.readError:
return None, None
+ if self.lib is None:
+ return self._simulate_read(eegcount, auxcount)
# calculate data amount for an interval of
interval = 0.05 # interval in [s]
@@ -811,9 +917,9 @@ def read(self, indices, eegcount, auxcount):
# copy remainder from last read back to sample buffer
ctypes.memmove(self.buffer, self.binning_buffer, self.binning_offset)
# new remainder size
- remainder = ((total_bytes / bytes_per_sample) % self.binning) * bytes_per_sample
+ remainder = ((total_bytes // bytes_per_sample) % self.binning) * bytes_per_sample
# number of binning aligned samples
- binning_samples = total_bytes / bytes_per_sample / self.binning * self.binning
+ binning_samples = (total_bytes // bytes_per_sample // self.binning) * self.binning
src_offset = binning_samples * bytes_per_sample
# copy new remainder to binning buffer
ctypes.memmove(self.binning_buffer, ctypes.byref(self.buffer, src_offset), remainder)
@@ -822,9 +928,9 @@ def read(self, indices, eegcount, auxcount):
# there must be at least one binning sample
if binning_samples == 0:
return None, None
- items = binning_samples * bytes_per_sample / np.dtype(np.int32).itemsize
+ items = (binning_samples * bytes_per_sample) // np.dtype(np.int32).itemsize
else:
- items = bytesread / np.dtype(np.int32).itemsize
+ items = bytesread // np.dtype(np.int32).itemsize
# channel order in buffer is S1CH1,S1CH2..S1CHn, S2CH1,S2CH2,..S2nCHn, ...
x = np.fromstring(self.buffer, np.int32, items)
@@ -915,6 +1021,13 @@ def readImpedances(self):
'''
if not self.running or (self.devicehandle == 0):
return None, None
+ if self.lib is None:
+ item_count = self.properties.CountEeg + 1
+ phase = (self._sim_sample_counter % 2000) / 2000.0 * np.pi
+ ramp = np.linspace(3000.0, 12000.0, item_count)
+ wobble = 500.0 * np.sin(np.linspace(0.0, np.pi, item_count) + phase)
+ imp = np.clip(ramp + wobble, 500.0, 50000.0).astype(np.uint32)
+ return imp, None
disconnected = None
# read impedance data from device
@@ -947,6 +1060,8 @@ def setImpedanceRange(self, good, bad):
'''
if self.devicehandle == 0:
return
+ if self.lib is None:
+ return
imp_settings = CHAMP_IMPEDANCE_SETUP()
imp_settings.Good = int(good)
imp_settings.Bad = int(bad)
@@ -962,6 +1077,9 @@ def setTrigger(self, trigger):
'''
if self.devicehandle == 0:
return
+ if self.lib is None:
+ self._sim_last_trigger = trigger & 0xFF
+ return
# 8-bit inputs (bits 0 - 7) + 8-bit outputs (bits 8 - 15) + 16 MSB reserved bits.
trigger = (trigger & 0xFF) << 8
@@ -976,6 +1094,7 @@ def getEmulationMode(self):
'''
emulation = 0
modules = 0
+ model_channels = 0
try:
ini = configparser.ConfigParser()
if self.x64:
@@ -984,17 +1103,34 @@ def getEmulationMode(self):
filename = "ActiChamp_x86.dll.ini"
if len(ini.read(filename)) > 0:
- emulation = ini.getint("Main", "Emulation")
- if emulation != 0:
- modules = ini.getint("Emulation", "Model") / 32
+ try:
+ emulation = ini.getint("Main", "Emulation")
+ except Exception:
+ emulation = 0
+ try:
+ model_channels = ini.getint("Emulation", "Model")
+ except Exception:
+ model_channels = 0
+ if emulation != 0 and model_channels > 0:
+ modules = int(model_channels // 32)
try:
self.enablePllConfiguration = (ini.getint("Main", "EnablePllConfiguration") != 0)
except:
self.enablePllConfiguration = False
except:
modules = 0
- self.EmulationMode = (modules > 0)
- return modules
+ if modules > 0:
+ self._sim_module_count = max(1, int(modules))
+ if self.lib is None:
+ if modules <= 0:
+ if model_channels > 0:
+ modules = int(max(1, model_channels // 32))
+ else:
+ modules = max(1, int(self._sim_module_count))
+ self.EmulationMode = True
+ else:
+ self.EmulationMode = (modules > 0)
+ return int(modules)
def setEmulationMode(self, modules):
''' Set/Reset emulation flag in INI file
@@ -1003,6 +1139,10 @@ def setEmulationMode(self, modules):
# not possible if device is already open
if self.devicehandle != 0:
return
+ try:
+ modules = int(modules)
+ except Exception:
+ modules = 0
# write new settings to INI file
ini = configparser.ConfigParser()
@@ -1023,6 +1163,11 @@ def setEmulationMode(self, modules):
fp.close()
else:
raise AmpError("INI file %s not found"%(filename))
+ if modules > 0:
+ self._sim_module_count = max(1, min(modules, 5))
+ if self.lib is None:
+ self.EmulationMode = True
+ return
# reload the DLL
self.loadLib()
@@ -1066,6 +1211,8 @@ def getDeviceStatus(self):
'''
if self.devicehandle == 0:
return 0, 0, 0, 0
+ if self.lib is None:
+ return int(self._sim_sample_counter), 0, float(sample_rate.get(self.settings.Rate, 0.0)), 0.0
status = CHAMP_DATA_STATUS()
err = self.lib.champGetDataStatus(self.devicehandle, ctypes.byref(status))
if err != CHAMP_ERR_OK:
@@ -1154,6 +1301,14 @@ def getBatteryVoltage(self):
#voltages.VDC = 0.0
if self.devicehandle == 0:
return 0, voltages, faultyVoltages
+ if self.lib is None:
+ voltages.VDC = 6.7
+ voltages.V3 = 3.3
+ voltages.DVDD3 = 3.3
+ voltages.AVDD3 = 3.3
+ voltages.AVDD5 = 5.0
+ voltages.REF = 2.048
+ return 0, voltages, faultyVoltages
# get amplifier voltages
err = self.lib.champGetVoltages(self.devicehandle, ctypes.byref(voltages))
@@ -1196,6 +1351,8 @@ def setButtonLed(self, period, dutyCycle):
'''
if self.devicehandle == 0:
return
+ if self.lib is None:
+ return
dutyCycle = max(min(dutyCycle,100),0) # limit to 0-100%
period = max(min(period,10000),1) # limit to 1-10000ms
# use a fixed period for on/off
@@ -1218,6 +1375,19 @@ def LedTest(self, step):
12 = set all electrodes to red
@return: TRUE if last electrode index reached
'''
+ if self.lib is None:
+ if not hasattr(self, "LED_index"):
+ self.LED_index = 0
+ if step == 0:
+ self.LED_index = 0
+ return True
+ if step in (1, 2):
+ self.LED_index += 1
+ if self.LED_index >= (self.properties.CountEeg + 1):
+ self.LED_index = 0
+ elif step in (11, 12):
+ self.LED_index = 0
+ return self.LED_index == 0
ledcount = self.properties.CountEeg + 1
led_array = (ctypes.c_int * ledcount)()
led_array[:] = [0]*len(led_array)
@@ -1254,7 +1424,7 @@ def hasPllOption(self):
def setPllInput(self):
''' Set the PLL input either to external or internal
'''
- if self.devicehandle == 0 or not self.hasPllOption() or self.getEmulationMode() != 0:
+ if self.devicehandle == 0 or self.lib is None or not self.hasPllOption() or self.getEmulationMode() != 0:
return
PllParamters = CHAMP_PLL()
@@ -1282,7 +1452,8 @@ def __init__(self, dtype=np.int16):
def GetSineWave(self, freq, samplerate, amplitude, time):
w = 2.0 * np.pi * freq
- t = np.linspace(0, time, samplerate * time)
+ samples = max(2, int(round(samplerate * time)))
+ t = np.linspace(0, time, samples)
return t, np.asarray(np.sin(w*t) * amplitude, dtype=self.dtype)
def GetTriangleWave(self, freq, samplerate, amplitude, time):
@@ -1296,9 +1467,9 @@ def GetSineWaveBuffers(self, NumChannels, StartFrequency, DeltaFrequency, StartA
cycles = 40.0
if type(StartFrequency) == list:
fl = StartFrequency*NumChannels
- fSin = np.array(fl[:NumChannels+1])
+ fSin = np.array(fl[:NumChannels], dtype=float)
else:
- fSin = np.arange(StartFrequency, StartFrequency + NumChannels * DeltaFrequency, DeltaFrequency)
+ fSin = np.arange(StartFrequency, StartFrequency + NumChannels * DeltaFrequency, DeltaFrequency, dtype=float)
if DeltaAmplitude != 0:
aSin = np.arange(StartAmplitude, StartAmplitude + NumChannels * DeltaAmplitude, DeltaAmplitude, dtype=self.dtype)
else:
@@ -1307,7 +1478,10 @@ def GetSineWaveBuffers(self, NumChannels, StartFrequency, DeltaFrequency, StartA
Tsin = 1.0/fSin
NumSamples = (Tsin * SampleRate)*cycles
- tl = list(np.linspace(0, 2.0 * np.pi * cycles, s)[:-1] for s in NumSamples)
+ tl = []
+ for s in NumSamples:
+ count = max(2, int(round(float(s))))
+ tl.append(np.linspace(0, 2.0 * np.pi * cycles, count)[:-1])
signals = list(np.asarray(np.sin(t) * a, dtype=self.dtype) for t,a in zip(tl,aSin))
return tl, signals
diff --git a/amplifier.py b/amplifier.py
index 829298d..d2a160b 100644
--- a/amplifier.py
+++ b/amplifier.py
@@ -711,7 +711,7 @@ def process_output(self):
''' Get data from amplifier
and return the eeg data block
'''
- t = time.clock()
+ t = time.perf_counter()
self.eeg_data.performance_timer = 0
self.eeg_data.performance_timer_max = 0
self.recordtime = 0.0
diff --git a/display.py b/display.py
index dd28885..a78a93a 100644
--- a/display.py
+++ b/display.py
@@ -45,7 +45,7 @@
------------------------------------------------------------
'''
-class DISP_Scope(Qwt.QwtPlot, ModuleBase):
+class DISP_Scope(ModuleBase, Qwt.QwtPlot):
""" EEG signal display widget.
"""
def __init__(self, *args, **keys):
@@ -676,7 +676,8 @@ class _ScopeLegend(Qwt.QwtLegend):
def __init__(self, *args):
Qwt.QwtLegend.__init__(self, *args)
layout = self.contentsWidget().layout()
- layout.setSpacing(0)
+ if layout is not None:
+ layout.setSpacing(0)
def heightForWidth(self, width):
return 0
@@ -703,8 +704,9 @@ def layoutContents(self):
item.setFixedHeight(itemHeight)
yBottom += itemHeight
layout = self.contentsWidget().layout()
- layout.setGeometry(Qt.QRect(Qt.QPoint(0,offset),
- Qt.QPoint(visibleSize.width(), visibleSize.height() -2 * offset)))
+ if layout is not None:
+ layout.setGeometry(Qt.QRect(Qt.QPoint(0,offset),
+ Qt.QPoint(visibleSize.width(), visibleSize.height() -2 * offset)))
self.contentsWidget().resize(visibleSize.width(), visibleSize.height())
return
diff --git a/display_fallback.py b/display_fallback.py
new file mode 100644
index 0000000..749dd35
--- /dev/null
+++ b/display_fallback.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+"""Fallback display module used when the full scope widget cannot initialize."""
+
+from PyQt4 import Qt
+from PyQt4 import QtGui
+
+from modbase import ModuleBase
+
+
+class DISP_ScopeLite(ModuleBase):
+ """Minimal placeholder display pane.
+
+ Keeps the GUI usable when the Qwt/pyqtgraph-based scope cannot be created
+ in the current Qt binding/runtime.
+ """
+
+ def __init__(self, *args, **keys):
+ reason = keys.pop("reason", "")
+ ModuleBase.__init__(self, usethread=False, name="Display", **keys)
+
+ self._pane = Qt.QFrame()
+ self._pane.setObjectName("DisplayFallback")
+ self._pane.setFrameShape(Qt.QFrame.StyledPanel)
+ layout = QtGui.QVBoxLayout(self._pane)
+ layout.setContentsMargins(12, 12, 12, 12)
+
+ title = Qt.QLabel("Scope view is unavailable in this environment.")
+ title.setWordWrap(True)
+ layout.addWidget(title)
+
+ if reason:
+ detail = Qt.QLabel("Reason: %s" % (reason,))
+ detail.setWordWrap(True)
+ layout.addWidget(detail)
+
+ hint = Qt.QLabel("Acquisition and recording can continue using the other modules.")
+ hint.setWordWrap(True)
+ layout.addWidget(hint)
+ layout.addStretch(1)
+
+ def get_display_pane(self):
+ return self._pane
+
+ def process_input(self, datablock):
+ return
+
+ def process_output(self):
+ return None
diff --git a/display_simple.py b/display_simple.py
new file mode 100644
index 0000000..67bc46a
--- /dev/null
+++ b/display_simple.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""Stable pyqtgraph-based scope view used on platforms where Qwt shim is unstable."""
+
+from __future__ import annotations
+
+from PyQt4 import Qt
+import numpy as np
+import pyqtgraph as pg
+import threading
+
+from modbase import *
+
+
+class DISP_ScopeSimple(ModuleBase):
+ """Lightweight signal display.
+
+ This module focuses on stability: it plots a few channels from incoming
+ data and avoids the legacy Qwt API surface.
+ """
+
+ def __init__(self, *args, **keys):
+ ModuleBase.__init__(self, usethread=True, name="Display", **keys)
+
+ self._pane = Qt.QFrame()
+ self._pane.setObjectName("DisplaySimple")
+ self._pane.setMinimumSize(Qt.QSize(400, 200))
+ layout = Qt.QVBoxLayout(self._pane)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ self.plot = pg.PlotWidget()
+ self.plot.setBackground("w")
+ self.plot.showGrid(x=True, y=True, alpha=0.25)
+ self.plot.setLabel("bottom", "Samples")
+ self.plot.setLabel("left", "Amplitude (uV)")
+ layout.addWidget(self.plot)
+
+ self._lock = threading.Lock()
+ self._sample_rate = 500.0
+ self._max_channels = 8
+ self._history_samples = 4000
+ self._pending = None
+ self._buffer = None
+ self._curves = []
+ self._curve_colors = ["#005f73", "#0a9396", "#ee9b00", "#ca6702",
+ "#bb3e03", "#ae2012", "#9b2226", "#3a86ff"]
+ self._update_requested = False
+ self.startTimer(50)
+
+ def get_display_pane(self):
+ return self._pane
+
+ def process_update(self, params):
+ if params is not None:
+ self._sample_rate = float(getattr(params, "sample_rate", 500.0))
+ channel_count = min(len(params.channel_properties), self._max_channels)
+ with self._lock:
+ if channel_count <= 0:
+ self._buffer = np.zeros((0, self._history_samples), dtype=float)
+ else:
+ self._buffer = np.zeros((channel_count, self._history_samples), dtype=float)
+ self._pending = None
+ self._update_requested = True
+ self._ensure_curves(channel_count)
+ return params
+
+ def process_start(self):
+ with self._lock:
+ if self._buffer is not None:
+ self._buffer[:] = 0.0
+ self._pending = None
+ self._update_requested = True
+
+ def process_input(self, datablock):
+ if datablock is None:
+ return
+ if datablock.recording_mode == RecordingMode.IMPEDANCE:
+ return
+
+ data = np.asarray(datablock.eeg_channels, dtype=float)
+ if data.ndim != 2 or data.shape[1] == 0:
+ return
+ channels = min(data.shape[0], self._max_channels)
+ if channels <= 0:
+ return
+
+ with self._lock:
+ if self._buffer is None or self._buffer.shape[0] != channels:
+ self._buffer = np.zeros((channels, self._history_samples), dtype=float)
+ self._ensure_curves(channels)
+ self._pending = data[:channels].copy()
+ self._update_requested = True
+
+ def process_output(self):
+ return None
+
+ def timerEvent(self, _event):
+ if not self._update_requested:
+ return
+ with self._lock:
+ pending = self._pending
+ self._pending = None
+ buffer = self._buffer
+ self._update_requested = False
+ if buffer is None or buffer.size == 0:
+ return
+ if pending is not None and pending.size > 0:
+ n = pending.shape[1]
+ if n >= buffer.shape[1]:
+ buffer[:] = pending[:, -buffer.shape[1]:]
+ else:
+ buffer[:, :-n] = buffer[:, n:]
+ buffer[:, -n:] = pending
+ self._redraw(buffer)
+
+ def _ensure_curves(self, channel_count):
+ while len(self._curves) < channel_count:
+ idx = len(self._curves)
+ color = self._curve_colors[idx % len(self._curve_colors)]
+ curve = self.plot.plot(pen=pg.mkPen(color, width=1))
+ self._curves.append(curve)
+ while len(self._curves) > channel_count:
+ curve = self._curves.pop()
+ self.plot.removeItem(curve)
+
+ def _redraw(self, buffer):
+ x = np.arange(buffer.shape[1], dtype=float)
+ spacing = 200.0
+ for idx, curve in enumerate(self._curves):
+ y = buffer[idx] + (len(self._curves) - idx - 1) * spacing
+ curve.setData(x, y)
diff --git a/main.py b/main.py
index 419190f..a7e9a5c 100644
--- a/main.py
+++ b/main.py
@@ -54,6 +54,11 @@
@version: 1.0
'''
import sys
+import warnings
+
+# Silence compatibility-noise warnings from the Qt4-on-Qt6 shim stack.
+warnings.filterwarnings("ignore", category=RuntimeWarning)
+warnings.filterwarnings("ignore", category=DeprecationWarning)
try:
from PyQt4.Qt import QString
@@ -102,6 +107,7 @@
import collections
import re
+import platform
from optparse import OptionParser
@@ -199,6 +205,23 @@ def InstantiateModules(run_as):
@param run_as: command line option (-r, --runas) for different module configurations
@return: list with instantiated module objects
'''
+ def _make_display():
+ # The legacy Qwt/pyqtgraph shim is unstable on some macOS Qt stacks.
+ # Use a simpler stable scope by default on macOS unless explicitly disabled.
+ use_simple_scope = (platform.system() == "Darwin")
+ if use_simple_scope:
+ try:
+ from display_simple import DISP_ScopeSimple
+ return DISP_ScopeSimple(instance=0)
+ except Exception as e:
+ print("WARNING: Simple display scope unavailable (%s)"%(e))
+ try:
+ return DISP_Scope(instance=0)
+ except Exception as e:
+ from display_fallback import DISP_ScopeLite
+ print("WARNING: Display scope fallback enabled (%s)"%(e))
+ return DISP_ScopeLite(instance=0, reason=str(e))
+
# get command line arguments
if 'RC' in run_as:
# run as remote client
@@ -206,7 +229,7 @@ def InstantiateModules(run_as):
TRG_Eeg(),
FLT_Eeg(),
IMP_Display(),
- DISP_Scope(instance=0)]
+ _make_display()]
else:
# run as actiCHamp recorder
modules = [AMP_ActiChamp(),
@@ -214,11 +237,15 @@ def InstantiateModules(run_as):
TRG_Eeg(),
StorageVision(),
FLT_Eeg(),
- dc_offset(),
- RDA_Server(),
+ dc_offset()]
+ try:
+ modules.append(RDA_Server())
+ except Exception as e:
+ print("WARNING: RDA Server disabled (%s)"%(e))
+ modules.extend([
IMP_Display(),
- DISP_Scope(instance=0)
- ]
+ _make_display()
+ ])
return modules
@@ -1294,9 +1321,9 @@ def fixup(i):
return int(i)
except ValueError:
return i
- a = map(fixup, re.findall("\d+|\w+", a))
- b = map(fixup, re.findall("\d+|\w+", b))
- return cmp(a[:n], b[:n])
+ a = list(map(fixup, re.findall(r"\d+|\w+", a)))
+ b = list(map(fixup, re.findall(r"\d+|\w+", b)))
+ return (a[:n] > b[:n]) - (a[:n] < b[:n])
@@ -1376,7 +1403,8 @@ def main(args):
smoketest_ms = 1500
print("Starting PyCorder, please wait ...\n")
- setpriority(priority=4)
+ if sys.platform.startswith("win"):
+ setpriority(priority=4)
app = Qt.QApplication(args)
rc = 0
try:
@@ -1384,7 +1412,11 @@ def main(args):
except ImportError:
_resources_rc = None
if _resources_rc is not None:
- _resources_rc.qInitResources()
+ # Qt resource registration has caused startup crashes on some
+ # macOS/PySide6 environments; skip it there and use file-based
+ # resources instead.
+ if not sys.platform.startswith("darwin"):
+ _resources_rc.qInitResources()
if smoketest_requested:
QtCore.QTimer.singleShot(smoketest_ms, app.quit)
rc = app.exec_()
@@ -1402,7 +1434,8 @@ def main(args):
platform_name = ""
if hasattr(Qt.QApplication, "platformName"):
platform_name = str(Qt.QApplication.platformName()).lower()
- if platform_name not in ("offscreen", "minimal"):
+ # Avoid aggressive raise/activate on macOS due sporadic Qt crashes.
+ if platform_name not in ("offscreen", "minimal") and not sys.platform.startswith("darwin"):
win.raise_()
win.activateWindow()
QtCore.QTimer.singleShot(0, win.raise_)
diff --git a/run_pycorder.sh b/run_pycorder.sh
index ca7adf6..9b7281b 100755
--- a/run_pycorder.sh
+++ b/run_pycorder.sh
@@ -4,6 +4,36 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR"
+# Prefer an actual UTF-8 locale for Qt (not just a UTF-8-looking env var).
+needs_utf8_locale=0
+if command -v locale >/dev/null 2>&1; then
+ current_charmap="$(locale charmap 2>/dev/null || true)"
+ case "$current_charmap" in
+ *UTF-8*|*utf8*) needs_utf8_locale=0 ;;
+ *) needs_utf8_locale=1 ;;
+ esac
+else
+ needs_utf8_locale=1
+fi
+
+if [[ "$needs_utf8_locale" -eq 1 ]]; then
+ UTF8_LOCALE=""
+ if command -v locale >/dev/null 2>&1; then
+ if locale -a 2>/dev/null | grep -qi '^en_US\.UTF-8$'; then
+ UTF8_LOCALE="en_US.UTF-8"
+ elif locale -a 2>/dev/null | grep -qi '^C\.UTF-8$'; then
+ UTF8_LOCALE="C.UTF-8"
+ fi
+ fi
+ UTF8_LOCALE="${UTF8_LOCALE:-en_US.UTF-8}"
+ export LANG="$UTF8_LOCALE"
+ export LC_ALL="$UTF8_LOCALE"
+ export LC_CTYPE="$UTF8_LOCALE"
+fi
+
+# Reduce verbose Qt warnings that are not actionable for end users.
+export QT_LOGGING_RULES="${QT_LOGGING_RULES:-qt.qpa.fonts=false}"
+
if command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="python3"
elif command -v python >/dev/null 2>&1; then
diff --git a/tools/gui_dummy_measurement_check.py b/tools/gui_dummy_measurement_check.py
new file mode 100644
index 0000000..9780272
--- /dev/null
+++ b/tools/gui_dummy_measurement_check.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+"""Headless GUI measurement check using the ActiChamp dummy backend."""
+
+from __future__ import annotations
+
+import argparse
+import os
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+
+def _repo_root() -> Path:
+ return Path(__file__).resolve().parents[1]
+
+
+def _ensure_utf8_locale() -> None:
+ """Make sure Qt starts with a true UTF-8 locale."""
+ try:
+ charmap = subprocess.check_output(
+ ["locale", "charmap"], text=True, stderr=subprocess.DEVNULL
+ ).strip()
+ except Exception:
+ charmap = ""
+ if "UTF-8" in charmap.upper() or "UTF8" in charmap.upper():
+ return
+
+ candidates: list[str] = []
+ try:
+ out = subprocess.check_output(["locale", "-a"], text=True, stderr=subprocess.DEVNULL)
+ candidates = [line.strip() for line in out.splitlines() if line.strip()]
+ except Exception:
+ candidates = []
+
+ selected = "en_US.UTF-8"
+ lower = {name.lower(): name for name in candidates}
+ if "en_us.utf-8" in lower:
+ selected = lower["en_us.utf-8"]
+ elif "c.utf-8" in lower:
+ selected = lower["c.utf-8"]
+
+ os.environ["LANG"] = selected
+ os.environ["LC_ALL"] = selected
+ os.environ["LC_CTYPE"] = selected
+
+
+def _pick_sample_rate(sample_rates, target_hz: float):
+ if not sample_rates:
+ raise RuntimeError("No sample rates available on amplifier object")
+ return min(sample_rates, key=lambda x: abs(float(x["value"]) - float(target_hz)))
+
+
+def run(duration_ms: int, sample_rate_hz: float, min_effective_ratio: float) -> int:
+ _ensure_utf8_locale()
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+ os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.fonts=false")
+
+ repo_root = _repo_root()
+ if str(repo_root) not in sys.path:
+ sys.path.insert(0, str(repo_root))
+
+ from PyQt4 import Qt, QtCore
+ from actichamp_w import CHAMP_MODE_NORMAL
+
+ # main.MainWindow parses sys.argv using optparse; strip our script flags.
+ argv_backup = list(sys.argv)
+ sys.argv = [argv_backup[0]]
+ import main
+
+ app = Qt.QApplication([])
+ try:
+ win = main.MainWindow()
+ win.usageConfirmed = True
+ amp = win.topmodule
+
+ selected_rate = _pick_sample_rate(amp.sample_rates, sample_rate_hz)
+ amp.sample_rate = selected_rate
+ amp.update_receivers()
+ configured_rate = float(selected_rate["value"])
+
+ state = {
+ "ok": False,
+ "samples": 0,
+ "running": False,
+ "display_module": type(win.modules[-1]).__name__,
+ "configured_rate": configured_rate,
+ "reported_rate": 0.0,
+ "effective_rate": 0.0,
+ "min_effective_rate": configured_rate * max(0.01, float(min_effective_ratio)),
+ "elapsed_s": 0.0,
+ }
+ started_at = {"t": 0.0}
+
+ def start_measurement():
+ amp._online_mode_changed(CHAMP_MODE_NORMAL)
+ started_at["t"] = time.perf_counter()
+ QtCore.QTimer.singleShot(duration_ms, check_measurement)
+
+ def check_measurement():
+ state["samples"] = int(amp.eeg_data.sample_counter)
+ state["running"] = bool(amp.isRunning())
+ state["reported_rate"] = float(getattr(amp.eeg_data, "sample_rate", 0.0) or 0.0)
+ state["elapsed_s"] = max(1e-6, time.perf_counter() - started_at["t"])
+ state["effective_rate"] = float(state["samples"]) / state["elapsed_s"]
+ state["ok"] = (
+ state["running"]
+ and state["samples"] > 0
+ and abs(state["reported_rate"] - state["configured_rate"]) < 1e-6
+ and state["effective_rate"] >= state["min_effective_rate"]
+ )
+ amp.stop(force=True)
+ win.close()
+ app.exit(0 if state["ok"] else 2)
+
+ QtCore.QTimer.singleShot(0, start_measurement)
+ rc = app.exec_()
+ print(
+ "gui_measurement_ok=%s running=%s samples=%d configured_rate=%.1f "
+ "reported_rate=%.1f effective_rate=%.1f min_effective_rate=%.1f "
+ "elapsed_s=%.3f display=%s rc=%d"
+ % (
+ state["ok"],
+ state["running"],
+ state["samples"],
+ state["configured_rate"],
+ state["reported_rate"],
+ state["effective_rate"],
+ state["min_effective_rate"],
+ state["elapsed_s"],
+ state["display_module"],
+ rc,
+ )
+ )
+ return rc
+ finally:
+ sys.argv = argv_backup
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--duration-ms",
+ type=int,
+ default=2500,
+ help="time to keep acquisition running before validation",
+ )
+ parser.add_argument(
+ "--sample-rate-hz",
+ type=float,
+ default=1000.0,
+ help="target sampling rate to validate with dummy acquisition",
+ )
+ parser.add_argument(
+ "--min-effective-ratio",
+ type=float,
+ default=0.60,
+ help="minimum accepted effective_rate/configured_rate ratio",
+ )
+ args = parser.parse_args()
+ return run(
+ duration_ms=max(200, args.duration_ms),
+ sample_rate_hz=max(1.0, args.sample_rate_hz),
+ min_effective_ratio=max(0.01, args.min_effective_ratio),
+ )
+
+
+if __name__ == "__main__":
+ sys.exit(main())
From 45dc5fbe28c4844096515283b420a1bad5534eef Mon Sep 17 00:00:00 2001
From: yasufumi <52214705+yasufumi-nakata@users.noreply.github.com>
Date: Tue, 24 Feb 2026 20:45:58 +0900
Subject: [PATCH 3/5] Ignore macOS Finder metadata files
---
.gitignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 7e99e36..0205d62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-*.pyc
\ No newline at end of file
+*.pyc
+.DS_Store
From 17761e236c4b5b8764a2b27b15ad66143e43916c Mon Sep 17 00:00:00 2001
From: yasufumi <52214705+yasufumi-nakata@users.noreply.github.com>
Date: Wed, 25 Feb 2026 01:23:53 +0900
Subject: [PATCH 4/5] Add one-shot dummy recording E2E check and fix storage
file writes
---
.gitignore | 1 +
README.md | 12 ++
storage.py | 55 +++++---
tools/gui_dummy_recording_check.py | 215 +++++++++++++++++++++++++++++
4 files changed, 264 insertions(+), 19 deletions(-)
create mode 100644 tools/gui_dummy_recording_check.py
diff --git a/.gitignore b/.gitignore
index 0205d62..8235bc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*.pyc
.DS_Store
+outputs/
diff --git a/README.md b/README.md
index b8882f8..580a011 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,8 @@ Quick start:
- `bash ./run_pycorder.sh --smoketest`
- Automated GUI measurement check with dummy data (headless/offscreen, validates 1000 Hz by default):
- `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py`
+- Automated end-to-end check (measurement + file creation in one run):
+ - `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_recording_check.py --sample-rate-hz 1000 --duration-ms 2600`
GUI startup (recommended):
- macOS/Linux:
@@ -58,6 +60,16 @@ Validate 1000 Hz dummy acquisition (automated):
You can test other rates similarly, for example:
- `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_measurement_check.py --sample-rate-hz 2000 --duration-ms 2600`
+Validate one-shot measurement + recording file creation:
+1. Run:
+ - `QT_QPA_PLATFORM=offscreen .venv/bin/python tools/gui_dummy_recording_check.py --sample-rate-hz 1000 --duration-ms 2600`
+2. Confirm the output contains:
+ - `gui_recording_ok=True`
+ - `configured_rate=1000.0`
+ - `reported_rate=1000.0`
+3. Output files are created under:
+ - `outputs/dummy_recordings/` (`.eeg`, `.vhdr`, `.vmrk`)
+
If GUI does not start:
1. Run `bash ./run_pycorder.sh --smoketest` to verify dependency/runtime startup.
2. If another instance is still running, stop it:
diff --git a/storage.py b/storage.py
index e9d9fea..9519f69 100644
--- a/storage.py
+++ b/storage.py
@@ -117,7 +117,7 @@ def getXML(self):
@return: objectify XML element::
e.g.
- D:\EEG
+ D:\\EEG
...
'''
@@ -223,16 +223,27 @@ def _get_auto_filename(self, searchdir):
numberstring += "?"
searchdir.setNameFilters(Qt.QStringList("%s%s.eeg"%(self.default_prefix, numberstring)))
searchdir.setFilter(Qt.QDir.Files)
- flist = searchdir.entryList()
+ flist = [str(f) for f in searchdir.entryList()]
# extract numbers
- flist.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
+ flist = [f[:-4] if f.lower().endswith(".eeg") else f for f in flist]
if len(self.default_prefix) > 0:
- flist.replaceInStrings(self.default_prefix, "", Qt.Qt.CaseInsensitive)
+ prefix = self.default_prefix
+ prefix_lower = prefix.lower()
+ cleaned = []
+ for f in flist:
+ if f.lower().startswith(prefix_lower):
+ cleaned.append(f[len(prefix):])
+ else:
+ cleaned.append(f)
+ flist = cleaned
numbers = []
for f in flist:
- num,ok = f.toInt()
- if ok and (num < 10**self.default_numbersize-1):
- numbers.append(num)
+ try:
+ num = int(f)
+ if num < 10**self.default_numbersize-1:
+ numbers.append(num)
+ except Exception:
+ continue
if len(numbers) > 0:
# get the highest number
numbers.sort()
@@ -257,21 +268,30 @@ def _get_unique_filename(self, filename):
raise Exception("path '%s' does not exist"%pn)
eegdir.setFilter(Qt.QDir.Files)
eegdir.setNameFilters(Qt.QStringList(u"%s*.eeg"%(fn)))
- allfiles = eegdir.entryList()
+ allfiles = [str(f) for f in eegdir.entryList()]
eegdir.setNameFilters(Qt.QStringList(u"%s_*.eeg"%(fn)))
- numberedfiles = eegdir.entryList()
+ numberedfiles = [str(f) for f in eegdir.entryList()]
- if allfiles.count() == 0:
+ if len(allfiles) == 0:
return os.path.join(pn, filename + ".eeg")
# extract numbers
- numberedfiles.replaceInStrings(".eeg", "", Qt.Qt.CaseInsensitive)
- numberedfiles.replaceInStrings(fn+"_", "", Qt.Qt.CaseInsensitive)
+ fn_prefix = (fn + "_").lower()
+ cleaned = []
+ for f in numberedfiles:
+ name = f[:-4] if f.lower().endswith(".eeg") else f
+ if name.lower().startswith(fn_prefix):
+ cleaned.append(name[len(fn)+1:])
+ else:
+ cleaned.append(name)
+ numberedfiles = cleaned
numbers = []
for f in numberedfiles:
- num,ok = f.toInt()
- if ok:
+ try:
+ num = int(f)
numbers.append(num)
+ except Exception:
+ continue
if len(numbers) > 0:
# get the highest number
numbers.sort()
@@ -369,7 +389,7 @@ def _prepare_recording(self):
# create EEG header file
try:
- self.header_file = open(headername, "w")
+ self.header_file = open(headername, "wb")
h = u"Brain Vision Data Exchange Header File Version 1.0" + crlf
h += u"; Data created by the actiCHamp PyCorder" + crlf + crlf
@@ -465,7 +485,7 @@ def _prepare_recording(self):
# create EEG marker file
try:
- self.marker_file = open(markername, "w")
+ self.marker_file = open(markername, "wb")
h = u"Brain Vision Data Exchange Marker File, Version 1.0" + crlf
h += crlf
# common infos.
@@ -984,6 +1004,3 @@ def _showExample(self):
-
-
-
diff --git a/tools/gui_dummy_recording_check.py b/tools/gui_dummy_recording_check.py
new file mode 100644
index 0000000..eaacc0f
--- /dev/null
+++ b/tools/gui_dummy_recording_check.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+"""End-to-end GUI dummy run: start measurement, record, and verify output files."""
+
+from __future__ import annotations
+
+import argparse
+import os
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+
+def _repo_root() -> Path:
+ return Path(__file__).resolve().parents[1]
+
+
+def _ensure_utf8_locale() -> None:
+ try:
+ charmap = subprocess.check_output(
+ ["locale", "charmap"], text=True, stderr=subprocess.DEVNULL
+ ).strip()
+ except Exception:
+ charmap = ""
+ if "UTF-8" in charmap.upper() or "UTF8" in charmap.upper():
+ return
+
+ selected = "en_US.UTF-8"
+ try:
+ out = subprocess.check_output(["locale", "-a"], text=True, stderr=subprocess.DEVNULL)
+ lower = {line.strip().lower(): line.strip() for line in out.splitlines() if line.strip()}
+ if "en_us.utf-8" in lower:
+ selected = lower["en_us.utf-8"]
+ elif "c.utf-8" in lower:
+ selected = lower["c.utf-8"]
+ except Exception:
+ pass
+
+ os.environ["LANG"] = selected
+ os.environ["LC_ALL"] = selected
+ os.environ["LC_CTYPE"] = selected
+
+
+def _pick_sample_rate(sample_rates, target_hz: float):
+ if not sample_rates:
+ raise RuntimeError("No sample rates available on amplifier object")
+ return min(sample_rates, key=lambda x: abs(float(x["value"]) - float(target_hz)))
+
+
+def run(duration_ms: int, sample_rate_hz: float, output_dir: Path, base_name: str) -> int:
+ _ensure_utf8_locale()
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+ os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.fonts=false")
+
+ repo_root = _repo_root()
+ if str(repo_root) not in sys.path:
+ sys.path.insert(0, str(repo_root))
+
+ from PyQt4 import Qt, QtCore
+ from actichamp_w import CHAMP_MODE_NORMAL
+ from modbase import EventType, ModuleEvent
+
+ argv_backup = list(sys.argv)
+ sys.argv = [argv_backup[0]]
+ import main
+
+ output_dir.mkdir(parents=True, exist_ok=True)
+ app = Qt.QApplication([])
+ try:
+ win = main.MainWindow()
+ win.usageConfirmed = True
+ amp = win.topmodule
+ storage = next(
+ m for m in main.flatten(win.modules) if m.__class__.__name__ == "StorageVision"
+ )
+
+ selected_rate = _pick_sample_rate(amp.sample_rates, sample_rate_hz)
+ amp.sample_rate = selected_rate
+ amp.update_receivers()
+ storage.default_path = str(output_dir)
+
+ state = {
+ "ok": False,
+ "configured_rate": float(selected_rate["value"]),
+ "reported_rate": 0.0,
+ "samples": 0,
+ "elapsed_s": 0.0,
+ "eeg_file": "",
+ "vhdr_file": "",
+ "vmrk_file": "",
+ "eeg_bytes": 0,
+ "vhdr_bytes": 0,
+ "vmrk_bytes": 0,
+ "error": "",
+ }
+ started = {"t": 0.0}
+
+ def start_measurement():
+ try:
+ amp._online_mode_changed(CHAMP_MODE_NORMAL)
+ started["t"] = time.perf_counter()
+ QtCore.QTimer.singleShot(200, start_saving)
+ except Exception as exc:
+ state["error"] = f"start_measurement: {exc}"
+ finalize(exit_code=2)
+
+ def start_saving():
+ try:
+ storage.process_event(
+ ModuleEvent(
+ module="E2E",
+ type=EventType.COMMAND,
+ info="StartSaving",
+ cmd_value=base_name,
+ )
+ )
+ if not storage.file_name or storage.data_file is None:
+ state["error"] = "StartSaving did not open recording file"
+ finalize(exit_code=2)
+ return
+ QtCore.QTimer.singleShot(duration_ms, stop_all)
+ except Exception as exc:
+ state["error"] = f"start_saving: {exc}"
+ finalize(exit_code=2)
+
+ def stop_all():
+ try:
+ storage.process_event(
+ ModuleEvent(module="E2E", type=EventType.COMMAND, info="StopSaving")
+ )
+ if amp.isRunning():
+ amp.stop(force=True)
+ except Exception as exc:
+ state["error"] = f"stop_all: {exc}"
+ finalize(exit_code=2)
+ return
+ QtCore.QTimer.singleShot(100, lambda: finalize(exit_code=0))
+
+ def finalize(exit_code: int):
+ try:
+ state["samples"] = int(getattr(amp.eeg_data, "sample_counter", 0))
+ state["reported_rate"] = float(getattr(amp.eeg_data, "sample_rate", 0.0) or 0.0)
+ if started["t"] > 0:
+ state["elapsed_s"] = max(1e-6, time.perf_counter() - started["t"])
+ eeg_file = storage.file_name or ""
+ if eeg_file:
+ eeg_path = Path(eeg_file)
+ vhdr_path = eeg_path.with_suffix(".vhdr")
+ vmrk_path = eeg_path.with_suffix(".vmrk")
+ state["eeg_file"] = str(eeg_path)
+ state["vhdr_file"] = str(vhdr_path)
+ state["vmrk_file"] = str(vmrk_path)
+ if eeg_path.exists():
+ state["eeg_bytes"] = eeg_path.stat().st_size
+ if vhdr_path.exists():
+ state["vhdr_bytes"] = vhdr_path.stat().st_size
+ if vmrk_path.exists():
+ state["vmrk_bytes"] = vmrk_path.stat().st_size
+
+ same_rate = abs(state["configured_rate"] - state["reported_rate"]) < 1e-6
+ files_ok = (
+ state["eeg_bytes"] > 0 and state["vhdr_bytes"] > 0 and state["vmrk_bytes"] > 0
+ )
+ measured = state["samples"] > 0
+ state["ok"] = exit_code == 0 and same_rate and files_ok and measured and not state["error"]
+ finally:
+ try:
+ win.close()
+ finally:
+ app.exit(exit_code)
+
+ QtCore.QTimer.singleShot(0, start_measurement)
+ rc = app.exec_()
+ print(
+ "gui_recording_ok=%s rc=%d configured_rate=%.1f reported_rate=%.1f "
+ "samples=%d elapsed_s=%.3f eeg_bytes=%d vhdr_bytes=%d vmrk_bytes=%d "
+ "eeg_file=%s error=%s"
+ % (
+ state["ok"],
+ rc,
+ state["configured_rate"],
+ state["reported_rate"],
+ state["samples"],
+ state["elapsed_s"],
+ state["eeg_bytes"],
+ state["vhdr_bytes"],
+ state["vmrk_bytes"],
+ state["eeg_file"] or "-",
+ state["error"] or "-",
+ )
+ )
+ return 0 if state["ok"] else 2
+ finally:
+ sys.argv = argv_backup
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--duration-ms", type=int, default=2500)
+ parser.add_argument("--sample-rate-hz", type=float, default=1000.0)
+ parser.add_argument("--output-dir", type=str, default="outputs/dummy_recordings")
+ parser.add_argument("--base-name", type=str, default="dummy_e2e")
+ args = parser.parse_args()
+
+ return run(
+ duration_ms=max(200, int(args.duration_ms)),
+ sample_rate_hz=max(1.0, float(args.sample_rate_hz)),
+ output_dir=Path(args.output_dir),
+ base_name=str(args.base_name).strip() or "dummy_e2e",
+ )
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+
From 2481032cab4678bcc15da051ad9ae08b08c9cba8 Mon Sep 17 00:00:00 2001
From: yasufumi-nakata <52214705+yasufumi-nakata@users.noreply.github.com>
Date: Mon, 1 Jun 2026 04:01:06 +0900
Subject: [PATCH 5/5] Fix Python 3 tutorial syntax checks
---
syscheck.py | 4 ++--
tutorial/tut_1.py | 10 +++++-----
tutorial/tut_2.py | 5 ++---
3 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/syscheck.py b/syscheck.py
index 2f8c4e9..726e362 100644
--- a/syscheck.py
+++ b/syscheck.py
@@ -57,7 +57,7 @@ def logIt(logentry):
@param logentry: log text
'''
global logentries
- print logentry
+ print(logentry)
logentries += logentry + "\r\n"
def logHeader():
@@ -194,7 +194,7 @@ def checkRms(rms_values, rms_limit):
sd = np.std(rms_values[channels_ok])
mean = rms_values[channels_ok].mean()
mask = lambda x: ((x > mean + 3.0*sd) or (x < mean - 3.0*sd)) and x > rms_limit
- outlier = np.array([mask(val) for val in rms_values], dtype=bool)
+ outlier = np.array([mask(val) for val in rms_values], dtype=bool)
channels_ok = ~(shorted | outlier)
if num_outlier == len(outlier):
break
diff --git a/tutorial/tut_1.py b/tutorial/tut_1.py
index 8ff1522..fd39b2c 100644
--- a/tutorial/tut_1.py
+++ b/tutorial/tut_1.py
@@ -75,13 +75,13 @@ def process_update(self, params):
@return: EEG_DataBlock object
'''
# print the channel configuration to the Python console
- print "%s, process_update()"%(self._object_name) # just to see where we are
- print " Number of channels: %d, Sample Rate = %d [Hz]"%(len(params.channel_properties), params.sample_rate)
- print " Channel names:"
+ print("%s, process_update()"%(self._object_name)) # just to see where we are
+ print(" Number of channels: %d, Sample Rate = %d [Hz]"%(len(params.channel_properties), params.sample_rate))
+ print(" Channel names:")
names = " "
for channel in params.channel_properties:
names += channel.name + ", "
- print names
+ print(names)
# modify channel properties
self._modify_properties(params)
@@ -124,7 +124,7 @@ def process_event(self, event):
elif event.type == EventType.STATUS:
eventinfo += "STATUS %s, %s"%(event.info, str(event.status_field))
- print eventinfo
+ print(eventinfo)
def process_input(self, datablock):
diff --git a/tutorial/tut_2.py b/tutorial/tut_2.py
index 346e229..53f88e5 100644
--- a/tutorial/tut_2.py
+++ b/tutorial/tut_2.py
@@ -68,14 +68,14 @@ def process_update(self, params):
mask = lambda x: ("_x2" in x.name) # selection function
mask_ref = np.array([mask(ch) for ch in self.params.channel_properties], dtype=bool) # create an boolean array with results of the mask function
self.mask_index = np.nonzero(mask_ref) # create an array of TRUE indices
- print self.mask_index
+ print(self.mask_index)
# search channels with "_loop" in channel name,
# all channels will be processed within a for-loop
mask = lambda x: ("_loop" in x.name) # selection function
mask_ref = np.array([mask(ch) for ch in self.params.channel_properties], dtype=bool) # create an boolean array with results of the mask function
self.loop = (np.nonzero(mask_ref)[0].size > 0) # use for loop if any channel name contains _loop
- print self.loop
+ print(self.loop)
return self.params # don't forget to pass the configuration down to the next module
@@ -123,4 +123,3 @@ def process_output(self):
self.dataavailable = False
return self.data
-