diff --git a/.gitignore b/.gitignore index 7e99e36..8235bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -*.pyc \ No newline at end of file +*.pyc +.DS_Store +outputs/ diff --git a/PyQt4/Qt.py b/PyQt4/Qt.py new file mode 100644 index 0000000..5ea8e7e --- /dev/null +++ b/PyQt4/Qt.py @@ -0,0 +1,244 @@ +# -*- 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 + + +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', + '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..0a8b769 --- /dev/null +++ b/PyQt4/QtCore.py @@ -0,0 +1,68 @@ +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 [] + + +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/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..46db4dc --- /dev/null +++ b/PyQt4/Qwt5.py @@ -0,0 +1,239 @@ +# 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 setText(self, text): + self._text = str(text) + 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): + xTop = 2 + 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 + 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): + 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 + 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..580a011 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,86 @@ # 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` +- 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: + 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` + +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: + - `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: DC Offset pane shows the DC voltage on each electrode diff --git a/actichamp_w.py b/actichamp_w.py index 83f7e60..36f0f6e 100644 --- a/actichamp_w.py +++ b/actichamp_w.py @@ -1,1295 +1,1487 @@ -# -*- 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 + 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 + 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 + 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 + ''' + self.properties.CountEeg = 0 + 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): + ''' 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: + modules = self.getEmulationMode() + self._apply_simulation_profile(modules) + return + + # 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: + self.running = False + self.devicehandle = 0 + return + 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: + 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) + + # 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") + 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) + 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") + if self.lib is None: + return + 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 + if self.lib is None: + return self._simulate_read(eegcount, auxcount) + + # 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 + 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 + 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 + if self.lib is None: + 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 + 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 + 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 + model_channels = 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: + 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 + 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 + @param modules: number of modules to emulate, 0= no emulation + ''' + # 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() + 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)) + 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() + # 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 + 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: + 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 + 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)) + 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 + 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 + 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 + ''' + 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) + 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 self.lib is None 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 + 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): + 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], dtype=float) + else: + 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: + aSin = np.zeros(NumChannels) + aSin[:] = StartAmplitude + + Tsin = 1.0/fSin + NumSamples = (Tsin * SampleRate)*cycles + 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 007b640..d2a160b 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.perf_counter() + 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..a78a93a 100644 --- a/display.py +++ b/display.py @@ -1,940 +1,993 @@ -# -*- 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(ModuleBase, Qwt.QwtPlot): + """ 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() + if layout is not None: + 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() + 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 + + +''' +------------------------------------------------------------ +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/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/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..a7e9a5c 100644 --- a/main.py +++ b/main.py @@ -1,1354 +1,1476 @@ -# -*- 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 +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 +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 +import platform +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 + ''' + 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 + modules = [RDA_Client(), + TRG_Eeg(), + FLT_Eeg(), + IMP_Display(), + _make_display()] + else: + # run as actiCHamp recorder + modules = [AMP_ActiChamp(), + MNT_Recording(), + TRG_Eeg(), + StorageVision(), + FLT_Eeg(), + dc_offset()] + try: + modules.append(RDA_Server()) + except Exception as e: + print("WARNING: RDA Server disabled (%s)"%(e)) + modules.extend([ + IMP_Display(), + _make_display() + ]) + 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 = 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]) + + + +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") + if sys.platform.startswith("win"): + 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: + # 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_() + 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() + # 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_) + 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..9b7281b --- /dev/null +++ b/run_pycorder.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +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 + 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..9519f69 100644 --- a/storage.py +++ b/storage.py @@ -1,1009 +1,1006 @@ -# -*- 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 = [str(f) for f in searchdir.entryList()] + # extract numbers + flist = [f[:-4] if f.lower().endswith(".eeg") else f for f in flist] + if len(self.default_prefix) > 0: + 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: + 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() + 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 = [str(f) for f in eegdir.entryList()] + eegdir.setNameFilters(Qt.QStringList(u"%s_*.eeg"%(fn))) + numberedfiles = [str(f) for f in eegdir.entryList()] + + if len(allfiles) == 0: + return os.path.join(pn, filename + ".eeg") + + # extract numbers + 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: + try: + num = int(f) + numbers.append(num) + except Exception: + continue + 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, "wb") + 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, "wb") + 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..726e362 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/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()) 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()) + 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_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 4656de1..53f88e5 100644 --- a/tutorial/tut_2.py +++ b/tutorial/tut_2.py @@ -1,126 +1,125 @@ -# -*- 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 + + +