much rewrite, very kane :)

This commit is contained in:
Stefan Kögl 2014-05-26 23:25:36 +02:00
parent 05745e3c52
commit 80d6aea666
7 changed files with 365 additions and 391 deletions

View File

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="icon" type="image/png" href="/icon.png" />
</head>
<body>
<img src="/texter_%d.mjpeg" alt="Smiley face" />
</body>
</html>

View File

@ -23,8 +23,9 @@ from __future__ import absolute_import
import os import os
import os.path import os.path
import re import re
import signal
import sys import sys
from collections import deque
from datetime import datetime from datetime import datetime
from chaosc.argparser_groups import * from chaosc.argparser_groups import *
from chaosc.lib import logger, resolve_host 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 PyQt4.QtNetwork import QTcpServer, QTcpSocket, QUdpSocket, QHostAddress
from dump_grabber.dump_grabber_ui import Ui_MainWindow 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: try:
from chaosc.c_osc_lib import OSCMessage, decode_osc from chaosc.c_osc_lib import OSCMessage, decode_osc
@ -42,62 +44,17 @@ except ImportError as e:
app = QtGui.QApplication([]) app = QtGui.QApplication([])
class TextStorage(object): class ExclusiveTextStorage(object):
def __init__(self, columns): def __init__(self, columns, default_font, column_width, line_height, scene):
super(TextStorage, self).__init__()
self.column_count = columns self.column_count = columns
self.colors = (QtCore.Qt.red, QtCore.Qt.green, QtGui.QColor(46, 100, 254)) self.colors = (QtCore.Qt.red, QtCore.Qt.green, QtGui.QColor(46, 100, 254))
self.lines = deque()
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.default_font = default_font self.default_font = default_font
self.column_width = column_width self.column_width = column_width
self.line_height = line_height self.line_height = line_height
self.graphics_scene = scene self.graphics_scene = scene
self.num_lines, self.offset = divmod(576, self.line_height) self.num_lines, self.offset = divmod(576, self.line_height)
self.data = deque()
def init_columns(self): def init_columns(self):
color = self.colors[0] color = self.colors[0]
@ -107,26 +64,38 @@ class ExclusiveTextStorage(TextStorage):
text_item.setPos(0, y * self.line_height) text_item.setPos(0, y * self.line_height)
self.lines.append(text_item) 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 = self.graphics_scene.addSimpleText(text, self.default_font)
text_item.setX(column * self.column_width) text_item.setX(column * self.column_width)
color = self.colors[column] color = self.colors[column]
text_item.setBrush(color) text_item.setBrush(color)
old_item = self.lines.pop(0) old_item = self.lines.popleft()
self.graphics_scene.removeItem(old_item) self.graphics_scene.removeItem(old_item)
self.lines.append(text_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): for iy, text_item in enumerate(self.lines):
text_item.setY(iy * self.line_height) text_item.setY(iy * self.line_height)
def add_text(self, column, text):
self.data.append((column, text))
class MainWindow(QtGui.QMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtChaoscClientBase):
def __init__(self, args, parent=None):
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, args, parent=None, columns=3, column_exclusive=False):
super(MainWindow, self).__init__(parent)
self.args = args self.args = args
#PsyQtChaoscClientBase.__init__(self)
super(MainWindow, self).__init__()
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setupUi(self) self.setupUi(self)
self.http_server = MjpegStreamingServer((args.http_host, args.http_port), 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.graphics_view.setScene(self.graphics_scene)
self.default_font = QtGui.QFont("Monospace", 14) self.default_font = QtGui.QFont("Monospace", 14)
self.default_font.setStyleHint(QtGui.QFont.Monospace) 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.graphics_scene.setFont(self.default_font)
self.font_metrics = QtGui.QFontMetrics(self.default_font) self.font_metrics = QtGui.QFontMetrics(self.default_font)
self.line_height = self.font_metrics.height() self.line_height = self.font_metrics.height()
columns = 3
self.column_width = 775 / columns 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 = ExclusiveTextStorage(columns, self.default_font, self.column_width, self.line_height, self.graphics_scene)
self.text_storage.init_columns() 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 = OSCMessage("/subscribe")
msg.appendTypedArg("localhost", "s") msg.appendTypedArg("localhost", "s")
msg.appendTypedArg(args.client_port, "i") msg.appendTypedArg(args.client_port, "i")
@ -165,6 +130,9 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.regex = re.compile("^/(uwe|merle|bjoern)/(.*?)$") self.regex = re.compile("^/(uwe|merle|bjoern)/(.*?)$")
def pubdir(self):
return os.path.dirname(os.path.abspath(__file__))
def closeEvent(self, event): def closeEvent(self, event):
msg = OSCMessage("/unsubscribe") msg = OSCMessage("/unsubscribe")
msg.appendTypedArg("localhost", "s") msg.appendTypedArg("localhost", "s")
@ -179,6 +147,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.text_storage.add_text(column, text) self.text_storage.add_text(column, text)
def render_image(self): def render_image(self):
self.text_storage.finish()
image = QtGui.QImage(768, 576, QtGui.QImage.Format_ARGB32_Premultiplied) image = QtGui.QImage(768, 576, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(QtCore.Qt.black) image.fill(QtCore.Qt.black)
painter = QtGui.QPainter(image) painter = QtGui.QPainter(image)
@ -191,7 +160,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
painter.end() painter.end()
buf = QBuffer() buf = QBuffer()
buf.open(QIODevice.WriteOnly) buf.open(QIODevice.WriteOnly)
image.save(buf, "JPG", 80) image.save(buf, "JPG", 85)
image_data = buf.data() image_data = buf.data()
return image_data return image_data
@ -207,6 +176,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
except AttributeError: except AttributeError:
pass pass
else: else:
if text == "temperatur":
text += "e"
if actor == "merle": if actor == "merle":
self.add_text(0, "%s = %s" % (text, ", ".join([str(i) for i in args]))) self.add_text(0, "%s = %s" % (text, ", ".join([str(i) for i in args])))
elif actor == "uwe": elif actor == "uwe":
@ -229,8 +200,11 @@ def main():
args = arg_parser.finalize() args = arg_parser.finalize()
http_host, http_port = resolve_host(args.http_host, args.http_port, args.address_family) 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 = MainWindow(args)
#window.show() sys.excepthook = window.sigint_handler
signal.signal(signal.SIGTERM, window.sigterm_handler)
app.exec_() app.exec_()

View File

@ -1,7 +1,7 @@
<HTML> <HTML>
<BODY> <BODY>
<img src="/camera.mjpeg" alt="Smiley face"> <img src="/camera_%d.mjpeg" alt="Smiley face">
</BODY> </BODY>
</HTML> </HTML>

View File

@ -24,79 +24,82 @@
from __future__ import absolute_import from __future__ import absolute_import
from chaosc.argparser_groups import * import atexit
from chaosc.lib import logger, resolve_host import random
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 os.path 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 import pyqtgraph as pg
from pyqtgraph.widgets.PlotWidget import PlotWidget 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: try:
from chaosc.c_osc_lib import OSCMessage, decode_osc from chaosc.c_osc_lib import OSCMessage, decode_osc
except ImportError as e: except ImportError as e:
logging.exception(e)
from chaosc.osc_lib import OSCMessage, decode_osc from chaosc.osc_lib import OSCMessage, decode_osc
qtapp = QtGui.QApplication([])
def get_steps(pulse_rate, rate):
beat_length = 60. / pulse_rate def get_steps(pulse, delta_ms):
steps_pre = int(beat_length / rate) + 1 beat_length = 60000. / pulse
steps_pre = int(beat_length / delta_ms) + 1
used_sleep_time = beat_length / steps_pre used_sleep_time = beat_length / steps_pre
steps = int(beat_length / used_sleep_time) steps = int(beat_length / used_sleep_time)
return steps, used_sleep_time return steps, used_sleep_time
class Generator(object): class Generator(object):
def __init__(self, pulse=92, delta=0.08): def __init__(self, pulse=92, delta=80):
self.count = 0 self.count = 0
self.pulse = 92 self.pulse = random.randint(85, 105)
self.delta = delta 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): def __call__(self):
while 1: while 1:
value = random.randint(0, steps) if self.count < int(self.steps / 100. * 30):
if self.count < int(steps / 100. * 20): value = random.randint(30, 35)
value = random.randint(0,20) elif self.count == int(self.steps / 100. * 30):
elif self.count < int(steps / 100. * 30): value = random.randint(random.randint(50,60), random.randint(60, 70))
value = random.randint(20, 30) elif self.count < int(self.steps / 100. * 45):
elif self.count < int(steps / 100. * 40): value = random.randint(30, 35)
value = random.randint(30,100) elif self.count < int(self.steps / 2.):
elif self.count < int(steps / 2.): value = random.randint(0, 15)
value = random.randint(100,200) elif self.count == int(self.steps / 2.):
elif self.count == int(steps / 2.):
value = 255 value = 255
elif self.count < int(steps / 100. * 60): elif self.count < int(self.steps / 100. * 60):
value = random.randint(100, 200) value = random.randint(random.randint(25,30), random.randint(30, 35))
elif self.count < int(steps / 100. * 70): elif self.count < int(self.steps / 100. * 70):
value = random.randint(50, 100) value = random.randint(random.randint(10,25), random.randint(25, 30))
elif self.count < int(steps / 100. * 80): elif self.count < self.steps:
value = random.randint(20, 50) value = random.randint(random.randint(15,25), random.randint(25, 30))
elif self.count <= steps: else:
value = random.randint(0,20)
elif self.count >= steps:
self.count = 0 self.count = 0
value = 30
self.count += 1 self.count += 1
yield value yield value
def set_pulse(self, pulse):
self.pulse = pulse
self.steps, _ = get_steps(pulse, self.delta / self.multiplier)
def retrigger(self): def retrigger(self):
self.count = self.steps / 2 self.count = self.steps / 2
@ -115,11 +118,14 @@ class Actor(object):
self.data = np.array([self.offset] * num_data) self.data = np.array([self.offset] * num_data)
self.head = 0 self.head = 0
self.pre_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.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): def __str__(self):
return "<Actor name:%r, active=%r, position=%r>" % (self.name, self.active, self.head) return "<Actor name:%r, position=%r>" % (self.name, self.head)
__repr__ = __str__ __repr__ = __str__
@ -146,26 +152,17 @@ class Actor(object):
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): def __init__(self, args, parent=None):
super(EkgPlotWidget, self).__init__(parent)
self.args = args 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.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.num_data = 100
self.hide() self.hide()
@ -173,8 +170,6 @@ class EkgPlotWidget(PlotWidget):
self.setYRange(0, 255) self.setYRange(0, 255)
self.setXRange(0, self.num_data) self.setXRange(0, self.num_data)
self.resize(768, 576) self.resize(768, 576)
colors = ["r", "g", "b"] colors = ["r", "g", "b"]
ba = self.getAxis("bottom") ba = self.getAxis("bottom")
@ -186,7 +181,6 @@ class EkgPlotWidget(PlotWidget):
self.active_actors = list() self.active_actors = list()
self.actors = dict() self.actors = dict()
self.lengths1 = [0]
self.max_value = 255 self.max_value = 255
actor_names = ["merle", "uwe", "bjoern"] actor_names = ["merle", "uwe", "bjoern"]
@ -197,11 +191,10 @@ class EkgPlotWidget(PlotWidget):
self.add_actor(actor_name, self.num_data, color, ix, self.max_actors, self.actor_height) self.add_actor(actor_name, self.num_data, color, ix, self.max_actors, self.actor_height)
self.set_positions() self.set_positions()
self.heartbeat_regex = re.compile("^/(.*?)/heartbeat$")
self.ekg_regex = re.compile("^/(.*?)/ekg$") def pubdir(self):
self.ctl_regex = re.compile("^/plot/(.*?)$") return os.path.dirname(os.path.abspath(__file__))
self.updated_actors = set()
self.new_round()
def add_actor(self, actor_name, num_data, color, ix, max_actors, actor_height): 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.plotItem)
self.addItem(actor_obj.plotPoint) self.addItem(actor_obj.plotPoint)
self.active_actors.append(actor_obj) 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): def set_positions(self):
for ix, actor_obj in enumerate(self.active_actors): for ix, actor_obj in enumerate(self.active_actors):
@ -220,51 +214,24 @@ class EkgPlotWidget(PlotWidget):
def active_actor_count(self): def active_actor_count(self):
return self.max_actors return self.max_actors
def new_round(self): def update(self, osc_address, args):
for ix, actor in enumerate(self.active_actors):
actor.updated = 0
def update_missing_actors(self): res = self.heartbeat_regex.match(osc_address)
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)
if res: if res:
actor_name = res.group(1) actor_name = res.group(1)
actor_obj = self.actors[actor_name] 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): def render_image(self):
self.update_missing_actors() for actor_obj in self.active_actors:
self.render() 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 = pg.exporters.ImageExporter.ImageExporter(self.plotItem)
exporter.parameters()['width'] = 768 exporter.parameters()['width'] = 768
img = exporter.export(toBytes=True) img = exporter.export(toBytes=True)
@ -272,7 +239,6 @@ class EkgPlotWidget(PlotWidget):
buf.open(QIODevice.WriteOnly) buf.open(QIODevice.WriteOnly)
img.save(buf, "JPG", 75) img.save(buf, "JPG", 75)
JpegData = buf.data() JpegData = buf.data()
self.new_round()
return JpegData return JpegData
def got_message(self): def got_message(self):
@ -282,10 +248,8 @@ class EkgPlotWidget(PlotWidget):
osc_address, typetags, args = decode_osc(data, 0, len(data)) osc_address, typetags, args = decode_osc(data, 0, len(data))
except Exception, e: except Exception, e:
logger.exception(e) logger.exception(e)
return
else: else:
self.update(osc_address, args[0]) self.update(osc_address, args)
return True
@ -293,18 +257,20 @@ def main():
arg_parser = ArgParser("ekgplotter") arg_parser = ArgParser("ekgplotter")
arg_parser.add_global_group() arg_parser.add_global_group()
client_group = arg_parser.add_client_group() client_group = arg_parser.add_client_group()
arg_parser.add_argument(client_group, '-x', "--http_host", default="::", arg_parser.add_argument(client_group, '-x', "--http_host", default='::',
help='my host, defaults to "::"') help='my host, defaults to "::"')
arg_parser.add_argument(client_group, '-X', "--http_port", default=9000, arg_parser.add_argument(client_group, '-X', '--http_port', default=9000,
type=int, help='my port, defaults to 9000') type=int, help='my port, defaults to 9000')
arg_parser.add_chaosc_group() arg_parser.add_chaosc_group()
arg_parser.add_subscriber_group() arg_parser.add_subscriber_group()
args = arg_parser.finalize() args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family) 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([]) window = EkgPlotWidget(args)
widget = EkgPlotWidget(args) sys.excepthook = window.sigint_handler
signal.signal(signal.SIGTERM, window.sigterm_handler)
qtapp.exec_() qtapp.exec_()

View File

@ -1,20 +1,20 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (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 # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with chaosc/psychosis. If not, see <http://www.gnu.org/licenses/>. # along with chaosc/psylib. If not, see <http://www.gnu.org/licenses/>.
# #
# Copyright (C) 2014 Stefan Kögl # Copyright (C) 2014 Stefan Kögl
@ -23,27 +23,47 @@ from __future__ import absolute_import
import os import os
import os.path import os.path
import re import re
import sys
from datetime import datetime from chaosc.lib import logger
from chaosc.argparser_groups import * from PyQt4 import QtCore
from chaosc.lib import logger, resolve_host from PyQt4.QtCore import QByteArray
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import QBuffer, QByteArray, QIODevice
from PyQt4.QtNetwork import QTcpServer, QTcpSocket 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): 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) super(MjpegStreamingServer, self).__init__(parent)
self.server_address = server_address self.server_address = server_address
self.newConnection.connect(self.new_connection) self.newConnection.connect(self.new_connection)
assert isinstance(parent, MjpegStreamingConsumerInterface)
self.widget = parent self.widget = parent
self.sockets = list() self.sockets = list()
self.img_data = None self.img_data = None
self.fps = fps
self.timer_delta = 1000 / fps
self.timer = QtCore.QTimer() self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.send_image) self.timer.timeout.connect(self.send_image)
self.timer.start(80) self.timer.start(self.timer_delta)
self.stream_clients = list() self.stream_clients = list()
self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$") self.get_regex = re.compile("^GET /(\w+?)\.(\w+?) HTTP/(\d+\.\d+)$")
self.host_regex = re.compile("^Host: (\w+?):(\d+)$") self.host_regex = re.compile("^Host: (\w+?):(\d+)$")
@ -51,8 +71,8 @@ class MjpegStreamingServer(QTcpServer):
def handle_request(self): def handle_request(self):
sock = self.sender() sock = self.sender()
logger.info("handle_request: %s %d", sock.peerAddress(), sock.peerPort())
sock_id = id(sock) sock_id = id(sock)
logger.info("handle_request: sock_id=%r", sock_id)
if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState): if sock.state() in (QTcpSocket.UnconnectedState, QTcpSocket.ClosingState):
logger.info("connection closed") logger.info("connection closed")
self.sockets.remove(sock) self.sockets.remove(sock)
@ -79,13 +99,25 @@ class MjpegStreamingServer(QTcpServer):
return return
else: else:
if ext == "ico": if ext == "ico":
directory = os.path.dirname(os.path.abspath(__file__)) directory = self.widget.pubdir()
try:
data = open(os.path.join(directory, "favicon.ico"), "rb").read() 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)) sock.write(QByteArray('HTTP/1.1 200 Ok\r\nContent-Type: image/x-ico\r\n\r\n%s' % data))
elif ext == "html": elif ext == "html":
directory = os.path.dirname(os.path.abspath(__file__)) directory = self.widget.pubdir()
try:
data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id data = open(os.path.join(directory, "index.html"), "rb").read() % sock_id
self.html_map[sock_id] = None 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)) 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": elif ext == "mjpeg":
try: try:
@ -117,29 +149,38 @@ class MjpegStreamingServer(QTcpServer):
sock.disconnected.disconnect(self.slot_remove_connection) sock.disconnected.disconnect(self.slot_remove_connection)
sock.close() sock.close()
sock.deleteLater() sock.deleteLater()
try:
self.sockets.remove(sock) self.sockets.remove(sock)
logger.info("connection removed: sock=%r, sock_id=%r", sock, sock_id) logger.info("connection %r removed", sock_id)
except ValueError, msg:
logger.info("connection %r was not stored?", sock_id)
try: try:
self.stream_clients.remove(sock) self.stream_clients.remove(sock)
except ValueError: except ValueError:
pass logger.info("connection %r was not streaming", sock_id)
try: try:
stream_client = self.html_map.pop(sock_id) stream_client = self.html_map.pop(sock_id)
except KeyError: except KeyError:
logger.info("socket has no child socket") logger.info("socket %r has no child socket", sock_id)
else: else:
try:
stream_client.close() stream_client.close()
except AttributeError, msg:
logger.info("no stream client")
else:
try: try:
self.stream_clients.remove(stream_client) self.stream_clients.remove(stream_client)
logger.info("removed stream_client=%r", id(stream_client)) logger.info("child connection %r removed from streaming", id(stream_client))
except ValueError: except ValueError:
pass pass
try: try:
self.sockets.remove(stream_client) self.sockets.remove(stream_client)
logger.info("removed child sock_id=%r", id(stream_client)) logger.info("child connection %r removed from storage", id(stream_client))
except ValueError: except ValueError:
pass pass

105
psylib/psylib/psyqt_base.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -24,6 +24,7 @@ from __future__ import absolute_import
import cPickle import cPickle
import os.path import os.path
import re import re
import sys
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
@ -39,11 +40,14 @@ from PyQt4.QtNetwork import QTcpServer, QTcpSocket
from chaosc.argparser_groups import ArgParser from chaosc.argparser_groups import ArgParser
from chaosc.lib import resolve_host, logger 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.texter_ui import Ui_MainWindow, _fromUtf8
from texter.edit_dialog_ui import Ui_EditDialog from texter.edit_dialog_ui import Ui_EditDialog
from texter.text_model import TextModel 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 # 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): def get_preview_text(text):
return re.sub(" +", " ", text.replace("\n", " ")).strip()[:20] 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): class EditDialog(QtGui.QWidget, Ui_EditDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -366,10 +232,10 @@ class TextAnimation(QtCore.QObject):
self.count += 1 self.count += 1
class MainWindow(KMainWindow, Ui_MainWindow): class MainWindow(KMainWindow, Ui_MainWindow, MjpegStreamingConsumerInterface, PsyQtClientBase):
def __init__(self, args, parent=None): def __init__(self, args, parent=None):
super(MainWindow, self).__init__(parent)
self.args = args self.args = args
super(MainWindow, self).__init__()
self.is_streaming = False self.is_streaming = False
self.live_center_action = None self.live_center_action = None
@ -391,11 +257,15 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog = None self.dialog = None
self.current_object = None self.current_object = None
self.current_index = -1 self.current_index = -1
self.win_id = self.winId()
self.is_auto_publish = False self.is_auto_publish = False
self.setupUi(self) 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.setLineWrapMode(QtGui.QTextEdit.LineWrapMode(QtGui.QTextEdit.FixedPixelWidth))
self.live_text.setLineWrapColumnOrWidth(768) self.live_text.setLineWrapColumnOrWidth(768)
@ -433,17 +303,29 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.create_toolbar() self.create_toolbar()
self.slot_load() self.slot_load()
app.focusChanged.connect(self.focusChanged) qtapp.focusChanged.connect(self.focusChanged)
self.start_streaming() self.start_streaming()
self.show() self.show()
def pubdir(self):
return os.path.dirname(os.path.abspath(__file__))
def getPreviewCoords(self): def getPreviewCoords(self):
public_rect = self.preview_text.geometry() public_rect = self.preview_text.geometry()
global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight())) global_rect = QtCore.QRect(self.mapToGlobal(public_rect.topLeft()), self.mapToGlobal(public_rect.bottomRight()))
return global_rect.x(), global_rect.y() 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): def filter_editor_actions(self):
disabled_action_names = [ disabled_action_names = [
"action_to_plain_text", "action_to_plain_text",
@ -631,9 +513,10 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.dialog.setButtons(KDialog.ButtonCodes(KDialog.Ok | KDialog.Cancel)) self.dialog.setButtons(KDialog.ButtonCodes(KDialog.Ok | KDialog.Cancel))
self.dialog.okClicked.connect(self.slot_save) self.dialog.okClicked.connect(self.slot_save)
self.dialog.exec_() self.dialog.exec_()
super(self, MainWindow).closeEvent(event)
def live_text_rect(self): def live_text_rect(self):
return 5, 5, 768, 576 return self.live_text.geometry()
def stop_streaming(self): def stop_streaming(self):
self.is_streaming = False self.is_streaming = False
@ -643,6 +526,24 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.http_server.listen(port=self.args.http_port) self.http_server.listen(port=self.args.http_port)
self.is_streaming = True 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): def focusChanged(self, old, new):
if new == self.preview_text: if new == self.preview_text:
self.live_editor_collection.clearAssociatedWidgets() self.live_editor_collection.clearAssociatedWidgets()
@ -651,13 +552,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
self.preview_editor_collection.clearAssociatedWidgets() self.preview_editor_collection.clearAssociatedWidgets()
self.live_editor_collection.addAssociatedWidget(self.toolbar) 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): def slot_auto_publish(self, state):
self.is_auto_publish = bool(state) self.is_auto_publish = bool(state)
@ -737,23 +631,6 @@ class MainWindow(KMainWindow, Ui_MainWindow):
if self.fade_animation.timer is None: if self.fade_animation.timer is None:
self.fade_animation.start_animation() 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): def slot_load_preview_text(self, index):
try: try:
@ -864,9 +741,11 @@ def main():
args = arg_parser.finalize() args = arg_parser.finalize()
args.http_host, args.http_port = resolve_host(args.http_host, args.http_port, args.address_family) 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) window = MainWindow(args, None)
app.exec_() sys.excepthook = window.sigint_handler
qtapp.exec_()
if __name__ == '__main__': if __name__ == '__main__':