Changeset - ccdfdc8183ad
bin/inputquneo
Show inline comments
 
@@ -17,25 +17,25 @@ import sys
 
sys.path.append('/usr/lib/python2.7/dist-packages')  # For pygame
 
import pygame.midi
 

	
 
curves = {
 
    23: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-2'),
 
    24: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-3'),
 
    25: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-4'),
 
    6: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-5'),
 
    18: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-6'),
 
}
 

	
 

	
 
class WatchMidi(object):
 
class WatchMidi:
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 
        pygame.midi.init()
 

	
 
        dev = self.findQuneo()
 
        self.inp = pygame.midi.Input(dev)
 
        task.LoopingCall(self.step).start(.05)
 

	
 
        self.noteIsOn = {}
 

	
 
        self.effectMap = {}  # note: effect class uri
bin/paintserver
Show inline comments
 
@@ -49,25 +49,25 @@ class Solve(PrettyErrorHandler, cyclone.
 
class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def post(self):
 
        body = json.loads(self.request.body)
 
        painting = body['painting']
 
        devs = [URIRef(d) for d in body['devices']]
 
        with metrics('solve').time():
 
            img = self.settings.solver.draw(painting)
 
            outSettings = self.settings.solver.bestMatches(img, devs)
 
            self.write(json.dumps({'settings': outSettings.asList()}))
 

	
 

	
 
class App(object):
 
class App:
 

	
 
    def __init__(self, show, session):
 
        self.show = show
 
        self.session = session
 

	
 
        self.graph = SyncedGraph(networking.rdfdb.url, "paintServer")
 
        self.graph.initiallySynced.addCallback(self.launch).addErrback(
 
            log.error)
 

	
 

	
 
    def launch(self, *args):
 

	
light9/Effects.py
Show inline comments
 
@@ -9,25 +9,25 @@ from light9 import Patch
 
from light9.namespaces import L9
 
log = logging.getLogger()
 

	
 
registered = []
 

	
 

	
 
def register(f):
 
    registered.append(f)
 
    return f
 

	
 

	
 
@register
 
class Strip(object):
 
class Strip:
 
    """list of r,g,b tuples for sending to an LED strip"""
 
    which = 'L'  # LR means both. W is the wide one
 
    pixels = []
 

	
 
    def __repr__(self):
 
        return '<Strip which=%r px0=%r>' % (self.which, self.pixels[0])
 

	
 
    @classmethod
 
    def solid(cls, which='L', color=(1, 1, 1), hsv=None):
 
        """hsv overrides color"""
 
        if hsv is not None:
 
            color = colorsys.hsv_to_rgb(hsv[0] % 1.0, hsv[1], hsv[2])
light9/ascoltami/main.py
Show inline comments
 
@@ -13,25 +13,25 @@ from twisted.internet.interfaces import 
 
gi.require_version('Gst', '1.0')
 
gi.require_version('Gtk', '3.0')
 

	
 
from gi.repository import Gst # type: ignore
 
from light9 import networking, showconfig
 
from light9.ascoltami.player import Player
 
from light9.ascoltami.playlist import NoSuchSong, Playlist
 
from light9.ascoltami.webapp import makeWebApp, songLocation, songUri
 

	
 
reactor = cast(IReactorCore, reactor)
 

	
 

	
 
class App(object):
 
class App:
 

	
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
        self.player = Player(onEOS=self.onEOS, autoStopOffset=0)
 
        self.show = show
 
        self.playlist = Playlist.fromShow(graph, show)
 

	
 
    def onEOS(self, song):
 
        self.player.pause()
 
        self.player.seek(0)
 

	
 
        thisSongUri = songUri(graph, URIRef(song))
light9/ascoltami/musictime_client.py
Show inline comments
 
@@ -2,25 +2,25 @@ import time, json, logging
 
from typing import Dict, cast
 
from twisted.internet.interfaces import IReactorTime
 

	
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks
 
import treq
 

	
 
from light9 import networking
 

	
 
log = logging.getLogger()
 

	
 

	
 
class MusicTime(object):
 
class MusicTime:
 
    """
 
    fetch times from ascoltami in a background thread; return times
 
    upon request, adjusted to be more precise with the system clock
 
    """
 

	
 
    def __init__(self, period=.2, onChange=lambda position: None, pollCurvecalc='ignored'):
 
        """period is the seconds between
 
        http time requests.
 

	
 
        We call onChange with the time in seconds and the total time
 

	
 
        The choice of period doesn't need to be tied to framerate,
light9/ascoltami/player.py
Show inline comments
 
@@ -2,25 +2,25 @@
 
"""
 
alternate to the mpd music player, for ascoltami
 
"""
 

	
 
import time, logging, traceback
 
from gi.repository import Gst # type: ignore
 
from twisted.internet import task
 
from light9.metrics import metrics
 
log = logging.getLogger()
 

	
 

	
 

	
 
class Player(object):
 
class Player:
 

	
 
    def __init__(self, autoStopOffset=4, onEOS=None):
 
        """autoStopOffset is the number of seconds before the end of
 
        song before automatically stopping (which is really pausing).
 
        onEOS is an optional function to be called when we reach the
 
        end of a stream (for example, can be used to advance the song).
 
        It is called with one argument which is the URI of the song that
 
        just finished."""
 
        self.autoStopOffset = autoStopOffset
 
        self.playbin = self.pipeline = Gst.ElementFactory.make('playbin', None)
 

	
 
        self.playStartTime = 0
light9/ascoltami/playlist.py
Show inline comments
 
from light9.showconfig import songOnDisk
 
from light9.namespaces import L9
 

	
 

	
 
class NoSuchSong(ValueError):
 
    """Raised when a song is requested that doesn't exist (e.g. one
 
    after the last song in the playlist)."""
 

	
 

	
 
class Playlist(object):
 
class Playlist:
 

	
 
    def __init__(self, graph, playlistUri):
 
        self.graph = graph
 
        self.playlistUri = playlistUri
 
        self.songs = list(graph.items(playlistUri))
 

	
 
    def nextSong(self, currentSong):
 
        """Returns the next song in the playlist or raises NoSuchSong if 
 
        we are at the end of the playlist."""
 
        try:
 
            currentIndex = self.songs.index(currentSong)
 
        except IndexError:
light9/collector/collector_client.py
Show inline comments
 
@@ -2,25 +2,25 @@ from light9 import networking
 
from light9.effect.settings import DeviceSettings
 
from light9.metrics import metrics
 
from twisted.internet import defer
 
from txzmq import ZmqEndpoint, ZmqFactory, ZmqPushConnection
 
import json, time, logging
 
import treq
 

	
 
log = logging.getLogger('coll_client')
 

	
 
_zmqClient = None
 

	
 

	
 
class TwistedZmqClient(object):
 
class TwistedZmqClient:
 

	
 
    def __init__(self, service):
 
        zf = ZmqFactory()
 
        e = ZmqEndpoint('connect', 'tcp://%s:%s' % (service.host, service.port))
 
        self.conn = ZmqPushConnection(zf, e)
 

	
 
    def send(self, msg):
 
        self.conn.push(msg)
 

	
 

	
 
def toCollectorJson(client, session, settings: DeviceSettings) -> str:
 
    assert isinstance(settings, DeviceSettings)
light9/collector/device.py
Show inline comments
 
import logging
 
from typing import Dict, List, Any, TypeVar, cast
 
from light9.namespaces import L9
 
from rdflib import Literal, URIRef
 
from webcolors import hex_to_rgb, rgb_to_hex
 
from colormath.color_objects import sRGBColor, CMYColor
 
import colormath.color_conversions
 
from light9.newtypes import VT, DeviceClass, HexColor, OutputAttr, OutputValue, DeviceUri, DeviceAttr, VTUnion
 

	
 
log = logging.getLogger('device')
 

	
 

	
 
class Device(object):
 
class Device:
 
    pass
 

	
 

	
 
class ChauvetColorStrip(Device):
 
    """
 
     device attrs:
 
       color
 
    """
 

	
 

	
 
class Mini15(Device):
 
    """
light9/collector/output.py
Show inline comments
 
@@ -4,25 +4,25 @@ import socket
 
import struct
 
import time
 
import usb.core
 
import logging
 
from twisted.internet import threads, reactor, task
 
from twisted.internet.interfaces import IReactorCore, IReactorTime
 
from light9.metrics import metrics
 

	
 
log = logging.getLogger('output')
 
logAllDmx = logging.getLogger('output.allDmx')
 

	
 

	
 
class Output(object):
 
class Output:
 
    """
 
    send a binary buffer of values to some output device. Call update
 
    as often as you want- the result will be sent as soon as possible,
 
    and with repeats as needed to outlast hardware timeouts.
 

	
 
    This base class doesn't ever call _write. Subclasses below have
 
    strategies for that.
 
    """
 
    uri: URIRef
 

	
 
    def __init__(self, uri: URIRef):
 
        self.uri = uri
light9/collector/weblisteners.py
Show inline comments
 
@@ -11,25 +11,25 @@ log = logging.getLogger('weblisteners')
 

	
 

	
 
def shortenOutput(out: OutputUri) -> str:
 
    return str(out).rstrip('/').rsplit('/', 1)[-1]
 

	
 

	
 
class UiListener(Protocol):
 

	
 
    async def sendMessage(self, msg):
 
        ...
 

	
 

	
 
class WebListeners(object):
 
class WebListeners:
 

	
 
    def __init__(self) -> None:
 
        self.clients: List[Tuple[UiListener, Dict[DeviceUri, Dict[OutputAttr, OutputValue]]]] = []
 
        self.pendingMessageForDev: Dict[DeviceUri, Tuple[Dict[OutputAttr, OutputValue], Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri,
 
                                                                                                                                 DmxMessageIndex]]]] = {}
 
        self.lastFlush = 0
 
        asyncio.create_task(self.flusher())
 

	
 
    def addClient(self, client: UiListener):
 
        self.clients.append((client, {}))  # seen = {dev: attrs}
 
        log.info('added client %s %s', len(self.clients), client)
 
        # todo: it would be nice to immediately fill in the client on the
light9/effect/effecteval.py
Show inline comments
 
@@ -55,25 +55,25 @@ def clamp(lo, hi, x):
 

	
 

	
 
def clamp255(x):
 
    return min(255, max(0, x))
 

	
 

	
 
def _8bit(f):
 
    if not isinstance(f, (int, float)):
 
        raise TypeError(repr(f))
 
    return clamp255(int(f * 255))
 

	
 

	
 
class EffectEval(object):
 
class EffectEval:
 
    """
 
    runs one effect's code to turn effect attr settings into output
 
    device settings. No state; suitable for reload().
 
    """
 

	
 
    def __init__(self, graph, effect, simpleOutputs):
 
        self.graph = graph
 
        self.effect = effect
 
        self.simpleOutputs = simpleOutputs
 

	
 
    def outputFromEffect(self, effectSettings, songTime, noteTime):
 
        """
light9/effect/sequencer/note.py
Show inline comments
 
@@ -11,25 +11,25 @@ from light9.namespaces import L9
 
from light9.newtypes import Curve, DeviceAttr, DeviceUri, NoteUri, typedValue
 

	
 
log = logging.getLogger('sequencer')
 

	
 

	
 
def pyType(n):
 
    ret = n.toPython()
 
    if isinstance(ret, Decimal):
 
        return float(ret)
 
    return ret
 

	
 

	
 
class Note(object):
 
class Note:
 

	
 
    def __init__(self, graph: SyncedGraph, uri: NoteUri, effectevalModule, simpleOutputs, timed=True):
 
        g = self.graph = graph
 
        self.uri = uri
 
        self.timed = timed
 
        self.effectEval = effectevalModule.EffectEval(graph, g.value(uri, L9['effectClass']), simpleOutputs)
 
        self.baseEffectSettings: Dict[URIRef, Any] = {}  # {effectAttr: value}
 
        for s in g.objects(uri, L9['setting']):
 
            settingValues = dict(g.predicate_objects(s))
 
            ea = cast(URIRef, settingValues[L9['effectAttr']])
 
            self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
 

	
light9/effect/sequencer/sequencer.py
Show inline comments
 
@@ -23,47 +23,47 @@ from light9.effect.settings import Devic
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.metrics import metrics
 
from light9.namespaces import L9, RDF
 
from light9.newtypes import NoteUri, Song
 

	
 
log = logging.getLogger('sequencer')
 

	
 

	
 
class StateUpdate(All):
 
    pass
 

	
 

	
 
class CodeWatcher(object):
 
class CodeWatcher:
 

	
 
    def __init__(self, onChange):
 
        self.onChange = onChange
 

	
 
        self.notifier = INotify()
 
        self.notifier.startReading()
 
        self.notifier.watch(FilePath(effecteval.__file__.replace('.pyc',
 
                                                                 '.py')),
 
                            callbacks=[self.codeChange])
 

	
 
    def codeChange(self, watch, path, mask):
 

	
 
        def go():
 
            log.info("reload effecteval")
 
            imp.reload(effecteval)
 
            self.onChange()
 

	
 
        # in case we got an event at the start of the write
 
        reactor.callLater(.1, go) # type: ignore
 

	
 

	
 
class Sequencer(object):
 
class Sequencer:
 
    """Notes from the graph + current song playback -> sendToCollector"""
 
    def __init__(self,
 
                 graph: SyncedGraph,
 
                 sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]],
 
                 fps=40,
 
                 ):
 
        self.graph = graph
 
        self.fps = fps
 
        metrics('update_loop_goal_fps').set(self.fps)
 
        metrics('update_loop_goal_latency').set(1 / self.fps)
 
        self.sendToCollector = sendToCollector
 
        self.music = MusicTime(period=.2)
light9/effect/settings.py
Show inline comments
 
@@ -27,25 +27,25 @@ def toHex(rgbFloat: Sequence[float]) -> 
 
    scaled = (max(0, min(255, int(v * 255))) for v in rgbFloat)
 
    return '#%02x%02x%02x' % tuple(scaled)  # type: ignore
 

	
 

	
 
def getVal(graph, subj):
 
    lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue'])
 
    ret = lit.toPython()
 
    if isinstance(ret, decimal.Decimal):
 
        ret = float(ret)
 
    return ret
 

	
 

	
 
class _Settings(object):
 
class _Settings:
 
    """
 
    default values are 0 or '#000000'. Internal rep must not store zeros or some
 
    comparisons will break.
 
    """
 

	
 
    def __init__(self, graph, settingsList):
 
        self.graph = graph  # for looking up all possible attrs
 
        self._compiled: Dict[URIRef, Dict[URIRef, Union[float, str]]] = {
 
        }  # dev: { attr: val }; val is number or colorhex
 
        for row in settingsList:
 
            self._compiled.setdefault(row[0], {})[row[1]] = row[2]
 
        # self._compiled may not be final yet- see _fromCompiled
light9/effect/simple_outputs.py
Show inline comments
 
import traceback
 
from light9.namespaces import L9, RDF
 
from light9.effect.scale import scale
 
from typing import Dict, List, Tuple, Any
 
from rdflib import URIRef
 

	
 

	
 
class SimpleOutputs(object):
 
class SimpleOutputs:
 
    """
 
    Watches graph for effects that are just fading output attrs. 
 
    Call `values` to get (dev,attr):value settings.
 
    """
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 

	
 
        # effect : [(dev, attr, value, isScaled)]
 
        self.effectOutputs: Dict[URIRef, List[Tuple[URIRef, URIRef, Any, bool]]] = {}
 

	
 
        self.graph.addHandler(self.updateEffectsFromGraph)
light9/effecteval/effect.py
Show inline comments
 
@@ -6,25 +6,25 @@ from light9.curvecalc.curve import Curve
 
from light9 import prof
 
from light9 import Submaster
 
from light9 import Effects  # gets reload() later
 
log = logging.getLogger('effect')
 

	
 
# consider http://waxeye.org/ for a parser that can be used in py and js
 

	
 

	
 
class CouldNotConvert(TypeError):
 
    pass
 

	
 

	
 
class CodeLine(object):
 
class CodeLine:
 
    """code string is immutable"""
 

	
 
    def __init__(self, graph, code):
 
        self.graph, self.code = graph, code
 

	
 
        self.outName, self.inExpr, self.expr, self.resources = self._asPython()
 
        self.pyResources = self._resourcesAsPython(self.resources)
 
        self.possibleVars = self.findVars(self.inExpr)
 

	
 
    @prof.logTime
 
    def _asPython(self):
 
        """
 
@@ -93,25 +93,25 @@ class CodeLine(object):
 
                elif rdfClass == L9['Submaster']:
 
                    out[localVar] = subs.get_sub_by_uri(uri)
 
                    break
 
                else:
 
                    out[localVar] = CouldNotConvert(uri)
 
                    break
 
            else:
 
                out[localVar] = CouldNotConvert(uri)
 

	
 
        return out
 

	
 

	
 
class EffectNode(object):
 
class EffectNode:
 

	
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    @prof.logTime
 
    def prepare(self):
 
        log.info("prepare effect %s", self.uri)
 
        # maybe there can be multiple lines of code as multiple
 
        # objects here, and we sort them by dependencies
 
        codeStrs = list(self.graph.objects(self.uri, L9['code']))
light9/effecteval/effectloop.py
Show inline comments
 
@@ -10,25 +10,25 @@ import treq
 

	
 
from light9 import Effects
 
from light9 import Submaster
 
from light9 import dmxclient
 
from light9 import networking
 
from light9.effecteval.effect import EffectNode
 
from light9.namespaces import L9, RDF
 
from light9.metrics import metrics
 

	
 
log = logging.getLogger('effectloop')
 

	
 

	
 
class EffectLoop(object):
 
class EffectLoop:
 
    """maintains a collection of the current EffectNodes, gets time from
 
    music player, sends dmx"""
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 
        self.currentSong = None
 
        self.currentEffects = [
 
        ]  # EffectNodes for the current song plus the submaster ones
 
        self.lastLogTime = 0
 
        self.lastLogMsg = ""
 
        self.lastErrorLog = 0
 
        self.graph.addHandler(self.setEffects)
 
@@ -184,25 +184,25 @@ class EffectLoop(object):
 
                    self.lastLogMsg = msg
 
                self.lastLogTime = now
 

	
 
    def logMessage(self, out):
 
        return ("send dmx: {%s}" %
 
                ", ".join("%r: %.3g" % (str(k), v)
 
                          for k, v in list(out.get_levels().items())))
 

	
 

	
 
Z = numpy.zeros((50, 3), dtype=numpy.float16)
 

	
 

	
 
class ControlBoard(object):
 
class ControlBoard:
 

	
 
    def __init__(
 
            self,
 
            dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0'
 
    ):
 
        log.info('opening %s', dev)
 
        self._dev = serial.Serial(dev, baudrate=115200)
 

	
 
    def _8bitMessage(self, floatArray):
 
        px255 = (numpy.clip(floatArray, 0, 1) * 255).astype(numpy.uint8)
 
        return px255.reshape((-1,)).tostring()
 

	
light9/io/udmx.py
Show inline comments
 
@@ -13,25 +13,25 @@ http://www.amazon.com/Interface-Adapter-
 
[4520784.157419] usb 1-2.3: Product: uDMX
 
[4520784.157422] usb 1-2.3: Manufacturer: www.anyma.ch
 
[4520784.157424] usb 1-2.3: SerialNumber: ilLUTZminator001
 

	
 
See https://www.illutzmination.de/udmxfirmware.html?&L=1
 
    sources/commandline/uDMX.c
 
or https://github.com/markusb/uDMX-linux/blob/master/uDMX.c
 
"""
 

	
 
cmd_SetChannelRange = 0x0002
 

	
 

	
 
class Udmx(object):
 
class Udmx:
 

	
 
    def __init__(self, bus):
 
        self.dev = None
 
        for dev in usb.core.find(idVendor=0x16c0,
 
                                 idProduct=0x05dc,
 
                                 find_all=True):
 
            print("udmx device at %r" % dev.bus)
 
            if bus is None or bus == dev.bus:
 
                self.dev = dev
 
        if not self.dev:
 
            raise IOError('no matching udmx device found for requested bus %r' %
 
                          bus)
light9/metrics.py
Show inline comments
 
@@ -92,25 +92,25 @@ def metricsRoute() -> Tuple[str, Type[cy
 
stuff we used to have in greplin. Might be nice to get (client-side-computed) min/max/stddev back.
 

	
 
class PmfStat(Stat):
 
  A stat that stores min, max, mean, standard deviation, and some
 
  percentiles for arbitrary floating-point data. This is potentially a
 
  bit expensive, so its child values are only updated once every
 
  twenty seconds.
 

	
 

	
 

	
 
metrics consumer side can do this with the changing counts:
 

	
 
class RecentFps(object):
 
class RecentFps:
 
  def __init__(self, window=20):
 
    self.window = window
 
    self.recentTimes = []
 

	
 
  def mark(self):
 
    now = time.time()
 
    self.recentTimes.append(now)
 
    self.recentTimes = self.recentTimes[-self.window:]
 

	
 
  def rate(self):
 
    def dec(innerFunc):
 
      def f(*a, **kw):
light9/namespaces.py
Show inline comments
 
from rdflib import URIRef, Namespace, RDF, RDFS  # noqa
 
from typing import Dict
 

	
 

	
 
# Namespace was showing up in profiles
 
class FastNs(object):
 
class FastNs:
 

	
 
    def __init__(self, base):
 
        self.ns = Namespace(base)
 
        self.cache: Dict[str, URIRef] = {}
 

	
 
    def __getitem__(self, term) -> URIRef:
 
        if term not in self.cache:
 
            self.cache[term] = self.ns[term]
 
        return self.cache[term]
 

	
 
    __getattr__ = __getitem__
 

	
light9/networking.py
Show inline comments
 
from urllib.parse import urlparse
 

	
 
from rdflib import URIRef
 

	
 
from .showconfig import getGraph, showUri
 
from .namespaces import L9
 

	
 

	
 
class ServiceAddress(object):
 
class ServiceAddress:
 

	
 
    def __init__(self, service):
 
        self.service = service
 

	
 
    def _url(self) -> URIRef:
 
        graph = getGraph()
 
        net = graph.value(showUri(), L9['networking'])
 
        ret = graph.value(net, self.service)
 
        if ret is None:
 
            raise ValueError("no url for %s -> %s -> %s" %
 
                             (showUri(), L9['networking'], self.service))
 
        assert isinstance(ret, URIRef)
light9/observable.py
Show inline comments
 
import logging
 
log = logging.getLogger('observable')
 

	
 

	
 
class _NoNewVal(object):
 
class _NoNewVal:
 
    pass
 

	
 

	
 
class Observable(object):
 
class Observable:
 
    """
 
    like knockout's observable. Hopefully this can be replaced by a
 
    better python one
 

	
 
    compare with:
 
    http://knockoutjs.com/documentation/observables.html
 
    https://github.com/drpancake/python-observable/blob/master/observable/observable.py
 
    """
 

	
 
    def __init__(self, val):
 
        self.val = val
 
        self.subscribers = set()
0 comments (0 inline, 0 general)