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 @@ + + + + + + + Smiley face + + 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 @@ -Smiley face +Smiley face 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__':