diff --git a/dump_grabber/dump_grabber/index.html b/dump_grabber/dump_grabber/index.html
new file mode 100644
index 0000000..df3af71
--- /dev/null
+++ b/dump_grabber/dump_grabber/index.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dump_grabber/dump_grabber/main.py b/dump_grabber/dump_grabber/main.py
index 06d0701..535cab3 100644
--- a/dump_grabber/dump_grabber/main.py
+++ b/dump_grabber/dump_grabber/main.py
@@ -23,8 +23,9 @@ from __future__ import absolute_import
import os
import os.path
import re
+import signal
import sys
-
+from collections import deque
from datetime import datetime
from chaosc.argparser_groups import *
from chaosc.lib import logger, resolve_host
@@ -33,7 +34,8 @@ from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
from PyQt4.QtNetwork import QTcpServer, QTcpSocket, QUdpSocket, QHostAddress
from dump_grabber.dump_grabber_ui import Ui_MainWindow
-from psylib.mjpeg_streaming_server import MjpegStreamingServer
+from psylib.mjpeg_streaming_server import *
+from psylib.psyqt_base import PsyQtChaoscClientBase
try:
from chaosc.c_osc_lib import OSCMessage, decode_osc
@@ -42,62 +44,17 @@ except ImportError as e:
app = QtGui.QApplication([])
-class TextStorage(object):
- def __init__(self, columns):
- super(TextStorage, self).__init__()
+class ExclusiveTextStorage(object):
+ def __init__(self, columns, default_font, column_width, line_height, scene):
self.column_count = columns
self.colors = (QtCore.Qt.red, QtCore.Qt.green, QtGui.QColor(46, 100, 254))
-
- def init_columns(self):
- raise NotImplementedError()
-
- def add_text(self, column, text):
- raise NotImplementedError()
-
-
-class ColumnTextStorage(TextStorage):
- def __init__(self, columns, default_font, column_width, line_height, scene):
- super(ColumnTextStorage, self).__init__(columns)
- self.columns = list()
- self.default_font = default_font
- self.column_width = column_width
- self.line_height = line_height
- self.graphics_scene = scene
- self.num_lines, self.offset = divmod(768, self.line_height)
-
- def init_columns(self):
- for x in range(self.column_count):
- column = list()
- color = self.colors[x]
- for y in range(self.num_lines):
- text_item = self.graphics_scene.addSimpleText("%d:%d" % (x, y), self.default_font)
- text_item.setBrush(color)
- text_item.setPos(x * self.column_width, y * self.line_height)
- column.append(text_item)
- self.columns.append(column)
-
- def add_text(self, column, text):
- text_item = self.graphics_scene.addSimpleText(text, self.default_font)
- color = self.colors[column]
- text_item.setBrush(color)
-
- old_item = self.columns[column].pop(0)
- self.graphics_scene.removeItem(old_item)
- self.columns[column].append(text_item)
- for iy, text_item in enumerate(self.columns[column]):
- text_item.setPos(column * self.column_width, iy * self.line_height)
-
-
-class ExclusiveTextStorage(TextStorage):
- def __init__(self, columns, default_font, column_width, line_height, scene):
- super(ExclusiveTextStorage, self).__init__(columns)
- self.column_count = columns
- self.lines = list()
+ self.lines = deque()
self.default_font = default_font
self.column_width = column_width
self.line_height = line_height
self.graphics_scene = scene
self.num_lines, self.offset = divmod(576, self.line_height)
+ self.data = deque()
def init_columns(self):
color = self.colors[0]
@@ -107,26 +64,38 @@ class ExclusiveTextStorage(TextStorage):
text_item.setPos(0, y * self.line_height)
self.lines.append(text_item)
- def add_text(self, column, text):
+ def __add_text(self, column, text):
text_item = self.graphics_scene.addSimpleText(text, self.default_font)
text_item.setX(column * self.column_width)
color = self.colors[column]
text_item.setBrush(color)
- old_item = self.lines.pop(0)
+ old_item = self.lines.popleft()
self.graphics_scene.removeItem(old_item)
self.lines.append(text_item)
+
+ def finish(self):
+ while 1:
+ try:
+ column, text = self.data.popleft()
+ self.__add_text(column, text)
+ except IndexError, msg:
+ break
+
+
for iy, text_item in enumerate(self.lines):
text_item.setY(iy * self.line_height)
+ def add_text(self, column, text):
+ self.data.append((column, text))
-
-
-class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
- def __init__(self, args, parent=None, columns=3, column_exclusive=False):
- super(MainWindow, self).__init__(parent)
+class MainWindow(QtGui.QMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtChaoscClientBase):
+ def __init__(self, args, parent=None):
self.args = args
+ #PsyQtChaoscClientBase.__init__(self)
+ super(MainWindow, self).__init__()
+ self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setupUi(self)
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
@@ -140,20 +109,16 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.graphics_view.setScene(self.graphics_scene)
self.default_font = QtGui.QFont("Monospace", 14)
self.default_font.setStyleHint(QtGui.QFont.Monospace)
- self.default_font.setBold(True)
+ #self.default_font.setBold(True)
self.graphics_scene.setFont(self.default_font)
self.font_metrics = QtGui.QFontMetrics(self.default_font)
self.line_height = self.font_metrics.height()
+ columns = 3
self.column_width = 775 / columns
self.text_storage = ExclusiveTextStorage(columns, self.default_font, self.column_width, self.line_height, self.graphics_scene)
self.text_storage.init_columns()
- self.osc_sock = QUdpSocket(self)
- logger.info("osc bind localhost %d", args.client_port)
- self.osc_sock.bind(QHostAddress("127.0.0.1"), args.client_port)
- self.osc_sock.readyRead.connect(self.got_message)
- self.osc_sock.error.connect(self.handle_osc_error)
msg = OSCMessage("/subscribe")
msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(args.client_port, "i")
@@ -165,6 +130,9 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.regex = re.compile("^/(uwe|merle|bjoern)/(.*?)$")
+ def pubdir(self):
+ return os.path.dirname(os.path.abspath(__file__))
+
def closeEvent(self, event):
msg = OSCMessage("/unsubscribe")
msg.appendTypedArg("localhost", "s")
@@ -179,6 +147,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.text_storage.add_text(column, text)
def render_image(self):
+ self.text_storage.finish()
image = QtGui.QImage(768, 576, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(QtCore.Qt.black)
painter = QtGui.QPainter(image)
@@ -191,7 +160,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
painter.end()
buf = QBuffer()
buf.open(QIODevice.WriteOnly)
- image.save(buf, "JPG", 80)
+ image.save(buf, "JPG", 85)
image_data = buf.data()
return image_data
@@ -207,6 +176,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
except AttributeError:
pass
else:
+ if text == "temperatur":
+ text += "e"
if actor == "merle":
self.add_text(0, "%s = %s" % (text, ", ".join([str(i) for i in args])))
elif actor == "uwe":
@@ -229,8 +200,11 @@ def main():
args = arg_parser.finalize()
http_host, http_port = resolve_host(args.http_host, args.http_port, args.address_family)
+ args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
+
window = MainWindow(args)
- #window.show()
+ sys.excepthook = window.sigint_handler
+ signal.signal(signal.SIGTERM, window.sigterm_handler)
app.exec_()
diff --git a/ekgplotter/ekgplotter/index.html b/ekgplotter/ekgplotter/index.html
index 3f46bbb..81a9f75 100644
--- a/ekgplotter/ekgplotter/index.html
+++ b/ekgplotter/ekgplotter/index.html
@@ -1,7 +1,7 @@
-
+
diff --git a/ekgplotter/ekgplotter/main_qt.py b/ekgplotter/ekgplotter/main_qt.py
index bec7536..ce03886 100644
--- a/ekgplotter/ekgplotter/main_qt.py
+++ b/ekgplotter/ekgplotter/main_qt.py
@@ -24,79 +24,82 @@
from __future__ import absolute_import
-from chaosc.argparser_groups import *
-from chaosc.lib import logger, resolve_host
-from datetime import datetime
-from operator import attrgetter
-from PyQt4 import QtGui, QtCore
-from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
-from PyQt4.QtNetwork import QTcpServer, QTcpSocket, QUdpSocket, QHostAddress
-from PyQt4.QtGui import QPixmap
-
-import logging
-import numpy as np
+import atexit
+import random
import os.path
+import re
+import signal
+import sys
+import exceptions
+
+from PyQt4 import QtCore, QtGui
+from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
+from PyQt4.QtNetwork import QUdpSocket, QHostAddress
+
+import numpy as np
+
import pyqtgraph as pg
from pyqtgraph.widgets.PlotWidget import PlotWidget
-import Queue
-import re
-import select
-import socket
-import threading
-import time
-
-from psylib.mjpeg_streaming_server import MjpegStreamingServer
+from chaosc.argparser_groups import ArgParser
+from chaosc.lib import logger, resolve_host
+from psylib.mjpeg_streaming_server import *
+from psylib.psyqt_base import PsyQtChaoscClientBase
try:
from chaosc.c_osc_lib import OSCMessage, decode_osc
except ImportError as e:
- logging.exception(e)
from chaosc.osc_lib import OSCMessage, decode_osc
+qtapp = QtGui.QApplication([])
-def get_steps(pulse_rate, rate):
- beat_length = 60. / pulse_rate
- steps_pre = int(beat_length / rate) + 1
+
+def get_steps(pulse, delta_ms):
+ beat_length = 60000. / pulse
+ steps_pre = int(beat_length / delta_ms) + 1
used_sleep_time = beat_length / steps_pre
steps = int(beat_length / used_sleep_time)
return steps, used_sleep_time
class Generator(object):
- def __init__(self, pulse=92, delta=0.08):
+ def __init__(self, pulse=92, delta=80):
self.count = 0
- self.pulse = 92
+ self.pulse = random.randint(85, 105)
self.delta = delta
- self.steps = get_steps(self.pulse, delta / 2)
+ self.multiplier = 4
+ self.steps, _ = get_steps(self.pulse, delta / self.multiplier)
def __call__(self):
while 1:
- value = random.randint(0, steps)
- if self.count < int(steps / 100. * 20):
- value = random.randint(0,20)
- elif self.count < int(steps / 100. * 30):
- value = random.randint(20, 30)
- elif self.count < int(steps / 100. * 40):
- value = random.randint(30,100)
- elif self.count < int(steps / 2.):
- value = random.randint(100,200)
- elif self.count == int(steps / 2.):
+ if self.count < int(self.steps / 100. * 30):
+ value = random.randint(30, 35)
+ elif self.count == int(self.steps / 100. * 30):
+ value = random.randint(random.randint(50,60), random.randint(60, 70))
+ elif self.count < int(self.steps / 100. * 45):
+ value = random.randint(30, 35)
+ elif self.count < int(self.steps / 2.):
+ value = random.randint(0, 15)
+ elif self.count == int(self.steps / 2.):
value = 255
- elif self.count < int(steps / 100. * 60):
- value = random.randint(100, 200)
- elif self.count < int(steps / 100. * 70):
- value = random.randint(50, 100)
- elif self.count < int(steps / 100. * 80):
- value = random.randint(20, 50)
- elif self.count <= steps:
- value = random.randint(0,20)
- elif self.count >= steps:
+ elif self.count < int(self.steps / 100. * 60):
+ value = random.randint(random.randint(25,30), random.randint(30, 35))
+ elif self.count < int(self.steps / 100. * 70):
+ value = random.randint(random.randint(10,25), random.randint(25, 30))
+ elif self.count < self.steps:
+ value = random.randint(random.randint(15,25), random.randint(25, 30))
+ else:
self.count = 0
+ value = 30
self.count += 1
yield value
+ def set_pulse(self, pulse):
+ self.pulse = pulse
+ self.steps, _ = get_steps(pulse, self.delta / self.multiplier)
+
+
def retrigger(self):
self.count = self.steps / 2
@@ -115,11 +118,14 @@ class Actor(object):
self.data = np.array([self.offset] * num_data)
self.head = 0
self.pre_head = 0
- self.plotItem = pg.PlotCurveItem(pen=pg.mkPen(color, width=3), name=name)
+ self.plotItem = pg.PlotCurveItem(pen=pg.mkPen(color, width=3), width=4, name=name)
+ #self.plotItem.setShadowPen(pg.mkPen("w", width=5))
self.plotPoint = pg.ScatterPlotItem(pen=pg.mkPen("w", width=5), brush=pg.mkBrush(color), size=5)
+ self.osci = None
+ self.osci_obj = None
def __str__(self):
- return "" % (self.name, self.active, self.head)
+ return "" % (self.name, self.head)
__repr__ = __str__
@@ -143,29 +149,20 @@ class Actor(object):
def render(self):
self.plotItem.setData(y=self.data, clear=True)
- self.plotPoint.setData(x=[self.pre_head], y = [self.data[self.pre_head]])
+ self.plotPoint.setData(x=[self.pre_head], y=[self.data[self.pre_head]])
-class EkgPlotWidget(PlotWidget):
+class EkgPlotWidget(PlotWidget, MjpegStreamingConsumerInterface, PsyQtChaoscClientBase):
def __init__(self, args, parent=None):
- super(EkgPlotWidget, self).__init__(parent)
self.args = args
+ PsyQtChaoscClientBase.__init__(self)
+ super(EkgPlotWidget, self).__init__()
+ self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
- self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
+ self.fps = 12.5
+ self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self, self.fps)
self.http_server.listen(port=args.http_port)
- self.osc_sock = QUdpSocket(self)
- logger.info("osc bind localhost %d", args.client_port)
- self.osc_sock.bind(QHostAddress("127.0.0.1"), args.client_port)
- self.osc_sock.readyRead.connect(self.got_message)
- self.osc_sock.error.connect(self.handle_osc_error)
- msg = OSCMessage("/subscribe")
- msg.appendTypedArg("localhost", "s")
- msg.appendTypedArg(args.client_port, "i")
- msg.appendTypedArg(self.args.authenticate, "s")
- if self.args.subscriber_label is not None:
- msg.appendTypedArg(self.args.subscriber_label, "s")
- self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress("127.0.0.1"), 7110)
self.num_data = 100
self.hide()
@@ -173,8 +170,6 @@ class EkgPlotWidget(PlotWidget):
self.setYRange(0, 255)
self.setXRange(0, self.num_data)
self.resize(768, 576)
-
-
colors = ["r", "g", "b"]
ba = self.getAxis("bottom")
@@ -186,10 +181,9 @@ class EkgPlotWidget(PlotWidget):
self.active_actors = list()
self.actors = dict()
- self.lengths1 = [0]
self.max_value = 255
- actor_names = ["merle", "uwe", "bjoern" ]
+ actor_names = ["merle", "uwe", "bjoern"]
self.max_actors = len(actor_names)
self.actor_height = self.max_value / self.max_actors
@@ -197,11 +191,10 @@ class EkgPlotWidget(PlotWidget):
self.add_actor(actor_name, self.num_data, color, ix, self.max_actors, self.actor_height)
self.set_positions()
+ self.heartbeat_regex = re.compile("^/(.*?)/heartbeat$")
- self.ekg_regex = re.compile("^/(.*?)/ekg$")
- self.ctl_regex = re.compile("^/plot/(.*?)$")
- self.updated_actors = set()
- self.new_round()
+ def pubdir(self):
+ return os.path.dirname(os.path.abspath(__file__))
def add_actor(self, actor_name, num_data, color, ix, max_actors, actor_height):
@@ -210,7 +203,8 @@ class EkgPlotWidget(PlotWidget):
self.addItem(actor_obj.plotItem)
self.addItem(actor_obj.plotPoint)
self.active_actors.append(actor_obj)
-
+ actor_obj.osci_obj = Generator(delta=self.http_server.timer_delta)
+ actor_obj.osci = actor_obj.osci_obj()
def set_positions(self):
for ix, actor_obj in enumerate(self.active_actors):
@@ -220,51 +214,24 @@ class EkgPlotWidget(PlotWidget):
def active_actor_count(self):
return self.max_actors
- def new_round(self):
- for ix, actor in enumerate(self.active_actors):
- actor.updated = 0
+ def update(self, osc_address, args):
- def update_missing_actors(self):
- liste = sorted(self.active_actors, key=attrgetter("updated"))
- max_values = liste[-1].updated
- if max_values == 0:
- # handling no signal
- for actor in self.active_actors:
- actor.add_value(0)
- return
- for ix, actor in enumerate(self.active_actors):
- diff = max_values - actor.updated
- if diff > 0:
- for i in range(diff):
- actor.add_value(0)
-
-
- def update(self, osc_address, value):
-
- res = self.ekg_regex.match(osc_address)
+ res = self.heartbeat_regex.match(osc_address)
if res:
actor_name = res.group(1)
actor_obj = self.actors[actor_name]
- actor_obj.add_value(value)
+ #logger.info("actor: %r, %r", actor_name, args)
+ if args[0] == 1:
+ actor_obj.osci_obj.retrigger()
+ actor_obj.osci_obj.set_pulse(args[1])
- def render(self):
- for ix, actor in enumerate(self.active_actors):
- actor.render()
-
- def closeEvent(self, event):
- msg = OSCMessage("/unsubscribe")
- msg.appendTypedArg("localhost", "s")
- msg.appendTypedArg(self.args.client_port, "i")
- msg.appendTypedArg(self.args.authenticate, "s")
- self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress("127.0.0.1"), 7110)
-
- def handle_osc_error(self, error):
- logger.info("osc socket error %d", error)
-
def render_image(self):
- self.update_missing_actors()
- self.render()
+ for actor_obj in self.active_actors:
+ osc = actor_obj.osci
+ for i in range(actor_obj.osci_obj.multiplier):
+ actor_obj.add_value(osc.next())
+ actor_obj.render()
exporter = pg.exporters.ImageExporter.ImageExporter(self.plotItem)
exporter.parameters()['width'] = 768
img = exporter.export(toBytes=True)
@@ -272,7 +239,6 @@ class EkgPlotWidget(PlotWidget):
buf.open(QIODevice.WriteOnly)
img.save(buf, "JPG", 75)
JpegData = buf.data()
- self.new_round()
return JpegData
def got_message(self):
@@ -282,10 +248,8 @@ class EkgPlotWidget(PlotWidget):
osc_address, typetags, args = decode_osc(data, 0, len(data))
except Exception, e:
logger.exception(e)
- return
else:
- self.update(osc_address, args[0])
- return True
+ self.update(osc_address, args)
@@ -293,18 +257,20 @@ def main():
arg_parser = ArgParser("ekgplotter")
arg_parser.add_global_group()
client_group = arg_parser.add_client_group()
- arg_parser.add_argument(client_group, '-x', "--http_host", default="::",
- help='my host, defaults to "::"')
- arg_parser.add_argument(client_group, '-X', "--http_port", default=9000,
- type=int, help='my port, defaults to 9000')
+ arg_parser.add_argument(client_group, '-x', "--http_host", default='::',
+ help='my host, defaults to "::"')
+ arg_parser.add_argument(client_group, '-X', '--http_port', default=9000,
+ type=int, help='my port, defaults to 9000')
arg_parser.add_chaosc_group()
arg_parser.add_subscriber_group()
args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family)
+ args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
- qtapp = QtGui.QApplication([])
- widget = EkgPlotWidget(args)
+ window = EkgPlotWidget(args)
+ sys.excepthook = window.sigint_handler
+ signal.signal(signal.SIGTERM, window.sigterm_handler)
qtapp.exec_()
diff --git a/psylib/psylib/mjpeg_streaming_server.py b/psylib/psylib/mjpeg_streaming_server.py
index c36a9df..9cadd99 100644
--- a/psylib/psylib/mjpeg_streaming_server.py
+++ b/psylib/psylib/mjpeg_streaming_server.py
@@ -1,20 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-# This file is part of chaosc and psychosis
+# This file is part of chaosc/psylib package
#
-# chaosc/psychosis is free software: you can redistribute it and/or modify
+# chaosc/psylib 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.
#
-# chaosc/psychosis is distributed in the hope that it will be useful,
+# chaosc/psylib 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 chaosc/psychosis. If not, see .
+# along with chaosc/psylib. If not, see .
#
# Copyright (C) 2014 Stefan Kögl
@@ -23,27 +23,47 @@ from __future__ import absolute_import
import os
import os.path
import re
-import sys
-from datetime import datetime
-from chaosc.argparser_groups import *
-from chaosc.lib import logger, resolve_host
-from PyQt4 import QtCore, QtGui
-from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
+from chaosc.lib import logger
+from PyQt4 import QtCore
+from PyQt4.QtCore import QByteArray
from PyQt4.QtNetwork import QTcpServer, QTcpSocket
+__all__ = ["MjpegStreamingConsumerInterface", "MjpegStreamingServer"]
+
+class MjpegStreamingConsumerInterface(object):
+ def pubdir(self):
+ """ returns the directory, from where your static files should be served
+
+ fast and dirty implementation e.g:
+ return os.path.dirname(os.path.abspath(__file__))
+ """
+
+ raise NotImplementedError()
+
+ def render_image(self):
+ """returns a QByteArray with the binary date of a jpg image
+
+ this method should implement the actual window/widget grabbing"""
+
+ raise NotImplementedError()
+
class MjpegStreamingServer(QTcpServer):
- def __init__(self, server_address, parent=None):
+ def __init__(self, server_address, parent=None, fps=12.5):
super(MjpegStreamingServer, self).__init__(parent)
self.server_address = server_address
self.newConnection.connect(self.new_connection)
+ assert isinstance(parent, MjpegStreamingConsumerInterface)
self.widget = parent
+
self.sockets = list()
self.img_data = None
+ self.fps = fps
+ self.timer_delta = 1000 / fps
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.send_image)
- self.timer.start(80)
+ self.timer.start(self.timer_delta)
self.stream_clients = list()
self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$")
self.host_regex = re.compile("^Host: (\w+?):(\d+)$")
@@ -51,8 +71,8 @@ class MjpegStreamingServer(QTcpServer):
def handle_request(self):
sock = self.sender()
- logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort())
sock_id = id(sock)
+ logger.info("handle_request: sock_id=%r", sock_id)
if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState):
logger.info("connection closed")
self.sockets.remove(sock)
@@ -79,14 +99,26 @@ class MjpegStreamingServer(QTcpServer):
return
else:
if ext == "ico":
- directory = os.path.dirname(os.path.abspath(__file__))
- data = open(os.path.join(directory, "favicon.ico"), "rb").read()
- sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data))
+ directory = self.widget.pubdir()
+ try:
+ data = open(os.path.join(directory, "favicon.ico"), "rb").read()
+ except IOError:
+ logger.error("request not found/handled - sending 404 not found")
+ sock.write("HTTP/1.1 404 Not Found\r\n")
+ return
+ else:
+ sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data))
elif ext == "html":
- directory = os.path.dirname(os.path.abspath(__file__))
- data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id
- self.html_map[sock_id] = None
- sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: text/html;encoding: utf-8\r\n\r\n%s' % data))
+ directory = self.widget.pubdir()
+ try:
+ data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id
+ self.html_map[sock_id] = None
+ except IOError:
+ logger.error("request not found/handled - sending 404 not found")
+ sock.write("HTTP/1.1 404 Not Found\r\n")
+ return
+ else:
+ sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: text/html;encoding: utf-8\r\n\r\n%s' % data))
elif ext == "mjpeg":
try:
_, html_sock_id = resource.split("_", 1)
@@ -117,31 +149,40 @@ class MjpegStreamingServer(QTcpServer):
sock.disconnected.disconnect(self.slot_remove_connection)
sock.close()
sock.deleteLater()
- self.sockets.remove(sock)
- logger.info("connection removed: sock=%r, sock_id=%r", sock, sock_id)
+ try:
+ self.sockets.remove(sock)
+ logger.info("connection %r removed", sock_id)
+ except ValueError, msg:
+ logger.info("connection %r was not stored?", sock_id)
+
try:
self.stream_clients.remove(sock)
except ValueError:
- pass
+ logger.info("connection %r was not streaming", sock_id)
+
try:
stream_client = self.html_map.pop(sock_id)
except KeyError:
- logger.info("socket has no child socket")
+ logger.info("socket %r has no child socket", sock_id)
else:
- stream_client.close()
try:
- self.stream_clients.remove(stream_client)
- logger.info("removed stream_client=%r", id(stream_client))
- except ValueError:
- pass
+ stream_client.close()
+ except AttributeError, msg:
+ logger.info("no stream client")
+ else:
+ try:
+ self.stream_clients.remove(stream_client)
+ logger.info("child connection %r removed from streaming", id(stream_client))
+ except ValueError:
+ pass
- try:
- self.sockets.remove(stream_client)
- logger.info("removed child sock_id=%r", id(stream_client))
- except ValueError:
- pass
+ try:
+ self.sockets.remove(stream_client)
+ logger.info("child connection %r removed from storage", id(stream_client))
+ except ValueError:
+ pass
def send_image(self):
diff --git a/psylib/psylib/psyqt_base.py b/psylib/psylib/psyqt_base.py
new file mode 100644
index 0000000..4d5506e
--- /dev/null
+++ b/psylib/psylib/psyqt_base.py
@@ -0,0 +1,105 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# This file is part of chaosc/psylib package
+#
+# chaosc/psylib 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.
+#
+# chaosc/psylib 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 chaosc/psylib. If not, see .
+#
+# Copyright (C) 2014 Stefan Kögl
+
+from __future__ import absolute_import
+
+import sys
+
+from PyQt4 import QtCore, QtGui
+from PyQt4.QtCore import QBuffer, QByteArray
+from PyQt4.QtNetwork import QUdpSocket, QHostAddress
+
+
+from chaosc.lib import logger
+
+try:
+ from chaosc.c_osc_lib import OSCMessage, decode_osc
+except ImportError as e:
+ from chaosc.osc_lib import OSCMessage, decode_osc
+
+
+class PsyQtClientBase(QtCore.QObject):
+
+ def __init__(self):
+ super(PsyQtClientBase, self).__init__()
+ # periodically trap into python interpreter domain to catch signals etc
+ timer = QtCore.QTimer()
+ timer.start(2000)
+ timer.timeout.connect(lambda: None)
+
+ def sigint_handler(self, ex_cls, ex, traceback):
+ """Handler for the SIGINT signal."""
+ if ex_cls == KeyboardInterrupt:
+ logger.info("found KeyboardInterrupt")
+ QtGui.QApplication.exit()
+ else:
+ logger.critical(''.join(traceback.format_tb(tb)))
+ logger.critical('{0}: {1}'.format(ex_cls, ex))
+
+class PsyQtChaoscClientBase(PsyQtClientBase):
+
+ def __init__(self):
+ super(PsyQtChaoscClientBase, self).__init__()
+ self.osc_sock = QUdpSocket(self)
+ logger.info("osc bind localhost %d", self.args.client_port)
+ self.osc_sock.bind(QHostAddress(self.args.client_host), self.args.client_port)
+ self.osc_sock.readyRead.connect(self.got_message)
+ self.osc_sock.error.connect(self.handle_osc_error)
+ self.subscribe()
+
+ def sigint_handler(self, ex_cls, ex, traceback):
+ """Handler for the SIGINT signal."""
+ if ex_cls == KeyboardInterrupt:
+ logger.info("found KeyboardInterrupt")
+ self.unsubscribe()
+ QtGui.QApplication.exit()
+ else:
+ logger.critical(''.join(traceback.format_tb(traceback)))
+ logger.critical('{0}: {1}'.format(ex_cls, ex))
+
+ def sigterm_handler(self, *args):
+ self.unsubscribe()
+ QtGui.QApplication.exit()
+
+ def subscribe(self):
+ logger.info("subscribe")
+ msg = OSCMessage("/subscribe")
+ msg.appendTypedArg("localhost", "s")
+ msg.appendTypedArg(self.args.client_port, "i")
+ msg.appendTypedArg(self.args.authenticate, "s")
+ if self.args.subscriber_label is not None:
+ msg.appendTypedArg(self.args.subscriber_label, "s")
+ self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress(self.args.chaosc_host), self.args.chaosc_port)
+
+ def unsubscribe(self):
+ logger.info("unsubscribe")
+ msg = OSCMessage("/unsubscribe")
+ msg.appendTypedArg("localhost", "s")
+ msg.appendTypedArg(self.args.client_port, "i")
+ msg.appendTypedArg(self.args.authenticate, "s")
+ self.osc_sock.writeDatagram(QByteArray(msg.encode_osc()), QHostAddress(self.args.chaosc_host), self.args.chaosc_port)
+
+ def handle_osc_error(self, error):
+ logger.info("osc socket error %d", error)
+
+ def closeEvent(self, event):
+ logger.info("closeEvent", event)
+ self.unsubscribe()
+ event.accept()
diff --git a/texter/texter/main.py b/texter/texter/main.py
index de4d46a..923642c 100644
--- a/texter/texter/main.py
+++ b/texter/texter/main.py
@@ -24,6 +24,7 @@ from __future__ import absolute_import
import cPickle
import os.path
import re
+import sys
from PyQt4 import QtCore, QtGui
@@ -39,11 +40,14 @@ from PyQt4.QtNetwork import QTcpServer, QTcpSocket
from chaosc.argparser_groups import ArgParser
from chaosc.lib import resolve_host, logger
+from psylib.mjpeg_streaming_server import *
+from psylib.psyqt_base import PsyQtClientBase
+
from texter.texter_ui import Ui_MainWindow, _fromUtf8
from texter.edit_dialog_ui import Ui_EditDialog
from texter.text_model import TextModel
-app = QtGui.QApplication([])
+qtapp = QtGui.QApplication([])
# NOTE: if the QIcon.fromTheme method does not find any icons, you can use
@@ -54,144 +58,6 @@ app = QtGui.QApplication([])
def get_preview_text(text):
return re.sub(" +", " ", text.replace("\n", " ")).strip()[:20]
-class MjpegStreamingServer(QTcpServer):
-
- def __init__(self, server_address, parent=None):
- super(MjpegStreamingServer, self).__init__(parent)
- self.server_address = server_address
- self.newConnection.connect(self.start_streaming)
- self.widget = parent.live_text
- self.win_id = self.widget.winId()
- self.sockets = list()
- self.img_data = None
- self.timer = QtCore.QTimer()
- self.timer.timeout.connect(self.render_image)
- self.timer.start(80)
- self.stream_clients = list()
- self.regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$")
- self.html_map = dict()
- self.coords = parent.live_text_rect()
-
- def handle_request(self):
- print "foo"
- sock = self.sender()
- logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort())
- sock_id = id(sock)
- if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState):
- logger.info("connection closed")
- self.sockets.remove(sock)
- sock.deleteLater()
- return
-
- client_data = str(sock.readAll())
- logger.info("request %r", client_data)
- line = client_data.split("\r\n")[0]
- logger.info("first line: %r", line)
- try:
- resource, ext, http_version = self.regex.match(line).groups()
- logger.info("resource=%r, ext=%r, http_version=%r", resource, ext, http_version)
- except AttributeError:
- logger.info("no matching request - sending 404 not found")
- sock.write("HTTP/1.1 404 Not Found\r\n")
- else:
- if ext == "ico":
- directory = os.path.dirname(os.path.abspath(__file__))
- data = open(os.path.join(directory, "favicon.ico"), "rb").read()
- sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data))
- elif ext == "html":
- directory = os.path.dirname(os.path.abspath(__file__))
- data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id
- self.html_map[sock_id] = None
- sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: text/html;encoding: utf-8\r\n\r\n%s' % data))
- #sock.close()
- elif ext == "mjpeg":
- try:
- _, html_sock_id = resource.split("_", 1)
- html_sock_id = int(html_sock_id)
- except ValueError:
- html_sock_id = None
-
- if sock not in self.stream_clients:
- logger.info("starting streaming...")
- if html_sock_id is not None:
- self.html_map[html_sock_id] = sock
- self.stream_clients.append(sock)
- sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: multipart/x-mixed-replace; boundary=--2342\r\n\r\n'))
- else:
- logger.error("request not found/handled - sending 404 not found")
- sock.write("HTTP/1.1 404 Not Found\r\n")
-
-
- def remove_stream_client(self):
- try:
- sock = self.sender()
- except RuntimeError:
- return
- sock_id = id(sock)
- logger.info("remove_stream_client: sock=%r, sock_id=%r", sock, sock_id)
- if sock.state() == QTcpSocket.UnconnectedState:
- sock.disconnected.disconnect(self.remove_stream_client)
- self.sockets.remove(sock)
- logger.info("removed sock_id=%r", sock_id)
- sock.close()
- try:
- self.stream_clients.remove(sock)
- except ValueError:
- pass
-
- try:
- stream_client = self.html_map.pop(sock_id)
- except KeyError:
- logger.info("socket has no child socket")
- else:
- stream_client.close()
- try:
- self.stream_clients.remove(stream_client)
- logger.info("removed stream_client=%r", id(stream_client))
- except ValueError:
- pass
-
- try:
- self.sockets.remove(stream_client)
- logger.info("removed child sock_id=%r", id(stream_client))
- except ValueError:
- pass
-
- def render_image(self):
- if not self.stream_clients:
- return
-
- #pixmap = QPixmap.grabWidget(self.widget, QtCore.QRect(10, 10, 768, 576))
- pixmap = QPixmap.grabWindow(self.win_id, 5, 5, 768, 576)
- buf = QBuffer()
- buf.open(QIODevice.WriteOnly)
- pixmap.save(buf, "JPG", 30)
- self.img_data = buf.data()
- len_data = len(self.img_data)
- array = QByteArray("--2342\r\nContent-Type: image/jpeg\r\nContent-length: %d\r\n\r\n%s\r\n\r\n\r\n" % (len_data, self.img_data))
- for sock in self.stream_clients:
- sock.write(array)
-
- def start_streaming(self):
- while self.hasPendingConnections():
- sock = self.nextPendingConnection()
- logger.info("new connection=%r", id(sock))
- sock.readyRead.connect(self.handle_request)
- sock.disconnected.connect(self.remove_stream_client)
- self.sockets.append(sock)
-
- def stop(self):
- for sock in self.sockets:
- sock.close()
- sock.deleteLater()
- for sock in self.stream_clients:
- sock.close()
- sock.deleteLater()
- self.stream_clients = list()
- self.sockets = list()
- self.html_map = dict()
- self.close()
-
class EditDialog(QtGui.QWidget, Ui_EditDialog):
def __init__(self, parent=None):
@@ -366,10 +232,10 @@ class TextAnimation(QtCore.QObject):
self.count += 1
-class MainWindow(KMainWindow, Ui_MainWindow):
+class MainWindow(KMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtClientBase):
def __init__(self, args, parent=None):
- super(MainWindow, self).__init__(parent)
self.args = args
+ super(MainWindow, self).__init__()
self.is_streaming = False
self.live_center_action = None
@@ -391,11 +257,15 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog = None
self.current_object = None
self.current_index = -1
+ self.win_id = self.winId()
self.is_auto_publish = False
self.setupUi(self)
- self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self)
+ self.coords = self.live_text_rect()
+
+ self.fps = 12.5
+ self.http_server = MjpegStreamingServer((args.http_host, args.http_port), self, self.fps)
self.live_text.setLineWrapMode(QtGui.QTextEdit.LineWrapMode(QtGui.QTextEdit.FixedPixelWidth))
self.live_text.setLineWrapColumnOrWidth(768)
@@ -433,17 +303,29 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.create_toolbar()
self.slot_load()
- app.focusChanged.connect(self.focusChanged)
+ qtapp.focusChanged.connect(self.focusChanged)
self.start_streaming()
self.show()
+ def pubdir(self):
+ return os.path.dirname(os.path.abspath(__file__))
+
+
def getPreviewCoords(self):
public_rect = self.preview_text.geometry()
global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight()))
return global_rect.x(), global_rect.y()
+ def render_image(self):
+ pixmap = QPixmap.grabWindow(self.win_id, self.coords.x() + 10, self.coords.y() + 10, 768, 576)
+ buf = QBuffer()
+ buf.open(QIODevice.WriteOnly)
+ pixmap.save(buf, "JPG", 75)
+ return buf.data()
+
+
def filter_editor_actions(self):
disabled_action_names = [
"action_to_plain_text",
@@ -631,9 +513,10 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog.setButtons(KDialog.ButtonCodes(KDialog.Ok | KDialog.Cancel))
self.dialog.okClicked.connect(self.slot_save)
self.dialog.exec_()
+ super(self, MainWindow).closeEvent(event)
def live_text_rect(self):
- return 5, 5, 768, 576
+ return self.live_text.geometry()
def stop_streaming(self):
self.is_streaming = False
@@ -643,6 +526,24 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.http_server.listen(port=self.args.http_port)
self.is_streaming = True
+ def fill_combo_box(self):
+ if self.dialog is not None:
+ self.dialog.deleteLater()
+ self.dialog = None
+
+ self.text_combo.clear()
+ current_row = -1
+ for index, list_obj in enumerate(self.model.text_db):
+ preview, text = list_obj
+ self.text_combo.addAction(preview)
+ if list_obj == self.current_object:
+ current_row = index
+
+ if current_row == -1:
+ current_row = self.current_index
+ self.slot_load_preview_text(current_row)
+ self.text_combo.setCurrentItem(current_row)
+
def focusChanged(self, old, new):
if new == self.preview_text:
self.live_editor_collection.clearAssociatedWidgets()
@@ -651,13 +552,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.preview_editor_collection.clearAssociatedWidgets()
self.live_editor_collection.addAssociatedWidget(self.toolbar)
- def custom_clear(self, cursor):
- cursor.beginEditBlock()
- cursor.movePosition(QtGui.QTextCursor.Start)
- cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
- cursor.removeSelectedText()
- cursor.endEditBlock()
-
def slot_auto_publish(self, state):
self.is_auto_publish = bool(state)
@@ -737,23 +631,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
if self.fade_animation.timer is None:
self.fade_animation.start_animation()
- def fill_combo_box(self):
- if self.dialog is not None:
- self.dialog.deleteLater()
- self.dialog = None
-
- self.text_combo.clear()
- current_row = -1
- for index, list_obj in enumerate(self.model.text_db):
- preview, text = list_obj
- self.text_combo.addAction(preview)
- if list_obj == self.current_object:
- current_row = index
-
- if current_row == -1:
- current_row = self.current_index
- self.slot_load_preview_text(current_row)
- self.text_combo.setCurrentItem(current_row)
def slot_load_preview_text(self, index):
try:
@@ -864,9 +741,11 @@ def main():
args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family)
+ args.chaosc_host, args.chaosc_port = resolve_host(args.chaosc_host, args.chaosc_port, args.address_family)
- window = MainWindow(args)
- app.exec_()
+ window = MainWindow(args, None)
+ sys.excepthook = window.sigint_handler
+ qtapp.exec_()
if __name__ == '__main__':